quico
Version:
A pure JavaScript implementation of QUIC, HTTP/3, QPACK, and WebTransport for Node.js
416 lines (305 loc) • 11.1 kB
JavaScript
/*
* quico: HTTP/3 and QUIC implementation for Node.js
* Copyright 2025 colocohen
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* This file is part of the open-source project hosted at:
* https://github.com/colocohen/quico
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*/
import process from 'node:process';
import dgram from 'node:dgram';
import crypto from 'node:crypto';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const flat_ranges = require('flat-ranges');
// ---- local modules (ESM) ----
import {
decrypt_quic_packet,
quic_derive_init_secrets,
quic_derive_from_tls_secrets,
build_quic_ext,
hkdf_expand_label,
encode_quic_frames,
encrypt_quic_packet,
parse_quic_datagram,
parse_quic_packet,
parse_quic_frames,
extract_tls_messages_from_chunks,
build_alpn_ext,
parse_transport_parameters
} from './libs/crypto.js';
import QUICSocket from './quic_socket.js';
import H3Socket from './h3_socket.js';
function Emitter(){
var listeners = {};
return {
on: function(name, fn){ (listeners[name] = listeners[name] || []).push(fn); },
emit: function(name){
var args = Array.prototype.slice.call(arguments, 1);
var arr = listeners[name] || [];
for (var i=0;i<arr.length;i++){ try{ arr[i].apply(null, args); }catch(e){} }
}
};
}
function createServer(options, handler){
options = options || {};
var ev = Emitter();
var context = {
udp4: null,
udp6: null,
port: null,
_handler: handler || null,
SNICallback: options.SNICallback || null,
connections: {},
address_binds: {},
timeout: null,
};
function set_http_stream(quic_connection_id,stream_id,params){
var is_new=false;
if(stream_id in context.connections[quic_connection_id].http_streams==false){
var req={
method: null,
path: null,
headers: {},
stream_id: Number(stream_id)
};
var res={
statusCode: null,
headers: {},
headersSent: false,
/*
close_wt: function(){
context.connections[quic_connection_id].h3_socket.close_wt(stream_id);
},
*/
writeHead: function(statusCode, headers) {
if(res.headersSent==false){
res.statusCode=statusCode;
res.headers[":status"]=String(statusCode);
if(typeof headers=='object'){
for(var field_name in headers){
res.headers[field_name]=headers[field_name];
}
}
//res.headersSent=true;
}
context.connections[quic_connection_id].h3_socket.http_header(stream_id,res.headers);
},
writeEarlyHints: function (hints){
},
write: function(chunk) {
if(typeof chunk=='string'){
var data=new TextEncoder().encode(chunk);
context.connections[quic_connection_id].h3_socket.http_body(Number(stream_id),data);
}else{
context.connections[quic_connection_id].h3_socket.http_body(Number(stream_id),chunk);
}
},
end: function(chunk) {
if(typeof chunk!=='undefined'){
if(typeof chunk=='string'){
var data=new TextEncoder().encode(chunk);
context.connections[quic_connection_id].h3_socket.http_body(Number(stream_id),data,true);
}else{
context.connections[quic_connection_id].h3_socket.http_body(Number(stream_id),chunk,true);
}
}else{
context.connections[quic_connection_id].h3_socket.http_body(Number(stream_id),null);
}
}
};
context.connections[quic_connection_id].http_streams[stream_id]={
req: req,
res: res
};
is_new=true;
}
if(typeof params == "object"){
if('request_headers' in params){
for(var field_name in params['request_headers']){
context.connections[quic_connection_id].http_streams[stream_id].req.headers[field_name]=params['request_headers'][field_name];
}
}
if(context.connections[quic_connection_id].http_streams[stream_id].req.method==null && ":method" in context.connections[quic_connection_id].http_streams[stream_id].req.headers==true){
context.connections[quic_connection_id].http_streams[stream_id].req.method=context.connections[quic_connection_id].http_streams[stream_id].req.headers[':method'];
}
if(context.connections[quic_connection_id].http_streams[stream_id].req.path==null && ":path" in context.connections[quic_connection_id].http_streams[stream_id].req.headers==true){
context.connections[quic_connection_id].http_streams[stream_id].req.path=context.connections[quic_connection_id].http_streams[stream_id].req.headers[':path'];
}
//console.log(context.connections[quic_connection_id].http_streams[stream_id]);
}
if(is_new==true){
context._handler(context.connections[quic_connection_id].http_streams[stream_id].req, context.connections[quic_connection_id].http_streams[stream_id].res);
}
}
function receiving_quic_packet(from_ip,from_port,data){
var quic_connection_id=null;
var address_str = from_ip + ':' + from_port;
var dcid_str=null;
if('dcid' in data && data.dcid && data.dcid.byteLength>0){
dcid_str = Array.from(data.dcid).join("");
}
if(dcid_str!==null){
if(dcid_str in context.connections==true){
quic_connection_id=dcid_str;
}
}else{
if(address_str in context.address_binds==true){
if(context.address_binds[address_str] in context.connections==true){
quic_connection_id=context.address_binds[address_str];
}
}
}
if(quic_connection_id==null){
if(dcid_str!==null){
quic_connection_id=dcid_str;
}else{
quic_connection_id=Math.floor(Math.random() * 9007199254740991);
}
}
if(address_str in context.address_binds==false || context.address_binds[address_str]!==quic_connection_id){
context.address_binds[address_str]=quic_connection_id;
}
if(quic_connection_id in context.connections==false){
context.connections[quic_connection_id]={
quic_socket: null,
h3_socket: null,
http_streams: {}
};
}
if(context.connections[quic_connection_id].quic_socket==null){
var quic_socket=new QUICSocket({
isServer: true,
SNICallback: context.SNICallback
});
context.connections[quic_connection_id].quic_socket=quic_socket;
quic_socket.on('packet',function(data_to_send){
send_udp_packet(from_ip,from_port,data_to_send);
});
quic_socket.on('connect',function(){
//console.log('now connected:');
var h3_socket=new H3Socket({
isServer: true
});
context.connections[quic_connection_id].h3_socket=h3_socket;
quic_socket.on('stream',function(stream_id,data,fin){
h3_socket.stream(stream_id,data,fin);
});
h3_socket.on('stream',function(stream_id,data,fin){
quic_socket.stream(stream_id,data,fin);
});
h3_socket.on('http_headers',function(stream_id,headers){
set_http_stream(quic_connection_id,stream_id,{
request_headers: headers
});
});
h3_socket.on('http_body',function(stream_id,payload){
set_http_stream(quic_connection_id,stream_id,{
add_request_body_chunk: payload
});
});
/*
quic_socket.on('datagram',function(context_id,data){
//console.log(data);
//context.connections[quic_connection_id].h3_socket.datagram(context_id,data);
});
*/
});
}
context.connections[quic_connection_id].quic_socket.packet(data);
//...
}
function receiving_udp_packet(from_ip,from_port,data){
var quic_packets=parse_quic_datagram(data);
if(quic_packets.length>0){
for(var i in quic_packets){
if(quic_packets[i]!==null){
receiving_quic_packet(from_ip,from_port,quic_packets[i]);
}
}
}
}
function send_udp_packet(to_ip,to_port,data,callback){
if(to_ip.indexOf(':')>=0){
context.udp6.send(data, to_port, to_ip, function(error){
if (error) {
if(typeof callback=='function'){
callback(false);
}
} else {
if(typeof callback=='function'){
callback(true);
}
}
});
}else{
context.udp4.send(data, to_port, to_ip, function(error){
if (error) {
if(typeof callback=='function'){
callback(false);
}
} else {
if(typeof callback=='function'){
callback(true);
}
}
});
}
}
function listen(port, host, callback){
if (typeof host === 'function') {
callback = host;
host = null;
}
context.port = port || 443;
host = host || '::';
// יצירת סוקט UDP4
context.udp4 = dgram.createSocket('udp4');
context.udp4.on('message', function(message, rinfo) {
receiving_udp_packet(rinfo.address, rinfo.port, new Uint8Array(message));
});
context.udp4.on('error', function(error) {
//console.error('UDP4 error:', err);
});
if (host === '::' || host.indexOf('.') !== -1) {
var host4 = host.indexOf('.') !== -1 ? host : '0.0.0.0';
context.udp4.bind(context.port, host4);
}
// יצירת סוקט UDP6
context.udp6 = dgram.createSocket({ type: 'udp6', ipv6Only: true });
context.udp6.on('message', function(message, rinfo) {
receiving_udp_packet(rinfo.address, rinfo.port, new Uint8Array(message));
});
context.udp6.on('error', function(error) {
//console.error('UDP6 error:', err);
});
var host6 = host.indexOf(':') !== -1 ? host : '::';
context.udp6.bind(context.port, host6, function() {
if (typeof callback === 'function') {
callback();
}
});
}
function close(){
}
var api={
context: context,
on: function(name, fn){ ev.on(name, fn); },
listen: listen,
close: close,
setTimeout: function(){
},
};
for (var k in api) if (Object.prototype.hasOwnProperty.call(api,k)) this[k] = api[k];
return this;
}
export { createServer };