diff --git a/CHANGELOG.md b/CHANGELOG.md index 76d5b50a934dee..0065d339f07a21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,8 @@ release. -8.9.2
+8.9.3
+8.9.2
8.9.1
8.9.0
8.8.1
diff --git a/deps/openssl/asm/arm-void-gas/aes/aes-armv4.S b/deps/openssl/asm/arm-void-gas/aes/aes-armv4.S index 333a522730ba6c..bd01abddce1d55 100644 --- a/deps/openssl/asm/arm-void-gas/aes/aes-armv4.S +++ b/deps/openssl/asm/arm-void-gas/aes/aes-armv4.S @@ -164,7 +164,7 @@ AES_encrypt: #if __ARM_ARCH__<7 sub r3,pc,#8 @ AES_encrypt #else - adr r3,AES_encrypt + adr r3,. #endif stmdb sp!,{r1,r4-r12,lr} mov r12,r0 @ inp @@ -410,7 +410,7 @@ _armv4_AES_set_encrypt_key: #if __ARM_ARCH__<7 sub r3,pc,#8 @ AES_set_encrypt_key #else - adr r3,private_AES_set_encrypt_key + adr r3,. #endif teq r0,#0 #if __ARM_ARCH__>=7 @@ -927,7 +927,7 @@ AES_decrypt: #if __ARM_ARCH__<7 sub r3,pc,#8 @ AES_decrypt #else - adr r3,AES_decrypt + adr r3,. #endif stmdb sp!,{r1,r4-r12,lr} mov r12,r0 @ inp diff --git a/deps/openssl/asm/arm-void-gas/aes/bsaes-armv7.S b/deps/openssl/asm/arm-void-gas/aes/bsaes-armv7.S index b21ed7078b194a..aa15709abb403a 100644 --- a/deps/openssl/asm/arm-void-gas/aes/bsaes-armv7.S +++ b/deps/openssl/asm/arm-void-gas/aes/bsaes-armv7.S @@ -81,7 +81,7 @@ .type _bsaes_decrypt8,%function .align 4 _bsaes_decrypt8: - adr r6,_bsaes_decrypt8 + adr r6,. vldmia r4!, {q9} @ round 0 key add r6,r6,#.LM0ISR-_bsaes_decrypt8 @@ -567,7 +567,7 @@ _bsaes_const: .type _bsaes_encrypt8,%function .align 4 _bsaes_encrypt8: - adr r6,_bsaes_encrypt8 + adr r6,. vldmia r4!, {q9} @ round 0 key sub r6,r6,#_bsaes_encrypt8-.LM0SR @@ -998,7 +998,7 @@ _bsaes_encrypt8_bitslice: .type _bsaes_key_convert,%function .align 4 _bsaes_key_convert: - adr r6,_bsaes_key_convert + adr r6,. vld1.8 {q7}, [r4]! @ load round 0 key sub r6,r6,#_bsaes_key_convert-.LM0 vld1.8 {q15}, [r4]! @ load round 1 key diff --git a/deps/openssl/asm/arm-void-gas/sha/sha256-armv4.S b/deps/openssl/asm/arm-void-gas/sha/sha256-armv4.S index 683f1cc0c874b5..4e1f0226e84bc0 100644 --- a/deps/openssl/asm/arm-void-gas/sha/sha256-armv4.S +++ b/deps/openssl/asm/arm-void-gas/sha/sha256-armv4.S @@ -88,7 +88,7 @@ sha256_block_data_order: #if __ARM_ARCH__<7 sub r3,pc,#8 @ sha256_block_data_order #else - adr r3,sha256_block_data_order + adr r3,. #endif #if __ARM_MAX_ARCH__>=7 && !defined(__KERNEL__) ldr r12,.LOPENSSL_armcap diff --git a/deps/openssl/asm/x64-elf-gas/bn/rsaz-avx2.s b/deps/openssl/asm/x64-elf-gas/bn/rsaz-avx2.s index d3d84e3f7a4b78..a2cccde63604f4 100644 --- a/deps/openssl/asm/x64-elf-gas/bn/rsaz-avx2.s +++ b/deps/openssl/asm/x64-elf-gas/bn/rsaz-avx2.s @@ -66,7 +66,7 @@ rsaz_1024_sqr_avx2: vmovdqu 256-128(%rsi),%ymm8 leaq 192(%rsp),%rbx - vpbroadcastq .Land_mask(%rip),%ymm15 + vmovdqu .Land_mask(%rip),%ymm15 jmp .LOOP_GRANDE_SQR_1024 .align 32 @@ -799,10 +799,10 @@ rsaz_1024_mul_avx2: vpmuludq 192-128(%rcx),%ymm11,%ymm12 vpaddq %ymm12,%ymm6,%ymm6 vpmuludq 224-128(%rcx),%ymm11,%ymm13 - vpblendd $3,%ymm14,%ymm9,%ymm9 + vpblendd $3,%ymm14,%ymm9,%ymm12 vpaddq %ymm13,%ymm7,%ymm7 vpmuludq 256-128(%rcx),%ymm11,%ymm0 - vpaddq %ymm9,%ymm3,%ymm3 + vpaddq %ymm12,%ymm3,%ymm3 vpaddq %ymm0,%ymm8,%ymm8 movq %rbx,%rax @@ -815,7 +815,9 @@ rsaz_1024_mul_avx2: vmovdqu -8+64-128(%rsi),%ymm13 movq %r10,%rax + vpblendd $0xfc,%ymm14,%ymm9,%ymm9 imull %r8d,%eax + vpaddq %ymm9,%ymm4,%ymm4 andl $0x1fffffff,%eax imulq 16-128(%rsi),%rbx @@ -1044,7 +1046,6 @@ rsaz_1024_mul_avx2: decl %r14d jnz .Loop_mul_1024 - vpermq $0,%ymm15,%ymm15 vpaddq (%rsp),%ymm12,%ymm0 vpsrlq $29,%ymm0,%ymm12 @@ -1684,7 +1685,7 @@ rsaz_avx2_eligible: .align 64 .Land_mask: -.quad 0x1fffffff,0x1fffffff,0x1fffffff,-1 +.quad 0x1fffffff,0x1fffffff,0x1fffffff,0x1fffffff .Lscatter_permd: .long 0,2,4,6,7,7,7,7 .Lgather_permd: diff --git a/deps/openssl/asm/x64-macosx-gas/bn/rsaz-avx2.s b/deps/openssl/asm/x64-macosx-gas/bn/rsaz-avx2.s index 1dea50d90463e7..f2bc63be34eee2 100644 --- a/deps/openssl/asm/x64-macosx-gas/bn/rsaz-avx2.s +++ b/deps/openssl/asm/x64-macosx-gas/bn/rsaz-avx2.s @@ -66,7 +66,7 @@ L$sqr_1024_no_n_copy: vmovdqu 256-128(%rsi),%ymm8 leaq 192(%rsp),%rbx - vpbroadcastq L$and_mask(%rip),%ymm15 + vmovdqu L$and_mask(%rip),%ymm15 jmp L$OOP_GRANDE_SQR_1024 .p2align 5 @@ -799,10 +799,10 @@ L$oop_mul_1024: vpmuludq 192-128(%rcx),%ymm11,%ymm12 vpaddq %ymm12,%ymm6,%ymm6 vpmuludq 224-128(%rcx),%ymm11,%ymm13 - vpblendd $3,%ymm14,%ymm9,%ymm9 + vpblendd $3,%ymm14,%ymm9,%ymm12 vpaddq %ymm13,%ymm7,%ymm7 vpmuludq 256-128(%rcx),%ymm11,%ymm0 - vpaddq %ymm9,%ymm3,%ymm3 + vpaddq %ymm12,%ymm3,%ymm3 vpaddq %ymm0,%ymm8,%ymm8 movq %rbx,%rax @@ -815,7 +815,9 @@ L$oop_mul_1024: vmovdqu -8+64-128(%rsi),%ymm13 movq %r10,%rax + vpblendd $0xfc,%ymm14,%ymm9,%ymm9 imull %r8d,%eax + vpaddq %ymm9,%ymm4,%ymm4 andl $0x1fffffff,%eax imulq 16-128(%rsi),%rbx @@ -1044,7 +1046,6 @@ L$oop_mul_1024: decl %r14d jnz L$oop_mul_1024 - vpermq $0,%ymm15,%ymm15 vpaddq (%rsp),%ymm12,%ymm0 vpsrlq $29,%ymm0,%ymm12 @@ -1684,7 +1685,7 @@ _rsaz_avx2_eligible: .p2align 6 L$and_mask: -.quad 0x1fffffff,0x1fffffff,0x1fffffff,-1 +.quad 0x1fffffff,0x1fffffff,0x1fffffff,0x1fffffff L$scatter_permd: .long 0,2,4,6,7,7,7,7 L$gather_permd: diff --git a/deps/openssl/asm/x64-win32-masm/bn/rsaz-avx2.asm b/deps/openssl/asm/x64-win32-masm/bn/rsaz-avx2.asm index c24d0c5e6a3d3d..40ce01fdc61a9b 100644 --- a/deps/openssl/asm/x64-win32-masm/bn/rsaz-avx2.asm +++ b/deps/openssl/asm/x64-win32-masm/bn/rsaz-avx2.asm @@ -90,7 +90,7 @@ $L$sqr_1024_no_n_copy:: vmovdqu ymm8,YMMWORD PTR[((256-128))+rsi] lea rbx,QWORD PTR[192+rsp] - vpbroadcastq ymm15,QWORD PTR[$L$and_mask] + vmovdqu ymm15,YMMWORD PTR[$L$and_mask] jmp $L$OOP_GRANDE_SQR_1024 ALIGN 32 @@ -860,10 +860,10 @@ $L$oop_mul_1024:: vpmuludq ymm12,ymm11,YMMWORD PTR[((192-128))+rcx] vpaddq ymm6,ymm6,ymm12 vpmuludq ymm13,ymm11,YMMWORD PTR[((224-128))+rcx] - vpblendd ymm9,ymm9,ymm14,3 + vpblendd ymm12,ymm9,ymm14,3 vpaddq ymm7,ymm7,ymm13 vpmuludq ymm0,ymm11,YMMWORD PTR[((256-128))+rcx] - vpaddq ymm3,ymm3,ymm9 + vpaddq ymm3,ymm3,ymm12 vpaddq ymm8,ymm8,ymm0 mov rax,rbx @@ -876,7 +876,9 @@ $L$oop_mul_1024:: vmovdqu ymm13,YMMWORD PTR[((-8+64-128))+rsi] mov rax,r10 + vpblendd ymm9,ymm9,ymm14,0fch imul eax,r8d + vpaddq ymm4,ymm4,ymm9 and eax,01fffffffh imul rbx,QWORD PTR[((16-128))+rsi] @@ -1105,7 +1107,6 @@ $L$oop_mul_1024:: dec r14d jnz $L$oop_mul_1024 - vpermq ymm15,ymm15,0 vpaddq ymm0,ymm12,YMMWORD PTR[rsp] vpsrlq ymm12,ymm0,29 @@ -1783,7 +1784,7 @@ rsaz_avx2_eligible ENDP ALIGN 64 $L$and_mask:: - DQ 01fffffffh,01fffffffh,01fffffffh,-1 + DQ 01fffffffh,01fffffffh,01fffffffh,01fffffffh $L$scatter_permd:: DD 0,2,4,6,7,7,7,7 $L$gather_permd:: diff --git a/deps/openssl/asm_obsolete/arm-void-gas/aes/aes-armv4.S b/deps/openssl/asm_obsolete/arm-void-gas/aes/aes-armv4.S index 333a522730ba6c..bd01abddce1d55 100644 --- a/deps/openssl/asm_obsolete/arm-void-gas/aes/aes-armv4.S +++ b/deps/openssl/asm_obsolete/arm-void-gas/aes/aes-armv4.S @@ -164,7 +164,7 @@ AES_encrypt: #if __ARM_ARCH__<7 sub r3,pc,#8 @ AES_encrypt #else - adr r3,AES_encrypt + adr r3,. #endif stmdb sp!,{r1,r4-r12,lr} mov r12,r0 @ inp @@ -410,7 +410,7 @@ _armv4_AES_set_encrypt_key: #if __ARM_ARCH__<7 sub r3,pc,#8 @ AES_set_encrypt_key #else - adr r3,private_AES_set_encrypt_key + adr r3,. #endif teq r0,#0 #if __ARM_ARCH__>=7 @@ -927,7 +927,7 @@ AES_decrypt: #if __ARM_ARCH__<7 sub r3,pc,#8 @ AES_decrypt #else - adr r3,AES_decrypt + adr r3,. #endif stmdb sp!,{r1,r4-r12,lr} mov r12,r0 @ inp diff --git a/deps/openssl/asm_obsolete/arm-void-gas/aes/bsaes-armv7.S b/deps/openssl/asm_obsolete/arm-void-gas/aes/bsaes-armv7.S index b21ed7078b194a..aa15709abb403a 100644 --- a/deps/openssl/asm_obsolete/arm-void-gas/aes/bsaes-armv7.S +++ b/deps/openssl/asm_obsolete/arm-void-gas/aes/bsaes-armv7.S @@ -81,7 +81,7 @@ .type _bsaes_decrypt8,%function .align 4 _bsaes_decrypt8: - adr r6,_bsaes_decrypt8 + adr r6,. vldmia r4!, {q9} @ round 0 key add r6,r6,#.LM0ISR-_bsaes_decrypt8 @@ -567,7 +567,7 @@ _bsaes_const: .type _bsaes_encrypt8,%function .align 4 _bsaes_encrypt8: - adr r6,_bsaes_encrypt8 + adr r6,. vldmia r4!, {q9} @ round 0 key sub r6,r6,#_bsaes_encrypt8-.LM0SR @@ -998,7 +998,7 @@ _bsaes_encrypt8_bitslice: .type _bsaes_key_convert,%function .align 4 _bsaes_key_convert: - adr r6,_bsaes_key_convert + adr r6,. vld1.8 {q7}, [r4]! @ load round 0 key sub r6,r6,#_bsaes_key_convert-.LM0 vld1.8 {q15}, [r4]! @ load round 1 key diff --git a/deps/openssl/asm_obsolete/arm-void-gas/sha/sha256-armv4.S b/deps/openssl/asm_obsolete/arm-void-gas/sha/sha256-armv4.S index 683f1cc0c874b5..4e1f0226e84bc0 100644 --- a/deps/openssl/asm_obsolete/arm-void-gas/sha/sha256-armv4.S +++ b/deps/openssl/asm_obsolete/arm-void-gas/sha/sha256-armv4.S @@ -88,7 +88,7 @@ sha256_block_data_order: #if __ARM_ARCH__<7 sub r3,pc,#8 @ sha256_block_data_order #else - adr r3,sha256_block_data_order + adr r3,. #endif #if __ARM_MAX_ARCH__>=7 && !defined(__KERNEL__) ldr r12,.LOPENSSL_armcap diff --git a/deps/openssl/openssl/CHANGES b/deps/openssl/openssl/CHANGES index e3d57b328c58e3..f2fc31a25c54b1 100644 --- a/deps/openssl/openssl/CHANGES +++ b/deps/openssl/openssl/CHANGES @@ -7,6 +7,51 @@ https://github.com/openssl/openssl/commits/ and pick the appropriate release branch. + Changes between 1.0.2m and 1.0.2n [7 Dec 2017] + + *) Read/write after SSL object in error state + + OpenSSL 1.0.2 (starting from version 1.0.2b) introduced an "error state" + mechanism. The intent was that if a fatal error occurred during a handshake + then OpenSSL would move into the error state and would immediately fail if + you attempted to continue the handshake. This works as designed for the + explicit handshake functions (SSL_do_handshake(), SSL_accept() and + SSL_connect()), however due to a bug it does not work correctly if + SSL_read() or SSL_write() is called directly. In that scenario, if the + handshake fails then a fatal error will be returned in the initial function + call. If SSL_read()/SSL_write() is subsequently called by the application + for the same SSL object then it will succeed and the data is passed without + being decrypted/encrypted directly from the SSL/TLS record layer. + + In order to exploit this issue an application bug would have to be present + that resulted in a call to SSL_read()/SSL_write() being issued after having + already received a fatal error. + + This issue was reported to OpenSSL by David Benjamin (Google). + (CVE-2017-3737) + [Matt Caswell] + + *) rsaz_1024_mul_avx2 overflow bug on x86_64 + + There is an overflow bug in the AVX2 Montgomery multiplication procedure + used in exponentiation with 1024-bit moduli. No EC algorithms are affected. + Analysis suggests that attacks against RSA and DSA as a result of this + defect would be very difficult to perform and are not believed likely. + Attacks against DH1024 are considered just feasible, because most of the + work necessary to deduce information about a private key may be performed + offline. The amount of resources required for such an attack would be + significant. However, for an attack on TLS to be meaningful, the server + would have to share the DH1024 private key among multiple clients, which is + no longer an option since CVE-2016-0701. + + This only affects processors that support the AVX2 but not ADX extensions + like Intel Haswell (4th generation). + + This issue was reported to OpenSSL by David Benjamin (Google). The issue + was originally found via the OSS-Fuzz project. + (CVE-2017-3738) + [Andy Polyakov] + Changes between 1.0.2l and 1.0.2m [2 Nov 2017] *) bn_sqrx8x_internal carry bug on x86_64 diff --git a/deps/openssl/openssl/Configure b/deps/openssl/openssl/Configure index fd7988e2b3d93a..60386d39598758 100755 --- a/deps/openssl/openssl/Configure +++ b/deps/openssl/openssl/Configure @@ -592,9 +592,9 @@ my %table=( "debug-VC-WIN64A","cl:-W3 -Gs0 -Gy -Zi -nologo -DOPENSSL_SYSNAME_WIN32 -DWIN32_LEAN_AND_MEAN -DL_ENDIAN -DUNICODE -D_UNICODE -D_CRT_SECURE_NO_DEPRECATE:::WIN64A::SIXTY_FOUR_BIT RC4_CHUNK_LL DES_INT EXPORT_VAR_AS_FN:".eval{my $asm=$x86_64_asm;$asm=~s/x86_64-gcc\.o/bn_asm.o/;$asm}.":auto:win32", # x86 Win32 target defaults to ANSI API, if you want UNICODE, complement # 'perl Configure VC-WIN32' with '-DUNICODE -D_UNICODE' -"VC-WIN32","cl:-W3 -Gs0 -GF -Gy -nologo -DOPENSSL_SYSNAME_WIN32 -DWIN32_LEAN_AND_MEAN -DL_ENDIAN -D_CRT_SECURE_NO_DEPRECATE:::WIN32::BN_LLONG RC4_INDEX EXPORT_VAR_AS_FN ${x86_gcc_opts}:${x86_asm}:win32n:win32", +"VC-WIN32","cl:-W3 -WX -Gs0 -GF -Gy -nologo -DOPENSSL_SYSNAME_WIN32 -DWIN32_LEAN_AND_MEAN -DL_ENDIAN -D_CRT_SECURE_NO_DEPRECATE -D_WINSOCK_DEPRECATED_NO_WARNINGS:::WIN32::BN_LLONG RC4_INDEX EXPORT_VAR_AS_FN ${x86_gcc_opts}:${x86_asm}:win32n:win32", # Unified CE target -"debug-VC-WIN32","cl:-W3 -Gs0 -GF -Gy -Zi -nologo -DOPENSSL_SYSNAME_WIN32 -DWIN32_LEAN_AND_MEAN -DL_ENDIAN -D_CRT_SECURE_NO_DEPRECATE:::WIN32::BN_LLONG RC4_INDEX EXPORT_VAR_AS_FN ${x86_gcc_opts}:${x86_asm}:win32n:win32", +"debug-VC-WIN32","cl:-W3 -WX -Gs0 -GF -Gy -Zi -nologo -DOPENSSL_SYSNAME_WIN32 -DWIN32_LEAN_AND_MEAN -DL_ENDIAN -D_CRT_SECURE_NO_DEPRECATE -D_WINSOCK_DEPRECATED_NO_WARNINGS:::WIN32::BN_LLONG RC4_INDEX EXPORT_VAR_AS_FN ${x86_gcc_opts}:${x86_asm}:win32n:win32", "VC-CE","cl::::WINCE::BN_LLONG RC4_INDEX EXPORT_VAR_AS_FN ${x86_gcc_opts}:${no_asm}:win32", # Borland C++ 4.5 diff --git a/deps/openssl/openssl/Makefile b/deps/openssl/openssl/Makefile index 484f2f45f77667..9212c288527852 100644 --- a/deps/openssl/openssl/Makefile +++ b/deps/openssl/openssl/Makefile @@ -4,7 +4,7 @@ ## Makefile for OpenSSL ## -VERSION=1.0.2m +VERSION=1.0.2n MAJOR=1 MINOR=0.2 SHLIB_VERSION_NUMBER=1.0.0 diff --git a/deps/openssl/openssl/Makefile.bak b/deps/openssl/openssl/Makefile.bak index b545261eaeee19..e766d35764ffb7 100644 --- a/deps/openssl/openssl/Makefile.bak +++ b/deps/openssl/openssl/Makefile.bak @@ -4,7 +4,7 @@ ## Makefile for OpenSSL ## -VERSION=1.0.2m +VERSION=1.0.2n MAJOR=1 MINOR=0.2 SHLIB_VERSION_NUMBER=1.0.0 diff --git a/deps/openssl/openssl/NEWS b/deps/openssl/openssl/NEWS index 1b72013ad18624..6f0c5c47b654bc 100644 --- a/deps/openssl/openssl/NEWS +++ b/deps/openssl/openssl/NEWS @@ -5,6 +5,11 @@ This file gives a brief overview of the major changes between each OpenSSL release. For more details please read the CHANGES file. + Major changes between OpenSSL 1.0.2m and OpenSSL 1.0.2n [7 Dec 2017] + + o Read/write after SSL object in error state (CVE-2017-3737) + o rsaz_1024_mul_avx2 overflow bug on x86_64 (CVE-2017-3738) + Major changes between OpenSSL 1.0.2l and OpenSSL 1.0.2m [2 Nov 2017] o bn_sqrx8x_internal carry bug on x86_64 (CVE-2017-3736) diff --git a/deps/openssl/openssl/README b/deps/openssl/openssl/README index b5aae6260ce889..80de6886a766b0 100644 --- a/deps/openssl/openssl/README +++ b/deps/openssl/openssl/README @@ -1,5 +1,5 @@ - OpenSSL 1.0.2m 2 Nov 2017 + OpenSSL 1.0.2n 7 Dec 2017 Copyright (c) 1998-2015 The OpenSSL Project Copyright (c) 1995-1998 Eric A. Young, Tim J. Hudson diff --git a/deps/openssl/openssl/apps/apps.c b/deps/openssl/openssl/apps/apps.c index c487bd92db2a63..29de1b75dd600b 100644 --- a/deps/openssl/openssl/apps/apps.c +++ b/deps/openssl/openssl/apps/apps.c @@ -148,6 +148,10 @@ #ifdef _WIN32 static int WIN32_rename(const char *from, const char *to); # define rename(from,to) WIN32_rename((from),(to)) +# ifdef fileno +# undef fileno +# endif +# define fileno(a) (int)_fileno(a) #endif typedef struct { @@ -2788,13 +2792,13 @@ unsigned char *next_protos_parse(unsigned short *outlen, const char *in) OPENSSL_free(out); return NULL; } - out[start] = i - start; + out[start] = (unsigned char)(i - start); start = i + 1; } else out[i + 1] = in[i]; } - *outlen = len + 1; + *outlen = (unsigned char)(len + 1); return out; } #endif /* ndef OPENSSL_NO_TLSEXT */ diff --git a/deps/openssl/openssl/apps/dsa.c b/deps/openssl/openssl/apps/dsa.c index 4ed21d891e8698..82a870eb291004 100644 --- a/deps/openssl/openssl/apps/dsa.c +++ b/deps/openssl/openssl/apps/dsa.c @@ -327,6 +327,9 @@ int MAIN(int argc, char **argv) } else if (outformat == FORMAT_MSBLOB || outformat == FORMAT_PVK) { EVP_PKEY *pk; pk = EVP_PKEY_new(); + if (pk == NULL) + goto end; + EVP_PKEY_set1_DSA(pk, dsa); if (outformat == FORMAT_PVK) i = i2b_PVK_bio(out, pk, pvk_encr, 0, passout); diff --git a/deps/openssl/openssl/apps/s_client.c b/deps/openssl/openssl/apps/s_client.c index 19914a19dae818..8f33a8d884614e 100644 --- a/deps/openssl/openssl/apps/s_client.c +++ b/deps/openssl/openssl/apps/s_client.c @@ -642,10 +642,11 @@ static int serverinfo_cli_parse_cb(SSL *s, unsigned int ext_type, unsigned char ext_buf[4 + 65536]; /* Reconstruct the type/len fields prior to extension data */ - ext_buf[0] = ext_type >> 8; - ext_buf[1] = ext_type & 0xFF; - ext_buf[2] = inlen >> 8; - ext_buf[3] = inlen & 0xFF; + inlen &= 0xffff; /* for formal memcpy correctness */ + ext_buf[0] = (unsigned char)(ext_type >> 8); + ext_buf[1] = (unsigned char)(ext_type); + ext_buf[2] = (unsigned char)(inlen >> 8); + ext_buf[3] = (unsigned char)(inlen); memcpy(ext_buf + 4, in, inlen); BIO_snprintf(pem_name, sizeof(pem_name), "SERVERINFO FOR EXTENSION %d", diff --git a/deps/openssl/openssl/apps/speed.c b/deps/openssl/openssl/apps/speed.c index 5259c16f1218a3..5383678b98644f 100644 --- a/deps/openssl/openssl/apps/speed.c +++ b/deps/openssl/openssl/apps/speed.c @@ -2829,8 +2829,8 @@ static void multiblock_speed(const EVP_CIPHER *evp_cipher) RAND_bytes(out, 16); len += 16; - aad[11] = len >> 8; - aad[12] = len; + aad[11] = (unsigned char)(len >> 8); + aad[12] = (unsigned char)(len); pad = EVP_CIPHER_CTX_ctrl(&ctx, EVP_CTRL_AEAD_TLS1_AAD, EVP_AEAD_TLS1_AAD_LEN, aad); diff --git a/deps/openssl/openssl/crypto/aes/asm/aes-armv4.pl b/deps/openssl/openssl/crypto/aes/asm/aes-armv4.pl index 4f8917089f6c2a..c1b5e352d76ff4 100644 --- a/deps/openssl/openssl/crypto/aes/asm/aes-armv4.pl +++ b/deps/openssl/openssl/crypto/aes/asm/aes-armv4.pl @@ -184,7 +184,7 @@ #if __ARM_ARCH__<7 sub r3,pc,#8 @ AES_encrypt #else - adr r3,AES_encrypt + adr r3,. #endif stmdb sp!,{r1,r4-r12,lr} mov $rounds,r0 @ inp @@ -430,7 +430,7 @@ #if __ARM_ARCH__<7 sub r3,pc,#8 @ AES_set_encrypt_key #else - adr r3,private_AES_set_encrypt_key + adr r3,. #endif teq r0,#0 #if __ARM_ARCH__>=7 @@ -952,7 +952,7 @@ #if __ARM_ARCH__<7 sub r3,pc,#8 @ AES_decrypt #else - adr r3,AES_decrypt + adr r3,. #endif stmdb sp!,{r1,r4-r12,lr} mov $rounds,r0 @ inp diff --git a/deps/openssl/openssl/crypto/aes/asm/bsaes-armv7.pl b/deps/openssl/openssl/crypto/aes/asm/bsaes-armv7.pl index 70b3f9656f4fa7..ec66b0502a6459 100644 --- a/deps/openssl/openssl/crypto/aes/asm/bsaes-armv7.pl +++ b/deps/openssl/openssl/crypto/aes/asm/bsaes-armv7.pl @@ -724,7 +724,7 @@ sub bitslice { .type _bsaes_decrypt8,%function .align 4 _bsaes_decrypt8: - adr $const,_bsaes_decrypt8 + adr $const,. vldmia $key!, {@XMM[9]} @ round 0 key add $const,$const,#.LM0ISR-_bsaes_decrypt8 @@ -819,7 +819,7 @@ sub bitslice { .type _bsaes_encrypt8,%function .align 4 _bsaes_encrypt8: - adr $const,_bsaes_encrypt8 + adr $const,. vldmia $key!, {@XMM[9]} @ round 0 key sub $const,$const,#_bsaes_encrypt8-.LM0SR @@ -923,7 +923,7 @@ sub bitslice_key { .type _bsaes_key_convert,%function .align 4 _bsaes_key_convert: - adr $const,_bsaes_key_convert + adr $const,. vld1.8 {@XMM[7]}, [$inp]! @ load round 0 key sub $const,$const,#_bsaes_key_convert-.LM0 vld1.8 {@XMM[15]}, [$inp]! @ load round 1 key diff --git a/deps/openssl/openssl/crypto/asn1/a_i2d_fp.c b/deps/openssl/openssl/crypto/asn1/a_i2d_fp.c index 0f56cd4e07451b..2e85e041e4cf40 100644 --- a/deps/openssl/openssl/crypto/asn1/a_i2d_fp.c +++ b/deps/openssl/openssl/crypto/asn1/a_i2d_fp.c @@ -87,6 +87,9 @@ int ASN1_i2d_bio(i2d_of_void *i2d, BIO *out, unsigned char *x) int i, j = 0, n, ret = 1; n = i2d(x, NULL); + if (n <= 0) + return 0; + b = (char *)OPENSSL_malloc(n); if (b == NULL) { ASN1err(ASN1_F_ASN1_I2D_BIO, ERR_R_MALLOC_FAILURE); diff --git a/deps/openssl/openssl/crypto/bio/b_print.c b/deps/openssl/openssl/crypto/bio/b_print.c index eb3ab759349cde..1c82f53d5a074d 100644 --- a/deps/openssl/openssl/crypto/bio/b_print.c +++ b/deps/openssl/openssl/crypto/bio/b_print.c @@ -385,7 +385,7 @@ _dopr(char **sbuffer, if (cflags == DP_C_SHORT) { short int *num; num = va_arg(args, short int *); - *num = currlen; + *num = (short int)currlen; } else if (cflags == DP_C_LONG) { /* XXX */ long int *num; num = va_arg(args, long int *); @@ -502,7 +502,7 @@ fmtint(char **sbuffer, if (!(flags & DP_F_UNSIGNED)) { if (value < 0) { signvalue = '-'; - uvalue = -(unsigned LLONG)value; + uvalue = 0 - (unsigned LLONG)value; } else if (flags & DP_F_PLUS) signvalue = '+'; else if (flags & DP_F_SPACE) diff --git a/deps/openssl/openssl/crypto/bn/asm/rsaz-avx2.pl b/deps/openssl/openssl/crypto/bn/asm/rsaz-avx2.pl index 712a77fe8ca3ab..2b3f8b0e21ecef 100755 --- a/deps/openssl/openssl/crypto/bn/asm/rsaz-avx2.pl +++ b/deps/openssl/openssl/crypto/bn/asm/rsaz-avx2.pl @@ -239,7 +239,7 @@ vmovdqu 32*8-128($ap), $ACC8 lea 192(%rsp), $tp0 # 64+128=192 - vpbroadcastq .Land_mask(%rip), $AND_MASK + vmovdqu .Land_mask(%rip), $AND_MASK jmp .LOOP_GRANDE_SQR_1024 .align 32 @@ -1070,10 +1070,10 @@ vpmuludq 32*6-128($np),$Yi,$TEMP1 vpaddq $TEMP1,$ACC6,$ACC6 vpmuludq 32*7-128($np),$Yi,$TEMP2 - vpblendd \$3, $ZERO, $ACC9, $ACC9 # correct $ACC3 + vpblendd \$3, $ZERO, $ACC9, $TEMP1 # correct $ACC3 vpaddq $TEMP2,$ACC7,$ACC7 vpmuludq 32*8-128($np),$Yi,$TEMP0 - vpaddq $ACC9, $ACC3, $ACC3 # correct $ACC3 + vpaddq $TEMP1, $ACC3, $ACC3 # correct $ACC3 vpaddq $TEMP0,$ACC8,$ACC8 mov %rbx, %rax @@ -1086,7 +1086,9 @@ vmovdqu -8+32*2-128($ap),$TEMP2 mov $r1, %rax + vpblendd \$0xfc, $ZERO, $ACC9, $ACC9 # correct $ACC3 imull $n0, %eax + vpaddq $ACC9,$ACC4,$ACC4 # correct $ACC3 and \$0x1fffffff, %eax imulq 16-128($ap),%rbx @@ -1322,15 +1324,12 @@ # But as we underutilize resources, it's possible to correct in # each iteration with marginal performance loss. But then, as # we do it in each iteration, we can correct less digits, and -# avoid performance penalties completely. Also note that we -# correct only three digits out of four. This works because -# most significant digit is subjected to less additions. +# avoid performance penalties completely. $TEMP0 = $ACC9; $TEMP3 = $Bi; $TEMP4 = $Yi; $code.=<<___; - vpermq \$0, $AND_MASK, $AND_MASK vpaddq (%rsp), $TEMP1, $ACC0 vpsrlq \$29, $ACC0, $TEMP1 @@ -1763,7 +1762,7 @@ .align 64 .Land_mask: - .quad 0x1fffffff,0x1fffffff,0x1fffffff,-1 + .quad 0x1fffffff,0x1fffffff,0x1fffffff,0x1fffffff .Lscatter_permd: .long 0,2,4,6,7,7,7,7 .Lgather_permd: diff --git a/deps/openssl/openssl/crypto/bn/bn_exp.c b/deps/openssl/openssl/crypto/bn/bn_exp.c index 35facd213a253d..c4b63e44ba369a 100644 --- a/deps/openssl/openssl/crypto/bn/bn_exp.c +++ b/deps/openssl/openssl/crypto/bn/bn_exp.c @@ -149,7 +149,7 @@ int BN_exp(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, BN_CTX *ctx) || BN_get_flags(a, BN_FLG_CONSTTIME) != 0) { /* BN_FLG_CONSTTIME only supported by BN_mod_exp_mont() */ BNerr(BN_F_BN_EXP, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); - return -1; + return 0; } BN_CTX_start(ctx); @@ -285,7 +285,7 @@ int BN_mod_exp_recp(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, || BN_get_flags(m, BN_FLG_CONSTTIME) != 0) { /* BN_FLG_CONSTTIME only supported by BN_mod_exp_mont() */ BNerr(BN_F_BN_MOD_EXP_RECP, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); - return -1; + return 0; } bits = BN_num_bits(p); @@ -1228,7 +1228,7 @@ int BN_mod_exp_mont_word(BIGNUM *rr, BN_ULONG a, const BIGNUM *p, || BN_get_flags(m, BN_FLG_CONSTTIME) != 0) { /* BN_FLG_CONSTTIME only supported by BN_mod_exp_mont() */ BNerr(BN_F_BN_MOD_EXP_MONT_WORD, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); - return -1; + return 0; } bn_check_top(p); @@ -1361,7 +1361,7 @@ int BN_mod_exp_simple(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, || BN_get_flags(m, BN_FLG_CONSTTIME) != 0) { /* BN_FLG_CONSTTIME only supported by BN_mod_exp_mont() */ BNerr(BN_F_BN_MOD_EXP_SIMPLE, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); - return -1; + return 0; } bits = BN_num_bits(p); diff --git a/deps/openssl/openssl/crypto/dsa/dsa_ameth.c b/deps/openssl/openssl/crypto/dsa/dsa_ameth.c index aac253095141a3..e22627f85152a1 100644 --- a/deps/openssl/openssl/crypto/dsa/dsa_ameth.c +++ b/deps/openssl/openssl/crypto/dsa/dsa_ameth.c @@ -133,6 +133,7 @@ static int dsa_pub_encode(X509_PUBKEY *pk, const EVP_PKEY *pkey) unsigned char *penc = NULL; int penclen; ASN1_STRING *str = NULL; + ASN1_OBJECT *aobj; dsa = pkey->pkey.dsa; if (pkey->save_parameters && dsa->p && dsa->q && dsa->g) { @@ -159,8 +160,11 @@ static int dsa_pub_encode(X509_PUBKEY *pk, const EVP_PKEY *pkey) goto err; } - if (X509_PUBKEY_set0_param(pk, OBJ_nid2obj(EVP_PKEY_DSA), - ptype, str, penc, penclen)) + aobj = OBJ_nid2obj(EVP_PKEY_DSA); + if (aobj == NULL) + goto err; + + if (X509_PUBKEY_set0_param(pk, aobj, ptype, str, penc, penclen)) return 1; err: diff --git a/deps/openssl/openssl/crypto/engine/eng_fat.c b/deps/openssl/openssl/crypto/engine/eng_fat.c index 4279dd94b135b7..55d3858bb1c6df 100644 --- a/deps/openssl/openssl/crypto/engine/eng_fat.c +++ b/deps/openssl/openssl/crypto/engine/eng_fat.c @@ -167,6 +167,7 @@ int ENGINE_register_complete(ENGINE *e) #endif ENGINE_register_RAND(e); ENGINE_register_pkey_meths(e); + ENGINE_register_pkey_asn1_meths(e); return 1; } diff --git a/deps/openssl/openssl/crypto/lhash/lhash.c b/deps/openssl/openssl/crypto/lhash/lhash.c index f3798872598a44..51bb258e74b8ac 100644 --- a/deps/openssl/openssl/crypto/lhash/lhash.c +++ b/deps/openssl/openssl/crypto/lhash/lhash.c @@ -107,7 +107,7 @@ * https://en.wikipedia.org/wiki/Linear_hashing * * Litwin, Witold (1980), "Linear hashing: A new tool for file and table - * addressing", Proc. 6th Conference on Very Large Databases: 212–223 + * addressing", Proc. 6th Conference on Very Large Databases: 212-223 * http://hackthology.com/pdfs/Litwin-1980-Linear_Hashing.pdf * * From the wikipedia article "Linear hashing is used in the BDB Berkeley diff --git a/deps/openssl/openssl/crypto/opensslv.h b/deps/openssl/openssl/crypto/opensslv.h index c944d562dae0a9..baee2d0865fd1d 100644 --- a/deps/openssl/openssl/crypto/opensslv.h +++ b/deps/openssl/openssl/crypto/opensslv.h @@ -30,11 +30,11 @@ extern "C" { * (Prior to 0.9.5a beta1, a different scheme was used: MMNNFFRBB for * major minor fix final patch/beta) */ -# define OPENSSL_VERSION_NUMBER 0x100020dfL +# define OPENSSL_VERSION_NUMBER 0x100020efL # ifdef OPENSSL_FIPS -# define OPENSSL_VERSION_TEXT "OpenSSL 1.0.2m-fips 2 Nov 2017" +# define OPENSSL_VERSION_TEXT "OpenSSL 1.0.2n-fips 7 Dec 2017" # else -# define OPENSSL_VERSION_TEXT "OpenSSL 1.0.2m 2 Nov 2017" +# define OPENSSL_VERSION_TEXT "OpenSSL 1.0.2n 7 Dec 2017" # endif # define OPENSSL_VERSION_PTEXT " part of " OPENSSL_VERSION_TEXT diff --git a/deps/openssl/openssl/crypto/rsa/rsa_gen.c b/deps/openssl/openssl/crypto/rsa/rsa_gen.c index 082c8da2efc2a7..a85493d6097b8e 100644 --- a/deps/openssl/openssl/crypto/rsa/rsa_gen.c +++ b/deps/openssl/openssl/crypto/rsa/rsa_gen.c @@ -110,6 +110,16 @@ static int rsa_builtin_keygen(RSA *rsa, int bits, BIGNUM *e_value, int bitsp, bitsq, ok = -1, n = 0; BN_CTX *ctx = NULL; + /* + * When generating ridiculously small keys, we can get stuck + * continually regenerating the same prime values. + */ + if (bits < 16) { + ok = 0; /* we set our own err */ + RSAerr(RSA_F_RSA_BUILTIN_KEYGEN, RSA_R_KEY_SIZE_TOO_SMALL); + goto err; + } + ctx = BN_CTX_new(); if (ctx == NULL) goto err; @@ -161,21 +171,10 @@ static int rsa_builtin_keygen(RSA *rsa, int bits, BIGNUM *e_value, if (!BN_GENCB_call(cb, 3, 0)) goto err; for (;;) { - /* - * When generating ridiculously small keys, we can get stuck - * continually regenerating the same prime values. Check for this and - * bail if it happens 3 times. - */ - unsigned int degenerate = 0; do { if (!BN_generate_prime_ex(rsa->q, bitsq, 0, NULL, NULL, cb)) goto err; - } while ((BN_cmp(rsa->p, rsa->q) == 0) && (++degenerate < 3)); - if (degenerate == 3) { - ok = 0; /* we set our own err */ - RSAerr(RSA_F_RSA_BUILTIN_KEYGEN, RSA_R_KEY_SIZE_TOO_SMALL); - goto err; - } + } while (BN_cmp(rsa->p, rsa->q) == 0); if (!BN_sub(r2, rsa->q, BN_value_one())) goto err; if (!BN_gcd(r1, r2, rsa->e, ctx)) diff --git a/deps/openssl/openssl/crypto/sha/asm/sha256-armv4.pl b/deps/openssl/openssl/crypto/sha/asm/sha256-armv4.pl index 4fee74d832d100..750216eb426704 100644 --- a/deps/openssl/openssl/crypto/sha/asm/sha256-armv4.pl +++ b/deps/openssl/openssl/crypto/sha/asm/sha256-armv4.pl @@ -205,7 +205,7 @@ sub BODY_16_XX { #if __ARM_ARCH__<7 sub r3,pc,#8 @ sha256_block_data_order #else - adr r3,sha256_block_data_order + adr r3,. #endif #if __ARM_MAX_ARCH__>=7 && !defined(__KERNEL__) ldr r12,.LOPENSSL_armcap diff --git a/deps/openssl/openssl/crypto/symhacks.h b/deps/openssl/openssl/crypto/symhacks.h index 239fa4fb1b77e7..3001957988f7d5 100644 --- a/deps/openssl/openssl/crypto/symhacks.h +++ b/deps/openssl/openssl/crypto/symhacks.h @@ -280,6 +280,8 @@ # define OPENSSL_add_all_algorithms_conf OPENSSL_add_all_algo_conf # undef EVP_PKEY_meth_set_verify_recover # define EVP_PKEY_meth_set_verify_recover EVP_PKEY_meth_set_vrfy_recover +# undef EVP_PKEY_meth_get_verify_recover +# define EVP_PKEY_meth_get_verify_recover EVP_PKEY_meth_get_vrfy_recover /* Hack some long EC names */ # undef EC_GROUP_set_point_conversion_form diff --git a/deps/openssl/openssl/crypto/x509v3/v3_lib.c b/deps/openssl/openssl/crypto/x509v3/v3_lib.c index 8350429aafbe31..1112802483a1d6 100644 --- a/deps/openssl/openssl/crypto/x509v3/v3_lib.c +++ b/deps/openssl/openssl/crypto/x509v3/v3_lib.c @@ -286,9 +286,9 @@ void *X509V3_get_d2i(STACK_OF(X509_EXTENSION) *x, int nid, int *crit, int X509V3_add1_i2d(STACK_OF(X509_EXTENSION) **x, int nid, void *value, int crit, unsigned long flags) { - int extidx = -1; - int errcode; - X509_EXTENSION *ext, *extmp; + int errcode, extidx = -1; + X509_EXTENSION *ext = NULL, *extmp; + STACK_OF(X509_EXTENSION) *ret = NULL; unsigned long ext_op = flags & X509V3_ADD_OP_MASK; /* @@ -347,13 +347,21 @@ int X509V3_add1_i2d(STACK_OF(X509_EXTENSION) **x, int nid, void *value, return 1; } - if (!*x && !(*x = sk_X509_EXTENSION_new_null())) - return -1; - if (!sk_X509_EXTENSION_push(*x, ext)) - return -1; + if ((ret = *x) == NULL + && (ret = sk_X509_EXTENSION_new_null()) == NULL) + goto m_fail; + if (!sk_X509_EXTENSION_push(ret, ext)) + goto m_fail; + *x = ret; return 1; + m_fail: + if (ret != *x) + sk_X509_EXTENSION_free(ret); + X509_EXTENSION_free(ext); + return -1; + err: if (!(flags & X509V3_ADD_SILENT)) X509V3err(X509V3_F_X509V3_ADD1_I2D, errcode); diff --git a/deps/openssl/openssl/crypto/x509v3/v3_scts.c b/deps/openssl/openssl/crypto/x509v3/v3_scts.c index 0b7c68180e7883..87a6ae1da9822b 100644 --- a/deps/openssl/openssl/crypto/x509v3/v3_scts.c +++ b/deps/openssl/openssl/crypto/x509v3/v3_scts.c @@ -156,7 +156,7 @@ static void timestamp_print(BIO *out, SCT_TIMESTAMP timestamp) gen = ASN1_GENERALIZEDTIME_new(); ASN1_GENERALIZEDTIME_adj(gen, (time_t)0, (int)(timestamp / 86400000), - (timestamp % 86400000) / 1000); + (int)(timestamp % 86400000) / 1000); /* * Note GeneralizedTime from ASN1_GENERALIZETIME_adj is always 15 * characters long with a final Z. Update it with fractional seconds. diff --git a/deps/openssl/openssl/doc/crypto/EVP_EncryptInit.pod b/deps/openssl/openssl/doc/crypto/EVP_EncryptInit.pod index dc9a2d76c5f60c..4cd24d7e0169f6 100644 --- a/deps/openssl/openssl/doc/crypto/EVP_EncryptInit.pod +++ b/deps/openssl/openssl/doc/crypto/EVP_EncryptInit.pod @@ -40,14 +40,14 @@ EVP_aes_128_cbc_hmac_sha256, EVP_aes_256_cbc_hmac_sha256 int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, ENGINE *impl, unsigned char *key, unsigned char *iv); int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, - int *outl, unsigned char *in, int inl); + int *outl, const unsigned char *in, int inl); int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl); int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, ENGINE *impl, unsigned char *key, unsigned char *iv); int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, - int *outl, unsigned char *in, int inl); + int *outl, const unsigned char *in, int inl); int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm, int *outl); diff --git a/deps/openssl/openssl/include/openssl/opensslv.h b/deps/openssl/openssl/include/openssl/opensslv.h index c944d562dae0a9..baee2d0865fd1d 100644 --- a/deps/openssl/openssl/include/openssl/opensslv.h +++ b/deps/openssl/openssl/include/openssl/opensslv.h @@ -30,11 +30,11 @@ extern "C" { * (Prior to 0.9.5a beta1, a different scheme was used: MMNNFFRBB for * major minor fix final patch/beta) */ -# define OPENSSL_VERSION_NUMBER 0x100020dfL +# define OPENSSL_VERSION_NUMBER 0x100020efL # ifdef OPENSSL_FIPS -# define OPENSSL_VERSION_TEXT "OpenSSL 1.0.2m-fips 2 Nov 2017" +# define OPENSSL_VERSION_TEXT "OpenSSL 1.0.2n-fips 7 Dec 2017" # else -# define OPENSSL_VERSION_TEXT "OpenSSL 1.0.2m 2 Nov 2017" +# define OPENSSL_VERSION_TEXT "OpenSSL 1.0.2n 7 Dec 2017" # endif # define OPENSSL_VERSION_PTEXT " part of " OPENSSL_VERSION_TEXT diff --git a/deps/openssl/openssl/include/openssl/ssl.h b/deps/openssl/openssl/include/openssl/ssl.h index 90aeb0ce4e1ead..3cf96a239bab44 100644 --- a/deps/openssl/openssl/include/openssl/ssl.h +++ b/deps/openssl/openssl/include/openssl/ssl.h @@ -1727,7 +1727,7 @@ extern "C" { # define SSL_ST_BEFORE 0x4000 # define SSL_ST_OK 0x03 # define SSL_ST_RENEGOTIATE (0x04|SSL_ST_INIT) -# define SSL_ST_ERR 0x05 +# define SSL_ST_ERR (0x05|SSL_ST_INIT) # define SSL_CB_LOOP 0x01 # define SSL_CB_EXIT 0x02 diff --git a/deps/openssl/openssl/include/openssl/symhacks.h b/deps/openssl/openssl/include/openssl/symhacks.h index 239fa4fb1b77e7..3001957988f7d5 100644 --- a/deps/openssl/openssl/include/openssl/symhacks.h +++ b/deps/openssl/openssl/include/openssl/symhacks.h @@ -280,6 +280,8 @@ # define OPENSSL_add_all_algorithms_conf OPENSSL_add_all_algo_conf # undef EVP_PKEY_meth_set_verify_recover # define EVP_PKEY_meth_set_verify_recover EVP_PKEY_meth_set_vrfy_recover +# undef EVP_PKEY_meth_get_verify_recover +# define EVP_PKEY_meth_get_verify_recover EVP_PKEY_meth_get_vrfy_recover /* Hack some long EC names */ # undef EC_GROUP_set_point_conversion_form diff --git a/deps/openssl/openssl/openssl.spec b/deps/openssl/openssl/openssl.spec index 18e23acc3b8912..b5cb87ecec40c8 100644 --- a/deps/openssl/openssl/openssl.spec +++ b/deps/openssl/openssl/openssl.spec @@ -7,7 +7,7 @@ Release: 1 Summary: Secure Sockets Layer and cryptography libraries and tools Name: openssl -Version: 1.0.2m +Version: 1.0.2n Source0: ftp://ftp.openssl.org/source/%{name}-%{version}.tar.gz License: OpenSSL Group: System Environment/Libraries diff --git a/deps/openssl/openssl/ssl/Makefile b/deps/openssl/openssl/ssl/Makefile index dd1296225006ef..7866a3ccd77bab 100644 --- a/deps/openssl/openssl/ssl/Makefile +++ b/deps/openssl/openssl/ssl/Makefile @@ -15,7 +15,8 @@ KRB5_INCLUDES= CFLAGS= $(INCLUDES) $(CFLAG) GENERAL=Makefile README ssl-lib.com install.com -TEST=ssltest.c heartbeat_test.c clienthellotest.c sslv2conftest.c dtlstest.c bad_dtls_test.c +TEST=ssltest.c heartbeat_test.c clienthellotest.c sslv2conftest.c dtlstest.c \ + bad_dtls_test.c fatalerrtest.c APPS= LIB=$(TOP)/libssl.a diff --git a/deps/openssl/openssl/ssl/bad_dtls_test.c b/deps/openssl/openssl/ssl/bad_dtls_test.c index 70d8578b588317..34af37d9a9f409 100644 --- a/deps/openssl/openssl/ssl/bad_dtls_test.c +++ b/deps/openssl/openssl/ssl/bad_dtls_test.c @@ -590,13 +590,13 @@ static int send_record(BIO *rbio, unsigned char type, unsigned long seqnr, unsigned char *enc; #ifdef SIXTY_FOUR_BIT_LONG - seq[0] = (seqnr >> 40) & 0xff; - seq[1] = (seqnr >> 32) & 0xff; + seq[0] = (unsigned char)(seqnr >> 40); + seq[1] = (unsigned char)(seqnr >> 32); #endif - seq[2] = (seqnr >> 24) & 0xff; - seq[3] = (seqnr >> 16) & 0xff; - seq[4] = (seqnr >> 8) & 0xff; - seq[5] = seqnr & 0xff; + seq[2] = (unsigned char)(seqnr >> 24); + seq[3] = (unsigned char)(seqnr >> 16); + seq[4] = (unsigned char)(seqnr >> 8); + seq[5] = (unsigned char)(seqnr); pad = 15 - ((len + SHA_DIGEST_LENGTH) % 16); enc = OPENSSL_malloc(len + SHA_DIGEST_LENGTH + 1 + pad); @@ -612,8 +612,8 @@ static int send_record(BIO *rbio, unsigned char type, unsigned long seqnr, HMAC_Update(&ctx, seq, 6); HMAC_Update(&ctx, &type, 1); HMAC_Update(&ctx, ver, 2); /* Version */ - lenbytes[0] = len >> 8; - lenbytes[1] = len & 0xff; + lenbytes[0] = (unsigned char)(len >> 8); + lenbytes[1] = (unsigned char)(len); HMAC_Update(&ctx, lenbytes, 2); /* Length */ HMAC_Update(&ctx, enc, len); /* Finally the data itself */ HMAC_Final(&ctx, enc + len, NULL); @@ -637,8 +637,8 @@ static int send_record(BIO *rbio, unsigned char type, unsigned long seqnr, BIO_write(rbio, ver, 2); BIO_write(rbio, epoch, 2); BIO_write(rbio, seq, 6); - lenbytes[0] = (len + sizeof(iv)) >> 8; - lenbytes[1] = (len + sizeof(iv)) & 0xff; + lenbytes[0] = (unsigned char)((len + sizeof(iv)) >> 8); + lenbytes[1] = (unsigned char)(len + sizeof(iv)); BIO_write(rbio, lenbytes, 2); BIO_write(rbio, iv, sizeof(iv)); diff --git a/deps/openssl/openssl/ssl/fatalerrtest.c b/deps/openssl/openssl/ssl/fatalerrtest.c new file mode 100644 index 00000000000000..0288c33fa2eb9c --- /dev/null +++ b/deps/openssl/openssl/ssl/fatalerrtest.c @@ -0,0 +1,109 @@ +/* + * Copyright 2017 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#include +#include +#include "ssltestlib.h" + +int main(int argc, char *argv[]) +{ + SSL_CTX *sctx, *cctx; + SSL *sssl, *cssl; + const char *msg = "Dummy"; + BIO *err = NULL, *wbio = NULL; + int ret = 1, len; + char buf[80]; + unsigned char dummyrec[] = { + 0x17, 0x03, 0x03, 0x00, 0x05, 'D', 'u', 'm', 'm', 'y' + }; + + if (argc != 3) { + printf("Incorrect number of parameters\n"); + return 1; + } + + SSL_library_init(); + SSL_load_error_strings(); + err = BIO_new_fp(stderr, BIO_NOCLOSE | BIO_FP_TEXT); + CRYPTO_malloc_debug_init(); + CRYPTO_set_mem_debug_options(V_CRYPTO_MDEBUG_ALL); + CRYPTO_mem_ctrl(CRYPTO_MEM_CHECK_ON); + + if (!create_ssl_ctx_pair(SSLv23_method(), SSLv23_method(), &sctx, &cctx, + argv[1], argv[2])) { + printf("Failed to create SSL_CTX pair\n"); + goto err; + } + + /* + * Deliberately set the cipher lists for client and server to be different + * to force a handshake failure. + */ + if (!SSL_CTX_set_cipher_list(sctx, "AES128-SHA") + || !SSL_CTX_set_cipher_list(cctx, "AES256-SHA")) { + printf("Failed to set cipher lists\n"); + goto err; + } + + if (!create_ssl_objects(sctx, cctx, &sssl, &cssl, NULL, NULL)) { + printf("Failed to create SSL objectx\n"); + goto err; + } + + wbio = SSL_get_wbio(cssl); + if (wbio == NULL) { + printf("Unexpected NULL bio received\n"); + goto err; + } + + if (create_ssl_connection(sssl, cssl)) { + printf("Unexpected success creating a connection\n"); + goto err; + } + + ERR_clear_error(); + + /* Inject a plaintext record from client to server */ + if (BIO_write(wbio, dummyrec, sizeof(dummyrec)) <= 0) { + printf("Unexpected failure injecting dummy record\n"); + goto err; + } + + /* SSL_read()/SSL_write should fail because of a previous fatal error */ + if ((len = SSL_read(sssl, buf, sizeof(buf - 1))) > 0) { + buf[len] = '\0'; + printf("Unexpected success reading data: %s\n", buf); + goto err; + } + if (SSL_write(sssl, msg, strlen(msg)) > 0) { + printf("Unexpected success writing data\n"); + goto err; + } + + ret = 0; + err: + SSL_free(sssl); + SSL_free(cssl); + SSL_CTX_free(sctx); + SSL_CTX_free(cctx); + ERR_print_errors_fp(stderr); + + if (ret) { + printf("Fatal err test: FAILED\n"); + } + + ERR_free_strings(); + ERR_remove_thread_state(NULL); + EVP_cleanup(); + CRYPTO_cleanup_all_ex_data(); + CRYPTO_mem_leaks(err); + BIO_free(err); + + return ret; +} diff --git a/deps/openssl/openssl/ssl/s23_clnt.c b/deps/openssl/openssl/ssl/s23_clnt.c index 92f41dd549ad64..add8c9916c8feb 100644 --- a/deps/openssl/openssl/ssl/s23_clnt.c +++ b/deps/openssl/openssl/ssl/s23_clnt.c @@ -757,10 +757,12 @@ static int ssl23_get_server_hello(SSL *s) s->version = TLS1_VERSION; s->method = TLSv1_client_method(); break; +#ifndef OPENSSL_NO_SSL3 case SSL3_VERSION: s->version = SSL3_VERSION; s->method = SSLv3_client_method(); break; +#endif } SSLerr(SSL_F_SSL23_GET_SERVER_HELLO, SSL_R_UNSUPPORTED_PROTOCOL); ssl3_send_alert(s, SSL3_AL_FATAL, SSL_AD_PROTOCOL_VERSION); diff --git a/deps/openssl/openssl/ssl/s3_pkt.c b/deps/openssl/openssl/ssl/s3_pkt.c index 04212c51e726d7..b9145684304176 100644 --- a/deps/openssl/openssl/ssl/s3_pkt.c +++ b/deps/openssl/openssl/ssl/s3_pkt.c @@ -1324,10 +1324,16 @@ int ssl3_read_bytes(SSL *s, int type, unsigned char *buf, int len, int peek) } #ifndef OPENSSL_NO_HEARTBEATS else if (rr->type == TLS1_RT_HEARTBEAT) { - tls1_process_heartbeat(s); + i = tls1_process_heartbeat(s); + + if (i < 0) + return i; - /* Exit and notify application to read again */ rr->length = 0; + if (s->mode & SSL_MODE_AUTO_RETRY) + goto start; + + /* Exit and notify application to read again */ s->rwstate = SSL_READING; BIO_clear_retry_flags(SSL_get_rbio(s)); BIO_set_retry_read(SSL_get_rbio(s)); diff --git a/deps/openssl/openssl/ssl/ssl.h b/deps/openssl/openssl/ssl/ssl.h index 90aeb0ce4e1ead..3cf96a239bab44 100644 --- a/deps/openssl/openssl/ssl/ssl.h +++ b/deps/openssl/openssl/ssl/ssl.h @@ -1727,7 +1727,7 @@ extern "C" { # define SSL_ST_BEFORE 0x4000 # define SSL_ST_OK 0x03 # define SSL_ST_RENEGOTIATE (0x04|SSL_ST_INIT) -# define SSL_ST_ERR 0x05 +# define SSL_ST_ERR (0x05|SSL_ST_INIT) # define SSL_CB_LOOP 0x01 # define SSL_CB_EXIT 0x02 diff --git a/deps/openssl/openssl/ssl/ssltest.c b/deps/openssl/openssl/ssl/ssltest.c index 2d6141cd954e00..f6a8f195eeb716 100644 --- a/deps/openssl/openssl/ssl/ssltest.c +++ b/deps/openssl/openssl/ssl/ssltest.c @@ -423,13 +423,13 @@ static unsigned char *next_protos_parse(unsigned short *outlen, OPENSSL_free(out); return NULL; } - out[start] = i - start; + out[start] = (unsigned char)(i - start); start = i + 1; } else out[i + 1] = in[i]; } - *outlen = len + 1; + *outlen = (unsigned char)(len + 1); return out; } @@ -554,6 +554,7 @@ static int cb_ticket2(SSL* s, unsigned char* key_name, unsigned char *iv, EVP_CI { fprintf(stderr, "ticket callback for SNI context should never be called\n"); EXIT(1); + return 0; } #endif diff --git a/deps/openssl/openssl/ssl/t1_lib.c b/deps/openssl/openssl/ssl/t1_lib.c index 6587e8bb685c94..1a4387b78eb93f 100644 --- a/deps/openssl/openssl/ssl/t1_lib.c +++ b/deps/openssl/openssl/ssl/t1_lib.c @@ -1916,7 +1916,7 @@ unsigned char *ssl_add_serverhello_tlsext(SSL *s, unsigned char *buf, s2n(TLSEXT_TYPE_application_layer_protocol_negotiation, ret); s2n(3 + len, ret); s2n(1 + len, ret); - *ret++ = len; + *ret++ = (unsigned char)len; memcpy(ret, selected, len); ret += len; } diff --git a/deps/openssl/openssl/test/Makefile b/deps/openssl/openssl/test/Makefile index a324eeb39ac317..a1f7eeb0ddeb3f 100644 --- a/deps/openssl/openssl/test/Makefile +++ b/deps/openssl/openssl/test/Makefile @@ -73,6 +73,7 @@ CLIENTHELLOTEST= clienthellotest BADDTLSTEST= bad_dtls_test SSLV2CONFTEST = sslv2conftest DTLSTEST = dtlstest +FATALERRTEST = fatalerrtest TESTS= alltests @@ -87,7 +88,7 @@ EXE= $(BNTEST)$(EXE_EXT) $(ECTEST)$(EXE_EXT) $(ECDSATEST)$(EXE_EXT) $(ECDHTEST) $(ASN1TEST)$(EXE_EXT) $(V3NAMETEST)$(EXE_EXT) $(HEARTBEATTEST)$(EXE_EXT) \ $(CONSTTIMETEST)$(EXE_EXT) $(VERIFYEXTRATEST)$(EXE_EXT) \ $(CLIENTHELLOTEST)$(EXE_EXT) $(SSLV2CONFTEST)$(EXE_EXT) $(DTLSTEST)$(EXE_EXT) \ - $(BADDTLSTEST)$(EXE_EXT) + $(BADDTLSTEST)$(EXE_EXT) $(FATALERRTEST)$(EXE_EXT) # $(METHTEST)$(EXE_EXT) @@ -102,7 +103,7 @@ OBJ= $(BNTEST).o $(ECTEST).o $(ECDSATEST).o $(ECDHTEST).o $(IDEATEST).o \ $(EVPTEST).o $(EVPEXTRATEST).o $(IGETEST).o $(JPAKETEST).o $(ASN1TEST).o $(V3NAMETEST).o \ $(HEARTBEATTEST).o $(CONSTTIMETEST).o $(VERIFYEXTRATEST).o \ $(CLIENTHELLOTEST).o $(SSLV2CONFTEST).o $(DTLSTEST).o ssltestlib.o \ - $(BADDTLSTEST).o + $(BADDTLSTEST).o $(FATALERRTEST).o SRC= $(BNTEST).c $(ECTEST).c $(ECDSATEST).c $(ECDHTEST).c $(IDEATEST).c \ $(MD2TEST).c $(MD4TEST).c $(MD5TEST).c \ @@ -114,7 +115,7 @@ SRC= $(BNTEST).c $(ECTEST).c $(ECDSATEST).c $(ECDHTEST).c $(IDEATEST).c \ $(EVPTEST).c $(EVPEXTRATEST).c $(IGETEST).c $(JPAKETEST).c $(SRPTEST).c $(ASN1TEST).c \ $(V3NAMETEST).c $(HEARTBEATTEST).c $(CONSTTIMETEST).c $(VERIFYEXTRATEST).c \ $(CLIENTHELLOTEST).c $(SSLV2CONFTEST).c $(DTLSTEST).c ssltestlib.c \ - $(BADDTLSTEST).c + $(BADDTLSTEST).c $(FATALERRTEST).c EXHEADER= HEADER= testutil.h ssltestlib.h $(EXHEADER) @@ -159,7 +160,7 @@ alltests: \ test_ss test_ca test_engine test_evp test_evp_extra test_ssl test_tsa test_ige \ test_jpake test_srp test_cms test_ocsp test_v3name test_heartbeat \ test_constant_time test_verify_extra test_clienthello test_sslv2conftest \ - test_dtls test_bad_dtls + test_dtls test_bad_dtls test_fatalerr test_evp: $(EVPTEST)$(EXE_EXT) evptests.txt ../util/shlib_wrap.sh ./$(EVPTEST) evptests.txt @@ -373,6 +374,10 @@ test_bad_dtls: $(BADDTLSTEST)$(EXE_EXT) @echo $(START) $@ ../util/shlib_wrap.sh ./$(BADDTLSTEST) +test_fatalerr: $(FATALERRTEST)$(EXE_EXT) + @echo $(START) $@ + ../util/shlib_wrap.sh ./$(FATALERRTEST) ../apps/server.pem ../apps/server.pem + test_sslv2conftest: $(SSLV2CONFTEST)$(EXE_EXT) @echo $(START) $@ ../util/shlib_wrap.sh ./$(SSLV2CONFTEST) @@ -561,6 +566,9 @@ $(CLIENTHELLOTEST)$(EXE_EXT): $(CLIENTHELLOTEST).o $(BADDTLSTEST)$(EXE_EXT): $(BADDTLSTEST).o @target=$(BADDTLSTEST) $(BUILD_CMD) +$(FATALERRTEST)$(EXE_EXT): $(FATALERRTEST).o ssltestlib.o $(DLIBSSL) $(DLIBCRYPTO) + @target=$(FATALERRTEST); exobj=ssltestlib.o; $(BUILD_CMD) + $(SSLV2CONFTEST)$(EXE_EXT): $(SSLV2CONFTEST).o @target=$(SSLV2CONFTEST) $(BUILD_CMD) @@ -776,6 +784,25 @@ exptest.o: ../include/openssl/opensslconf.h ../include/openssl/opensslv.h exptest.o: ../include/openssl/ossl_typ.h ../include/openssl/rand.h exptest.o: ../include/openssl/safestack.h ../include/openssl/stack.h exptest.o: ../include/openssl/symhacks.h exptest.c +fatalerrtest.o: ../include/openssl/asn1.h ../include/openssl/bio.h +fatalerrtest.o: ../include/openssl/buffer.h ../include/openssl/comp.h +fatalerrtest.o: ../include/openssl/crypto.h ../include/openssl/dtls1.h +fatalerrtest.o: ../include/openssl/e_os2.h ../include/openssl/ec.h +fatalerrtest.o: ../include/openssl/ecdh.h ../include/openssl/ecdsa.h +fatalerrtest.o: ../include/openssl/err.h ../include/openssl/evp.h +fatalerrtest.o: ../include/openssl/hmac.h ../include/openssl/kssl.h +fatalerrtest.o: ../include/openssl/lhash.h ../include/openssl/obj_mac.h +fatalerrtest.o: ../include/openssl/objects.h ../include/openssl/opensslconf.h +fatalerrtest.o: ../include/openssl/opensslv.h ../include/openssl/ossl_typ.h +fatalerrtest.o: ../include/openssl/pem.h ../include/openssl/pem2.h +fatalerrtest.o: ../include/openssl/pkcs7.h ../include/openssl/pqueue.h +fatalerrtest.o: ../include/openssl/safestack.h ../include/openssl/sha.h +fatalerrtest.o: ../include/openssl/srtp.h ../include/openssl/ssl.h +fatalerrtest.o: ../include/openssl/ssl2.h ../include/openssl/ssl23.h +fatalerrtest.o: ../include/openssl/ssl3.h ../include/openssl/stack.h +fatalerrtest.o: ../include/openssl/symhacks.h ../include/openssl/tls1.h +fatalerrtest.o: ../include/openssl/x509.h ../include/openssl/x509_vfy.h +fatalerrtest.o: fatalerrtest.c ssltestlib.h heartbeat_test.o: ../e_os.h ../include/openssl/asn1.h ../include/openssl/bio.h heartbeat_test.o: ../include/openssl/buffer.h ../include/openssl/comp.h heartbeat_test.o: ../include/openssl/crypto.h ../include/openssl/dsa.h diff --git a/deps/openssl/openssl/util/copy-if-different.pl b/deps/openssl/openssl/util/copy-if-different.pl index e1245f54aff120..5420f3f2bd3041 100644 --- a/deps/openssl/openssl/util/copy-if-different.pl +++ b/deps/openssl/openssl/util/copy-if-different.pl @@ -12,7 +12,8 @@ foreach my $arg (@ARGV) { $arg =~ s|\\|/|g; # compensate for bug/feature in cygwin glob... - foreach (glob qq("$arg")) + $arg = qq("$arg") if ($arg =~ /\s/); # compensate for bug in 5.10... + foreach (glob $arg) { push @filelist, $_; } diff --git a/deps/openssl/openssl/util/copy.pl b/deps/openssl/openssl/util/copy.pl index a6b2a54ec6f06a..9c0e68c41467ab 100644 --- a/deps/openssl/openssl/util/copy.pl +++ b/deps/openssl/openssl/util/copy.pl @@ -19,7 +19,8 @@ next; } $arg =~ s|\\|/|g; # compensate for bug/feature in cygwin glob... - foreach (glob qq("$arg")) + $arg = qq("$arg") if ($arg =~ /\s/); # compensate for bug in 5.10... + foreach (glob $arg) { push @filelist, $_; } diff --git a/deps/openssl/openssl/util/libeay.num b/deps/openssl/openssl/util/libeay.num index fddfe1cbb2024f..f5b4f215098ed5 100755 --- a/deps/openssl/openssl/util/libeay.num +++ b/deps/openssl/openssl/util/libeay.num @@ -4417,7 +4417,8 @@ EC_GROUP_get_mont_data 4772 EXIST::FUNCTION:EC i2d_re_X509_tbs 4773 EXIST::FUNCTION: EVP_PKEY_asn1_set_item 4774 EXIST::FUNCTION: EVP_PKEY_meth_get_init 4775 EXIST::FUNCTION: -EVP_PKEY_meth_get_verify_recover 4776 EXIST::FUNCTION: +EVP_PKEY_meth_get_verify_recover 4776 EXIST:!VMS:FUNCTION: +EVP_PKEY_meth_get_vrfy_recover 4776 EXIST:VMS:FUNCTION: EVP_PKEY_meth_get_keygen 4777 EXIST::FUNCTION: EVP_PKEY_meth_get_derive 4778 EXIST::FUNCTION: EVP_PKEY_meth_get_verifyctx 4779 EXIST::FUNCTION: diff --git a/deps/openssl/openssl/util/mk1mf.pl b/deps/openssl/openssl/util/mk1mf.pl index 6b31496ed1e694..ee14dafea6e0ec 100755 --- a/deps/openssl/openssl/util/mk1mf.pl +++ b/deps/openssl/openssl/util/mk1mf.pl @@ -765,7 +765,7 @@ sub fix_asm { $t=&bname($_); $tt="\$(OBJ_D)${o}$t${obj}"; - $tt.=" \$(OBJ_D)${o}ssltestlib${obj}" if $t eq "dtlstest"; + $tt.=" \$(OBJ_D)${o}ssltestlib${obj}" if $t eq "dtlstest" or $t eq "fatalerrtest"; $rules.=&do_link_rule("\$(TEST_D)$o$t$exep",$tt,"\$(LIBS_DEP)","\$(L_LIBS) \$(EX_LIBS)"); } diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 0dc86f1c0080fc..4a54a25bfc990e 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -510,6 +510,11 @@ console.log(buf2.toString()); ### Class Method: Buffer.alloc(size[, fill[, encoding]]) * `size` {integer} The desired length of the new `Buffer`. @@ -1253,6 +1258,19 @@ Example: Fill a `Buffer` with a two-byte character console.log(Buffer.allocUnsafe(3).fill('\u0222')); ``` +If `value` contains invalid characters, it is truncated; if no valid +fill data remains, no filling is performed: + +```js +const buf = Buffer.allocUnsafe(5); +// Prints: +console.log(buf.fill('a')); +// Prints: +console.log(buf.fill('aazz', 'hex')); +// Prints: +console.log(buf.fill('zz', 'hex')); +``` + ### buf.includes(value[, byteOffset][, encoding]) + +* `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) + +The `'close'` event is emitted when the `Http2Stream` is destroyed. Once +this event is emitted, the `Http2Stream` instance is no longer usable. + +The listener callback is passed a single argument specifying the HTTP/2 error +code specified when closing the stream. If the code is any value other than +`NGHTTP2_NO_ERROR` (`0`), an `'error'` event will also be emitted. + #### Event: 'error' - -The `'streamClosed'` event is emitted when the `Http2Stream` is destroyed. Once -this event is emitted, the `Http2Stream` instance is no longer usable. - -The listener callback is passed a single argument specifying the HTTP/2 error -code specified when closing the stream. If the code is any value other than -`NGHTTP2_NO_ERROR` (`0`), an `'error'` event will also be emitted. - #### Event: 'timeout' * `options` {Object} * `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size for deflating header fields. **Default:** `4Kib` + * `maxHeaderListPairs` {number} Sets the maximum number of header entries. + **Default:** `128`. The minimum value is `4`. + * `maxOutstandingPings` {number} Sets the maximum number of outstanding, + unacknowledged pings. The default is `10`. * `maxSendHeaderBlockLength` {number} Sets the maximum allowed size for a serialized, compressed block of headers. Attempts to send headers that exceed this limit will result in a `'frameError'` event being emitted @@ -1525,6 +1540,15 @@ server.listen(80); ### http2.createSecureServer(options[, onRequestHandler]) * `options` {Object} @@ -1533,6 +1557,10 @@ added: v8.4.0 `false`. See the [`'unknownProtocol'`][] event. See [ALPN negotiation][]. * `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size for deflating header fields. **Default:** `4Kib` + * `maxHeaderListPairs` {number} Sets the maximum number of header entries. + **Default:** `128`. The minimum value is `4`. + * `maxOutstandingPings` {number} Sets the maximum number of outstanding, + unacknowledged pings. The default is `10`. * `maxSendHeaderBlockLength` {number} Sets the maximum allowed size for a serialized, compressed block of headers. Attempts to send headers that exceed this limit will result in a `'frameError'` event being emitted @@ -1590,12 +1618,25 @@ server.listen(80); ### http2.connect(authority[, options][, listener]) * `authority` {string|URL} * `options` {Object} * `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size for deflating header fields. **Default:** `4Kib` + * `maxHeaderListPairs` {number} Sets the maximum number of header entries. + **Default:** `128`. The minimum value is `1`. + * `maxOutstandingPings` {number} Sets the maximum number of outstanding, + unacknowledged pings. The default is `10`. * `maxReservedRemoteStreams` {number} Sets the maximum number of reserved push streams the client will accept at any given time. Once the current number of currently reserved push streams exceeds reaches this limit, new push streams @@ -1747,7 +1788,13 @@ server.on('stream', (stream, headers) => { ``` ### Settings Object - + The `http2.getDefaultSettings()`, `http2.getPackedSettings()`, `http2.createServer()`, `http2.createSecureServer()`, `http2session.settings()`, `http2session.localSettings`, and @@ -1773,8 +1820,8 @@ properties. concurrently at any given time in an `Http2Session`. The minimum value is 0. The maximum allowed value is 231-1. * `maxHeaderListSize` {number} Specifies the maximum size (uncompressed octets) - of header list that will be accepted. There is no default value. The minimum - allowed value is 0. The maximum allowed value is 232-1. + of header list that will be accepted. The minimum allowed value is 0. The + maximum allowed value is 232-1. **Default:** 65535. All additional properties on the settings object are ignored. diff --git a/doc/changelogs/CHANGELOG_V8.md b/doc/changelogs/CHANGELOG_V8.md index 4c4c0ae447f705..82354ee7e7a8b1 100644 --- a/doc/changelogs/CHANGELOG_V8.md +++ b/doc/changelogs/CHANGELOG_V8.md @@ -7,6 +7,7 @@ +8.9.3
8.9.2
8.9.1
8.9.0
@@ -45,6 +46,46 @@ [Node.js Long Term Support Plan](https://github.com/nodejs/LTS) and will be supported actively until April 2019 and maintained until December 2019. + +## 2017-12-08, Version 8.9.3 'Carbon' (LTS), @MylesBorins + +This is a security release. All Node.js users should consult the security release summary at https://nodejs.org/en/blog/vulnerability/december-2017-security-releases/ for details on patched vulnerabilities. + +Fixes for the following CVEs are included in this release: + +* CVE-2017-15896 +* CVE-2017-15897 +* CVE-2017-3738 (from the openssl project) + +### Notable Changes + +* **buffer**: + * buffer allocated with an invalid content will now be zero filled (Anna Henningsen) [#17428](https://github.com/nodejs/node/pull/17428) +* **deps**: + * openssl updated to 1.0.2n (Shigeki Ohtsu) [#17526](https://github.com/nodejs/node/pull/17526) + +### Commits + +* [[`b05ef978d3`](https://github.com/nodejs/node/commit/b05ef978d3)] - **buffer**: zero-fill buffer allocated with invalid content (Anna Henningsen) [#17428](https://github.com/nodejs/node/pull/17428) +* [[`18652b6860`](https://github.com/nodejs/node/commit/18652b6860)] - **deps**: update openssl asm and asm_obsolete files (Shigeki Ohtsu) [#17526](https://github.com/nodejs/node/pull/17526) +* [[`e6c308e237`](https://github.com/nodejs/node/commit/e6c308e237)] - **deps**: add -no_rand_screen to openssl s_client (Shigeki Ohtsu) [nodejs/io.js#1836](https://github.com/nodejs/io.js/pull/1836) +* [[`a85f94bd59`](https://github.com/nodejs/node/commit/a85f94bd59)] - **deps**: fix asm build error of openssl in x86_win32 (Shigeki Ohtsu) [iojs/io.js#1389](https://github.com/iojs/io.js/pull/1389) +* [[`b5552c854c`](https://github.com/nodejs/node/commit/b5552c854c)] - **deps**: fix openssl assembly error on ia32 win32 (Fedor Indutny) [iojs/io.js#1389](https://github.com/iojs/io.js/pull/1389) +* [[`afad1f23a2`](https://github.com/nodejs/node/commit/afad1f23a2)] - **deps**: copy all openssl header files to include dir (Shigeki Ohtsu) [#17526](https://github.com/nodejs/node/pull/17526) +* [[`9fdd3bddf5`](https://github.com/nodejs/node/commit/9fdd3bddf5)] - **deps**: upgrade openssl sources to 1.0.2n (Shigeki Ohtsu) [#17526](https://github.com/nodejs/node/pull/17526) +* [[`db09f245bf`](https://github.com/nodejs/node/commit/db09f245bf)] - **doc**: warn against filling buffer with invalid data (Anna Henningsen) [#17428](https://github.com/nodejs/node/pull/17428) +* [[`42f09ed461`](https://github.com/nodejs/node/commit/42f09ed461)] - **http2**: use correct connect event for TLS Socket (James M Snell) [#17328](https://github.com/nodejs/node/pull/17328) +* [[`aba3544b50`](https://github.com/nodejs/node/commit/aba3544b50)] - **http2**: use 'close' event instead of 'streamClosed' (James M Snell) [#17328](https://github.com/nodejs/node/pull/17328) +* [[`bd035d75bd`](https://github.com/nodejs/node/commit/bd035d75bd)] - **http2**: general cleanups in core.js (James M Snell) [#17209](https://github.com/nodejs/node/pull/17209) +* [[`a5e3ba2cb3`](https://github.com/nodejs/node/commit/a5e3ba2cb3)] - **http2**: major update to internals (James M Snell) [#17105](https://github.com/nodejs/node/pull/17105) +* [[`d7f37cebed`](https://github.com/nodejs/node/commit/d7f37cebed)] - **http2**: simplify subsequent rstStream calls (Anatoli Papirovski) [#16753](https://github.com/nodejs/node/pull/16753) +* [[`22ee960775`](https://github.com/nodejs/node/commit/22ee960775)] - **http2**: refactor multiple internals (James M Snell) [#16676](https://github.com/nodejs/node/pull/16676) +* [[`319beaf45b`](https://github.com/nodejs/node/commit/319beaf45b)] - **http2**: allocate on every chunk send (James M Snell) [#16669](https://github.com/nodejs/node/pull/16669) +* [[`7d68488524`](https://github.com/nodejs/node/commit/7d68488524)] - **openssl**: fix keypress requirement in apps on win32 (Shigeki Ohtsu) [iojs/io.js#1389](https://github.com/iojs/io.js/pull/1389) +* [[`8e8fac29de`](https://github.com/nodejs/node/commit/8e8fac29de)] - **src**: fix -Winconsistent-missing-override warning (Ben Noordhuis) [#16726](https://github.com/nodejs/node/pull/16726) +* [[`26b43c87ee`](https://github.com/nodejs/node/commit/26b43c87ee)] - **src**: add method to compute storage in WriteWrap (Anna Henningsen) [#16727](https://github.com/nodejs/node/pull/16727) +* [[`99d775ca07`](https://github.com/nodejs/node/commit/99d775ca07)] - **test**: fix flaky test-http2-create-client-connect (David Benjamin) [#16130](https://github.com/nodejs/node/pull/16130) + ## 2017-12-05, Version 8.9.2 'Carbon' (LTS), @gibfahn diff --git a/lib/buffer.js b/lib/buffer.js index 251b24cceede42..6d28e84bc7ff3d 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -238,7 +238,9 @@ Buffer.alloc = function(size, fill, encoding) { // be interpreted as a start offset. if (typeof encoding !== 'string') encoding = undefined; - return createUnsafeBuffer(size).fill(fill, encoding); + const ret = createUnsafeBuffer(size); + if (fill_(ret, fill, encoding) > 0) + return ret; } return new FastBuffer(size); }; @@ -796,15 +798,20 @@ Buffer.prototype.includes = function includes(val, byteOffset, encoding) { // buffer.fill(buffer[, offset[, end]]) // buffer.fill(string[, offset[, end]][, encoding]) Buffer.prototype.fill = function fill(val, start, end, encoding) { + fill_(this, val, start, end, encoding); + return this; +}; + +function fill_(buf, val, start, end, encoding) { // Handle string cases: if (typeof val === 'string') { if (typeof start === 'string') { encoding = start; start = 0; - end = this.length; + end = buf.length; } else if (typeof end === 'string') { encoding = end; - end = this.length; + end = buf.length; } if (encoding !== undefined && typeof encoding !== 'string') { @@ -832,19 +839,17 @@ Buffer.prototype.fill = function fill(val, start, end, encoding) { } // Invalid ranges are not set to a default, so can range check early. - if (start < 0 || end > this.length) + if (start < 0 || end > buf.length) throw new RangeError('Out of range index'); if (end <= start) - return this; + return 0; start = start >>> 0; - end = end === undefined ? this.length : end >>> 0; + end = end === undefined ? buf.length : end >>> 0; - binding.fill(this, val, start, end, encoding); - - return this; -}; + return binding.fill(buf, val, start, end, encoding); +} Buffer.prototype.write = function(string, offset, length, encoding) { diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 86782b0762c543..0939f8f33354f0 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -204,6 +204,8 @@ E('ERR_HTTP2_OUT_OF_STREAMS', 'No stream ID is available because maximum stream ID has been reached'); E('ERR_HTTP2_PAYLOAD_FORBIDDEN', (code) => `Responses with ${code} 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/compat.js b/lib/internal/http2/compat.js index 89a22270086418..5ddde39d387785 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -250,7 +250,7 @@ class Http2ServerRequest extends Readable { stream.on('close', onStreamClosedRequest); stream.on('aborted', onStreamAbortedRequest); const onfinish = this[kFinish].bind(this); - stream.on('streamClosed', onfinish); + stream.on('close', onfinish); stream.on('finish', onfinish); this.on('pause', onRequestPause); this.on('resume', onRequestResume); @@ -383,7 +383,7 @@ class Http2ServerResponse extends Stream { stream.on('close', onStreamClosedResponse); stream.on('aborted', onStreamAbortedResponse); const onfinish = this[kFinish].bind(this); - stream.on('streamClosed', onfinish); + stream.on('close', onfinish); stream.on('finish', onfinish); } diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index a6dbd626ec882f..adfda207abf211 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; @@ -77,6 +79,7 @@ const kServer = Symbol('server'); const kSession = Symbol('session'); const kState = Symbol('state'); const kType = Symbol('type'); +const kUpdateTimer = Symbol('update-timer'); const kDefaultSocketTimeout = 2 * 60 * 1000; const kRenegTest = /TLS session renegotiation disabled for this socket/; @@ -100,7 +103,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,12 +147,12 @@ 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); - debug(`[${sessionName(type)}] headers were received on ` + - `stream ${id}: ${cat}`); + owner[kUpdateTimer](); + debug(`Http2Stream ${id} [Http2Session ` + + `${sessionName(type)}]: headers received`); const streams = owner[kState].streams; const endOfStream = !!(flags & NGHTTP2_FLAG_END_STREAM); @@ -160,11 +162,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 +173,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); @@ -196,7 +197,8 @@ function onSessionHeaders(id, cat, flags, headers) { } else { event = endOfStream ? 'trailers' : 'headers'; } - debug(`[${sessionName(type)}] emitting stream '${event}' event`); + debug(`Http2Stream ${id} [Http2Session ` + + `${sessionName(type)}]: emitting stream '${event}' event`); process.nextTick(emit, stream, event, obj, flags, headers); } if (endOfStream) { @@ -210,16 +212,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); @@ -230,28 +224,17 @@ function onSessionTrailers(id) { return headersList; } -// Called when the stream is closed. The streamClosed event is emitted on the -// 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 +// Called when the stream is closed. The close event is emitted on the +// Http2Stream instance +function onStreamClose(code) { + const stream = this[kOwner]; + stream[kUpdateTimer](); 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)); } @@ -264,32 +247,21 @@ function afterFDClose(err) { // Called when an error event needs to be triggered function onSessionError(error) { const owner = this[kOwner]; - _unrefActive(owner); + owner[kUpdateTimer](); process.nextTick(emit, owner, 'error', 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]; + stream[kUpdateTimer](); 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); } @@ -298,8 +270,8 @@ function onSessionRead(nread, buf, handle) { // Resets the cached settings. function onSettings(ack) { const owner = this[kOwner]; - debug(`[${sessionName(owner[kType])}] new settings received`); - _unrefActive(owner); + debug(`Http2Session ${sessionName(owner[kType])}: new settings received`); + owner[kUpdateTimer](); let event = 'remoteSettings'; if (ack) { if (owner[kState].pendingAck > 0) @@ -310,11 +282,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 @@ -322,10 +291,10 @@ function onSettings(ack) { // session (which may, in turn, forward it on to the server) function onPriority(id, parent, weight, exclusive) { const owner = this[kOwner]; - debug(`[${sessionName(owner[kType])}] priority advisement for stream ` + - `${id}: \n parent: ${parent},\n weight: ${weight},\n` + - ` exclusive: ${exclusive}`); - _unrefActive(owner); + debug(`Http2Stream ${id} [Http2Session ` + + `${sessionName(owner[kType])}]: priority [parent: ${parent}, ` + + `weight: ${weight}, exclusive: ${exclusive}]`); + owner[kUpdateTimer](); const streams = owner[kState].streams; const stream = streams.get(id); const emitter = stream === undefined ? owner : stream; @@ -344,9 +313,9 @@ function emitFrameError(self, id, type, code) { // frame. This should be exceedingly rare. function onFrameError(id, type, code) { const owner = this[kOwner]; - debug(`[${sessionName(owner[kType])}] error sending frame type ` + + debug(`Http2Session ${sessionName(owner[kType])}: error sending frame type ` + `${type} on stream ${id}, code: ${code}`); - _unrefActive(owner); + owner[kUpdateTimer](); const streams = owner[kState].streams; const stream = streams.get(id); const emitter = stream !== undefined ? stream : owner; @@ -369,7 +338,8 @@ function emitGoaway(self, code, lastStreamID, buf) { // Called by the native layer when a goaway frame has been received function onGoawayData(code, lastStreamID, buf) { const owner = this[kOwner]; - debug(`[${sessionName(owner[kType])}] goaway data received`); + debug(`Http2Session ${sessionName(owner[kType])}: goaway ${code} received ` + + `[last stream id: ${lastStreamID}]`); process.nextTick(emitGoaway, owner, code, lastStreamID, buf); } @@ -379,7 +349,6 @@ function onGoawayData(code, lastStreamID, buf) { // frameLen <= n <= maxPayloadLen. function onSelectPadding(fn) { return function getPadding() { - debug('fetching padding for frame'); const frameLen = paddingBuffer[PADDING_BUF_FRAME_LENGTH]; const maxFramePayloadLen = paddingBuffer[PADDING_BUF_MAX_PAYLOAD_LENGTH]; paddingBuffer[PADDING_BUF_RETURN_VALUE] = @@ -395,7 +364,8 @@ function onSelectPadding(fn) { // will be deferred until the socket is ready to go. function requestOnConnect(headers, options) { const session = this[kSession]; - debug(`[${sessionName(session[kType])}] connected.. initializing request`); + debug(`Http2Session ${sessionName(session[kType])}: connected, ` + + 'initializing request'); const streams = session[kState].streams; validatePriorityOptions(options); @@ -417,15 +387,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 +402,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,25 +467,29 @@ 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 // of the socket. No other code should read from or write to the socket. function setupHandle(session, socket, type, options) { return function() { - debug(`[${sessionName(type)}] setting up session handle`); + debug(`Http2Session ${sessionName(type)}: setting up session handle`); session[kState].connecting = false; 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; @@ -545,137 +514,97 @@ function setupHandle(session, socket, type, options) { // Submits a SETTINGS frame to be sent to the remote peer. function submitSettings(settings) { const type = this[kType]; - debug(`[${sessionName(type)}] submitting actual settings`); - _unrefActive(this); + debug(`Http2Session ${sessionName(type)}: submitting settings`); + this[kUpdateTimer](); 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); - } - } - debug(`[${sessionName(type)}] settings complete`); + this[kHandle].settings(); } // 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`); - _unrefActive(this); - - 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`); +function submitPriority(options) { + this[kUpdateTimer](); + + // If the parent is the id, do nothing because a + // stream cannot be made to depend on itself. + if (options.parent === this[kID]) + return; + + 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`); - _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(); +function submitRstStream(code) { + this[kUpdateTimer](); + + const state = this[kState]; + if (state.rst) return; + state.rst = true; + state.rstCode = code; + + 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(`Http2Session ${sessionName(this[kType])}: shutdown failed`); 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); } // 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) { + debug(`Http2Session ${sessionName(type)}: submitting shutdown request`); + 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 +649,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(); @@ -773,7 +705,9 @@ class Http2Session extends EventEmitter { const setupFn = setupHandle(this, socket, type, options); if (socket.connecting) { this[kState].connecting = true; - socket.once('connect', setupFn); + const connectEvent = + socket instanceof tls.TLSSocket ? 'secureConnect' : 'connect'; + socket.once(connectEvent, setupFn); } else { setupFn(); } @@ -785,8 +719,41 @@ 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); - debug(`[${sessionName(type)}] http2session created`); + this.setMaxListeners(kMaxStreams); + debug(`Http2Session ${sessionName(type)}: created`); + } + + [kUpdateTimer]() { + _unrefActive(this); + } + + 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) { @@ -830,9 +797,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 +809,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 +824,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 +851,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); @@ -901,102 +866,22 @@ class Http2Session extends EventEmitter { throw new errors.Error('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK', this[kState].pendingAck); } - debug(`[${sessionName(this[kType])}] sending settings`); + debug(`Http2Session ${sessionName(this[kType])}: sending settings`); state.pendingAck++; if (state.connecting) { - debug(`[${sessionName(this[kType])}] session still connecting, ` + - 'queue settings'); this.once('connect', submitSettings.bind(this, settings)); return; } 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, do not call again, - // skip straight to destroy - stream.destroy(); - 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]; if (state.destroyed || state.destroying) return; - debug(`[${sessionName(this[kType])}] destroying nghttp2session`); + debug(`Http2Session ${sessionName(this[kType])}: destroying`); state.destroying = true; state.destroyed = false; @@ -1017,7 +902,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 @@ -1041,7 +926,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); @@ -1067,7 +952,7 @@ class Http2Session extends EventEmitter { options.lastStreamID); } - debug(`[${sessionName(type)}] initiating shutdown`); + debug(`Http2Session ${sessionName(type)}: initiating shutdown`); state.shuttingDown = true; if (callback) { @@ -1075,13 +960,11 @@ class Http2Session extends EventEmitter { } if (state.connecting) { - debug(`[${sessionName(type)}] session still connecting, queue ` + - 'shutdown'); this.once('connect', submitShutdown.bind(this, options)); return; } - debug(`[${sessionName(type)}] sending shutdown`); + debug(`Http2Session ${sessionName(type)}: sending shutdown`); submitShutdown.call(this, options); } @@ -1097,7 +980,7 @@ class Http2Session extends EventEmitter { handle.chunksSentSinceLastWrite : null; if (chunksSentSinceLastWrite !== null && chunksSentSinceLastWrite !== handle.updateChunksSent()) { - _unrefActive(this); + this[kUpdateTimer](); return; } } @@ -1120,7 +1003,6 @@ class ServerHttp2Session extends Http2Session { class ClientHttp2Session extends Http2Session { constructor(options, socket) { super(NGHTTP2_SESSION_CLIENT, options, socket); - debug(`[${sessionName(this[kType])}] clienthttp2session created`); } // Submits a new HTTP2 request to the connected peer. Returns the @@ -1129,13 +1011,15 @@ class ClientHttp2Session extends Http2Session { const state = this[kState]; if (state.destroyed || state.destroying) throw new errors.Error('ERR_HTTP2_INVALID_SESSION'); - debug(`[${sessionName(this[kType])}] initiating request`); - _unrefActive(this); + debug(`Http2Session ${sessionName(this[kType])}: initiating request`); + + this[kUpdateTimer](); + assertIsObject(headers, 'headers'); assertIsObject(options, 'options'); headers = Object.assign(Object.create(null), headers); - options = Object.assign(Object.create(null), options); + options = Object.assign({}, options); if (headers[HTTP2_HEADER_METHOD] === undefined) headers[HTTP2_HEADER_METHOD] = HTTP2_METHOD_GET; @@ -1178,21 +1062,16 @@ class ClientHttp2Session extends Http2Session { options.getTrailers); } - const stream = new ClientHttp2Stream(this, undefined, {}); - - const onConnect = requestOnConnect.bind(stream, headers, options); + const stream = new ClientHttp2Stream(this, undefined, undefined, {}); // Close the writable side of the stream if options.endStream is set. if (options.endStream) stream.end(); + const onConnect = requestOnConnect.bind(stream, headers, options); if (state.connecting) { - debug(`[${sessionName(this[kType])}] session still connecting, queue ` + - 'stream init'); stream.on('connect', onConnect); } else { - debug(`[${sessionName(this[kType])}] session connected, immediate ` + - 'stream init'); onConnect(); } return stream; @@ -1229,55 +1108,40 @@ function trackWriteState(stream, bytes) { } function afterDoStreamWrite(status, handle, req) { - const session = handle[kOwner]; - _unrefActive(session); + const stream = handle[kOwner]; + const session = stream[kSession]; + + stream[kUpdateTimer](); - 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) + 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(); -} - -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() { @@ -1285,43 +1149,24 @@ 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() { const session = this[kSession]; - debug(`[${sessionName(session[kType])}] session connected. emiting stream ` + - 'connect'); + debug(`Http2Session ${sessionName(session[kType])}: session connected`); this[kState].connecting = false; process.nextTick(emit, this, 'connect'); } -function streamOnceReady() { - const session = this[kSession]; - debug(`[${sessionName(session[kType])}] stream ${this[kID]} is ready`); - this.uncork(); -} - function abort(stream) { if (!stream[kState].aborted && !(stream._writableState.ended || stream._writableState.ending)) { @@ -1330,16 +1175,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; @@ -1353,24 +1196,32 @@ class Http2Stream extends Duplex { writeQueueSize: 0 }; - this.once('ready', streamOnceReady); - this.once('streamClosed', onStreamClosed); this.once('finish', onHandleFinish); this.on('resume', streamOnResume); this.on('pause', streamOnPause); session.once('close', state.closeHandler); if (session[kState].connecting) { - debug(`[${sessionName(session[kType])}] session is still connecting, ` + - 'queuing stream init'); state.connecting = true; session.once('connect', streamOnSessionConnect.bind(this)); } - debug(`[${sessionName(session[kType])}] http2stream created`); } - [kInit](id) { + [kUpdateTimer]() { + _unrefActive(this); + if (this[kSession]) + _unrefActive(this[kSession]); + } + + [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.uncork(); this.emit('ready'); } @@ -1407,8 +1258,7 @@ class Http2Stream extends Duplex { handle.chunksSentSinceLastWrite : null; if (chunksSentSinceLastWrite !== null && chunksSentSinceLastWrite !== handle.updateChunksSent()) { - _unrefActive(this); - _unrefActive(this[kSession]); + this[kUpdateTimer](); return; } } @@ -1435,8 +1285,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]() { @@ -1450,12 +1300,13 @@ class Http2Stream extends Duplex { this.once('ready', this._write.bind(this, data, encoding, cb)); return; } + + this[kUpdateTimer](); + 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; @@ -1473,12 +1324,13 @@ class Http2Stream extends Duplex { this.once('ready', this._writev.bind(this, data, cb)); return; } + + this[kUpdateTimer](); + 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; @@ -1498,16 +1350,12 @@ class Http2Stream extends Duplex { } _read(nread) { - if (this[kID] === undefined) { - this.once('ready', this._read.bind(this, nread)); - return; - } if (this.destroyed) { this.push(null); return; } - _unrefActive(this); - process.nextTick(handleFlushData, this[kSession][kHandle], this[kID]); + if (this[kHandle] !== undefined) + process.nextTick(handleFlushData, this[kHandle]); } // Submits an RST-STREAM frame to shutdown this stream. @@ -1516,19 +1364,17 @@ 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 (code < 0 || code > 2 ** 32 - 1) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'code'); + + const fn = submitRstStream.bind(this, code); if (this[kID] === undefined) { - debug( - `[${sessionName(session[kType])}] queuing rstStream for new stream`); - this.once('ready', this.rstStream.bind(this, code)); + this.once('ready', fn); return; } - debug(`[${sessionName(session[kType])}] sending rstStream for stream ` + - `${this[kID]}: ${code}`); - _unrefActive(this); - session.rstStream(this, code); + fn(); } rstWithNoError() { @@ -1551,25 +1397,20 @@ 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'); - const session = this[kSession]; + + assertIsObject(options, 'options'); + options = Object.assign({}, options); + validatePriorityOptions(options); + + const fn = submitPriority.bind(this, options); if (this[kID] === undefined) { - debug(`[${sessionName(session[kType])}] queuing priority for new stream`); - this.once('ready', this.priority.bind(this, options)); + this.once('ready', fn); return; } - debug(`[${sessionName(session[kType])}] sending priority for stream ` + - `${this[kID]}`); - _unrefActive(this); - session.priority(this, options); + fn(); } // Called by this.destroy(). @@ -1581,11 +1422,13 @@ class Http2Stream extends Duplex { _destroy(err, callback) { const session = this[kSession]; if (this[kID] === undefined) { - debug(`[${sessionName(session[kType])}] queuing destroy for new stream`); this.once('ready', this._destroy.bind(this, err, callback)); return; } + debug(`Http2Stream ${this[kID]} [Http2Session ` + + `${sessionName(session[kType])}]: destroying stream`); + const state = this[kState]; session[kState].writeQueueSize -= state.writeQueueSize; state.writeQueueSize = 0; @@ -1595,15 +1438,13 @@ 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]; - - debug(`[${sessionName(session[kType])}] destroying stream ${self[kID]}`); +function continueStreamDestroy(err, callback) { + const session = this[kSession]; + const state = this[kState]; // Submit RST-STREAM frame if one hasn't been sent already and the // stream hasn't closed normally... @@ -1611,16 +1452,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 @@ -1629,17 +1470,22 @@ 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`); + abort(this); + this.push(null); // Close the readable side + this.end(); // Close the writable side + process.nextTick(emit, this, 'close', code); } -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) { @@ -1665,32 +1511,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) { @@ -1809,18 +1648,18 @@ function afterOpen(session, options, headers, streamOptions, err, fd) { function streamOnError(err) { // we swallow the error for parity with HTTP1 // all the errors that ends here are not critical for the project - debug('ServerHttp2Stream errored, avoiding uncaughtException', err); + debug(`Http2Stream ${this[kID]} [Http2Session ` + + `${this[kSession][kType]}: error`, 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); - debug(`[${sessionName(session[kType])}] created serverhttp2stream`); } // true if the HEADERS frame has been sent @@ -1840,16 +1679,14 @@ class ServerHttp2Stream extends Http2Stream { throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); const session = this[kSession]; - debug(`[${sessionName(session[kType])}] initiating push stream for stream` + - ` ${this[kID]}`); - - _unrefActive(this); - const state = session[kState]; - const streams = state.streams; - if (!session.remoteSettings.enablePush) throw new errors.Error('ERR_HTTP2_PUSH_DISABLED'); + debug(`Http2Stream ${this[kID]} [Http2Session ` + + `${sessionName(session[kType])}]: initiating push stream`); + + this[kUpdateTimer](); + if (typeof options === 'function') { callback = options; options = undefined; @@ -1859,7 +1696,7 @@ class ServerHttp2Stream extends Http2Stream { throw new errors.TypeError('ERR_INVALID_CALLBACK'); assertIsObject(options, 'options'); - options = Object.assign(Object.create(null), options); + options = Object.assign({}, options); options.endStream = !!options.endStream; assertIsObject(headers, 'headers'); @@ -1875,58 +1712,45 @@ class ServerHttp2Stream extends Http2Stream { headers[HTTP2_HEADER_PATH] = '/'; let headRequest = false; - if (headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) { - headRequest = true; - options.endStream = true; - } + if (headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) + headRequest = options.endStream = true; + options.readable = !options.endStream; const headersList = mapToHeaders(headers); - if (!Array.isArray(headersList)) { - // An error occurred! + if (!Array.isArray(headersList)) throw headersList; - } 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; - - const stream = new ServerHttp2Stream(session, ret, options, headers); - - // 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; - } - - streams.set(ret, stream); - process.nextTick(callback, stream, headers, 0); + } + process.nextTick(emit, this, 'error', err); + return; } + + const id = ret.id(); + const stream = new ServerHttp2Stream(session, ret, id, options, headers); + session[kState].streams.set(id, stream); + + if (options.endStream) + stream.end(); + + if (headRequest) + stream[kState].headRequest = true; + + process.nextTick(callback, stream, headers, 0); } // Initiate a response on this Http2Stream @@ -1934,16 +1758,16 @@ class ServerHttp2Stream extends Http2Stream { const session = this[kSession]; if (this.destroyed) throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); - debug(`[${sessionName(session[kType])}] initiating response for stream ` + - `${this[kID]}`); - _unrefActive(this); + debug(`Http2Stream ${this[kID]} [Http2Session ` + + `${sessionName(session[kType])}]: initiating response`); + this[kUpdateTimer](); const state = this[kState]; if (state.headersSent) throw new errors.Error('ERR_HTTP2_HEADERS_SENT'); assertIsObject(options, 'options'); - options = Object.assign(Object.create(null), options); + options = Object.assign({}, options); options.endStream = !!options.endStream; let streamOptions = 0; @@ -1974,30 +1798,19 @@ class ServerHttp2Stream extends Http2Stream { } const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse); - if (!Array.isArray(headersList)) { - // An error occurred! + if (!Array.isArray(headersList)) throw headersList; - } + state.headersSent = true; // Close the writable side if the endStream option is set 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); } } @@ -2011,9 +1824,9 @@ class ServerHttp2Stream extends Http2Stream { const session = this[kSession]; if (this.destroyed) throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); - debug(`[${sessionName(session[kType])}] initiating response for stream ` + - `${this[kID]}`); - _unrefActive(this); + debug(`Http2Stream ${this[kID]} [Http2Session ` + + `${sessionName(session[kType])}]: initiating response`); + this[kUpdateTimer](); const state = this[kState]; if (state.headersSent) @@ -2094,9 +1907,9 @@ class ServerHttp2Stream extends Http2Stream { const session = this[kSession]; if (this.destroyed) throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); - debug(`[${sessionName(session[kType])}] initiating response for stream ` + - `${this[kID]}`); - _unrefActive(this); + debug(`Http2Stream ${this[kID]} [Http2Session ` + + `${sessionName(session[kType])}]: initiating response`); + this[kUpdateTimer](); const state = this[kState]; if (state.headersSent) @@ -2161,7 +1974,8 @@ class ServerHttp2Stream extends Http2Stream { throw new errors.Error('ERR_HTTP2_HEADERS_AFTER_RESPOND'); const session = this[kSession]; - debug(`[${sessionName(session[kType])}] sending additional headers`); + debug(`Http2Stream ${this[kID]} [Http2Session ` + + `${sessionName(session[kType])}]: sending additional headers`); assertIsObject(headers, 'headers'); headers = Object.assign(Object.create(null), headers); @@ -2175,7 +1989,7 @@ class ServerHttp2Stream extends Http2Stream { } } - _unrefActive(this); + this[kUpdateTimer](); const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse); @@ -2183,18 +1997,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); } } } @@ -2202,13 +2008,12 @@ 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`); } } @@ -2237,7 +2042,7 @@ const setTimeout = { } } else { enroll(this, msecs); - _unrefActive(this); + this[kUpdateTimer](); if (callback !== undefined) { if (typeof callback !== 'function') throw new errors.TypeError('ERR_INVALID_CALLBACK'); @@ -2257,13 +2062,12 @@ Object.defineProperty(Http2Session.prototype, 'setTimeout', setTimeout); function socketDestroy(error) { const session = this[kSession]; const type = session[kType]; - debug(`[${sessionName(type)}] socket destroy called`); + debug(`Http2Session ${sessionName(type)}: socket destroy called`); delete this[kServer]; // destroy the session first so that it will stop trying to // send data while we close the socket. session.destroy(); this.destroy = this[kDestroySocket]; - debug(`[${sessionName(type)}] destroying the socket`); this.destroy(error); } @@ -2272,7 +2076,8 @@ function socketDestroy(error) { // a sessionError; failing that, destroy, remove the error listener, and // re-emit the error event function sessionOnError(error) { - debug(`[${sessionName(this[kType])}] server session error: ${error.message}`); + debug(`Http2Session ${sessionName(this[kType])}: session error: ` + + `${error.message}`); if (this[kServer] !== undefined && this[kServer].emit('sessionError', error)) return; if (this[kSocket] !== undefined && this[kSocket].emit('sessionError', error)) @@ -2287,7 +2092,7 @@ function sessionOnError(error) { function socketOnError(error) { const session = this[kSession]; const type = session && session[kType]; - debug(`[${sessionName(type)}] server socket error: ${error.message}`); + debug(`Http2Session ${sessionName(type)}: socket error: ${error.message}`); if (kRenegTest.test(error.message)) return this.destroy(); if (session !== undefined && @@ -2300,12 +2105,11 @@ function socketOnError(error) { // Handles the on('stream') event for a session and forwards // it on to the server object. function sessionOnStream(stream, headers, flags, rawHeaders) { - debug(`[${sessionName(this[kType])}] emit server stream event`); this[kServer].emit('stream', stream, headers, flags, rawHeaders); } function sessionOnPriority(stream, parent, weight, exclusive) { - debug(`[${sessionName(this[kType])}] priority change received`); + debug(`Http2Session ${sessionName(this[kType])}: priority change received`); this[kServer].emit('priority', stream, parent, weight, exclusive); } @@ -2316,7 +2120,6 @@ function sessionOnSocketError(error, socket) { // When the session times out on the server, attempt a graceful shutdown function sessionOnTimeout() { - debug('session timeout'); process.nextTick(() => { const state = this[kState]; // if destroyed or destryoing, do nothing @@ -2340,7 +2143,7 @@ function sessionOnTimeout() { } function connectionListener(socket) { - debug('[server] received a connection'); + debug('Http2Session server: received a connection'); const options = this[kOptions] || {}; if (socket.alpnProtocol === false || socket.alpnProtocol === 'http/1.1') { @@ -2409,7 +2212,6 @@ class Http2SecureServer extends TLSServer { if (typeof requestListener === 'function') this.on('request', requestListener); this.on('tlsClientError', onErrorSecureServerSession); - debug('http2secureserver created'); } setTimeout(msecs, callback) { @@ -2431,7 +2233,6 @@ class Http2Server extends NETServer { this.on('newListener', setupCompat); if (typeof requestListener === 'function') this.on('request', requestListener); - debug('http2server created'); } setTimeout(msecs, callback) { @@ -2447,13 +2248,12 @@ class Http2Server extends NETServer { function setupCompat(ev) { if (ev === 'request') { - debug('setting up compatibility handler'); this.removeListener('newListener', setupCompat); this.on('stream', onServerStream); } } -function socketOnClose(hadError) { +function socketOnClose() { const session = this[kSession]; if (session !== undefined && !session.destroyed) { // Skip unconsume because the socket is destroyed. @@ -2465,7 +2265,8 @@ 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(`Http2Session ${sessionName(this[kType])}]: session error: ` + + `${error.message}`); if (this[kSocket] !== undefined && this[kSocket].emit('sessionError', error)) return; this.destroy(); @@ -2487,8 +2288,6 @@ function connect(authority, options, listener) { assertIsObject(authority, 'authority', ['string', 'object', 'URL']); - debug(`connecting to ${authority}`); - const protocol = authority.protocol || options.protocol || 'https:'; const port = '' + (authority.port !== '' ? authority.port : (authority.protocol === 'http:' ? 80 : 443)); @@ -2542,7 +2341,6 @@ function createSecureServer(options, handler) { 'options', 'object'); } - debug('creating http2secureserver'); return new Http2SecureServer(options, handler); } @@ -2551,7 +2349,6 @@ function createServer(options, handler) { handler = options; options = Object.create(null); } - debug('creating htt2pserver'); return new Http2Server(options, handler); } @@ -2572,7 +2369,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); @@ -2588,9 +2385,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'); @@ -2638,7 +2435,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 03d0639624083d..1800dc5cff33ff 100644 --- a/lib/internal/http2/util.js +++ b/lib/internal/http2/util.js @@ -172,7 +172,9 @@ const IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS = 1; 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_FLAGS = 5; +const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5; +const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6; +const IDX_OPTIONS_FLAGS = 7; function updateOptionsBuffer(options) { var flags = 0; @@ -201,6 +203,16 @@ function updateOptionsBuffer(options) { optionsBuffer[IDX_OPTIONS_PADDING_STRATEGY] = options.paddingStrategy; } + if (typeof options.maxHeaderListPairs === 'number') { + flags |= (1 << IDX_OPTIONS_MAX_HEADER_LIST_PAIRS); + 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; } @@ -253,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) { @@ -310,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/node.gyp b/node.gyp index 3486d5d21bc269..df531768c63300 100644 --- a/node.gyp +++ b/node.gyp @@ -242,8 +242,6 @@ 'src/js_stream.h', 'src/module_wrap.h', 'src/node.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', diff --git a/src/async-wrap.h b/src/async-wrap.h index ca87f3a3bcf147..9510e26577357d 100644 --- a/src/async-wrap.h +++ b/src/async-wrap.h @@ -42,7 +42,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.h b/src/env.h index 74bc48d9b97d68..5f424a39fe5b74 100644 --- a/src/env.h +++ b/src/env.h @@ -311,6 +311,8 @@ class ModuleWrap; V(context, v8::Context) \ V(domain_array, v8::Array) \ 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) \ diff --git a/src/node_buffer.cc b/src/node_buffer.cc index b39bbbb5d28265..b93ec413d90b77 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -591,6 +591,8 @@ void Fill(const FunctionCallbackInfo& args) { THROW_AND_RETURN_IF_OOB(start <= end); THROW_AND_RETURN_IF_OOB(fill_length + start <= ts_obj_length); + args.GetReturnValue().Set(static_cast(fill_length)); + // First check if Buffer has been passed. if (Buffer::HasInstance(args[1])) { SPREAD_BUFFER_ARG(args[1], fill_obj); @@ -612,8 +614,10 @@ void Fill(const FunctionCallbackInfo& args) { enc == UTF8 ? str_obj->Utf8Length() : enc == UCS2 ? str_obj->Length() * sizeof(uint16_t) : str_obj->Length(); - if (str_length == 0) + if (str_length == 0) { + args.GetReturnValue().Set(0); return; + } // Can't use StringBytes::Write() in all cases. For example if attempting // to write a two byte character into a one byte Buffer. @@ -643,7 +647,7 @@ void Fill(const FunctionCallbackInfo& args) { // TODO(trevnorris): Should this throw? Because of the string length was // greater than 0 but couldn't be written then the string was invalid. if (str_length == 0) - return; + return args.GetReturnValue().Set(0); } start_fill: diff --git a/src/node_http2.cc b/src/node_http2.cc index c8aa9e0729d4f7..dca2dde1f3142d 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -5,6 +5,7 @@ #include "node_http2_state.h" #include +#include namespace node { @@ -13,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; @@ -20,12 +23,11 @@ using v8::Undefined; namespace http2 { -Freelist stream_free_list; - -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_); @@ -67,8 +69,17 @@ Http2Options::Http2Options(Environment* env) { buffer.GetValue(IDX_OPTIONS_PADDING_STRATEGY)); SetPaddingStrategy(strategy); } + + 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 = @@ -79,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++; @@ -87,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++; @@ -95,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++; @@ -103,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++; @@ -111,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++; @@ -119,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++; @@ -128,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(); @@ -141,6 +153,7 @@ inline Local Http2Settings::Pack() { return Undefined(env_->isolate()); } + inline void Http2Settings::Update(Environment* env, Http2Session* session, get_setting fn) { @@ -173,13 +186,17 @@ inline void Http2Settings::RefreshDefaults(Environment* env) { DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE; buffer[IDX_SETTINGS_MAX_FRAME_SIZE] = DEFAULT_SETTINGS_MAX_FRAME_SIZE; + buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] = + DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE; buffer[IDX_SETTINGS_COUNT] = (1 << IDX_SETTINGS_HEADER_TABLE_SIZE) | (1 << IDX_SETTINGS_ENABLE_PUSH) | (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE) | - (1 << IDX_SETTINGS_MAX_FRAME_SIZE); + (1 << IDX_SETTINGS_MAX_FRAME_SIZE) | + (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE); } + Http2Priority::Http2Priority(Environment* env, Local parent, Local weight, @@ -193,24 +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(); - Init(type, *opts); + bool hasGetPaddingCallback = + padding_strategy_ == PADDING_STRATEGY_MAX || + padding_strategy_ == PADDING_STRATEGY_CALLBACK; + + 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(); +} + - // 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. +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); @@ -224,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(); +} + + +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; } -ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen, - size_t maxPayloadLen) { - DEBUG_HTTP2("Http2Session: using max frame size padding\n"); + +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 @@ -272,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); -} -// 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(); +// 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); - 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); + if (ret < 0) + return ret; + + total += ret; + } + // Send any data that was queued up while processing the received data. + SendPendingData(); + return total; } -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 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; +} - 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; - } - nghttp2_stream* str = - nghttp2_session_find_stream(s, stream->id()); - 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; +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); + + Http2Stream* stream = session->FindStream(id); + if (stream == nullptr) { + new Http2Stream(session, id, frame->headers.cat); } 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); + stream->StartHeaders(frame->headers.cat); } + 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::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; } -// 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::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; } -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); - Local context = env->context(); - - bool skipUnconsume = args[0]->BooleanValue(context).ToChecked(); - if (!skipUnconsume) - session->Unconsume(); - session->Close(); +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::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(); -} -void Http2Session::SubmitPriority(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); +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; +} + + +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; +} + + +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; +} + + +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; +} + + +inline void Http2Session::GetTrailers(Http2Stream* stream, uint32_t* flags) { + if (stream->HasTrailers()) { + Http2Stream::SubmitTrailers submit_trailers{this, stream, flags}; + stream->OnTrailers(submit_trailers); + } +} + + +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); +} + + +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; +} + - 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); - Nghttp2Stream* stream; - if (!(stream = session->FindStream(id))) { - // invalid stream - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } +// Implementation of the JavaScript API - args.GetReturnValue().Set(stream->SubmitPriority(*priority, silent)); +void HttpErrorString(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + uint32_t val = args[0]->Uint32Value(env->context()).ToChecked(); + args.GetReturnValue().Set( + String::NewFromOneByte( + env->isolate(), + reinterpret_cast(nghttp2_strerror(val)), + v8::NewStringType::kInternalized).ToLocalChecked()); } -void Http2Session::SubmitSettings(const FunctionCallbackInfo& args) { - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Environment* env = session->env(); +// 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( - session->Nghttp2Session::SubmitSettings(*settings, settings.length())); + 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(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); + 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); } - DEBUG_HTTP2("Http2Session: sending rst_stream for stream %d, code: %d\n", - id, code); - args.GetReturnValue().Set(stream->SubmitRstStream(code)); + args.GetReturnValue().Set(true); + DEBUG_HTTP2SESSION2(session, "set next stream id to %d", id); } -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 +template +void Http2Session::RefreshSettings(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(); - - 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); - - 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); + Http2Settings::Update(env, session, fn); + DEBUG_HTTP2SESSION(session, "settings refreshed for session"); } -void Http2Session::SubmitResponse(const FunctionCallbackInfo& args) { -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); - CHECK(args[1]->IsArray()); -#endif +void Http2Session::RefreshState(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(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, "refreshing state"); - int32_t id = args[0]->Int32Value(context).ToChecked(); - Local headers = args[1].As(); - int options = args[2]->IntegerValue(context).ToChecked(); + AliasedBuffer& buffer = + env->http2_state()->session_state_buffer; - DEBUG_HTTP2("Http2Session: submitting response for stream %d: headers: %d, " - "options: %d\n", id, headers->Length(), options); + nghttp2_session* s = **session; - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } + 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); +} - Headers list(isolate, context, headers); - args.GetReturnValue().Set( - stream->SubmitResponse(*list, list.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"); } -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::Consume(const FunctionCallbackInfo& args) { Http2Session* session; - Nghttp2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + CHECK(args[0]->IsExternal()); + session->Consume(args[0].As()); +} + +void Http2Session::Destroy(const FunctionCallbackInfo& 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, "destroying session"); - int32_t id = args[0]->Int32Value(context).ToChecked(); - int fd = args[1]->Int32Value(context).ToChecked(); - Local headers = args[2].As(); + Environment* env = Environment::GetCurrent(args); + Local context = env->context(); - int64_t offset = args[3]->IntegerValue(context).ToChecked(); - int64_t length = args[4]->IntegerValue(context).ToChecked(); - int options = args[5]->IntegerValue(context).ToChecked(); + bool skipUnconsume = args[0]->BooleanValue(context).ToChecked(); -#if defined(DEBUG) && DEBUG - CHECK_GE(offset, 0); -#endif + if (!skipUnconsume) + session->Unconsume(); + session->Close(); +} - 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); - } +void Http2Session::Destroying(const FunctionCallbackInfo& args) { + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->MarkDestroying(); + DEBUG_HTTP2SESSION(session, "preparing to destroy session"); +} - session->chunks_sent_since_last_write_ = 0; - Headers list(isolate, context, headers); +void Http2Session::Settings(const FunctionCallbackInfo& args) { + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Environment* env = session->env(); - 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(); @@ -712,58 +1778,25 @@ 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); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); uint32_t length = session->chunks_sent_since_last_write_; @@ -774,422 +1807,259 @@ void Http2Session::UpdateChunksSent(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(length); } -void Http2Session::SubmitPushPromise(const FunctionCallbackInfo& args) { - Http2Session* session; + +void Http2Stream::RstStream(const FunctionCallbackInfo& args) { 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); + 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); } -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; -} +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()); -void Http2Session::AllocateSend(uv_buf_t* buf) { - buf->base = stream_alloc(); - buf->len = kAllocBufferSize; -} + Local headers = args[0].As(); + int options = args[1]->IntegerValue(context).ToChecked(); -void Http2Session::Send(uv_buf_t* buf, size_t length) { - DEBUG_HTTP2("Http2Session: Attempting to send data\n"); - if (stream_ == nullptr || !stream_->IsAlive() || stream_->IsClosing()) { - return; - } - HandleScope scope(env()->isolate()); - auto AfterWrite = [](WriteWrap* req_wrap, int status) { - req_wrap->Dispose(); - }; - Local req_wrap_obj = - env()->write_wrap_constructor_function() - ->NewInstance(env()->context()).ToLocalChecked(); - WriteWrap* write_req = WriteWrap::New(env(), - req_wrap_obj, - this, - AfterWrite); + Headers list(isolate, context, headers); - chunks_sent_since_last_write_++; - uv_buf_t actual = uv_buf_init(buf->base, length); - if (stream_->DoWrite(write_req, &actual, 1, nullptr)) { - write_req->Dispose(); - } + args.GetReturnValue().Set( + stream->SubmitResponse(*list, list.length(), options)); + DEBUG_HTTP2STREAM(stream, "response submitted"); } -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, - std::queue* headers, - 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; +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()); - Local holder = Array::New(isolate); - Local fn = env()->push_values_to_array_function(); - Local argv[NODE_PUSH_VAL_TO_ARRAY_MAX * 2]; + int fd = args[0]->Int32Value(context).ToChecked(); + Local headers = args[1].As(); -#if defined(DEBUG) && DEBUG - CHECK_LE(cat, NGHTTP2_HCAT_HEADERS); -#endif + int64_t offset = args[2]->IntegerValue(context).ToChecked(); + int64_t length = args[3]->IntegerValue(context).ToChecked(); + int options = args[4]->IntegerValue(context).ToChecked(); - // 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). - do { - size_t j = 0; - while (!headers->empty() && j < arraysize(argv) / 2) { - nghttp2_header item = headers->front(); - // 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; - headers->pop(); - 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(); - } - } while (!headers->empty()); + stream->session()->SetChunksSinceLastWrite(); - Local args[4] = { - Integer::New(isolate, stream->id()), - Integer::New(isolate, cat), - Integer::New(isolate, flags), - holder - }; - MakeCallback(env()->onheaders_string(), arraysize(args), args); + 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::OnStreamClose(int32_t id, uint32_t code) { - Isolate* isolate = env()->isolate(); - Local context = env()->context(); - HandleScope scope(isolate); - Context::Scope context_scope(context); +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 argv[2] = { - Integer::New(isolate, id), - Integer::NewFromUnsigned(isolate, code) - }; - MakeCallback(env()->onstreamclose_string(), arraysize(argv), argv); -} + Local headers = args[0].As(); -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); + Headers list(isolate, context, headers); + args.GetReturnValue().Set(stream->SubmitInfo(*list, list.length())); + DEBUG_HTTP2STREAM2(stream, "%d informational headers sent", + headers->Length()); } -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::GetID(const FunctionCallbackInfo& args) { + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + args.GetReturnValue().Set(stream->id()); } -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::Destroy(const FunctionCallbackInfo& args) { + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + DEBUG_HTTP2STREAM(stream, "destroying stream"); + stream->Destroy(); } -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::FlushData(const FunctionCallbackInfo& args) { + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + stream->ReadStart(); + DEBUG_HTTP2STREAM(stream, "data flushed to js"); } -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) - }; - if (length > 0) { - argv[2] = Buffer::Copy(isolate, - reinterpret_cast(data), - length).ToLocalChecked(); - } +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()); - MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv); -} + Local headers = args[0].As(); + int options = args[1]->IntegerValue(context).ToChecked(); -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; -} + Headers list(isolate, context, headers); + DEBUG_HTTP2STREAM(parent, "creating push promise"); -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); - } + 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, @@ -1232,60 +2102,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(); @@ -1322,7 +2190,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); @@ -1373,8 +2240,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 770bf28075e5db..ff9597bbf0c785 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 @@ -291,13 +414,10 @@ const char* nghttp2_errname(int rv) { } } -#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 MAX_MAX_FRAME_SIZE 16777215 -#define MIN_MAX_FRAME_SIZE DEFAULT_SETTINGS_MAX_FRAME_SIZE -#define MAX_INITIAL_WINDOW_SIZE 2147483647 +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); @@ -306,6 +426,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 @@ -323,20 +444,35 @@ class Http2Options { return options_; } + void SetMaxHeaderPairs(uint32_t max) { + max_header_pairs_ = max; + } + + uint32_t GetMaxHeaderPairs() const { + return max_header_pairs_; + } + void SetPaddingStrategy(padding_strategy_type val) { -#if DEBUG - CHECK_LE(val, PADDING_STRATEGY_CALLBACK); -#endif padding_strategy_ = static_cast(val); } - padding_strategy_type GetPaddingStrategy() { + padding_strategy_type GetPaddingStrategy() const { 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 @@ -383,80 +519,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); - ssize_t OnCallbackPadding(size_t frame, - size_t maxPayloadLen); + 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; - bool HasGetPaddingCallback() override { - return padding_strategy_ == PADDING_STRATEGY_MAX || - padding_strategy_ == PADDING_STRATEGY_CALLBACK; + // Required for StreamBase + int ReadStop() override; + + // Required for StreamBase + int DoShutdown(ShutdownWrap* req_wrap); + + // 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); + + // Shutdown the writable side of the stream + inline void Shutdown(); + + // Destroy this stream instance and free all held memory. + inline void Destroy(); + + inline bool IsDestroyed() const { + return flags_ & NGHTTP2_STREAM_FLAG_DESTROYED; } - ssize_t GetPadding(size_t frameLength, size_t maxPayloadLen) override { - if (padding_strategy_ == PADDING_STRATEGY_MAX) { - return OnMaxFrameSizePadding(frameLength, maxPayloadLen); + inline bool IsWritable() const { + return !(flags_ & NGHTTP2_STREAM_FLAG_SHUT); + } + + 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, - std::queue* headers, - nghttp2_headers_category cat, - uint8_t flags) override; - void OnStreamClose(int32_t id, uint32_t code) override; - void Send(uv_buf_t* bufs, size_t total) 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 AllocateSend(uv_buf_t* buf) 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_; } + + inline bool AddHeader(nghttp2_rcbuf* name, + nghttp2_rcbuf* value, + uint8_t flags); - AsyncWrap* GetAsyncWrap() override { - return static_cast(this); + inline nghttp2_header* headers() { + return current_headers_.data(); } - void* Cast() override { - return reinterpret_cast(this); + 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); + // Required for StreamBase bool IsAlive() override { return true; @@ -467,47 +656,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); @@ -515,17 +872,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_; @@ -533,9 +998,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 5fc0b629ccb205..00000000000000 --- a/src/node_http2_core-inl.h +++ /dev/null @@ -1,920 +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 -#include "freelist.h" - -namespace node { -namespace http2 { - -#define FREELIST_MAX 10240 - -// Instances of Nghttp2Stream are created and pooled in order to speed -// allocation under load. -extern Freelist stream_free_list; - -#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) { - Nghttp2Stream::Init(id, handle, frame->headers.cat); - } else { - stream->StartHeaders(frame->headers.cat); - } - return 0; -} - -// 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); - // The header name and value are stored in a reference counted buffer - // provided to us by nghttp2. We need to increment the reference counter - // here, then decrement it when we're done using it later. - nghttp2_rcbuf_incref(name); - nghttp2_rcbuf_incref(value); - nghttp2_header header; - header.name = name; - header.value = value; - stream->headers()->emplace(header); - 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_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; - - uv_buf_t dest; - AllocateSend(&dest); - size_t destLength = 0; // amount of data stored in dest - size_t destRemaining = dest.len; // amount space remaining 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) { - 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.base + destOffset, src + srcOffset, destRemaining); - destLength += destRemaining; - Send(&dest, destLength); - destOffset = 0; - destLength = 0; - srcRemaining -= destRemaining; - srcOffset += destRemaining; - destRemaining = dest.len; - } - - if (srcRemaining > 0) { - memcpy(dest.base + 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(&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) { - session_type_ = type; - DEBUG_HTTP2("Nghttp2Session %s: initializing session\n", TypeName()); - destroying_ = false; - - 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 - -inline Nghttp2Stream* Nghttp2Stream::Init( - int32_t id, - Nghttp2Session* session, - nghttp2_headers_category category, - int options) { - DEBUG_HTTP2("Nghttp2Stream %d: initializing stream\n", id); - Nghttp2Stream* stream = stream_free_list.pop(); - stream->ResetState(id, session, category, options); - session->AddStream(stream); - return stream; -} - - -// Resets the state of the stream instance to defaults -inline void Nghttp2Stream::ResetState( - int32_t id, - Nghttp2Session* session, - nghttp2_headers_category category, - int options) { - DEBUG_HTTP2("Nghttp2Stream %d: resetting stream state\n", id); - session_ = session; - while (!queue_.empty()) { - nghttp2_stream_write* head = queue_.front(); - delete head; - queue_.pop(); - } - while (!data_chunks_.empty()) - data_chunks_.pop(); - while (!current_headers_.empty()) - current_headers_.pop(); - current_headers_category_ = category; - flags_ = NGHTTP2_STREAM_FLAG_NONE; - id_ = id; - code_ = NGHTTP2_NO_ERROR; - prev_local_window_size_ = 65535; - queue_index_ = 0; - queue_offset_ = 0; - getTrailers_ = options & STREAM_OPTION_GET_TRAILERS; -} - - -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) { - // Remove this stream from the associated session - session_->RemoveStream(this->id()); - session_ = nullptr; - } - - // Free any remaining incoming data chunks. - while (!data_chunks_.empty()) - 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(); - } - - // Free any remaining headers - while (!current_headers_.empty()) - current_headers_.pop(); - - // Return this stream instance to the freelist - stream_free_list.push(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 = Nghttp2Stream::Init(ret, session_); - if (options & STREAM_OPTION_EMPTY_PAYLOAD) - stream->Shutdown(); - 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) { - Nghttp2Stream* stream = Nghttp2Stream::Init(ret, this, - NGHTTP2_HCAT_HEADERS, - options); - if (options & STREAM_OPTION_EMPTY_PAYLOAD) - stream->Shutdown(); - 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 2e885b73dab8f5..00000000000000 --- a/src/node_http2_core.h +++ /dev/null @@ -1,503 +0,0 @@ -#ifndef SRC_NODE_HTTP2_CORE_H_ -#define SRC_NODE_HTTP2_CORE_H_ - -#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS - -#include "util-inl.h" -#include "uv.h" -#include "nghttp2/nghttp2.h" - -#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 - -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; -}; - -// 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); - - // Frees this session instance - inline ~Nghttp2Session(); - inline void MarkDestroying(); - bool IsDestroying() { - return destroying_; - } - - 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 Send(uv_buf_t* buf, size_t length) {} - virtual void OnHeaders( - Nghttp2Stream* stream, - std::queue* headers, - 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; } - virtual void AllocateSend(uv_buf_t* buf) = 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) {} - - inline void SendPendingData(); - - 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_; - std::unordered_map streams_; - bool destroying_ = false; - - friend class Nghttp2Stream; -}; - - - -class Nghttp2Stream { - public: - static inline Nghttp2Stream* Init( - int32_t id, - Nghttp2Session* session, - nghttp2_headers_category category = NGHTTP2_HCAT_HEADERS, - int options = 0); - - inline ~Nghttp2Stream() { -#if defined(DEBUG) && DEBUG - CHECK_EQ(session_, nullptr); -#endif - DEBUG_HTTP2("Nghttp2Stream %d: freed\n", id_); - } - - inline void FlushDataChunks(); - - // Resets the state of the stream instance to defaults - inline void ResetState( - int32_t id, - Nghttp2Session* session, - nghttp2_headers_category category = NGHTTP2_HCAT_HEADERS, - int options = 0); - - // 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. - // - // Also note: this does not actually destroy the instance. - // instead, it frees the held memory, removes the stream - // from the parent session, and returns the instance to - // the FreeList so that it can be reused. - 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 std::queue* headers() { - return ¤t_headers_; - } - - inline nghttp2_headers_category headers_category() const { - return current_headers_category_; - } - - void StartHeaders(nghttp2_headers_category category) { - DEBUG_HTTP2("Nghttp2Stream %d: starting headers, category: %d\n", - id_, category); - // We shouldn't be in the middle of a headers block already. - // Something bad happened if this fails -#if defined(DEBUG) && DEBUG - CHECK(current_headers_.empty()); -#endif - current_headers_category_ = category; - } - - private: - // The Parent HTTP/2 Session - Nghttp2Session* session_ = nullptr; - - // The Stream Identifier - int32_t id_ = 0; - - // Internal state flags - int flags_ = 0; - - // 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; - - // 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; - std::queue current_headers_; - - // Inbound Data... This is the data received via DATA frames for this stream. - std::queue data_chunks_; - - // The RST_STREAM code used to close this stream - int32_t code_ = NGHTTP2_NO_ERROR; - - int32_t prev_local_window_size_ = 65535; - - // True if this stream will have outbound trailers - bool getTrailers_ = false; - - 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 a945f07b686b4a..a7ad23fb519886 100755 --- a/src/node_http2_state.h +++ b/src/node_http2_state.h @@ -47,6 +47,8 @@ namespace http2 { IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH, 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_version.h b/src/node_version.h index f615681cc3bb09..e06cb2bced4031 100644 --- a/src/node_version.h +++ b/src/node_version.h @@ -29,7 +29,7 @@ #define NODE_VERSION_IS_LTS 1 #define NODE_VERSION_LTS_CODENAME "Carbon" -#define NODE_VERSION_IS_RELEASE 0 +#define NODE_VERSION_IS_RELEASE 1 #ifndef NODE_STRINGIFY #define NODE_STRINGIFY(n) NODE_STRINGIFY_HELPER(n) diff --git a/src/stream_base-inl.h b/src/stream_base-inl.h index ab07da65081302..807e138ef7b6df 100644 --- a/src/stream_base-inl.h +++ b/src/stream_base-inl.h @@ -167,6 +167,10 @@ char* WriteWrap::Extra(size_t offset) { offset; } +size_t WriteWrap::ExtraSize() const { + return storage_size_ - ROUND_UP(sizeof(*this), kAlignSize); +} + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/stream_base.h b/src/stream_base.h index 9833a82636d28e..20cb0155c9d7b9 100644 --- a/src/stream_base.h +++ b/src/stream_base.h @@ -77,6 +77,7 @@ class WriteWrap: public ReqWrap, size_t extra = 0); inline void Dispose(); inline char* Extra(size_t offset = 0); + inline size_t ExtraSize() const; inline StreamBase* wrap() const { return wrap_; } diff --git a/test/known_issues/test-http2-client-http1-server.js b/test/known_issues/test-http2-client-http1-server.js new file mode 100644 index 00000000000000..53f7bf42c465e1 --- /dev/null +++ b/test/known_issues/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('close', 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/parallel.status b/test/parallel/parallel.status index 2958fad4d0f8fa..2297aad9c88edf 100644 --- a/test/parallel/parallel.status +++ b/test/parallel/parallel.status @@ -18,7 +18,5 @@ test-npm-install: PASS,FLAKY [$system==solaris] # Also applies to SmartOS [$system==freebsd] -test-http2-compat-serverrequest-pipe: PASS,FLAKY -test-http2-pipe: PASS,FLAKY [$system==aix] diff --git a/test/parallel/test-buffer-alloc.js b/test/parallel/test-buffer-alloc.js index faa6b2cd1e8bbd..6d7e5581ad665d 100644 --- a/test/parallel/test-buffer-alloc.js +++ b/test/parallel/test-buffer-alloc.js @@ -993,3 +993,14 @@ assert.strictEqual(Buffer.prototype.toLocaleString, Buffer.prototype.toString); const buf = Buffer.from('test'); assert.strictEqual(buf.toLocaleString(), buf.toString()); } + +{ + // Ref: https://github.com/nodejs/node/issues/17423 + const buf = Buffer.alloc(0x1000, 'This is not correctly encoded', 'hex'); + assert(buf.every((byte) => byte === 0), `Buffer was not zeroed out: ${buf}`); +} + +{ + const buf = Buffer.alloc(0x1000, 'c', 'hex'); + assert(buf.every((byte) => byte === 0), `Buffer was not zeroed out: ${buf}`); +} diff --git a/test/parallel/test-buffer-fill.js b/test/parallel/test-buffer-fill.js index 5278cb9b61b03f..2ca79170538a87 100644 --- a/test/parallel/test-buffer-fill.js +++ b/test/parallel/test-buffer-fill.js @@ -439,3 +439,9 @@ assert.strictEqual( assert.strictEqual( Buffer.allocUnsafeSlow(16).fill('Љ', 'utf8').toString('utf8'), 'Љ'.repeat(8)); + +{ + const buf = Buffer.from('a'.repeat(1000)); + buf.fill('This is not correctly encoded', 'hex'); + assert.strictEqual(buf.toString(), 'a'.repeat(1000)); +} diff --git a/test/parallel/test-http2-client-data-end.js b/test/parallel/test-http2-client-data-end.js index 569979e73efb5c..43665029630c12 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 8b91f2d21041a0..bb93366247aef7 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-onconnect-errors.js b/test/parallel/test-http2-client-onconnect-errors.js index 51ceb83677ccf2..08007753654878 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 7fc3841622e1dd..b062107e4ab7f7 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 073b2d4837d2d2..eb3a0087d7893c 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,13 +19,15 @@ 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)); - req.on('streamClosed', common.mustCall((code) => { + // second call doesn't do anything + assert.doesNotThrow(() => req.rstStream(8)); + + req.on('close', common.mustCall((code) => { assert.strictEqual(req.destroyed, true); assert.strictEqual(code, 0); server.close(); 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 d3a8ea9d8b5a8b..00000000000000 --- 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-client-stream-destroy-before-connect.js b/test/parallel/test-http2-client-stream-destroy-before-connect.js index 4eefc4b9f322b3..06afbf3ce8ceb2 100644 --- a/test/parallel/test-http2-client-stream-destroy-before-connect.js +++ b/test/parallel/test-http2-client-stream-destroy-before-connect.js @@ -41,7 +41,7 @@ server.on('listening', common.mustCall(() => { })(err); })); - req.on('streamClosed', common.mustCall((code) => { + req.on('close', common.mustCall((code) => { assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR); assert.strictEqual(code, NGHTTP2_INTERNAL_ERROR); server.close(); diff --git a/test/parallel/test-http2-client-unescaped-path.js b/test/parallel/test-http2-client-unescaped-path.js index 3a7a607bc3539a..adfbd61fe762b4 100644 --- a/test/parallel/test-http2-client-unescaped-path.js +++ b/test/parallel/test-http2-client-unescaped-path.js @@ -30,7 +30,7 @@ server.listen(0, common.mustCall(() => { type: Error, message: 'Stream closed with error code 1' })); - req.on('streamClosed', common.mustCall(maybeClose)); + req.on('close', common.mustCall(maybeClose)); } for (let i = 0; i <= count; i += 1) diff --git a/test/parallel/test-http2-compat-serverrequest-trailers.js b/test/parallel/test-http2-compat-serverrequest-trailers.js index 713e4babd20107..b4d90281918d9e 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-compat-serverresponse-end.js b/test/parallel/test-http2-compat-serverresponse-end.js index fafb3ea76dd204..366d52321554fe 100644 --- a/test/parallel/test-http2-compat-serverresponse-end.js +++ b/test/parallel/test-http2-compat-serverresponse-end.js @@ -183,10 +183,10 @@ const { { - // Should be able to call .end with cb from stream 'streamClosed' + // Should be able to call .end with cb from stream 'close' const server = createServer(mustCall((request, response) => { response.writeHead(HTTP_STATUS_OK, { foo: 'bar' }); - response.stream.on('streamClosed', mustCall(() => { + response.stream.on('close', mustCall(() => { response.end(mustCall()); })); })); diff --git a/test/parallel/test-http2-compat-socket.js b/test/parallel/test-http2-compat-socket.js index ae8b4255a39484..c6a09802981d16 100644 --- a/test/parallel/test-http2-compat-socket.js +++ b/test/parallel/test-http2-compat-socket.js @@ -64,8 +64,8 @@ server.on('request', common.mustCall(function(request, response) { assert.strictEqual(request.socket.connecting, false); // socket events are bound and emitted on Http2Stream - request.socket.on('streamClosed', common.mustCall()); - request.socket.once('streamClosed', common.mustCall()); + request.socket.on('close', common.mustCall()); + request.socket.once('close', common.mustCall()); request.socket.on('testEvent', common.mustCall()); request.socket.emit('testEvent'); })); diff --git a/test/parallel/test-http2-create-client-connect.js b/test/parallel/test-http2-create-client-connect.js index 7fdea9aef41a08..cd7d8b4fc8c3f9 100644 --- a/test/parallel/test-http2-create-client-connect.js +++ b/test/parallel/test-http2-create-client-connect.js @@ -3,6 +3,7 @@ // Tests http2.connect() const common = require('../common'); +const Countdown = require('../common/countdown'); if (!common.hasCrypto) common.skip('missing crypto'); const fixtures = require('../common/fixtures'); @@ -25,13 +26,12 @@ const URL = url.URL; [{ port: port, hostname: '127.0.0.1' }, { protocol: 'http:' }] ]; - let count = items.length; + const serverClose = new Countdown(items.length + 1, + () => setImmediate(() => server.close())); const maybeClose = common.mustCall((client) => { client.destroy(); - if (--count === 0) { - setImmediate(() => server.close()); - } + serverClose.dec(); }, items.length); items.forEach((i) => { @@ -42,7 +42,7 @@ const URL = url.URL; // Will fail because protocol does not match the server. h2.connect({ port: port, protocol: 'https:' }) - .on('socketError', common.mustCall()); + .on('socketError', common.mustCall(() => serverClose.dec())); })); } @@ -70,13 +70,12 @@ const URL = url.URL; [{ port: port, hostname: '127.0.0.1', protocol: 'https:' }, opts] ]; - let count = items.length; + const serverClose = new Countdown(items.length, + () => setImmediate(() => server.close())); const maybeClose = common.mustCall((client) => { client.destroy(); - if (--count === 0) { - setImmediate(() => server.close()); - } + serverClose.dec(); }, items.length); items.forEach((i) => { diff --git a/test/parallel/test-http2-getpackedsettings.js b/test/parallel/test-http2-getpackedsettings.js index 2f7c76e9b6a6a3..7461176c5fcde7 100644 --- a/test/parallel/test-http2-getpackedsettings.js +++ b/test/parallel/test-http2-getpackedsettings.js @@ -6,9 +6,11 @@ if (!common.hasCrypto) const assert = require('assert'); const http2 = require('http2'); -const check = Buffer.from([0x00, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x05, - 0x00, 0x00, 0x40, 0x00, 0x00, 0x04, 0x00, 0x00, - 0xff, 0xff, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]); +const check = Buffer.from([0x00, 0x01, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x05, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x04, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x06, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]); const val = http2.getPackedSettings(http2.getDefaultSettings()); assert.deepStrictEqual(val, check); @@ -102,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 0e6869c0b9f12f..83f85b279d5c6e 100644 --- a/test/parallel/test-http2-info-headers-errors.js +++ b/test/parallel/test-http2-info-headers-errors.js @@ -7,29 +7,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) => ( @@ -51,7 +37,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 93d161557c1b0c..3471e46fdf4ca4 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-multiheaders-raw.js b/test/parallel/test-http2-multiheaders-raw.js index 5db253da3c3fa1..c06bf23bff3071 100644 --- a/test/parallel/test-http2-multiheaders-raw.js +++ b/test/parallel/test-http2-multiheaders-raw.js @@ -42,7 +42,7 @@ server.on('stream', common.mustCall((stream, headers, flags, rawHeaders) => { server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); const req = client.request(src); - req.on('streamClosed', common.mustCall(() => { + req.on('close', common.mustCall(() => { server.close(); client.destroy(); })); diff --git a/test/parallel/test-http2-multiheaders.js b/test/parallel/test-http2-multiheaders.js index 7a118214e8f68b..5e477104091cb1 100644 --- a/test/parallel/test-http2-multiheaders.js +++ b/test/parallel/test-http2-multiheaders.js @@ -54,7 +54,7 @@ server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); const req = client.request(src); req.on('response', common.mustCall(checkHeaders)); - req.on('streamClosed', common.mustCall(() => { + req.on('close', common.mustCall(() => { server.close(); client.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 00000000000000..6f4169756c0b4a --- /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-options-max-reserved-streams.js b/test/parallel/test-http2-options-max-reserved-streams.js index 17009a4c11e3e3..d54ca6a7886b3c 100644 --- a/test/parallel/test-http2-options-max-reserved-streams.js +++ b/test/parallel/test-http2-options-max-reserved-streams.js @@ -34,7 +34,7 @@ server.on('stream', common.mustCall((stream) => { pushedStream.respond({ ':status': 200 }); pushedStream.on('aborted', common.mustCall()); pushedStream.on('error', common.mustNotCall()); - pushedStream.on('streamClosed', + pushedStream.on('close', common.mustCall((code) => assert.strictEqual(code, 8))); })); @@ -67,12 +67,12 @@ server.on('listening', common.mustCall(() => { client.on('stream', common.mustCall((stream) => { stream.resume(); stream.on('end', common.mustCall()); - stream.on('streamClosed', common.mustCall(maybeClose)); + stream.on('close', common.mustCall(maybeClose)); })); req.on('response', common.mustCall()); req.resume(); req.on('end', common.mustCall()); - req.on('streamClosed', common.mustCall(maybeClose)); + req.on('close', common.mustCall(maybeClose)); })); diff --git a/test/parallel/test-http2-ping.js b/test/parallel/test-http2-ping.js new file mode 100644 index 00000000000000..4892d67b4d738d --- /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 819fab5154758d..8b446f4f88118b 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 a743b800c4d6a9..00000000000000 --- a/test/parallel/test-http2-priority-errors.js +++ /dev/null @@ -1,110 +0,0 @@ -// Flags: --expose-http2 -'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-priority-parent-self.js b/test/parallel/test-http2-priority-parent-self.js deleted file mode 100644 index 55a161bf17fed2..00000000000000 --- a/test/parallel/test-http2-priority-parent-self.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); -const h2 = require('http2'); - -const server = h2.createServer(); -const invalidOptValueError = (value) => ({ - type: TypeError, - code: 'ERR_INVALID_OPT_VALUE', - message: `The value "${value}" is invalid for option "parent"` -}); - -// we use the lower-level API here -server.on('stream', common.mustCall((stream) => { - common.expectsError( - () => stream.priority({ - parent: stream.id, - weight: 1, - exclusive: false - }), - invalidOptValueError(stream.id) - ); - stream.respond({ - 'content-type': 'text/html', - ':status': 200 - }); - stream.end('hello world'); -})); - -server.listen(0, common.mustCall(() => { - - const client = h2.connect(`http://localhost:${server.address().port}`); - const req = client.request({ ':path': '/' }); - - req.on( - 'ready', - () => common.expectsError( - () => req.priority({ - parent: req.id, - weight: 1, - exclusive: false - }), - invalidOptValueError(req.id) - ) - ); - - req.on('response', common.mustCall()); - req.resume(); - req.on('end', common.mustCall(() => { - server.close(); - client.destroy(); - })); - req.end(); - -})); diff --git a/test/parallel/test-http2-respond-errors.js b/test/parallel/test-http2-respond-errors.js index b91d1e6949ca35..45dbe8530e9018 100644 --- a/test/parallel/test-http2-respond-errors.js +++ b/test/parallel/test-http2-respond-errors.js @@ -7,29 +7,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) => ( @@ -51,7 +38,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-file-errors.js b/test/parallel/test-http2-respond-file-errors.js index b57d9b1046cca0..c2c749873c82ac 100644 --- a/test/parallel/test-http2-respond-file-errors.js +++ b/test/parallel/test-http2-respond-file-errors.js @@ -117,7 +117,7 @@ server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); const req = client.request(); - req.on('streamClosed', common.mustCall(() => { + req.on('close', common.mustCall(() => { client.destroy(); server.close(); })); diff --git a/test/parallel/test-http2-respond-file-fd-errors.js b/test/parallel/test-http2-respond-file-fd-errors.js index e5cc280613f72e..9458b2f49af087 100644 --- a/test/parallel/test-http2-respond-file-fd-errors.js +++ b/test/parallel/test-http2-respond-file-fd-errors.js @@ -144,7 +144,7 @@ server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); const req = client.request(); - req.on('streamClosed', common.mustCall(() => { + req.on('close', common.mustCall(() => { client.destroy(); server.close(); })); diff --git a/test/parallel/test-http2-respond-file-fd-range.js b/test/parallel/test-http2-respond-file-fd-range.js index fdf9b5e2cd3b4b..8479dca518558e 100644 --- a/test/parallel/test-http2-respond-file-fd-range.js +++ b/test/parallel/test-http2-respond-file-fd-range.js @@ -71,7 +71,7 @@ server.listen(0, () => { req.on('end', common.mustCall(() => { assert.strictEqual(check, data.toString('utf8', 8, 11)); })); - req.on('streamClosed', common.mustCall(maybeClose)); + req.on('close', common.mustCall(maybeClose)); req.end(); } @@ -88,7 +88,7 @@ server.listen(0, () => { req.on('end', common.mustCall(() => { assert.strictEqual(check, data.toString('utf8', 8, 28)); })); - req.on('streamClosed', common.mustCall(maybeClose)); + req.on('close', common.mustCall(maybeClose)); req.end(); } diff --git a/test/parallel/test-http2-respond-no-data.js b/test/parallel/test-http2-respond-no-data.js index 74f85a9cd0cccd..d891fe4e8ddd2b 100644 --- a/test/parallel/test-http2-respond-no-data.js +++ b/test/parallel/test-http2-respond-no-data.js @@ -13,7 +13,7 @@ const server = http2.createServer(); const status = [204, 205, 304]; server.on('stream', common.mustCall((stream) => { - stream.on('streamClosed', common.mustCall(() => { + stream.on('close', common.mustCall(() => { assert.strictEqual(stream.destroyed, true); })); stream.respond({ ':status': status.shift() }); diff --git a/test/parallel/test-http2-respond-with-fd-errors.js b/test/parallel/test-http2-respond-with-fd-errors.js index 2fc1eb7102feb1..1d32a2f45c28bc 100644 --- a/test/parallel/test-http2-respond-with-fd-errors.js +++ b/test/parallel/test-http2-respond-with-fd-errors.js @@ -12,32 +12,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) => ( @@ -58,8 +44,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-response-splitting.js b/test/parallel/test-http2-response-splitting.js index 8aae2156e43fd9..1d9b616105f450 100644 --- a/test/parallel/test-http2-response-splitting.js +++ b/test/parallel/test-http2-response-splitting.js @@ -67,7 +67,7 @@ server.listen(0, common.mustCall(() => { })); req.resume(); req.on('end', common.mustCall()); - req.on('streamClosed', common.mustCall(maybeClose)); + req.on('close', common.mustCall(maybeClose)); } doTest(str, 'location', str); diff --git a/test/parallel/test-http2-rststream-errors.js b/test/parallel/test-http2-rststream-errors.js index d5ed2f7be1ef80..eacf7855117503 100644 --- a/test/parallel/test-http2-rststream-errors.js +++ b/test/parallel/test-http2-rststream-errors.js @@ -7,29 +7,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) => ( @@ -51,7 +37,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 00000000000000..ef3a79c0fd143a --- /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 777b20eb3ff2b5..56e329dcff1cd2 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-rst-before-respond.js b/test/parallel/test-http2-server-rst-before-respond.js index 25dfa7e010610d..47ba68bd29ed81 100644 --- a/test/parallel/test-http2-server-rst-before-respond.js +++ b/test/parallel/test-http2-server-rst-before-respond.js @@ -35,7 +35,7 @@ server.on('listening', common.mustCall(() => { req.on('headers', common.mustNotCall()); - req.on('streamClosed', common.mustCall((code) => { + req.on('close', common.mustCall((code) => { assert.strictEqual(h2.constants.NGHTTP2_NO_ERROR, code); server.close(); client.destroy(); diff --git a/test/parallel/test-http2-server-rst-stream.js b/test/parallel/test-http2-server-rst-stream.js index dd38efb42f9d47..4b04f29c8ec7c0 100644 --- a/test/parallel/test-http2-server-rst-stream.js +++ b/test/parallel/test-http2-server-rst-stream.js @@ -43,7 +43,7 @@ server.listen(0, common.mustCall(() => { ':method': 'POST', rstmethod: test[0] }); - req.on('streamClosed', common.mustCall((code) => { + req.on('close', common.mustCall((code) => { assert.strictEqual(code, test[1]); countdown.dec(); })); diff --git a/test/parallel/test-http2-server-socketerror.js b/test/parallel/test-http2-server-socketerror.js index 24945e531af42e..9f52b9280d2779 100644 --- a/test/parallel/test-http2-server-socketerror.js +++ b/test/parallel/test-http2-server-socketerror.js @@ -49,7 +49,7 @@ server.listen(0, common.mustCall(() => { const req = client.request(); req.resume(); req.on('end', common.mustCall()); - req.on('streamClosed', common.mustCall(() => { + req.on('close', common.mustCall(() => { client.destroy(); server.close(); })); diff --git a/test/parallel/test-http2-server-stream-session-destroy.js b/test/parallel/test-http2-server-stream-session-destroy.js index f2cc4a1f77cafe..24d064a448f87d 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 1ef5e6bf236b99..638f9a60f2c395 100644 --- a/test/parallel/test-http2-shutdown-errors.js +++ b/test/parallel/test-http2-shutdown-errors.js @@ -30,7 +30,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-stream-client.js b/test/parallel/test-http2-stream-client.js index 3b802f6e6d5a2a..aa722c5ff2b6d9 100644 --- a/test/parallel/test-http2-stream-client.js +++ b/test/parallel/test-http2-stream-client.js @@ -21,7 +21,7 @@ server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); const req = client.request(); req.resume(); - req.on('streamClosed', common.mustCall(() => { + req.on('close', common.mustCall(() => { client.destroy(); server.close(); })); diff --git a/test/parallel/test-http2-stream-destroy-event-order.js b/test/parallel/test-http2-stream-destroy-event-order.js index 433a86e7d55381..6db511f7d11c59 100644 --- a/test/parallel/test-http2-stream-destroy-event-order.js +++ b/test/parallel/test-http2-stream-destroy-event-order.js @@ -12,7 +12,7 @@ let req; const server = http2.createServer(); server.on('stream', common.mustCall((stream) => { stream.on('error', common.mustCall(() => { - stream.on('streamClosed', common.mustCall((code) => { + stream.on('close', common.mustCall((code) => { assert.strictEqual(code, 2); client.destroy(); server.close(); diff --git a/test/parallel/test-http2-too-large-headers.js b/test/parallel/test-http2-too-large-headers.js new file mode 100644 index 00000000000000..f7ac25170b846f --- /dev/null +++ b/test/parallel/test-http2-too-large-headers.js @@ -0,0 +1,32 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const http2 = require('http2'); +const assert = require('assert'); +const { + NGHTTP2_ENHANCE_YOUR_CALM +} = http2.constants; + +const server = http2.createServer({ settings: { maxHeaderListSize: 100 } }); +server.on('stream', common.mustNotCall()); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + client.on('remoteSettings', () => { + const req = client.request({ 'foo': 'a'.repeat(1000) }); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 11' + })); + req.on('close', common.mustCall((code) => { + assert.strictEqual(code, NGHTTP2_ENHANCE_YOUR_CALM); + server.close(); + client.destroy(); + })); + }); + +})); diff --git a/test/parallel/test-http2-too-many-headers.js b/test/parallel/test-http2-too-many-headers.js new file mode 100644 index 00000000000000..eff0fa9c351c32 --- /dev/null +++ b/test/parallel/test-http2-too-many-headers.js @@ -0,0 +1,34 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const http2 = require('http2'); +const assert = require('assert'); +const { + NGHTTP2_ENHANCE_YOUR_CALM +} = http2.constants; + +// By default, the maximum number of header fields allowed per +// block is 128, including the HTTP pseudo-header fields. The +// minimum value for servers is 4, setting this to any value +// less than 4 will still leave the minimum to 4. +const server = http2.createServer({ maxHeaderListPairs: 0 }); +server.on('stream', common.mustNotCall()); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ foo: 'bar' }); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 11' + })); + req.on('close', common.mustCall((code) => { + assert.strictEqual(code, NGHTTP2_ENHANCE_YOUR_CALM); + server.close(); + client.destroy(); + })); + +})); diff --git a/test/parallel/test-http2-util-update-options-buffer.js b/test/parallel/test-http2-util-update-options-buffer.js index 952b9e2dd52ee6..4388d55682a54b 100644 --- a/test/parallel/test-http2-util-update-options-buffer.js +++ b/test/parallel/test-http2-util-update-options-buffer.js @@ -15,7 +15,9 @@ const IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS = 1; 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_FLAGS = 5; +const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5; +const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6; +const IDX_OPTIONS_FLAGS = 7; { updateOptionsBuffer({ @@ -23,7 +25,9 @@ const IDX_OPTIONS_FLAGS = 5; maxReservedRemoteStreams: 2, maxSendHeaderBlockLength: 3, peerMaxConcurrentStreams: 4, - paddingStrategy: 5 + paddingStrategy: 5, + maxHeaderListPairs: 6, + maxOutstandingPings: 7 }); strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1); @@ -31,6 +35,8 @@ const IDX_OPTIONS_FLAGS = 5; strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH], 3); 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]; @@ -39,29 +45,38 @@ const IDX_OPTIONS_FLAGS = 5; 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_OUTSTANDING_PINGS)); } { optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH] = 0; + optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS] = 0; updateOptionsBuffer({ maxDeflateDynamicTableSize: 1, maxReservedRemoteStreams: 2, peerMaxConcurrentStreams: 4, - paddingStrategy: 5 + paddingStrategy: 5, + maxHeaderListPairs: 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-http2-write-callbacks.js b/test/parallel/test-http2-write-callbacks.js index e196ca8d3c5fa1..44e33573a680b6 100644 --- a/test/parallel/test-http2-write-callbacks.js +++ b/test/parallel/test-http2-write-callbacks.js @@ -30,7 +30,7 @@ server.listen(0, common.mustCall(() => { req.setEncoding('utf8'); req.on('data', (chunk) => actual += chunk); req.on('end', common.mustCall(() => assert.strictEqual(actual, 'abcxyz'))); - req.on('streamClosed', common.mustCall(() => { + req.on('close', common.mustCall(() => { client.destroy(); server.close(); })); diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index d688aa562bd06e..7fe9681ee208f9 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)