Skip to content

Commit

Permalink
Added http2 alpn negotiation
Browse files Browse the repository at this point in the history
  • Loading branch information
parthverma1 committed May 16, 2024
1 parent 2b56b9c commit 94f2115
Show file tree
Hide file tree
Showing 7 changed files with 5,837 additions and 74 deletions.
4 changes: 4 additions & 0 deletions lib/autohttp/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Agent } from 'https';
export class AutoHttp2Agent extends Agent{

}
7 changes: 7 additions & 0 deletions lib/autohttp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {AutoHttp2Agent} from "./agent";
import {request} from "./request";

export default {
Agent: AutoHttp2Agent,
request: request
}
105 changes: 105 additions & 0 deletions lib/autohttp/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {URL} from "node:url";
import {RequestOptions} from "https";
import * as http from "http";
import * as https from "https";
import * as tls from 'tls';
import {EventEmitter} from "node:events";
import {request as http2Request} from '../../lib/http2/request'

function parseSession(buf) {
return {
sessionId: buf.slice(17, 17 + 32).toString('hex'),
masterKey: buf.slice(51, 51 + 48).toString('hex')
};
}

// @ts-ignore
class MultiProtocolRequest extends EventEmitter implements http.ClientRequest {
private socket: tls.TLSSocket;
private queuedOps: string[] = [];

constructor(socket: tls.TLSSocket, options: RequestOptions) {
super();
this.socket = socket;
this.emit('socket', socket);
this.socket.on('error', (e) => this.emit('error', e))
this.socket.on('tlsClientError', (e) => this.emit('error', e))
this.socket.once('secureConnect', () => {
const protocol = this.socket.alpnProtocol;
if (!protocol) {
this.emit('error', this.socket.authorizationError)
this.socket.end();
return;
}


options.createConnection = () => {
return this.socket
};
let req;
if (protocol === 'h2') {
// @ts-ignore
req = http2Request({...options});
} else if (protocol === 'http/1.1') {
req = https.request(options);
} else {
this.emit('error', 'Unknown protocol' + protocol)
return;

}
this.registerCallbacks(req);
this.processQueuedOpens(req);

})
}

registerCallbacks(ob: any) {
ob.on('drain', (...args) => this.emit('drain', ...args))
ob.on('error', (...args) => this.emit('error', ...args))
ob.on('data', (...args) => this.emit('data', ...args))
ob.on('end', (...args) => this.emit('end', ...args))
ob.on('close', (...args) => this.emit('close', ...args))
ob.on('socket', (...args) => this.emit('socket', ...args))
ob.on('response', (...args) => this.emit('response', ...args))

ob.once('error', (...args) => this.emit('error', ...args))
}

private processQueuedOpens(ob: any) {
this.queuedOps.forEach((op) => {
if (op === 'end') {
ob.end()
}
})
}

end() {
this.queuedOps.push('end');
return this;
}
}

export function request(options: RequestOptions): http.ClientRequest {
options.port = Number(options.port)
// @ts-ignore
const uri: URL = options.uri;

const newOptions: tls.ConnectionOptions = {
port: options.port ? Number(options.port) : 443,
ALPNProtocols: ['h2'],
ca: options.ca,
key: options.key,
cert: options.cert,
host: uri.hostname,
servername: uri.hostname,
rejectUnauthorized: options.rejectUnauthorized
// minVersion: "TLSv1.3",
// maxVersion: "TLSv1.3"
}

const socket = tls.connect(newOptions);
// socket.enableTrace()
//@ts-ignore
return new MultiProtocolRequest(socket, options);
}

131 changes: 58 additions & 73 deletions lib/http2/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,60 @@ import {URL} from "node:url";
import {RequestOptions} from "https";
import * as http from "http";
import * as http2 from 'http2';
import {EventEmitter} from "node:events";
export function request(options: RequestOptions, callback?: (res: http.IncomingMessage) => void ): http.ClientRequest{
const ca = options.ca;
const key = options.key;
const rejectUnauthorized = options.rejectUnauthorized;
const headers = options.headers;
const cert = options.cert;

// @ts-ignore
const uri:URL = options.uri;
const newoptions: http2.ClientSessionOptions | http2.SecureClientSessionOptions = {}

if(options.createConnection){
// @ts-ignore
newoptions.createConnection = options.createConnection;
}
else{
// @ts-ignore
newoptions.ca = options.ca;
// @ts-ignore
newoptions.key = options.key;
// @ts-ignore
newoptions.cert = options.cert;
// @ts-ignore
newoptions.rejectUnauthorized = options.rejectUnauthorized;
}

const client = http2.connect(uri, {
ca,
key,
cert,
rejectUnauthorized,
});

// @ts-ignore
const client = http2.connect(uri, newoptions);

const path = options.path
const method = options.method;
const req = client.request({[http2.constants.HTTP2_HEADER_PATH]: path, [http2.constants.HTTP2_HEADER_METHOD]: method, ...headers })
req.on('end', () => {
client.close();
})
req.on('error',()=>{
console.log(req.rstCode)
})

//@ts-ignore
return new DummyClientRequest(req, client);

}


// @ts-ignore
class DummyClientRequest implements http.ClientRequest {
class DummyClientRequest extends EventEmitter implements http.ClientRequest {
private _req: http2.ClientHttp2Stream;
private _client: http2.ClientHttp2Session
private response: http2.IncomingHttpHeaders;
constructor(req: http2.ClientHttp2Stream, client: http2.ClientHttp2Session){
super();
this._req = req;
this._client = client;
this.on = this.on.bind(this);
this.once = this.once.bind(this)
this.registerListeners();
}

get _header(){
Expand All @@ -52,74 +66,45 @@ class DummyClientRequest implements http.ClientRequest {
return '2.0'
}

setDefaultEncoding(encoding: BufferEncoding): this {
this._req.setDefaultEncoding(encoding)
return this;
get rawHeaders(){
return Object.entries(this.response).map(([key, value])=>`${key}: ${value}`).join('/r/n')
}

get statusCode(){
return this.response[http2.constants.HTTP2_HEADER_STATUS];
}

on(eventName:string, cb: (arg1: any, arg2?: any, arg3?: any)=>void){
if(eventName === 'drain'){
this._req.on('drain', cb)
}
else if(eventName === 'error'){
this._req.on('error', cb);
}
else if(eventName === 'response'){
this._req.on('response', (response)=>{
cb({
statusCode: response[http2.constants.HTTP2_HEADER_STATUS],
rawHeaders: Object.entries(response).map(([key, value])=>`${key}: ${value}`).join('/r/n'),
on: this.on,
once: this.once,
httpVersion: this.httpVersion
})

})
}
else if(eventName === 'data'){
this._req.on('data', cb)
}
else if(eventName === 'end'){
this._req.on('end', cb);
}

else if(eventName === 'close'){
this._req.on('close', cb);
}
else if(eventName === 'socket'){
cb(this._client.socket)
}
else if(eventName === 'error'){
this._req.on('error', cb);
this._client.on('error', cb);
}


else {
console.log('unknown eventName', eventName, 'received')
}

setDefaultEncoding(encoding: BufferEncoding): this {
this._req.setDefaultEncoding(encoding)
return this;
}
once(eventName, cb){
if(eventName === 'end'){
this._req.on('end', cb);
}

else if(eventName === 'close'){
this._req.on('close', cb);
}
else if(eventName === 'error'){
this._req.once('error', cb);
this._client.once('error', cb);
}
else {
console.log('unknown once eventName', eventName, 'received')
}
return this;
// on(event:string, cb){
// console.log('event registered', event);
// return super.on(event, cb)
// }
private registerListeners(

){
this._req.on('drain', (...args)=>this.emit('drain', ...args))
this._req.on('error', (...args)=>this.emit('error', ...args))
this._req.on('data', (...args)=>this.emit('data', ...args))
this._req.on('end', (...args)=>this.emit('end', ...args))
this._req.on('close', (...args)=>this.emit('close', ...args))
this._req.on('socket', (...args)=>this.emit('socket', this._client.socket))
this._client.on('error', (...args)=>this.emit('error', ...args))
this._req.on('response', (response)=>{
console.log(response)
this.response = response;
this.emit('response',this);
} )
//
this._req.once('end', () => this.emit('end'))
this._req.once('close', () => this.emit('close'))
// this._req.once('error', (...args) => this.emit('error', ...args))
// this._client.once('error', (...args) => this.emit('error', ...args))
}


// @ts-ignore
end(){
this._req.end()
Expand Down
3 changes: 2 additions & 1 deletion request.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var tls = require('tls')
var http = require('http')
var https = require('https')
var http2 = require('./lib/http2').default;
var autohttp2 = require('./lib/autohttp').default;
var url = require('url')
var util = require('util')
var stream = require('stream')
Expand Down Expand Up @@ -589,7 +590,7 @@ Request.prototype.init = function (options) {
}

var protocol = self.proxy && !self.tunnel ? self.proxy.protocol : self.uri.protocol
var defaultModules = {'http:': { h2: http, http1: http, auto: http }, 'https:': { http1: https, h2: http2, auto: https }}
var defaultModules = {'http:': { h2: http, http1: http, auto: http }, 'https:': { http1: https, h2: http2, auto: autohttp2 }}
var httpModules = self.httpModules || {}

self.httpModule = httpModules[protocol]?.[options.protocol] || defaultModules[protocol][options.protocol]
Expand Down
15 changes: 15 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const request = require('./index');

const fs = require('fs')
// request("https://postman-echo.com/get",{protocol: 'http1'}, (err, resp, body)=>console.log(body))
const TEST_URL = "https://postman-echo.com/get";
const httpsurl = 'https://localhost:443'
const http2url = 'https://localhost:3000/h2'
const r = request(TEST_URL,{
protocol: 'auto',
timing:true,
strictSSL:false,
// ca: fs.readFileSync('/etc/ssl/cert.pem'),
// key: fs.readFileSync('/Users/parth.verma@postman.com/temp/t/key.pem'),
// cert: fs.readFileSync('/Users/parth.verma@postman.com/temp/t/cert.pem')
}, (err, resp, body)=>console.log({ body }))

0 comments on commit 94f2115

Please sign in to comment.