mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 09:35:59 -04:00
Merge branch 'main' into agent
This commit is contained in:
@@ -107,6 +107,9 @@ pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, co
|
||||
event.acquireRef();
|
||||
defer _ = event.releaseRef(self.page._session);
|
||||
|
||||
// Increment event count for Event Timing API
|
||||
self.page.window._performance._event_counts.increment(event._type_string.str());
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
||||
}
|
||||
|
||||
@@ -779,64 +779,66 @@ fn jsValueToTypedArray(comptime T: type, js_val: js.Value) !?[]T {
|
||||
}
|
||||
|
||||
const backing_store_ptr = v8.v8__ArrayBuffer__GetBackingStore(array_buffer orelse return null);
|
||||
if (byte_len == 0) {
|
||||
return &[_]T{};
|
||||
}
|
||||
|
||||
const backing_store_handle = v8.std__shared_ptr__v8__BackingStore__get(&backing_store_ptr).?;
|
||||
const data = v8.v8__BackingStore__Data(backing_store_handle);
|
||||
const base = @as([*]u8, @ptrCast(data)) + byte_offset;
|
||||
|
||||
// 2. Validate alignment
|
||||
if (@intFromPtr(base) % @alignOf(T) != 0) {
|
||||
return error.InvalidAlignment;
|
||||
}
|
||||
const num_elements = byte_len / @sizeOf(T);
|
||||
|
||||
switch (T) {
|
||||
u8 => {
|
||||
if (force_u8 or js_val.isUint8Array() or js_val.isUint8ClampedArray()) {
|
||||
if (byte_len == 0) return &[_]u8{};
|
||||
const arr_ptr = @as([*]u8, @ptrCast(@alignCast(data)));
|
||||
return arr_ptr[byte_offset .. byte_offset + byte_len];
|
||||
return base[0..num_elements];
|
||||
}
|
||||
},
|
||||
i8 => {
|
||||
if (js_val.isInt8Array()) {
|
||||
if (byte_len == 0) return &[_]i8{};
|
||||
const arr_ptr = @as([*]i8, @ptrCast(@alignCast(data)));
|
||||
return arr_ptr[byte_offset .. byte_offset + byte_len];
|
||||
const ptr = @as([*]i8, @ptrCast(@alignCast(base)));
|
||||
return ptr[0..num_elements];
|
||||
}
|
||||
},
|
||||
u16 => {
|
||||
if (js_val.isUint16Array()) {
|
||||
if (byte_len == 0) return &[_]u16{};
|
||||
const arr_ptr = @as([*]u16, @ptrCast(@alignCast(data)));
|
||||
return arr_ptr[byte_offset .. byte_offset + byte_len / 2];
|
||||
const ptr = @as([*]u16, @ptrCast(@alignCast(base)));
|
||||
return ptr[0..num_elements];
|
||||
}
|
||||
},
|
||||
i16 => {
|
||||
if (js_val.isInt16Array()) {
|
||||
if (byte_len == 0) return &[_]i16{};
|
||||
const arr_ptr = @as([*]i16, @ptrCast(@alignCast(data)));
|
||||
return arr_ptr[byte_offset .. byte_offset + byte_len / 2];
|
||||
const ptr = @as([*]i16, @ptrCast(@alignCast(base)));
|
||||
return ptr[0..num_elements];
|
||||
}
|
||||
},
|
||||
u32 => {
|
||||
if (js_val.isUint32Array()) {
|
||||
if (byte_len == 0) return &[_]u32{};
|
||||
const arr_ptr = @as([*]u32, @ptrCast(@alignCast(data)));
|
||||
return arr_ptr[byte_offset .. byte_offset + byte_len / 4];
|
||||
const ptr = @as([*]u32, @ptrCast(@alignCast(base)));
|
||||
return ptr[0..num_elements];
|
||||
}
|
||||
},
|
||||
i32 => {
|
||||
if (js_val.isInt32Array()) {
|
||||
if (byte_len == 0) return &[_]i32{};
|
||||
const arr_ptr = @as([*]i32, @ptrCast(@alignCast(data)));
|
||||
return arr_ptr[byte_offset .. byte_offset + byte_len / 4];
|
||||
const ptr = @as([*]i32, @ptrCast(@alignCast(base)));
|
||||
return ptr[0..num_elements];
|
||||
}
|
||||
},
|
||||
u64 => {
|
||||
if (js_val.isBigUint64Array()) {
|
||||
if (byte_len == 0) return &[_]u64{};
|
||||
const arr_ptr = @as([*]u64, @ptrCast(@alignCast(data)));
|
||||
return arr_ptr[byte_offset .. byte_offset + byte_len / 8];
|
||||
const ptr = @as([*]u64, @ptrCast(@alignCast(base)));
|
||||
return ptr[0..num_elements];
|
||||
}
|
||||
},
|
||||
i64 => {
|
||||
if (js_val.isBigInt64Array()) {
|
||||
if (byte_len == 0) return &[_]i64{};
|
||||
const arr_ptr = @as([*]i64, @ptrCast(@alignCast(data)));
|
||||
return arr_ptr[byte_offset .. byte_offset + byte_len / 8];
|
||||
const ptr = @as([*]i64, @ptrCast(@alignCast(base)));
|
||||
return ptr[0..num_elements];
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
|
||||
@@ -859,6 +859,7 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/URL.zig"),
|
||||
@import("../webapi/Window.zig"),
|
||||
@import("../webapi/Performance.zig"),
|
||||
@import("../webapi/EventCounts.zig"),
|
||||
@import("../webapi/PluginArray.zig"),
|
||||
@import("../webapi/MutationObserver.zig"),
|
||||
@import("../webapi/IntersectionObserver.zig"),
|
||||
|
||||
@@ -393,3 +393,141 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_exists>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
testing.expectEqual('object', typeof ec);
|
||||
testing.expectEqual(false, ec === null);
|
||||
testing.expectEqual(false, ec === undefined);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_same_object>
|
||||
{
|
||||
// performance.eventCounts should return the same object on each access
|
||||
testing.expectEqual(true, performance.eventCounts === performance.eventCounts);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_size>
|
||||
{
|
||||
// EventCounts tracks 36 event types per Event Timing spec
|
||||
const ec = performance.eventCounts;
|
||||
testing.expectEqual('number', typeof ec.size);
|
||||
testing.expectEqual(36, ec.size);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_has_tracked_events>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
// These should be tracked
|
||||
testing.expectEqual(true, ec.has("click"));
|
||||
testing.expectEqual(true, ec.has("keydown"));
|
||||
testing.expectEqual(true, ec.has("keyup"));
|
||||
testing.expectEqual(true, ec.has("mousedown"));
|
||||
testing.expectEqual(true, ec.has("mouseup"));
|
||||
testing.expectEqual(true, ec.has("pointerdown"));
|
||||
testing.expectEqual(true, ec.has("input"));
|
||||
testing.expectEqual(true, ec.has("touchstart"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_has_non_tracked_events>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
// These should NOT be tracked
|
||||
testing.expectEqual(false, ec.has("load"));
|
||||
testing.expectEqual(false, ec.has("scroll"));
|
||||
testing.expectEqual(false, ec.has("resize"));
|
||||
testing.expectEqual(false, ec.has("DOMContentLoaded"));
|
||||
testing.expectEqual(false, ec.has("nonexistent"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_get_returns_number>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
// get() should return a number for tracked events
|
||||
testing.expectEqual('number', typeof ec.get("click"));
|
||||
testing.expectEqual('number', typeof ec.get("keydown"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_get_returns_zero_for_non_tracked>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
// get() should return 0 for non-tracked events (not undefined)
|
||||
testing.expectEqual(0, ec.get("load"));
|
||||
testing.expectEqual(0, ec.get("nonexistent"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_keys>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
const keys = Array.from(ec.keys());
|
||||
testing.expectEqual(36, keys.length);
|
||||
testing.expectEqual(true, keys.includes("click"));
|
||||
testing.expectEqual(true, keys.includes("keydown"));
|
||||
testing.expectEqual(true, keys.includes("mousedown"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_values>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
const values = Array.from(ec.values());
|
||||
testing.expectEqual(36, values.length);
|
||||
// All values should be numbers
|
||||
for (const v of values) {
|
||||
testing.expectEqual('number', typeof v);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_entries>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
const entries = Array.from(ec.entries());
|
||||
testing.expectEqual(36, entries.length);
|
||||
// Each entry should be [string, number]
|
||||
for (const e of entries) {
|
||||
testing.expectEqual(true, Array.isArray(e));
|
||||
testing.expectEqual(2, e.length);
|
||||
testing.expectEqual('string', typeof e[0]);
|
||||
testing.expectEqual('number', typeof e[1]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_forEach>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
let count = 0;
|
||||
let sawClick = false;
|
||||
ec.forEach((value, key, obj) => {
|
||||
count++;
|
||||
testing.expectEqual('number', typeof value);
|
||||
testing.expectEqual('string', typeof key);
|
||||
testing.expectEqual(ec, obj);
|
||||
if (key === "click") sawClick = true;
|
||||
});
|
||||
testing.expectEqual(36, count);
|
||||
testing.expectEqual(true, sawClick);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_iterator>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
let count = 0;
|
||||
for (const [key, value] of ec) {
|
||||
count++;
|
||||
testing.expectEqual('string', typeof key);
|
||||
testing.expectEqual('number', typeof value);
|
||||
}
|
||||
testing.expectEqual(36, count);
|
||||
}
|
||||
</script>
|
||||
|
||||
172
src/browser/webapi/EventCounts.zig
Normal file
172
src/browser/webapi/EventCounts.zig
Normal file
@@ -0,0 +1,172 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
pub fn registerTypes() []const type {
|
||||
return &.{
|
||||
EventCounts,
|
||||
KeyIterator,
|
||||
ValueIterator,
|
||||
EntryIterator,
|
||||
};
|
||||
}
|
||||
|
||||
const EventCounts = @This();
|
||||
|
||||
// Event types tracked per the Event Timing spec
|
||||
// https://w3c.github.io/event-timing/#sec-events-exposed
|
||||
const tracked_event_types = [_][]const u8{
|
||||
"auxclick",
|
||||
"click",
|
||||
"contextmenu",
|
||||
"dblclick",
|
||||
"mousedown",
|
||||
"mouseenter",
|
||||
"mouseleave",
|
||||
"mouseout",
|
||||
"mouseover",
|
||||
"mouseup",
|
||||
"pointerover",
|
||||
"pointerenter",
|
||||
"pointerdown",
|
||||
"pointerup",
|
||||
"pointercancel",
|
||||
"pointerout",
|
||||
"pointerleave",
|
||||
"gotpointercapture",
|
||||
"lostpointercapture",
|
||||
"touchstart",
|
||||
"touchend",
|
||||
"touchcancel",
|
||||
"keydown",
|
||||
"keypress",
|
||||
"keyup",
|
||||
"beforeinput",
|
||||
"input",
|
||||
"compositionstart",
|
||||
"compositionupdate",
|
||||
"compositionend",
|
||||
"dragstart",
|
||||
"dragend",
|
||||
"dragenter",
|
||||
"dragleave",
|
||||
"dragover",
|
||||
"drop",
|
||||
};
|
||||
|
||||
// Counts stored in a fixed array
|
||||
_counts: [tracked_event_types.len]u32 = [_]u32{0} ** tracked_event_types.len,
|
||||
|
||||
pub fn increment(self: *EventCounts, event_type: []const u8) void {
|
||||
if (getIndex(event_type)) |idx| {
|
||||
self._counts[idx] +|= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: *const EventCounts, event_type: []const u8) u32 {
|
||||
if (getIndex(event_type)) |idx| {
|
||||
return self._counts[idx];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn has(_: *const EventCounts, event_type: []const u8) bool {
|
||||
return getIndex(event_type) != null;
|
||||
}
|
||||
|
||||
pub fn getSize(_: *const EventCounts) u32 {
|
||||
return tracked_event_types.len;
|
||||
}
|
||||
|
||||
pub fn keys(self: *EventCounts, page: *Page) !*KeyIterator {
|
||||
return .init(.{ .event_counts = self }, page);
|
||||
}
|
||||
|
||||
pub fn values(self: *EventCounts, page: *Page) !*ValueIterator {
|
||||
return .init(.{ .event_counts = self }, page);
|
||||
}
|
||||
|
||||
pub fn entries(self: *EventCounts, page: *Page) !*EntryIterator {
|
||||
return .init(.{ .event_counts = self }, page);
|
||||
}
|
||||
|
||||
pub fn forEach(self: *EventCounts, cb_: js.Function, js_this_: ?js.Object) !void {
|
||||
const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_;
|
||||
|
||||
for (tracked_event_types, self._counts) |event_type, count| {
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
cb.tryCall(void, .{ count, event_type, self }, &caught) catch {
|
||||
log.debug(.js, "forEach callback", .{ .caught = caught, .source = "EventCounts" });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn getIndex(event_type: []const u8) ?usize {
|
||||
for (tracked_event_types, 0..) |t, i| {
|
||||
if (std.mem.eql(u8, t, event_type)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(EventCounts);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "EventCounts";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const get = bridge.function(EventCounts.get, .{});
|
||||
pub const has = bridge.function(EventCounts.has, .{});
|
||||
pub const size = bridge.accessor(EventCounts.getSize, null, .{});
|
||||
pub const keys = bridge.function(EventCounts.keys, .{});
|
||||
pub const values = bridge.function(EventCounts.values, .{});
|
||||
pub const entries = bridge.function(EventCounts.entries, .{});
|
||||
pub const forEach = bridge.function(EventCounts.forEach, .{});
|
||||
pub const symbol_iterator = bridge.iterator(EventCounts.entries, .{});
|
||||
};
|
||||
|
||||
// Iterator implementation
|
||||
pub const Iterator = struct {
|
||||
index: u32 = 0,
|
||||
event_counts: *EventCounts,
|
||||
|
||||
pub const Entry = struct { []const u8, u32 };
|
||||
|
||||
pub fn next(self: *Iterator, _: *const Page) ?Iterator.Entry {
|
||||
const index = self.index;
|
||||
if (index >= tracked_event_types.len) {
|
||||
return null;
|
||||
}
|
||||
self.index = index + 1;
|
||||
|
||||
return .{ tracked_event_types[index], self.event_counts._counts[index] };
|
||||
}
|
||||
};
|
||||
|
||||
const GenericIterator = @import("collections/iterator.zig").Entry;
|
||||
pub const KeyIterator = GenericIterator(Iterator, "0");
|
||||
pub const ValueIterator = GenericIterator(Iterator, "1");
|
||||
pub const EntryIterator = GenericIterator(Iterator, null);
|
||||
@@ -2,6 +2,8 @@ const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const datetime = @import("../../datetime.zig");
|
||||
|
||||
const EventCounts = @import("EventCounts.zig");
|
||||
|
||||
pub fn registerTypes() []const type {
|
||||
return &.{ Performance, Entry, Mark, Measure, PerformanceTiming, PerformanceNavigation };
|
||||
}
|
||||
@@ -14,6 +16,7 @@ _time_origin: u64,
|
||||
_entries: std.ArrayList(*Entry) = .{},
|
||||
_timing: PerformanceTiming = .{},
|
||||
_navigation: PerformanceNavigation = .{},
|
||||
_event_counts: EventCounts = .{},
|
||||
|
||||
/// Get high-resolution timestamp in microseconds, rounded to 5μs increments
|
||||
/// to match browser behavior (prevents fingerprinting)
|
||||
@@ -54,6 +57,10 @@ pub fn getNavigation(self: *Performance) *PerformanceNavigation {
|
||||
return &self._navigation;
|
||||
}
|
||||
|
||||
pub fn getEventCounts(self: *Performance) *EventCounts {
|
||||
return &self._event_counts;
|
||||
}
|
||||
|
||||
pub fn mark(
|
||||
self: *Performance,
|
||||
name: []const u8,
|
||||
@@ -277,6 +284,7 @@ pub const JsApi = struct {
|
||||
pub const timeOrigin = bridge.accessor(Performance.getTimeOrigin, null, .{});
|
||||
pub const timing = bridge.accessor(Performance.getTiming, null, .{});
|
||||
pub const navigation = bridge.accessor(Performance.getNavigation, null, .{});
|
||||
pub const eventCounts = bridge.accessor(Performance.getEventCounts, null, .{});
|
||||
};
|
||||
|
||||
pub const Entry = struct {
|
||||
|
||||
@@ -115,32 +115,39 @@ pub fn decode(self: *TextDecoder, input_: ?[]const u8, opts_: ?DecodeOpts) ![]co
|
||||
const opts: DecodeOpts = opts_ orelse .{};
|
||||
const input = input_ orelse "";
|
||||
|
||||
// For non-streaming calls, we don't need a persistent decoder
|
||||
if (!opts.stream) {
|
||||
// Reset decoder state if we had one
|
||||
if (self._decoder) |decoder| {
|
||||
if (opts.stream) {
|
||||
// Streaming mode: create decoder if needed, keep it alive
|
||||
if (self._decoder == null) {
|
||||
self._decoder = html5ever.encoding_decoder_new(self._encoding_handle);
|
||||
if (self._decoder == null) {
|
||||
return error.OutOfMemory;
|
||||
}
|
||||
}
|
||||
return self._decode(input, self._decoder, false);
|
||||
}
|
||||
|
||||
if (self._decoder) |decoder| {
|
||||
// Non-streaming with existing decoder: flush with is_last=true, then free
|
||||
defer {
|
||||
html5ever.encoding_decoder_free(decoder);
|
||||
self._decoder = null;
|
||||
}
|
||||
} else if (self._decoder == null) {
|
||||
self._decoder = html5ever.encoding_decoder_new(self._encoding_handle);
|
||||
if (self._decoder == null) {
|
||||
return error.OutOfMemory;
|
||||
}
|
||||
return self._decode(input, decoder, true);
|
||||
}
|
||||
|
||||
return self._decode(input, self._decoder);
|
||||
// non-streaming, no existing decoder
|
||||
return self._decode(input, null, true);
|
||||
}
|
||||
|
||||
fn _decode(self: *TextDecoder, input: []const u8, streaming_decoder: ?*anyopaque) ![]const u8 {
|
||||
if (input.len == 0) {
|
||||
fn _decode(self: *TextDecoder, input: []const u8, streaming_decoder: ?*anyopaque, is_last: bool) ![]const u8 {
|
||||
if (input.len == 0 and !is_last) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Calculate max output size
|
||||
// Calculate max output size (add extra for potential buffered bytes when finishing)
|
||||
const max_out = html5ever.encoding_max_utf8_buffer_length(
|
||||
self._encoding_handle,
|
||||
input.len,
|
||||
if (input.len == 0) 4 else input.len,
|
||||
);
|
||||
|
||||
if (max_out == 0) {
|
||||
@@ -158,7 +165,7 @@ fn _decode(self: *TextDecoder, input: []const u8, streaming_decoder: ?*anyopaque
|
||||
input.len,
|
||||
output.ptr,
|
||||
output.len,
|
||||
0, // is_last = false for streaming
|
||||
@intFromBool(is_last),
|
||||
)
|
||||
else
|
||||
html5ever.encoding_decode(
|
||||
|
||||
@@ -54,6 +54,7 @@ _got_upgrade: bool = false,
|
||||
|
||||
_conn: ?*http.Connection,
|
||||
_http_client: *HttpClient,
|
||||
_req_headers: http.Headers,
|
||||
|
||||
// buffered outgoing messages
|
||||
_send_queue: std.ArrayList(Message) = .empty,
|
||||
@@ -66,6 +67,9 @@ _recv_buffer: std.ArrayList(u8) = .empty,
|
||||
_close_code: u16 = 1000,
|
||||
_close_reason: []const u8 = "",
|
||||
|
||||
// negotiated protocol
|
||||
_protocol: []const u8 = "",
|
||||
|
||||
// Event handlers
|
||||
_on_open: ?js.Function.Temp = null,
|
||||
_on_message: ?js.Function.Temp = null,
|
||||
@@ -84,13 +88,21 @@ pub const BinaryType = enum {
|
||||
arraybuffer,
|
||||
};
|
||||
|
||||
pub fn init(url: []const u8, protocols_: ?[]const u8, page: *Page) !*WebSocket {
|
||||
if (protocols_) |protocols| {
|
||||
if (protocols.len > 0) {
|
||||
log.warn(.not_implemented, "WS protocols", .{ .protocols = protocols });
|
||||
fn isValidProtocol(protocol: []const u8) bool {
|
||||
if (protocol.len == 0) return false;
|
||||
for (protocol) |c| {
|
||||
// Control characters
|
||||
if (c <= 31 or c == 127) return false;
|
||||
// Separators per RFC 2616
|
||||
switch (c) {
|
||||
'(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t' => return false,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn init(url: []const u8, protocols: [][]const u8, page: *Page) !*WebSocket {
|
||||
{
|
||||
if (url.len < 6) {
|
||||
return error.SyntaxError;
|
||||
@@ -103,6 +115,11 @@ pub fn init(url: []const u8, protocols_: ?[]const u8, page: *Page) !*WebSocket {
|
||||
if (std.mem.indexOfScalar(u8, url, '#') != null) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
for (protocols) |protocol| {
|
||||
if (!isValidProtocol(protocol)) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const arena = try page.getArena(.medium, "WebSocket");
|
||||
@@ -124,12 +141,21 @@ pub fn init(url: []const u8, protocols_: ?[]const u8, page: *Page) !*WebSocket {
|
||||
try conn.setWriteCallback(receivedDataCallback);
|
||||
try conn.setHeaderCallback(receivedHeaderCallback);
|
||||
|
||||
var headers = try http_client.newHeaders();
|
||||
errdefer headers.deinit();
|
||||
if (protocols.len > 0) {
|
||||
const header = try std.fmt.allocPrintSentinel(arena, "Sec-WebSocket-Protocol: {s}", .{try std.mem.join(arena, ", ", protocols)}, 0);
|
||||
try headers.add(header);
|
||||
try conn.setHeaders(&headers);
|
||||
}
|
||||
|
||||
const self = try page._factory.eventTargetWithAllocator(arena, WebSocket{
|
||||
._page = page,
|
||||
._conn = conn,
|
||||
._arena = arena,
|
||||
._proto = undefined,
|
||||
._url = resolved_url,
|
||||
._req_headers = headers,
|
||||
._http_client = http_client,
|
||||
});
|
||||
conn.transport = .{ .websocket = self };
|
||||
@@ -206,6 +232,7 @@ pub fn disconnected(self: *WebSocket, err_: ?anyerror) void {
|
||||
fn cleanup(self: *WebSocket) void {
|
||||
if (self._conn) |conn| {
|
||||
self._http_client.removeConn(conn);
|
||||
self._req_headers.deinit();
|
||||
self._conn = null;
|
||||
self.releaseRef(self._page._session);
|
||||
self._send_queue.clearRetainingCapacity();
|
||||
@@ -356,6 +383,10 @@ pub fn getBinaryType(self: *const WebSocket) []const u8 {
|
||||
return @tagName(self._binary_type);
|
||||
}
|
||||
|
||||
pub fn getProtocol(self: *const WebSocket) []const u8 {
|
||||
return self._protocol;
|
||||
}
|
||||
|
||||
pub fn setBinaryType(self: *WebSocket, value: []const u8) void {
|
||||
if (std.meta.stringToEnum(BinaryType, value)) |bt| {
|
||||
self._binary_type = bt;
|
||||
@@ -653,23 +684,24 @@ fn receivedHeaderCallback(buffer: [*]const u8, header_count: usize, buf_len: usi
|
||||
return buf_len;
|
||||
}
|
||||
|
||||
if (self._got_upgrade) {
|
||||
// dont' care about headers once we've gotten the upgrade header
|
||||
return buf_len;
|
||||
}
|
||||
|
||||
const colon = std.mem.indexOfScalarPos(u8, header, 0, ':') orelse {
|
||||
// weird, continue...
|
||||
return buf_len;
|
||||
};
|
||||
|
||||
if (std.ascii.eqlIgnoreCase(header[0..colon], "upgrade") == false) {
|
||||
return buf_len;
|
||||
}
|
||||
|
||||
const header_name = header[0..colon];
|
||||
const value = std.mem.trim(u8, header[colon + 1 ..], " \t\r\n");
|
||||
if (std.ascii.eqlIgnoreCase(value, "websocket")) {
|
||||
self._got_upgrade = true;
|
||||
|
||||
if (std.ascii.eqlIgnoreCase(header_name, "upgrade")) {
|
||||
if (std.ascii.eqlIgnoreCase(value, "websocket")) {
|
||||
self._got_upgrade = true;
|
||||
}
|
||||
} else if (std.ascii.eqlIgnoreCase(header_name, "sec-websocket-protocol")) {
|
||||
// TODO, we should validate this against our sent list.
|
||||
self._protocol = self._arena.dupe(u8, value) catch |err| {
|
||||
log.err(.websocket, "dupe protocol", .{ .err = err });
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
return buf_len;
|
||||
@@ -713,7 +745,7 @@ pub const JsApi = struct {
|
||||
pub const bufferedAmount = bridge.accessor(WebSocket.getBufferedAmount, null, .{});
|
||||
pub const binaryType = bridge.accessor(WebSocket.getBinaryType, WebSocket.setBinaryType, .{});
|
||||
|
||||
pub const protocol = bridge.property("", .{ .template = false });
|
||||
pub const protocol = bridge.accessor(WebSocket.getProtocol, null, .{});
|
||||
pub const extensions = bridge.property("", .{ .template = false });
|
||||
|
||||
pub const onopen = bridge.accessor(WebSocket.getOnOpen, WebSocket.setOnOpen, .{});
|
||||
|
||||
Reference in New Issue
Block a user