mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-07-20 15:03:00 +10:00
ca groundwork
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# Client Handshake process
|
||||
|
||||
Drop clients need to complete a handshake in order to connect to a Drop server. It also trades certificates for encrypted P2P connections.
|
||||
|
||||
## 1. Client requests a handshake
|
||||
Client makes request: `POST /api/v1/client/initiate` with information about the client.
|
||||
|
||||
Server responds with a URL to send the user to. It generates a device ID, which has all the metadata attached.
|
||||
|
||||
## 2. User signs in
|
||||
Client sends user to the provided URL (in external browser). User signs in using the existing authentication stack.
|
||||
|
||||
Server sends redirect to drop://handshake/[id]/[token], where the token is an authentication token to generate the necessary certificates, and the ID is the client ID as generated by the server.
|
||||
|
||||
## 3. Client requests certificates
|
||||
Client makes request: `POST /api/v1/client/handshake` with the token recieved in the previous step.
|
||||
|
||||
The server uses it's CA to generate a public-private key pair, the CN of the client ID. It then sends that pair, plus the CA's public key, to the client, which stores it all.
|
||||
|
||||
The certificate lasts for a year, and is rotated when it has 3 months or less left on it's expiry.
|
||||
|
||||
## 4.a Client requests one-time device endpoint
|
||||
The client generates a nonce and signs it with their private key. This is then attached to any device-related request.
|
||||
|
||||
## 4.b Client wants a long-lived session
|
||||
The client does the same as above, but instead makes the request to `POST /api/v1/client/session`, which generates a session token that lasts for a day. This can then be used in the request to provide authentication.
|
||||
@@ -0,0 +1,34 @@
|
||||
import path from "path";
|
||||
import droplet from "@drop/droplet";
|
||||
import { CertificateStore } from "./store";
|
||||
|
||||
export type CertificateBundle = {
|
||||
priv: string;
|
||||
pub: string;
|
||||
cert: string;
|
||||
};
|
||||
|
||||
/*
|
||||
This is designed to handle client certificates, as described in the README.md
|
||||
*/
|
||||
export class CertificateAuthority {
|
||||
private certificateStore: CertificateStore;
|
||||
|
||||
private root: CertificateBundle;
|
||||
|
||||
constructor(store: CertificateStore, root: CertificateBundle) {
|
||||
this.certificateStore = store;
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
static async new(store: CertificateStore) {
|
||||
const root = await store.fetch("ca");
|
||||
if (root === undefined) {
|
||||
const [priv, pub, cert] = droplet.generateRootCa();
|
||||
const bundle: CertificateBundle = { priv, pub, cert };
|
||||
await store.store("ca", bundle);
|
||||
return new CertificateAuthority(store, bundle);
|
||||
}
|
||||
return new CertificateAuthority(store, root);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { CertificateBundle } from "./ca";
|
||||
|
||||
export type CertificateStore = {
|
||||
store(name: string, data: CertificateBundle): Promise<void>;
|
||||
fetch(name: string): Promise<CertificateBundle | undefined>;
|
||||
};
|
||||
|
||||
export const fsCertificateStore = (base: string) => {
|
||||
const store: CertificateStore = {
|
||||
async store(name: string, data: CertificateBundle) {
|
||||
const filepath = path.join(base, name);
|
||||
fs.writeFileSync(filepath, JSON.stringify(data));
|
||||
},
|
||||
async fetch(name: string) {
|
||||
const filepath = path.join(base, name);
|
||||
if (!fs.existsSync(filepath)) return undefined;
|
||||
return JSON.parse(fs.readFileSync(filepath, "utf-8"));
|
||||
},
|
||||
};
|
||||
return store;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
# Drop Download System
|
||||
The Drop download system uses a torrent-*like* system. It is not torrenting, nor is it compatible with torrenting clients.
|
||||
|
||||
## Clients
|
||||
Drop clients have built-in HTTP APIs that they forward with UPnP. This API exposes different capabilities for different Drop features, like download aggegration and P2P networking. When they sign on, they send a list of supported capabilities to the server.
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
The download co-ordinator's job is to keep track of all the currently online clients.
|
||||
|
||||
When a client signs on and registers itself as a peer
|
||||
|
||||
*/
|
||||
|
||||
class DownloadCoordinator {
|
||||
|
||||
}
|
||||
@@ -26,11 +26,11 @@ export class SessionHandler {
|
||||
|
||||
return data[userSessionKey];
|
||||
}
|
||||
async setSession(h3: H3Event, data: any) {
|
||||
async setSession(h3: H3Event, data: any, expend = false) {
|
||||
const result = await this.sessionProvider.updateSession(h3, userSessionKey, data);
|
||||
if (!result) {
|
||||
const toCreate = { [userSessionKey]: data };
|
||||
await this.sessionProvider.setSession(h3, toCreate);
|
||||
await this.sessionProvider.setSession(h3, toCreate, expend);
|
||||
}
|
||||
}
|
||||
async clearSession(h3: H3Event) {
|
||||
@@ -52,11 +52,11 @@ export class SessionHandler {
|
||||
return user;
|
||||
}
|
||||
|
||||
async setUserId(h3: H3Event, userId: string) {
|
||||
async setUserId(h3: H3Event, userId: string, extend = false) {
|
||||
const result = await this.sessionProvider.updateSession(h3, userIdKey, userId);
|
||||
if (!result) {
|
||||
const toCreate = { [userIdKey]: userId };
|
||||
await this.sessionProvider.setSession(h3, toCreate);
|
||||
await this.sessionProvider.setSession(h3, toCreate, extend);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import moment from "moment";
|
||||
import { Session, SessionProvider } from "./types";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export default function createMemorySessionHandler() {
|
||||
const sessions: { [key: string]: Session } = {}
|
||||
const sessions: { [key: string]: Session } = {};
|
||||
|
||||
const sessionCookieName = "drop-session";
|
||||
const sessionCookieName = "drop-session";
|
||||
|
||||
const memoryProvider: SessionProvider = {
|
||||
async setSession(h3, data) {
|
||||
const existingCookie = getCookie(h3, sessionCookieName);
|
||||
if (existingCookie) delete sessions[existingCookie]; // Clear any previous session
|
||||
const memoryProvider: SessionProvider = {
|
||||
async setSession(h3, data, extend = false) {
|
||||
const existingCookie = getCookie(h3, sessionCookieName);
|
||||
if (existingCookie) delete sessions[existingCookie]; // Clear any previous session
|
||||
|
||||
const cookie = uuidv4();
|
||||
const expiry = moment().add(31, 'day');
|
||||
setCookie(h3, sessionCookieName, cookie, { expires: expiry.toDate() });
|
||||
const cookie = uuidv4();
|
||||
const expiry = moment().add(31, extend ? "month" : "day");
|
||||
setCookie(h3, sessionCookieName, cookie, { expires: expiry.toDate() });
|
||||
|
||||
sessions[cookie] = data;
|
||||
return true;
|
||||
},
|
||||
async updateSession(h3, key, data) {
|
||||
const cookie = getCookie(h3, sessionCookieName);
|
||||
if (!cookie) return false;
|
||||
sessions[cookie] = data;
|
||||
return true;
|
||||
},
|
||||
async updateSession(h3, key, data) {
|
||||
const cookie = getCookie(h3, sessionCookieName);
|
||||
if (!cookie) return false;
|
||||
|
||||
sessions[cookie] = Object.assign({}, sessions[cookie], { [key]: data });
|
||||
return true;
|
||||
},
|
||||
async getSession(h3) {
|
||||
const cookie = getCookie(h3, sessionCookieName);
|
||||
if (!cookie) return undefined;
|
||||
sessions[cookie] = Object.assign({}, sessions[cookie], { [key]: data });
|
||||
return true;
|
||||
},
|
||||
async getSession(h3) {
|
||||
const cookie = getCookie(h3, sessionCookieName);
|
||||
if (!cookie) return undefined;
|
||||
|
||||
return sessions[cookie] as any; // Wild type cast because we let the user specify types if they want
|
||||
},
|
||||
async clearSession(h3) {
|
||||
const cookie = getCookie(h3, sessionCookieName);
|
||||
if (!cookie) return;
|
||||
return sessions[cookie] as any; // Wild type cast because we let the user specify types if they want
|
||||
},
|
||||
async clearSession(h3) {
|
||||
const cookie = getCookie(h3, sessionCookieName);
|
||||
if (!cookie) return;
|
||||
|
||||
delete sessions[cookie];
|
||||
deleteCookie(h3, sessionCookieName);
|
||||
},
|
||||
};
|
||||
delete sessions[cookie];
|
||||
deleteCookie(h3, sessionCookieName);
|
||||
},
|
||||
};
|
||||
|
||||
return memoryProvider;
|
||||
}
|
||||
return memoryProvider;
|
||||
}
|
||||
|
||||
Vendored
+9
-5
@@ -3,8 +3,12 @@ import { H3Event } from "h3";
|
||||
export type Session = { [key: string]: any };
|
||||
|
||||
export interface SessionProvider {
|
||||
setSession: (h3: H3Event, data: Session) => Promise<boolean>;
|
||||
updateSession: (h3: H3Event, key: string, data: any) => Promise<boolean>;
|
||||
getSession: <T extends Session>(h3: H3Event) => Promise<T | undefined>;
|
||||
clearSession: (h3: H3Event) => Promise<void>;
|
||||
}
|
||||
setSession: (
|
||||
h3: H3Event,
|
||||
data: Session,
|
||||
extend?: boolean
|
||||
) => Promise<boolean>;
|
||||
updateSession: (h3: H3Event, key: string, data: any) => Promise<boolean>;
|
||||
getSession: <T extends Session>(h3: H3Event) => Promise<T | undefined>;
|
||||
clearSession: (h3: H3Event) => Promise<void>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user