First pass at migrating to rcp-style api

This commit is contained in:
Martin Kleinschrodt 2018-10-05 15:00:47 +02:00
parent 986521ff6f
commit d94053c25b
18 changed files with 6107 additions and 520 deletions

6
lerna.json Normal file
View File

@ -0,0 +1,6 @@
{
"packages": [
"packages/*"
],
"version": "0.0.0"
}

5623
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,8 @@
"packages/*"
],
"devDependencies": {
"lerna": "^3.4.0",
"typescript": "^3.0.3"
}
},
"dependencies": {}
}

View File

@ -9,11 +9,6 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg="
},
"big-integer": {
"version": "1.6.30",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.30.tgz",
@ -289,10 +284,21 @@
"colors": "^1.1.2",
"elementtree": "^0.1.6",
"lodash": "^4.3.0",
"plist": "github:xiangpingmeng/plist.js#5ccd600b0b7fd3ae204edc9e69c1c30406d07747",
"shelljs": "^0.7.0",
"tostr": "^0.1.0",
"xcode": "^1.0.0"
},
"dependencies": {
"plist": {
"version": "github:xiangpingmeng/plist.js#5ccd600b0b7fd3ae204edc9e69c1c30406d07747",
"from": "github:xiangpingmeng/plist.js#5ccd600b0b7fd3ae204edc9e69c1c30406d07747",
"requires": {
"base64-js": "0.0.8",
"util-deprecate": "1.0.2",
"xmlbuilder": "4.0.0",
"xmldom": "0.1.x"
}
}
}
},
"cordova-ios": {
@ -759,16 +765,6 @@
"resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz",
"integrity": "sha1-z4uvrm7d/0tafvsYUmnqr0YQ3b0="
},
"plist": {
"version": "github:xiangpingmeng/plist.js#5ccd600b0b7fd3ae204edc9e69c1c30406d07747",
"from": "github:xiangpingmeng/plist.js",
"requires": {
"base64-js": "0.0.8",
"util-deprecate": "1.0.2",
"xmlbuilder": "4.0.0",
"xmldom": "0.1.x"
}
},
"rechoir": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
@ -842,11 +838,6 @@
"resolved": "https://registry.npmjs.org/tostr/-/tostr-0.1.0.tgz",
"integrity": "sha1-cjm6H6gHyBAMlTLNIxvwQat3lik="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"uuid": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz",
@ -883,21 +874,6 @@
}
}
},
"xmlbuilder": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.0.0.tgz",
"integrity": "sha1-mLj2UcowqmJANvEn0RzGbce5B6M=",
"requires": {
"lodash": "^3.5.0"
},
"dependencies": {
"lodash": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
"integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y="
}
}
},
"xmldom": {
"version": "0.1.27",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz",

View File

@ -1,4 +1,6 @@
import { errFromRequest } from "./error";
import { Err, ErrorCode } from "./error";
import { marshal, unmarshal } from "./encoding";
import { Request, Response, Sender } from "./transport";
export type Method = "GET" | "POST" | "PUT" | "DELETE";
@ -13,11 +15,7 @@ export async function request(
return new Promise<XMLHttpRequest>((resolve, reject) => {
req.onreadystatechange = () => {
if (req.readyState === 4) {
if (req.status.toString()[0] !== "2") {
reject(errFromRequest(req));
} else {
resolve(req);
}
resolve(req);
}
};
@ -28,7 +26,25 @@ export async function request(
}
req.send(body);
} catch (e) {
throw errFromRequest(req);
reject(new Err(ErrorCode.FAILED_CONNECTION));
}
});
}
export class AjaxSender implements Sender {
constructor(public url: string) {}
async send(req: Request): Promise<Response> {
const res = await request(
"POST",
this.url,
marshal(req),
new Map<string, string>([["Content-Type", "application/json"], ["Accept", "application/json"]])
);
return unmarshal(res.responseText);
}
async receive(): Promise<Response> {
throw new Err(ErrorCode.NOT_SUPPORTED);
}
}

View File

@ -8,6 +8,7 @@ import { Invite } from "./invite";
import { DateString } from "./encoding";
import { API } from "./api";
import { Client } from "./client";
import { AjaxSender } from "./ajax";
import { Messages } from "./messages";
import { localize as $l } from "./locale";
import { DeviceInfo, getDeviceInfo } from "./platform";
@ -70,7 +71,7 @@ export class App extends EventTarget implements Storable {
version = "3.0";
storage = new LocalStorage();
api: API = new Client(this);
api: API = new Client(this, new AjaxSender("http://127.0.0.1:3000/"));
settings = defaultSettings;
messages = new Messages("https://padlock.io/messages.json");
stats: Stats = {};

View File

@ -1,4 +1,5 @@
import { DateString, Base64String, stringToBase64, base64ToString } from "./encoding";
import { Request, Response } from "./transport";
import { marshal, DateString, Base64String, stringToBase64 } from "./encoding";
import {
getProvider,
RSAPublicKey,
@ -65,23 +66,28 @@ export class Session implements SessionInfo, Storable {
constructor(public id = "") {}
async getAuthHeader(): Promise<string> {
const msg = new Date().toISOString();
const sig = await this.sign(msg);
return `SRP-HMAC sid=${this.id},msg=${stringToBase64(msg)},sig=${sig}`;
async authenticate(r: Request | Response): Promise<void> {
const session = this.id;
const time = new Date().toISOString();
const data = (<Request>r).params || (<Response>r).result;
const signature = await this._sign(session + "_" + time + "_" + marshal(data));
r.auth = { session, time, signature };
}
async verifyAuthHeader(header: string) {
const { msg, sig } = parseAuthHeader(header);
return this.verify(sig, base64ToString(msg));
}
async verify(r: Request | Response): Promise<boolean> {
if (!r.auth) {
return false;
}
const { signature, session, time } = r.auth;
const data = (<Request>r).params || (<Response>r).result;
async sign(message: string): Promise<Base64String> {
return await getProvider().sign(this.key, stringToBase64(message), defaultHMACParams());
}
// Make sure message isn't older than 1 minute to prevent replay attacks
const age = Date.now() - new Date(time).getTime();
if (age > 60 * 1000) {
return false;
}
async verify(signature: Base64String, message: string): Promise<boolean> {
return await getProvider().verify(this.key, signature, stringToBase64(message), defaultHMACParams());
return this._verify(signature, session + "_" + time + "_" + marshal(data));
}
async serialize() {
@ -100,6 +106,14 @@ export class Session implements SessionInfo, Storable {
this.key = raw.key || "";
return this;
}
private async _sign(message: string): Promise<Base64String> {
return await getProvider().sign(this.key, stringToBase64(message), defaultHMACParams());
}
private async _verify(signature: Base64String, message: string): Promise<boolean> {
return await getProvider().verify(this.key, signature, stringToBase64(message), defaultHMACParams());
}
}
export interface AccountInfo {

View File

@ -1,11 +1,11 @@
import { API, CreateAccountParams, CreateStoreParams, CreateOrgParams } from "./api";
import { request, Method } from "./ajax";
import { Sender } from "./transport";
import { DeviceInfo } from "./platform";
import { Session, Account, AccountID, Auth } from "./auth";
import { Invite } from "./invite";
import { marshal, unmarshal, Base64String } from "./encoding";
import { Store } from "./store";
import { Base64String } from "./encoding";
import { Org } from "./org";
import { Store } from "./store";
import { Err, ErrorCode } from "./error";
export interface ClientSettings {
@ -21,190 +21,118 @@ export interface ClientState {
}
export class Client implements API {
constructor(public state: ClientState) {}
constructor(public state: ClientState, private sender: Sender) {}
get session() {
return this.state.session;
}
get basePath() {
return this.state.settings.customServer ? this.state.settings.customServerUrl : "https://cloud.padlock.io";
}
urlForPath(path: string): string {
return `${this.basePath}/${path}`.replace(/([^:]\/)\/+/g, "$1");
}
async request(method: Method, path: string, data?: string, headers?: Map<string, string>): Promise<string> {
const url = this.urlForPath(path);
headers = headers || new Map<string, string>();
async call(method: string, params?: any[]) {
const { session } = this.state;
if (!headers.get("Content-Type")) {
headers.set("Content-Type", "application/json");
}
headers.set("Accept", "application/vnd.padlock;version=2");
const req = { method, params };
if (session) {
headers.set("Authorization", await session.getAuthHeader());
if (data) {
headers.set("X-Signature", await session.sign(data));
}
await session.authenticate(req);
}
headers.set("X-Device", marshal(this.state.device));
// headers.set("X-Device", marshal(this.state.device));
const res = await request(method, url, data, headers);
const res = await this.sender.send(req);
const body = res.responseText;
if (session) {
if (!(await session.verifyAuthHeader(res.getResponseHeader("Authorization") || ""))) {
// TODO: Better error code
throw new Err(ErrorCode.INVALID_SESSION);
}
if (body && !(await session.verify(res.getResponseHeader("X-Signature") || "", body))) {
// TODO: Better error code
throw new Err(ErrorCode.INVALID_SESSION);
}
if (res.error) {
throw new Err((res.error.code as any) as ErrorCode, res.error.message);
}
return body;
if (session && !(await session.verify(res))) {
throw new Err(ErrorCode.INVALID_RESPONSE);
}
return res;
}
async verifyEmail(params: { email: string }) {
const res = await this.request("POST", "verify", marshal(params));
return unmarshal(res);
const res = await this.call("verifyEmail", [params]);
return res.result;
}
async initAuth(params: { email: string }) {
const res = await this.request("POST", "auth", marshal(params));
const raw = unmarshal(res);
return {
auth: await new Auth(params.email).deserialize(raw.auth),
B: raw.B
};
const res = await this.call("initAuth", [params]);
const { auth, B } = res.result;
return { auth: await new Auth(params.email).deserialize(auth), B };
}
async createSession(params: { account: AccountID; M: Base64String; A: Base64String }): Promise<Session> {
const res = await this.request("POST", "session", marshal(params));
return new Session().deserialize(unmarshal(res));
const res = await this.call("createSession", [params]);
return new Session().deserialize(res.result);
}
async revokeSession(session: Session): Promise<void> {
await this.request("DELETE", `session/${session.id}`, undefined);
await this.call("revokeSession", [await session.serialize()]);
}
async getSessions() {
const res = await this.request("GET", `account/sessions`);
return unmarshal(res);
const res = await this.call("getSessions");
return res.result;
}
async createAccount(params: CreateAccountParams): Promise<Account> {
const raw = {
auth: await params.auth.serialize(),
account: await params.account.serialize(),
emailVerification: params.emailVerification
};
const res = await this.request("POST", "account", marshal(raw));
return new Account().deserialize(unmarshal(res));
const res = await this.call("createAccount", [
{
auth: await params.auth.serialize(),
account: await params.account.serialize(),
emailVerification: params.emailVerification
}
]);
return new Account().deserialize(res.result);
}
async getAccount(account: Account): Promise<Account> {
const res = await this.request("GET", "account");
return await account.deserialize(unmarshal(res));
const res = await this.call("getAccount");
return await account.deserialize(res.result);
}
async updateAccount(account: Account): Promise<Account> {
const res = await this.request("PUT", "account", marshal(await account.serialize()));
return account.deserialize(unmarshal(res));
const res = await this.call("updateAccount", [await account.serialize()]);
return account.deserialize(res.result);
}
async getStore(store: Store): Promise<Store> {
const res = await this.request("GET", `store/${store.pk}`, undefined);
return store.deserialize(unmarshal(res));
const res = await this.call("getStore", [await store.serialize()]);
return store.deserialize(res.result);
}
async createStore(params: CreateStoreParams): Promise<Store> {
const res = await this.request("POST", "store", marshal(params));
return new Store("").deserialize(unmarshal(res));
const res = await this.call("createStore", [params]);
return new Store("").deserialize(res.result);
}
async updateStore(store: Store): Promise<Store> {
const res = await this.request("PUT", `store/${store.pk}`, marshal(await store.serialize()));
return store.deserialize(unmarshal(res));
const res = await this.call("updateStore", [await store.serialize()]);
return store.deserialize(res.result);
}
async getOrg(org: Org): Promise<Org> {
const res = await this.request("GET", `org/${org.pk}`, undefined);
return org.deserialize(unmarshal(res));
const res = await this.call("getOrg", [await org.serialize()]);
return org.deserialize(res.result);
}
async createOrg(params: CreateOrgParams): Promise<Org> {
const res = await this.request("POST", "org", marshal(params));
return new Org("").deserialize(unmarshal(res));
const res = await this.call("createOrg", [params]);
return new Org("").deserialize(res.result);
}
async updateOrg(org: Org): Promise<Org> {
const res = await this.request("PUT", `org/${org.pk}`, marshal(await org.serialize()));
return org.deserialize(unmarshal(res));
const res = await this.call("updateOrg", [await org.serialize()]);
return org.deserialize(res.result);
}
async updateInvite(invite: Invite): Promise<Invite> {
const res = await this.request(
"PUT",
`org/${invite.group!.id}/invite/${invite.id}`,
marshal(await invite.serialize())
);
return invite.deserialize(unmarshal(res));
const res = await this.call("updateInvite", [await invite.serialize()]);
return invite.deserialize(res.result);
}
async deleteInvite(invite: Invite): Promise<void> {
await this.request("DELETE", `store/${invite.group!.id}/invite/${invite.id}`);
await this.call("deleteInvite", [await invite.serialize()]);
}
//
// async createOrganization(params: CreateOrganizationParams): Promise<Organization> {
// const res = await this.request("POST", "org", marshal(params));
// return new Organization("", this.state.account!).deserialize(unmarshal(res));
// }
//
// async getOrganization(org: Organization): Promise<Organization> {
// const res = await this.request("GET", `org/${org.pk}`, undefined);
// return org.deserialize(unmarshal(res));
// }
//
// async updateOrganization(org: Organization): Promise<Organization> {
// const res = await this.request("PUT", `org/${org.pk}`, marshal(await org.serialize()));
// return org.deserialize(unmarshal(res));
// }
// async updateInvite(org: Organization, invite: Invite): Promise<Organization> {
// const res = await this.request("PUT", `org/${org.pk}/invite`, marshal(await invite.serialize()));
// return org.deserialize(unmarshal(res));
// }
//
// subscribe(stripeToken = "", coupon = "", source = ""): Promise<XMLHttpRequest> {
// const params = new URLSearchParams();
// params.set("stripeToken", stripeToken);
// params.set("coupon", coupon);
// params.set("source", source);
// return this.request(
// "POST",
// "subscribe",
// params.toString(),
// new Map<string, string>().set("Content-Type", "application/x-www-form-urlencoded")
// );
// }
//
// cancelSubscription(): Promise<XMLHttpRequest> {
// return this.request("POST", "unsubscribe");
// }
//
// getPlans(): Promise<any[]> {
// return this.request("GET", this.urlForPath("plans")).then(res => <any[]>JSON.parse(res));
// }
}

View File

@ -27,6 +27,8 @@ export enum ErrorCode {
INVALID_CREDENTIALS = "invalid_credentials",
ACCOUNT_EXISTS = "account_exists",
EMAIL_VERIFICATION_FAILED = "email_verification_failed",
INVALID_RESPONSE = "invalid_response",
INVALID_REQUEST = "invalid_request",
// Generic Errors
CLIENT_ERROR = "client_error",

View File

@ -131,6 +131,7 @@ ${Buffer.from(base64ToBytes(key)).toString("base64")}
signer.update(Buffer.from(base64ToBytes(data)));
const sig = signer.sign({
key: key,
passphrase: "",
// @ts-ignore
saltLength: params.saltLength,
padding: constants.RSA_PKCS1_PSS_PADDING

View File

@ -0,0 +1,30 @@
export interface Request {
method: string;
params?: any[];
auth?: Authentication;
}
export interface Response {
result: any;
error?: Error;
auth?: Authentication;
}
export interface Authentication {
session: string;
time: string;
signature: string;
}
export interface Error {
code: number | string;
message: string;
}
export interface Sender {
send(req: Request): Promise<Response>;
}
export interface Receiver {
listen(handler: (req: Request) => Promise<Response>): void;
}

View File

@ -8,21 +8,15 @@
"private": false,
"devDependencies": {
"@types/fs-extra": "^5.0.3",
"@types/koa": "^2.0.46",
"@types/koa-route": "^3.2.3",
"@types/levelup": "0.0.30",
"@types/nodemailer": "^4.6.2",
"ts-node": "^7.0.0",
"typescript": "^2.9.2"
},
"dependencies": {
"@koa/cors": "2",
"@padlock/core": "^1.0.0",
"@types/koa-bodyparser": "^5.0.1",
"fs-extra": "^6.0.1",
"koa": "^2.5.1",
"koa-bodyparser": "^4.2.1",
"koa-route": "^3.2.0",
"level": "^4.0.0",
"nodemailer": "^4.6.7"
}

View File

@ -9,9 +9,9 @@ import { Invite } from "@padlock/core/src/invite";
import { uuid } from "@padlock/core/src/util";
import { Server as SRPServer } from "@padlock/core/src/srp";
import { NodeCryptoProvider } from "@padlock/core/src/node-crypto-provider";
import { DeviceInfo } from "@padlock/core/src/platform";
import { Sender } from "./sender";
import { EmailVerificationMessage, InviteCreatedMessage, InviteAcceptedMessage, MemberAddedMessage } from "./messages";
import { RequestState } from "./server";
import { randomBytes } from "crypto";
const crypto = new NodeCryptoProvider();
@ -48,8 +48,12 @@ export class EmailVerification implements Storable {
const pendingAuths = new Map<string, SRPServer>();
export class ServerAPI implements API {
constructor(private storage: Storage, private sender: Sender, private state: RequestState) {}
export class Context implements API {
session?: Session;
account?: Account;
device?: DeviceInfo;
constructor(public storage: Storage, public sender: Sender) {}
async verifyEmail({ email }: { email: string }) {
const v = new EmailVerification(email);
@ -107,7 +111,7 @@ export class ServerAPI implements API {
const session = new Session(uuid());
session.account = account;
session.device = this.state.device;
session.device = this.device;
session.key = srp.K!;
acc.sessions.add(session.id);
@ -231,7 +235,7 @@ export class ServerAPI implements API {
const acc = new Account(member.id);
await this.storage.get(acc);
acc.groups.push(store.info);
if (!acc.id !== account.id) {
if (acc.id !== account.id) {
this.sender.send(member.email, new MemberAddedMessage(store));
}
await this.storage.set(acc);
@ -280,7 +284,7 @@ export class ServerAPI implements API {
const acc = new Account(member.id);
await this.storage.get(acc);
acc.groups.push(org.info);
if (!acc.id !== account.id) {
if (acc.id !== account.id) {
this.sender.send(member.email, new MemberAddedMessage(org));
}
await this.storage.set(acc);
@ -351,7 +355,7 @@ export class ServerAPI implements API {
}
private _requireAuth(): { account: Account; session: Session } {
const { account, session } = this.state;
const { account, session } = this;
if (!session || !account) {
throw new Err(ErrorCode.INVALID_SESSION);

View File

@ -1,124 +0,0 @@
import { Store } from "@padlock/core/src/store";
import { Org } from "@padlock/core/src/org";
import { Account, Auth, Session } from "@padlock/core/src/auth";
import { CreateStoreParams, CreateOrgParams } from "@padlock/core/src/api";
import { Err, ErrorCode } from "@padlock/core/src/error";
import { Invite } from "@padlock/core/src/invite";
import { Context } from "./server";
export async function verifyEmail(ctx: Context) {
const { email } = ctx.request.body;
if (typeof email !== "string") {
throw new Err(ErrorCode.BAD_REQUEST, "No email provided!");
}
ctx.body = await ctx.api.verifyEmail({ email });
}
export async function initAuth(ctx: Context) {
const { email } = ctx.request.body;
if (typeof email !== "string") {
throw new Err(ErrorCode.BAD_REQUEST);
}
const { auth, B } = await ctx.api.initAuth({ email });
ctx.body = {
auth: await auth.serialize(),
B
};
}
export async function createSession(ctx: Context) {
// TODO: check params
const session = await ctx.api.createSession(ctx.request.body);
ctx.body = await session.serialize();
}
export async function revokeSession(ctx: Context, id: string) {
await ctx.api.revokeSession(new Session(id));
ctx.body = "";
}
export async function getSessions(ctx: Context) {
ctx.body = await ctx.api.getSessions();
}
export async function getAccount(ctx: Context) {
const account = await ctx.api.getAccount(ctx.state.account!);
ctx.body = await account.serialize();
}
export async function createAccount(ctx: Context) {
// TODO: Check params
const { account, auth, emailVerification } = ctx.request.body;
const acc = await ctx.api.createAccount({
account: await new Account().deserialize(account),
auth: await new Auth().deserialize(auth),
emailVerification
});
ctx.body = await acc.serialize();
}
export async function updateAccount(ctx: Context) {
const account = await new Account().deserialize(ctx.request.body);
const res = await ctx.api.updateAccount(account);
ctx.body = await res.serialize();
}
export async function getStore(ctx: Context, id: string) {
const store = await ctx.api.getStore(new Store(id));
ctx.body = await store.serialize();
}
export async function updateStore(ctx: Context, id: string) {
const store = await new Store(id).deserialize(ctx.request.body);
const res = await ctx.api.updateStore(store);
ctx.body = await res.serialize();
}
export async function createStore(ctx: Context) {
const store = await ctx.api.createStore(ctx.request.body as CreateStoreParams);
ctx.body = await store.serialize();
}
export async function getOrg(ctx: Context, id: string) {
const org = await ctx.api.getOrg(new Org(id));
ctx.body = await org.serialize();
}
export async function updateOrg(ctx: Context, id: string) {
const org = await new Org(id).deserialize(ctx.request.body);
const res = await ctx.api.updateOrg(org);
ctx.body = await res.serialize();
}
export async function createOrg(ctx: Context) {
const org = await ctx.api.createOrg(ctx.request.body as CreateOrgParams);
ctx.body = await org.serialize();
}
export async function updateInvite(ctx: Context) {
const invite = await new Invite().deserialize(ctx.request.body);
const res = await ctx.api.updateInvite(invite);
ctx.body = await res.serialize();
}
export async function deleteStoreInvite(ctx: Context, storeID: string, inviteID: string) {
const store = await ctx.api.getStore(new Store(storeID));
const invite = store.invites.find(inv => inv.id === inviteID);
if (!invite) {
throw new Err(ErrorCode.NOT_FOUND);
}
await ctx.api.deleteInvite(invite);
ctx.status = 204;
}
export async function deleteOrgInvite(ctx: Context, storeID: string, inviteID: string) {
const org = await ctx.api.getOrg(new Org(storeID));
const invite = org.invites.find(inv => inv.id === inviteID);
if (!invite) {
throw new Err(ErrorCode.NOT_FOUND);
}
await ctx.api.deleteInvite(invite);
ctx.status = 204;
}

View File

@ -1,100 +0,0 @@
import { Account, Session, parseAuthHeader } from "@padlock/core/src/auth";
import { Err, ErrorCode } from "@padlock/core/src/error";
import { marshal, unmarshal } from "@padlock/core/src/encoding";
import { Context } from "./server";
import { ServerAPI } from "./api";
export async function authenticate(ctx: Context, next: () => Promise<void>) {
const authHeader = ctx.headers["authorization"];
if (!authHeader) {
await next();
return;
}
const { sid } = parseAuthHeader(authHeader);
const session = new Session(sid);
try {
await ctx.storage.get(session);
} catch (e) {
if (e.code === ErrorCode.NOT_FOUND) {
throw new Err(ErrorCode.INVALID_SESSION);
} else {
throw e;
}
}
if (session.expires && new Date(session.expires) < new Date()) {
throw new Err(ErrorCode.SESSION_EXPIRED);
}
// TODO: Check date to prevent replay attacks
if (!(await session.verifyAuthHeader(authHeader))) {
throw new Err(ErrorCode.INVALID_SESSION, "Failed to verify Authorization header");
}
const signature = ctx.headers["x-signature"];
if (ctx.request.rawBody && !(await session.verify(signature, ctx.request.rawBody))) {
// TODO: Better error code
throw new Err(ErrorCode.INVALID_SESSION, "Failed to verify signature" + signature + ctx.request.rawBody);
}
const account = new Account(session.account);
await ctx.storage.get(account);
ctx.state.session = session;
ctx.state.account = account;
session.device = ctx.state.device;
session.lastUsed = new Date().toISOString();
await ctx.storage.set(session);
await next();
ctx.set("Authorization", await session.getAuthHeader());
ctx.set("X-Signature", await session.sign(marshal(ctx.body)));
}
export async function handleError(ctx: Context, next: () => Promise<void>) {
try {
await next();
} catch (e) {
console.log(e);
if (e instanceof Err) {
ctx.status = e.status;
ctx.body = {
error: e.code,
message: e.message
};
} else {
ctx.status = 500;
ctx.body = {
error: ErrorCode.SERVER_ERROR,
message:
"Something went wrong while we were processing your request. " +
"Our team has been notified and will resolve the problem as soon as possible!"
};
console.error(e);
ctx.sender.send("support@padlock.io", {
title: "Padlock Error Notification",
text: `The following error occurred at ${new Date().toString()}:\n\n${e.stack}`,
html: ""
});
}
}
}
export async function device(ctx: Context, next: () => Promise<void>) {
const deviceHeader = ctx.request.header["x-device"];
if (deviceHeader) {
ctx.state.device = await unmarshal(deviceHeader);
}
await next();
}
export async function api(ctx: Context, next: () => Promise<void>) {
ctx.api = new ServerAPI(ctx.storage, ctx.sender, ctx.state);
await next();
}

View File

@ -1,103 +1,250 @@
import * as Koa from "koa";
import * as route from "koa-route";
// @ts-ignore
import * as body from "koa-bodyparser";
// @ts-ignore
import * as cors from "@koa/cors";
import { Storage } from "@padlock/core/src/storage";
import { Session, Account } from "@padlock/core/src/auth";
import { DeviceInfo } from "@padlock/core/src/platform";
import { API } from "@padlock/core/src/api";
import { Session, Account, Auth } from "@padlock/core/src/auth";
import { setProvider } from "@padlock/core/src/crypto";
import { NodeCryptoProvider } from "@padlock/core/src/node-crypto-provider";
import { Receiver, Request, Response } from "@padlock/core/src/transport";
import { Err, ErrorCode } from "@padlock/core/src/error";
import { Store } from "@padlock/core/src/store";
import { Org } from "@padlock/core/src/org";
import { Invite } from "@padlock/core/src/invite";
import { HTTPReceiver } from "./transport";
import { Context } from "./api";
import { LevelDBStorage } from "./storage";
import { Sender, EmailSender } from "./sender";
import * as middleware from "./middleware";
import * as handlers from "./handlers";
setProvider(new NodeCryptoProvider());
export interface RequestState {
session?: Session;
account?: Account;
device?: DeviceInfo;
}
export interface Request extends Koa.Request {
body: any;
rawBody: string;
}
export interface Context extends Koa.Context {
storage: Storage;
sender: Sender;
api: API;
state: RequestState;
request: Request;
}
export class Server {
private koa: Koa;
constructor(private storage: Storage, private sender: Sender, private receivers: Receiver[]) {}
constructor(private storage: Storage, private sender: Sender) {
this.koa = new Koa();
Object.assign(this.koa.context, {
storage: this.storage,
sender: this.sender
});
this.koa.use(
cors({
exposeHeaders: ["Authorization", "X-Signature", "X-Sub-Status", "X-Stripe-Pub-Key", "X-Sub-Trial-End"],
allowHeaders: [
"Authorization",
"Content-Type",
"X-Signature",
"X-Device",
"X-Device-App-Version",
"X-Device-Platform",
"X-Device-UUID",
"X-Device-Manufacturer",
"X-Device-OS-Version",
"X-Device-Model",
"X-Device-Hostname"
]
})
);
this.koa.use(body());
this.koa.use(middleware.handleError);
this.koa.use(middleware.device);
this.koa.use(middleware.authenticate);
this.koa.use(middleware.api);
this.koa.use(route.post("/verify", handlers.verifyEmail));
this.koa.use(route.post("/auth", handlers.initAuth));
this.koa.use(route.post("/session", handlers.createSession));
this.koa.use(route.delete("/session/:id", handlers.revokeSession));
this.koa.use(route.get("/account/sessions", handlers.getSessions));
this.koa.use(route.post("/account", handlers.createAccount));
this.koa.use(route.get("/account", handlers.getAccount));
this.koa.use(route.put("/account", handlers.updateAccount));
this.koa.use(route.post("/store", handlers.createStore));
this.koa.use(route.get("/store/:id", handlers.getStore));
this.koa.use(route.put("/store/:id", handlers.updateStore));
this.koa.use(route.put("/store/:sid/invite/:iid", handlers.updateInvite));
this.koa.use(route.delete("/store/:sid/invite/:iid", handlers.deleteStoreInvite));
this.koa.use(route.post("/org", handlers.createOrg));
this.koa.use(route.get("/org/:id", handlers.getOrg));
this.koa.use(route.put("/org/:id", handlers.updateOrg));
this.koa.use(route.put("/org/:sid/invite/:iid", handlers.updateInvite));
this.koa.use(route.delete("/org/:sid/invite/:iid", handlers.deleteOrgInvite));
start() {
for (const receiver of this.receivers) {
receiver.listen(async (req: Request) => {
const context = new Context(this.storage, this.sender);
await this._authenticate(req, context);
const res = { result: null };
try {
await this._process(req, res, context);
} catch (e) {
this._handleError(e, res);
}
if (context.session) {
await context.session.authenticate(res);
}
return res;
});
}
}
start(port: number) {
this.koa.listen(port);
private async _process(req: Request, res: Response, ctx: Context): Promise<void> {
const { method, params } = req;
const { account } = ctx;
let session: Session;
let acc: Account;
let store: Store;
let org: Org;
let invite: Invite;
switch (method) {
case "verifyEmail":
if (!params || params.length !== 1 || typeof params[0].email !== "string") {
throw new Err(ErrorCode.BAD_REQUEST);
}
res.result = await ctx.verifyEmail({ email: params[0].email });
break;
case "initAuth":
if (!params || params.length !== 1 || typeof params[0].email !== "string") {
throw new Err(ErrorCode.BAD_REQUEST);
}
const { auth: _auth, B } = await ctx.initAuth({ email: params[0].email });
res.result = {
auth: await _auth.serialize(),
B
};
break;
case "createSession":
// TODO: check params
if (!params || params.length !== 1) {
throw new Err(ErrorCode.BAD_REQUEST);
}
session = await ctx.createSession(params[0]);
res.result = await session.serialize();
break;
case "revokeSession":
if (!params || params.length !== 1) {
throw new Err(ErrorCode.BAD_REQUEST);
}
session = await new Session().deserialize(params[0]);
await ctx.revokeSession(session);
res.result = null;
break;
case "getSessions":
res.result = await ctx.getSessions();
break;
case "getAccount":
if (!account) {
throw new Err(ErrorCode.BAD_REQUEST);
}
res.result = await account.serialize();
break;
case "createAccount":
if (!params || params.length !== 1) {
throw new Err(ErrorCode.BAD_REQUEST);
}
acc = await ctx.createAccount({
account: await new Account().deserialize(params[0].account),
auth: await new Auth().deserialize(params[0].auth),
emailVerification: params[0].emailVerification
});
res.result = await acc.serialize();
break;
case "updateAccount":
if (!params || params.length !== 1) {
throw new Err(ErrorCode.BAD_REQUEST);
}
acc = await ctx.updateAccount(await new Account().deserialize(params[0]));
res.result = await acc.serialize();
break;
case "getStore":
if (!params || params.length !== 1) {
throw new Err(ErrorCode.BAD_REQUEST);
}
store = await ctx.getStore(await new Store().deserialize(params[0]));
res.result = await store.serialize();
break;
case "updateStore":
if (!params || params.length !== 1) {
throw new Err(ErrorCode.BAD_REQUEST);
}
store = await ctx.updateStore(await new Store().deserialize(params[0]));
res.result = await store.serialize();
break;
case "createStore":
// TODO: Validate params
if (!params || params.length !== 1) {
throw new Err(ErrorCode.BAD_REQUEST);
}
store = await ctx.createStore(params[0]);
res.result = await store.serialize();
break;
case "getOrg":
// TODO: Validate params
if (!params || params.length !== 1) {
throw new Err(ErrorCode.BAD_REQUEST);
}
org = await ctx.getOrg(new Org(params[0].id));
res.result = await org.serialize();
break;
case "updateOrg":
if (!params || params.length !== 1) {
throw new Err(ErrorCode.BAD_REQUEST);
}
org = await ctx.updateOrg(await new Org().deserialize(params[0]));
res.result = await org.serialize();
break;
case "createOrg":
if (!params || params.length !== 1) {
throw new Err(ErrorCode.BAD_REQUEST);
}
org = await ctx.createOrg(params[0]);
res.result = await org.serialize();
break;
case "updateInvite":
if (!params || params.length !== 1) {
throw new Err(ErrorCode.BAD_REQUEST);
}
invite = await ctx.updateInvite(await new Invite().deserialize(params[0]));
res.result = await invite.serialize();
break;
case "deleteInvite":
if (!params || params.length !== 1) {
throw new Err(ErrorCode.BAD_REQUEST);
}
invite = await new Invite().deserialize(params[0]);
await ctx.deleteInvite(invite);
break;
default:
throw new Err(ErrorCode.INVALID_REQUEST);
}
}
private async _authenticate(req: Request, ctx: Context) {
if (!req.auth) {
return;
}
const session = new Session(req.auth.session);
try {
await ctx.storage.get(session);
} catch (e) {
if (e.code === ErrorCode.NOT_FOUND) {
throw new Err(ErrorCode.INVALID_SESSION);
} else {
throw e;
}
}
if (session.expires && new Date(session.expires) < new Date()) {
throw new Err(ErrorCode.SESSION_EXPIRED);
}
if (!(await session.verify(req))) {
throw new Err(ErrorCode.INVALID_REQUEST);
}
const account = new Account(session.account);
await ctx.storage.get(account);
ctx.session = session;
ctx.account = account;
// TODO
// session.device = req.device;
session.lastUsed = new Date().toISOString();
await ctx.storage.set(session);
}
_handleError(e: Error, res: Response) {
if (e instanceof Err) {
res.error = {
code: e.code,
message: e.message
};
} else {
console.error(e.stack);
this.sender.send("support@padlock.io", {
title: "Padlock Error Notification",
text: `The following error occurred at ${new Date().toString()}:\n\n${e.stack}`,
html: ""
});
res.error = {
code: ErrorCode.SERVER_ERROR,
message:
"Something went wrong while we were processing your request. " +
"Our team has been notified and will resolve the problem as soon as possible!"
};
}
}
}
@ -108,5 +255,5 @@ const sender = new EmailSender({
password: process.env.PC_EMAIL_PASSWORD || ""
});
const storage = new LevelDBStorage(process.env.PC_LEVELDB_PATH || "db");
const server = new Server(storage, sender);
server.start(3000);
const server = new Server(storage, sender, [new HTTPReceiver(3000)]);
server.start();

View File

@ -0,0 +1,54 @@
import { createServer, IncomingMessage } from "http";
import { Receiver, Request, Response } from "@padlock/core/src/transport";
import { marshal, unmarshal } from "@padlock/core/src/encoding";
function readBody(request: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
const body: Buffer[] = [];
request
.on("data", chunk => {
body.push(chunk);
})
.on("error", e => {
reject(e);
})
.on("end", () => {
resolve(Buffer.concat(body).toString());
// at this point, `body` has the entire request body stored in it as a string
});
});
}
export class HTTPReceiver implements Receiver {
constructor(public port: number) {}
async listen(handler: (req: Request) => Promise<Response>) {
const server = createServer(async (httpReq, httpRes) => {
httpRes.on("error", e => {
// todo
console.error(e);
});
httpRes.setHeader("Access-Control-Allow-Origin", "*");
httpRes.setHeader("Access-Control-Allow-Methods", "OPTIONS, POST");
httpRes.setHeader("Access-Control-Allow-Headers", "Content-Type");
switch (httpReq.method) {
case "OPTIONS":
httpRes.end();
break;
case "POST":
httpRes.setHeader("Content-Type", "application/json");
const body = await readBody(httpReq);
const res = await handler(unmarshal(body));
httpRes.write(marshal(res));
httpRes.end();
break;
default:
throw "blah";
}
});
server.listen(this.port);
}
}

View File

@ -1,5 +1,5 @@
{
"name": "padlock-app",
"name": "@padlock/ui",
"version": "3.0.0",
"lockfileVersion": 1,
"requires": true,
@ -52,6 +52,14 @@
"@polymer/polymer": "^3.0.0"
}
},
"@polymer/lit-element": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@polymer/lit-element/-/lit-element-0.6.1.tgz",
"integrity": "sha512-eselNs2lA4n1R1rSyPXW8XR3ISmbYVQcDtIMSINHVNZ56pCtCHOtgmaG8C4k0gONwikEMJTP9blNHD/gdYIxFA==",
"requires": {
"lit-html": "^0.11.4"
}
},
"@polymer/paper-spinner": {
"version": "3.0.0-pre.19",
"resolved": "https://registry.npmjs.org/@polymer/paper-spinner/-/paper-spinner-3.0.0-pre.19.tgz",
@ -94,15 +102,20 @@
"resolved": "https://registry.npmjs.org/autosize/-/autosize-4.0.2.tgz",
"integrity": "sha512-jnSyH2d+qdfPGpWlcuhGiHmqBJ6g3X+8T+iRwFrHPLVcdoGJE/x6Qicm6aDHfTsbgZKxyV8UU/YB2p4cjKDRRA=="
},
"moment": {
"version": "2.22.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
"integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y="
"lit-html": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-0.11.4.tgz",
"integrity": "sha512-yfJUxQrRjhYo4cdz3Db9YlkHs9v+rTZA4fvE/dqCOrA1Q2Bmx52X2OtEgGQ+JhyCb6ddDTndLijZjsSoQ44G7Q=="
},
"moment-duration-format": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/moment-duration-format/-/moment-duration-format-2.2.2.tgz",
"integrity": "sha1-uVdhLeJgFsmtnrYIfAVFc+USd3k="
"reflect-metadata": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz",
"integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==",
"dev": true
},
"virtual-scroller": {
"version": "git+https://github.com/valdrinkoshi/virtual-scroller.git#dc49037dd2138e4a222ca49d61f8a1fd70b7f8bd",
"from": "git+https://github.com/valdrinkoshi/virtual-scroller.git"
}
}
}