Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle WebGL reading of pixels #480

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 133 additions & 7 deletions src/canvas.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,118 @@
import { getDataKeySync } from './crypto.js'
import Seedrandom from 'seedrandom'
import { postDebugMessage } from './utils.js'

/**
* @param {HTMLCanvasElement} canvas
* Copies the contents of a 2D canvas to a WebGL texture
* @param {CanvasRenderingContext2D} ctx2d
* @param {WebGLRenderingContext | WebGL2RenderingContext} ctx3d
*/
export function copy2dContextToWebGLContext (ctx2d, ctx3d) {
const canvas2d = ctx2d.canvas
const imageData = ctx2d.getImageData(0, 0, canvas2d.width, canvas2d.height)
const pixelData = new Uint8Array(imageData.data.buffer)

const texture = ctx3d.createTexture()
ctx3d.bindTexture(ctx3d.TEXTURE_2D, texture)
ctx3d.texImage2D(ctx3d.TEXTURE_2D, 0, ctx3d.RGBA, canvas2d.width, canvas2d.height, 0, ctx3d.RGBA, ctx3d.UNSIGNED_BYTE, pixelData)
ctx3d.texParameteri(ctx3d.TEXTURE_2D, ctx3d.TEXTURE_WRAP_S, ctx3d.CLAMP_TO_EDGE)
ctx3d.texParameteri(ctx3d.TEXTURE_2D, ctx3d.TEXTURE_WRAP_T, ctx3d.CLAMP_TO_EDGE)
ctx3d.texParameteri(ctx3d.TEXTURE_2D, ctx3d.TEXTURE_MIN_FILTER, ctx3d.LINEAR)
ctx3d.texParameteri(ctx3d.TEXTURE_2D, ctx3d.TEXTURE_MAG_FILTER, ctx3d.LINEAR)

const vertexShaderSource = `
attribute vec2 a_position;
varying vec2 v_texCoord;

void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_position * 0.5 + 0.5;
}`
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_texture;
varying vec2 v_texCoord;

void main() {
gl_FragColor = texture2D(u_texture, v_texCoord);
}`

const vertexShader = createShader(ctx3d, ctx3d.VERTEX_SHADER, vertexShaderSource)
const fragmentShader = createShader(ctx3d, ctx3d.FRAGMENT_SHADER, fragmentShaderSource)

const program = ctx3d.createProgram()
// Shouldn't happen but bail happy if it does
if (!program || !vertexShader || !fragmentShader) {
postDebugMessage('Unable to initialize the shader program')
return
}
ctx3d.attachShader(program, vertexShader)
ctx3d.attachShader(program, fragmentShader)
ctx3d.linkProgram(program)

if (!ctx3d.getProgramParameter(program, ctx3d.LINK_STATUS)) {
// Shouldn't happen but bail happy if it does
postDebugMessage('Unable to initialize the shader program: ' + ctx3d.getProgramInfoLog(program))
}
const positionAttributeLocation = ctx3d.getAttribLocation(program, 'a_position')
const positionBuffer = ctx3d.createBuffer()
ctx3d.bindBuffer(ctx3d.ARRAY_BUFFER, positionBuffer)
const positions = [
-1.0, -1.0,
1.0, -1.0,
-1.0, 1.0,
1.0, 1.0
]
ctx3d.bufferData(ctx3d.ARRAY_BUFFER, new Float32Array(positions), ctx3d.STATIC_DRAW)

ctx3d.useProgram(program)
ctx3d.enableVertexAttribArray(positionAttributeLocation)
ctx3d.vertexAttribPointer(positionAttributeLocation, 2, ctx3d.FLOAT, false, 0, 0)

const textureUniformLocation = ctx3d.getUniformLocation(program, 'u_texture')
ctx3d.activeTexture(ctx3d.TEXTURE0)
ctx3d.bindTexture(ctx3d.TEXTURE_2D, texture)
ctx3d.uniform1i(textureUniformLocation, 0)
ctx3d.drawArrays(ctx3d.TRIANGLE_STRIP, 0, 4)
}

/**
* @param {WebGLRenderingContext | WebGL2RenderingContext} gl
* @param {number} type
* @param {string} source
* @returns {WebGLShader | null}
*/
function createShader (gl, type, source) {
const shader = gl.createShader(type)
if (!shader) {
return null
}
gl.shaderSource(shader, source)
gl.compileShader(shader)
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
postDebugMessage('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader))
return null
}
return shader
}

/**
* @typedef {Object} OffscreenCanvasInfo
* @property {HTMLCanvasElement | OffscreenCanvas} offScreenCanvas
* @property {CanvasRenderingContext2D | WebGL2RenderingContext | WebGLRenderingContext} offScreenCtx
* @property {WebGL2RenderingContext | WebGLRenderingContext} [offScreenWebGlCtx]
*/

/**
* @param {HTMLCanvasElement | OffscreenCanvas} canvas
* @param {string} domainKey
* @param {string} sessionKey
* @param {any} getImageDataProxy
* @param {CanvasRenderingContext2D | WebGL2RenderingContext | WebGLRenderingContext} ctx?
* @param {boolean} [shouldCopy2dContextToWebGLContext]
* @returns {OffscreenCanvasInfo | null}
*/
export function computeOffScreenCanvas (canvas, domainKey, sessionKey, getImageDataProxy, ctx) {
export function computeOffScreenCanvas (canvas, domainKey, sessionKey, getImageDataProxy, ctx, shouldCopy2dContextToWebGLContext = false) {
if (!ctx) {
// @ts-expect-error - Type 'null' is not assignable to type 'CanvasRenderingContext2D | WebGL2RenderingContext | WebGLRenderingContext'.
ctx = canvas.getContext('2d')
Expand All @@ -19,14 +123,16 @@ export function computeOffScreenCanvas (canvas, domainKey, sessionKey, getImageD
offScreenCanvas.width = canvas.width
offScreenCanvas.height = canvas.height
const offScreenCtx = offScreenCanvas.getContext('2d')
// Should not happen, but just in case
if (!offScreenCtx) {
return null
}

let rasterizedCtx = ctx
// If we're not a 2d canvas we need to rasterise first into 2d
const rasterizeToCanvas = !(ctx instanceof CanvasRenderingContext2D)
if (rasterizeToCanvas) {
// @ts-expect-error - Type 'CanvasRenderingContext2D | null' is not assignable to type 'CanvasRenderingContext2D | WebGL2RenderingContext | WebGLRenderingContext'.
rasterizedCtx = offScreenCtx
// @ts-expect-error - 'offScreenCtx' is possibly 'null'.
offScreenCtx.drawImage(canvas, 0, 0)
}

Expand All @@ -35,14 +141,34 @@ export function computeOffScreenCanvas (canvas, domainKey, sessionKey, getImageD
imageData = modifyPixelData(imageData, sessionKey, domainKey, canvas.width)

if (rasterizeToCanvas) {
// @ts-expect-error - Type 'null' is not assignable to type 'CanvasRenderingContext2D'.
clearCanvas(offScreenCtx)
}

// @ts-expect-error - 'offScreenCtx' is possibly 'null'.
offScreenCtx.putImageData(imageData, 0, 0)

return { offScreenCanvas, offScreenCtx }
/** @type {OffscreenCanvasInfo} */
const output = { offScreenCanvas, offScreenCtx }
if (shouldCopy2dContextToWebGLContext) {
const offScreenWebGlCanvas = document.createElement('canvas')
offScreenWebGlCanvas.width = canvas.width
offScreenWebGlCanvas.height = canvas.height
let offScreenWebGlCtx
if (ctx instanceof WebGLRenderingContext) {
offScreenWebGlCtx = offScreenWebGlCanvas.getContext('webgl')
} else {
offScreenWebGlCtx = offScreenWebGlCanvas.getContext('webgl2')
}
if (offScreenWebGlCtx) {
try {
// Clone the 2d context back into the pages webgl context
copy2dContextToWebGLContext(offScreenCtx, offScreenWebGlCtx)
output.offScreenWebGlCtx = offScreenWebGlCtx
} catch (e) {
postDebugMessage('Failed to call readPixels on offscreen canvas', e)
}
}
}
return output
}

/**
Expand Down
29 changes: 24 additions & 5 deletions src/features/fingerprinting-canvas.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DDGProxy, DDGReflect } from '../utils'
import { computeOffScreenCanvas } from '../canvas'
import { DDGProxy, DDGReflect, postDebugMessage } from '../utils'
import { computeOffScreenCanvas, copy2dContextToWebGLContext } from '../canvas'
import ContentFeature from '../content-feature'

export default class FingerprintingCanvas extends ContentFeature {
Expand Down Expand Up @@ -112,6 +112,7 @@ export default class FingerprintingCanvas extends ContentFeature {
if ('WebGL2RenderingContext' in globalThis) {
glContexts.push(WebGL2RenderingContext)
}
const webGLReadMethods = ['readPixels']
for (const context of glContexts) {
for (const methodName of unsafeGlMethods) {
// Some methods are browser specific
Expand All @@ -126,6 +127,23 @@ export default class FingerprintingCanvas extends ContentFeature {
unsafeProxy.overload()
}
}

if (this.getFeatureSettingEnabled('webGlReadMethods')) {
for (const methodName of webGLReadMethods) {
const webGLReadMethodsProxy = new DDGProxy(featureName, context.prototype, methodName, {
apply (target, thisArg, args) {
if (thisArg) {
const { offScreenWebGlCtx } = getCachedOffScreenCanvasOrCompute(thisArg.canvas, domainKey, sessionKey, true)
if (offScreenWebGlCtx) {
return DDGReflect.apply(target, offScreenWebGlCtx, args)
}
}
return DDGReflect.apply(target, thisArg, args)
}
})
webGLReadMethodsProxy.overload()
}
}
}
}

Expand Down Expand Up @@ -153,17 +171,18 @@ export default class FingerprintingCanvas extends ContentFeature {
/**
* Get cached offscreen if one exists, otherwise compute one
*
* @param {HTMLCanvasElement} canvas
* @param {HTMLCanvasElement | OffscreenCanvas} canvas
* @param {string} domainKey
* @param {string} sessionKey
* @returns {import('../canvas').OffscreenCanvasInfo}
*/
function getCachedOffScreenCanvasOrCompute (canvas, domainKey, sessionKey) {
function getCachedOffScreenCanvasOrCompute (canvas, domainKey, sessionKey, copy2dContextToWebGLContext) {
let result
if (canvasCache.has(canvas)) {
result = canvasCache.get(canvas)
} else {
const ctx = canvasContexts.get(canvas)
result = computeOffScreenCanvas(canvas, domainKey, sessionKey, getImageDataProxy, ctx)
result = computeOffScreenCanvas(canvas, domainKey, sessionKey, getImageDataProxy, ctx, copy2dContextToWebGLContext)
canvasCache.set(canvas, result)
}
return result
Expand Down