Skip to content

Commit 221c190

Browse files
committed
Version protocol handshake & backwards compatibility #1150
1 parent d377951 commit 221c190

3 files changed

Lines changed: 166 additions & 12 deletions

File tree

browser/lib/src/client.ts

Lines changed: 132 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const isFileLike = (file: FileOrFileLike): file is FileLike =>
3232
'blob' in file && 'name' in file;
3333

3434
const JSON_AD_MIME = 'application/ad+json';
35+
const ATOMIC_SERVER_VERSION_HEADER = 'X-Atomic-Server-Version';
36+
const MIN_DID_AUTH_SERVER_MINOR = 40;
3537

3638
interface FetchResourceOptions extends ParseOpts {
3739
/**
@@ -65,6 +67,9 @@ interface HTTPResourceResult {
6567
/** Contains a `fetch` instance, provides methods to GET and POST several types */
6668
export class Client {
6769
private __fetchOverride?: typeof fetch;
70+
private warnedDidAuthCompatibilityOrigins = new Set<string>();
71+
private supportsDidAuthByOrigin = new Map<string, boolean>();
72+
private serverVersionByOrigin = new Map<string, string>();
6873

6974
public constructor(fetchOverride?: typeof fetch) {
7075
if (fetchOverride) {
@@ -164,19 +169,25 @@ export class Client {
164169

165170
// Sign the request with the actual URL being fetched (not the raw DID
166171
// subject) since the server verifies against the full HTTP URL.
167-
if (signInfo && !subject.startsWith('https://atomicdata.dev')) {
168-
// Cookies only work in browsers for same-origin requests right now
169-
// https://github.com/atomicdata-dev/atomic-data-browser/issues/253
170-
if (hasBrowserAPI() && subject.startsWith(window.location.origin)) {
171-
if (!checkAuthenticationCookie()) {
172-
setCookieAuthentication(signInfo.serverURL, signInfo.agent);
172+
if (signInfo) {
173+
if (
174+
this.shouldSkipDidAuthForLegacyServer(url, signInfo.agent.subject)
175+
) {
176+
this.warnDidAuthCompatibility(url);
177+
} else if (!subject.startsWith('https://atomicdata.dev')) {
178+
// Cookies only work in browsers for same-origin requests right now
179+
// https://github.com/atomicdata-dev/atomic-data-browser/issues/253
180+
if (hasBrowserAPI() && subject.startsWith(window.location.origin)) {
181+
if (!checkAuthenticationCookie()) {
182+
setCookieAuthentication(signInfo.serverURL, signInfo.agent);
183+
}
184+
} else {
185+
requestHeaders = await signRequest(
186+
url,
187+
signInfo.agent,
188+
requestHeaders,
189+
);
173190
}
174-
} else {
175-
requestHeaders = await signRequest(
176-
url,
177-
signInfo.agent,
178-
requestHeaders,
179-
);
180191
}
181192
}
182193

@@ -191,6 +202,7 @@ export class Client {
191202
method: method ?? 'GET',
192203
body: bodyReq,
193204
});
205+
this.recordServerVersionFromResponse(url, response);
194206
const body = await response.text();
195207

196208
if (response.status === 200) {
@@ -323,4 +335,112 @@ export class Client {
323335

324336
return fetch(...params);
325337
}
338+
339+
/**
340+
* Legacy servers (<0.40) cannot verify DID-based auth for cross-origin
341+
* requests. We default to legacy compatibility unless a custom handshake is
342+
* implemented elsewhere.
343+
*/
344+
private shouldSkipDidAuthForLegacyServer(
345+
url: string,
346+
agentSubject?: string,
347+
): boolean {
348+
if (!agentSubject?.startsWith('did:ad:')) {
349+
return false;
350+
}
351+
352+
if (!hasBrowserAPI()) {
353+
return false;
354+
}
355+
356+
const requestOrigin = this.tryGetOrigin(url);
357+
358+
if (!requestOrigin) {
359+
return false;
360+
}
361+
362+
if (requestOrigin === window.location.origin) {
363+
return false;
364+
}
365+
366+
const supportsDidAuth = this.supportsDidAuthByOrigin.get(requestOrigin);
367+
368+
// Legacy-safe default: if we don't know this origin yet, assume <0.40.
369+
return supportsDidAuth !== true;
370+
}
371+
372+
private warnDidAuthCompatibility(url: string): void {
373+
if (!hasBrowserAPI()) {
374+
return;
375+
}
376+
377+
const origin = this.tryGetOrigin(url);
378+
379+
if (!origin) {
380+
return;
381+
}
382+
383+
if (this.warnedDidAuthCompatibilityOrigins.has(origin)) {
384+
return;
385+
}
386+
387+
const version = this.serverVersionByOrigin.get(origin);
388+
const reason = version
389+
? `server version '${version}' does not support DID auth`
390+
: `server version unknown (assuming <0.40)`;
391+
392+
this.warnedDidAuthCompatibilityOrigins.add(origin);
393+
console.warn(
394+
`[atomic-lib] Skipping signed auth request for DID agent on cross-origin request to '${origin}': ${reason}.`,
395+
);
396+
}
397+
398+
private recordServerVersionFromResponse(
399+
url: string,
400+
response: Response,
401+
): void {
402+
const version = response.headers.get(ATOMIC_SERVER_VERSION_HEADER);
403+
404+
if (!version) {
405+
return;
406+
}
407+
408+
const origin = this.tryGetOrigin(url);
409+
410+
if (!origin) {
411+
return;
412+
}
413+
414+
this.serverVersionByOrigin.set(origin, version);
415+
this.supportsDidAuthByOrigin.set(
416+
origin,
417+
this.versionSupportsDidAuth(version),
418+
);
419+
}
420+
421+
private versionSupportsDidAuth(version: string): boolean {
422+
const match = version.match(/^(\d+)\.(\d+)(?:\.(\d+))?/);
423+
424+
if (!match) {
425+
return false;
426+
}
427+
428+
const major = Number.parseInt(match[1], 10);
429+
const minor = Number.parseInt(match[2], 10);
430+
431+
if (Number.isNaN(major) || Number.isNaN(minor)) {
432+
return false;
433+
}
434+
435+
return major > 0 || (major === 0 && minor >= MIN_DID_AUTH_SERVER_MINOR);
436+
}
437+
438+
private tryGetOrigin(url: string): string | undefined {
439+
try {
440+
return new URL(url, hasBrowserAPI() ? window.location.origin : undefined)
441+
.origin;
442+
} catch {
443+
return undefined;
444+
}
445+
}
326446
}

browser/lib/src/websockets.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createAuthentication } from './authentication.js';
2+
import { hasBrowserAPI } from './hasBrowserAPI.js';
23
import { parseAndApplyCommit } from './index.js';
34
import { JSONADParser } from './parse.js';
45
import type { Resource } from './resource.js';
@@ -115,6 +116,19 @@ export class WSClient {
115116
return;
116117
}
117118

119+
// Legacy-safe fallback: remote servers may not support DID-based websocket auth.
120+
if (
121+
agent.subject.startsWith('did:ad:') &&
122+
hasBrowserAPI() &&
123+
!this.isSameOriginWebSocket()
124+
) {
125+
console.warn(
126+
`Skipping websocket authentication for DID agent on cross-origin socket ${this.ws.url}. Assuming legacy server compatibility (<0.40).`,
127+
);
128+
129+
return;
130+
}
131+
118132
await this.openPromise;
119133

120134
if (this.authenticatedWith === agent.subject) {
@@ -306,4 +320,18 @@ export class WSClient {
306320
this.ws.addEventListener('message', listener);
307321
});
308322
}
323+
324+
private isSameOriginWebSocket(): boolean {
325+
if (!hasBrowserAPI()) {
326+
return true;
327+
}
328+
329+
try {
330+
const wsOrigin = new URL(this.ws.url).origin;
331+
332+
return wsOrigin === window.location.origin;
333+
} catch {
334+
return false;
335+
}
336+
}
309337
}

server/src/serve.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ fn spawn_dht_announcer(appstate: crate::appstate::AppState) {
113113

114114
// Increase the maximum payload size (for POSTing a body, for example) to 50MB
115115
const PAYLOAD_MAX: usize = 50_242_880;
116+
const SERVER_VERSION_HEADER: &str = "X-Atomic-Server-Version";
117+
const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
116118

117119
/// Start the server
118120
pub async fn serve(config: crate::config::Config) -> AtomicServerResult<()> {
@@ -140,6 +142,10 @@ pub async fn serve(config: crate::config::Config) -> AtomicServerResult<()> {
140142
.app_data(web::PayloadConfig::new(PAYLOAD_MAX))
141143
.app_data(web::Data::new(appstate.clone()))
142144
.wrap(cors)
145+
.wrap(
146+
middleware::DefaultHeaders::new()
147+
.add((SERVER_VERSION_HEADER, SERVER_VERSION)),
148+
)
143149
.wrap(tracing_actix_web::TracingLogger::default())
144150
.wrap(middleware::Compress::default())
145151
// Here are the actual handlers / endpoints

0 commit comments

Comments
 (0)