Skip to content

Commit 62f6160

Browse files
committed
feat: add support for header authentication
fixes #119
1 parent 7bc99e4 commit 62f6160

File tree

3 files changed

+117
-18
lines changed

3 files changed

+117
-18
lines changed

src/edge.js

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,15 @@ export async function handleApiRequest(request, env) {
145145
return handleApiCall(url, request, env);
146146
}
147147

148-
let authActions;
149-
const auth = url.searchParams.get('Authorization');
148+
const protocols = request.headers.get('sec-websocket-protocol')?.split(',');
149+
const token = protocols?.find((hdr) => hdr !== 'yjs')?.trim();
150+
let auth = '';
151+
if (token) {
152+
auth = `Bearer ${token}`;
153+
console.log('[worker] use token from sec-websocket-protocol header.');
154+
} else {
155+
auth = url.searchParams.get('Authorization');
156+
}
150157

151158
// We need to massage the path somewhat because on connections from localhost safari sends
152159
// a path with only one slash for some reason.
@@ -169,6 +176,7 @@ export async function handleApiRequest(request, env) {
169176

170177
// Check if we have the authorization for the room (this is a poor man's solution as right now
171178
// only da-admin knows).
179+
let authActions;
172180
try {
173181
const opts = { method: 'HEAD' };
174182
if (auth) {
@@ -221,6 +229,7 @@ export async function handleApiRequest(request, env) {
221229
['X-timing-docroom-get-duration', timingDocRoomGetDuration],
222230
['X-auth-actions', authActions],
223231
];
232+
224233
if (auth) {
225234
headers.push(['Authorization', auth]);
226235
}
@@ -353,13 +362,18 @@ export class DocRoom {
353362
const timingSetupWebSocketDuration = Date.now() - timingBeforeSetupWebsocket;
354363

355364
const reqHeaders = request.headers;
356-
const respheaders = new Headers();
357-
respheaders.set('X-1-timing-da-admin-head-duration', reqHeaders.get('X-timing-da-admin-head-duration'));
358-
respheaders.set('X-2-timing-docroom-get-duration', reqHeaders.get('X-timing-docroom-get-duration'));
359-
respheaders.set('X-4-timing-da-admin-get-duration', timingData.get('timingDaAdminGetDuration'));
360-
respheaders.set('X-5-timing-read-state-duration', timingData.get('timingReadStateDuration'));
361-
respheaders.set('X-7-timing-setup-websocket-duration', timingSetupWebSocketDuration);
362-
respheaders.set('X-9-timing-full-duration', Date.now() - reqHeaders.get('X-timing-start'));
365+
const respheaders = new Headers({
366+
'X-1-timing-da-admin-head-duration': reqHeaders.get('X-timing-da-admin-head-duration'),
367+
'X-2-timing-docroom-get-duration': reqHeaders.get('X-timing-docroom-get-duration'),
368+
'X-4-timing-da-admin-get-duration': timingData.get('timingDaAdminGetDuration'),
369+
'X-5-timing-read-state-duration': timingData.get('timingReadStateDuration'),
370+
'X-7-timing-setup-websocket-duration': timingSetupWebSocketDuration,
371+
'X-9-timing-full-duration': Date.now() - reqHeaders.get('X-timing-start'),
372+
});
373+
const protocols = reqHeaders.get('sec-websocket-protocol')?.split(',');
374+
if (protocols?.includes('yjs')) {
375+
respheaders.set('sec-websocket-protocol', 'yjs');
376+
}
363377

364378
// Now we return the other end of the pair to the client.
365379
return new Response(null, { status: successCode, headers: respheaders, webSocket: pair[0] });

test/edge.test.js

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -312,16 +312,18 @@ describe('Worker test suite', () => {
312312

313313
const daadmin = { blah: 1234 };
314314
const dr = new DocRoom({ storage: null }, { daadmin });
315-
const headers = new Map();
316-
headers.set('Upgrade', 'websocket');
317-
headers.set('Authorization', 'au123');
318-
headers.set('X-collab-room', 'http://foo.bar/1/2/3.html');
315+
const headers = new Headers({
316+
Upgrade: 'websocket',
317+
Authorization: 'au123',
318+
'X-collab-room': 'http://foo.bar/1/2/3.html',
319+
});
319320

320321
const req = {
321322
headers,
322323
url: 'http://localhost:4711/',
323324
};
324325
const resp = await dr.fetch(req, {}, 306);
326+
assert.equal(resp.headers.get('sec-websocket-protocol'), undefined);
325327
assert.equal(306 /* fabricated websocket response code */, resp.status);
326328

327329
assert.equal(1, bindCalled.length);
@@ -345,6 +347,43 @@ describe('Worker test suite', () => {
345347
}
346348
});
347349

350+
it('Test DocRoom fetch (with protocols)', async () => {
351+
const savedNWSP = DocRoom.newWebSocketPair;
352+
const savedBS = persistence.bindState;
353+
354+
try {
355+
persistence.bindState = async (nm, d, c) => new Map();
356+
357+
const wsp0 = {};
358+
const wsp1 = {
359+
accept() { },
360+
addEventListener(type) { },
361+
close() { },
362+
};
363+
DocRoom.newWebSocketPair = () => [wsp0, wsp1];
364+
365+
const daadmin = { blah: 1234 };
366+
const dr = new DocRoom({ storage: null }, { daadmin });
367+
const headers = new Headers({
368+
Upgrade: 'websocket',
369+
Authorization: 'au123',
370+
'X-collab-room': 'http://foo.bar/1/2/3.html',
371+
'sec-websocket-protocol': 'yjs,foobar',
372+
});
373+
374+
const req = {
375+
headers,
376+
url: 'http://localhost:4711/',
377+
};
378+
const resp = await dr.fetch(req, {}, 306);
379+
assert.equal(resp.headers.get('sec-websocket-protocol'), 'yjs');
380+
assert.equal(306 /* fabricated websocket response code */, resp.status);
381+
} finally {
382+
DocRoom.newWebSocketPair = savedNWSP;
383+
persistence.bindState = savedBS;
384+
}
385+
});
386+
348387
it('Test DocRoom fetch expects websocket', async () => {
349388
const dr = new DocRoom({ storage: null }, null);
350389

@@ -658,12 +697,10 @@ describe('Worker test suite', () => {
658697
assert.equal('https://admin.da.live/laaa.html', rfreq.headers.get('X-collab-room'));
659698
});
660699

661-
it('Test handleApiRequest via Service Binding', async () => {
662-
const headers = new Map();
663-
headers.set('myheader', 'myval');
700+
it('Test handleApiRequest via Service Binding (param auth)', async () => {
664701
const req = {
665702
url: 'http://do.re.mi/https://admin.da.live/laaa.html?Authorization=lala',
666-
headers,
703+
headers: new Headers(),
667704
};
668705

669706
// eslint-disable-next-line consistent-return
@@ -684,9 +721,54 @@ describe('Worker test suite', () => {
684721
assert.equal(410, res.status);
685722
});
686723

724+
it('Test handleApiRequest via Service Binding (header auth)', async () => {
725+
const req = {
726+
url: 'http://do.re.mi/https://admin.da.live/laaa.html',
727+
headers: new Headers({
728+
'sec-websocket-protocol': 'yjs,test-token',
729+
}),
730+
};
731+
732+
// eslint-disable-next-line consistent-return
733+
const mockDaAdminFetch = async (url, opts) => {
734+
assert.equal(opts.headers.get('Authorization'), 'Bearer test-token');
735+
return new Response(null, { status: 200 });
736+
};
737+
738+
// eslint-disable-next-line no-shadow
739+
const mockRoomFetch = async (req) => new Response(null, {
740+
status: 200,
741+
headers: {
742+
'sec-websocket-protocol': req.headers.get('sec-websocket-protocol'),
743+
},
744+
});
745+
746+
const mockRoom = {
747+
fetch: mockRoomFetch,
748+
};
749+
750+
const rooms = {
751+
idFromName: (name) => `id${hash(name)}`,
752+
get: (id) => mockRoom,
753+
};
754+
755+
const env = {
756+
daadmin: { fetch: mockDaAdminFetch },
757+
rooms,
758+
};
759+
760+
const res = await handleApiRequest(req, env);
761+
assert.equal(200, res.status);
762+
assert.deepEqual(Object.fromEntries(res.headers.entries()), {
763+
// test that service passes the sec-websocket-protocol header to docroom
764+
'sec-websocket-protocol': 'yjs,test-token',
765+
});
766+
});
767+
687768
it('Test handleApiRequest wrong host', async () => {
688769
const req = {
689770
url: 'http://do.re.mi/https://some.where.else/hihi.html',
771+
headers: new Headers(),
690772
};
691773

692774
const res = await handleApiRequest(req, {});
@@ -696,6 +778,7 @@ describe('Worker test suite', () => {
696778
it('Test handleApiRequest document not found (404)', async () => {
697779
const req = {
698780
url: 'http://do.re.mi/https://admin.da.live/nonexistent.html',
781+
headers: new Headers(),
699782
};
700783

701784
const mockFetch = async (url, opts) => new Response(null, { status: 404 });
@@ -710,6 +793,7 @@ describe('Worker test suite', () => {
710793
it('Test handleApiRequest not authorized', async () => {
711794
const req = {
712795
url: 'http://do.re.mi/https://admin.da.live/hihi.html',
796+
headers: new Headers(),
713797
};
714798

715799
const mockFetch = async (url, opts) => new Response(null, { status: 401 });
@@ -723,6 +807,7 @@ describe('Worker test suite', () => {
723807
it('Test handleApiRequest da-admin fetch exception', async () => {
724808
const req = {
725809
url: 'http://do.re.mi/https://admin.da.live/test.html',
810+
headers: new Headers(),
726811
};
727812

728813
// Mock daadmin.fetch to throw an exception

wrangler.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ RETURN_STACK_TRACES = "false"
6868
# ----------------------------------------------------------------------
6969
# dev environment (local)
7070
[env.dev]
71-
name = "da-collab-dev"
71+
name = "da-collab-local"
7272

7373
services = [
7474
{ binding = "daadmin", service = "da-admin-local" }

0 commit comments

Comments
 (0)