Skip to content

Commit

Permalink
Add controllers setting
Browse files Browse the repository at this point in the history
  • Loading branch information
Arlen22 committed Apr 24, 2020
1 parent 3328da3 commit bcdcbab
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 32 deletions.
36 changes: 23 additions & 13 deletions src/interface-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ServerConfig_AccessOptions,
ServerConfig_PutSaver,
Config,
ServerConfig_Controller,
} from "./server-config";
import { as } from "./server-types";

Expand Down Expand Up @@ -400,25 +401,32 @@ class CheckRepeat<T> extends TypeCheck<T> {
export const checkRepeat = <T>(cb: () => TypeCheck<T>, expected: string) =>
new CheckRepeat(cb, expected);

// export function checkServerConfig(obj, checker: boolean): true | {};
// export function checkServerConfig(obj, checker: TypeCheck<ServerConfig>): true | {};

const checkAccessPerms = checkObject<ServerConfig_AccessOptions>({
mkdir: checkBoolean,
upload: checkBoolean,
websockets: checkBoolean,
writeErrors: checkBoolean,
registerNotice: checkBoolean,
putsaver: checkBoolean,
loginlink: checkBoolean,
transfer: checkBoolean,
datafolder: checkBoolean
});

export const checkController = checkArray(checkObject<ServerConfig_Controller>({
publicKey: checkString,
permissions: checkUnion(checkBooleanFalse, checkAccessPerms),
allowRestart: checkBoolean,
allowSave: checkBoolean
}));
export function checkServerConfig(obj): readonly [boolean, string | {}] {
// if(checker === undefined) checker = new CheckInterface(false);
// else if (typeof checker === "boolean") checker = new CheckInterface(checker);

// let checker = new CheckInterface(showUnionNulls);
// let { checkBoolean, checkString, checkStringEnum, checkNumber, checkNumberEnum, checkBooleanFalse, checkNull } = checker;
const checkAccessPerms = checkObject<ServerConfig_AccessOptions>({
mkdir: checkBoolean,
upload: checkBoolean,
websockets: checkBoolean,
writeErrors: checkBoolean,
registerNotice: checkBoolean,
putsaver: checkBoolean,
loginlink: checkBoolean,
transfer: checkBoolean,
datafolder: checkBoolean
});

const putsaverOptional = as<OptionalCheckermap<ServerConfig_PutSaver, never>>({
backupFolder: checkString,
etag: checkStringEnum("optional", "required", "disabled"),
Expand Down Expand Up @@ -526,6 +534,7 @@ export function checkServerConfig(obj): readonly [boolean, string | {}] {
localAddressPermissions: checkRecord(checkString, checkAccessPerms),
port: checkNumber,
}),
controllers: checkController,
directoryIndex: checkObject<ServerConfig["directoryIndex"]>({
defaultType: checkStringEnum("html", "json"),
icons: checkRecord(checkString, checkArray(checkString)),
Expand All @@ -549,4 +558,5 @@ export function checkServerConfig(obj): readonly [boolean, string | {}] {
//not conform to ServerConfig and the server is about to exit. The error data is in `res`.
// console.log("Check server config result: " + JSON.stringify(res, null, 2));
return [res, errHash] as const;

}
17 changes: 17 additions & 0 deletions src/server-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ export function normalizeSettings(_set: ServerConfigSchema, settingsFile) {
gzipBackups: set.putsaver.gzipBackups(true),
},
datafolder: set.datafolder({}),
controllers: set.controllers([]),
directoryIndex: {
// ...{
defaultType: set.directoryIndex.defaultType("html"),
Expand Down Expand Up @@ -434,6 +435,7 @@ export interface ServerConfigSchema {
* which exports the object
*/
authAccounts?: { [K: string]: ServerConfig_AuthAccountsValue };
controllers?: ServerConfig_Controller[];
// /** client-side data folder loader which loads datafolders directly into the browser */
// EXPERIMENTAL_clientside_datafolders?: Partial<ServerConfig_ClientsideDatafolders>,
/**
Expand Down Expand Up @@ -490,6 +492,8 @@ export interface ServerConfig {
* which exports the object
*/
authAccounts: { [K: string]: ServerConfig_AuthAccountsValue };
/** An array of controllers which can control TiddlyServer */
controllers: ServerConfig_Controller[];
// /** client-side data folder loader which loads datafolders directly into the browser */
// EXPERIMENTAL_clientside_datafolders: ServerConfig_ClientsideDatafolders,
/**
Expand Down Expand Up @@ -621,6 +625,19 @@ export interface ServerConfig_BindInfo {
/** always bind a separate server instance to 127.0.0.1 regardless of any other settings */
_bindLocalhost: boolean;
}
export interface ServerConfig_Controller {
/** The public key of this controller. */
publicKey: string
/** Allow the browser to order restarts of the server listeners and data folders with new settings applied */
allowRestart: boolean
/** Allow changed settings to be saved back to the loaded settings.json file */
allowSave: boolean
/**
* Connections from this browser will use these permissions instead of the permissions from the local address.
* Permissions only apply if not logged in. Tree authList is unaffected. Set to false to not use this.
*/
permissions: ServerConfig_AccessOptions | false
}
export interface ServerConfig_Logging {
/** access log file */
logAccess: string | false;
Expand Down
94 changes: 85 additions & 9 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import * as WebSocket from "ws";
import { handler as morgan } from "./logger";
import { handleAuthRoute } from "./auth-route";
import { checkCookieAuth } from "./cookies";
import { checkServerConfig } from "./interface-checker";
import { checkServerConfig, checkController } from "./interface-checker";
import { RequestEvent } from "./request-event";
import { Observable, Subject, Subscription } from "rxjs";
import { filter } from "rxjs/operators";
Expand All @@ -26,11 +26,14 @@ import {
ServerConfig,
ServerEventEmitter,
testAddress,
ServerConfigSchema,
ServerConfig_AccessOptions,
} from "./server-types";
import { StateObject } from "./state-object";
import { handleTreeRoute } from "./tiddlyserver";
import { EventEmitter } from "./event-emitter-types";
import { Socket } from "net";
import { ServerConfig_Controller } from "./server-config";
export { checkServerConfig, loadSettings, libsReady };
const { Server: WebSocketServer } = WebSocket;

Expand Down Expand Up @@ -59,7 +62,7 @@ interface ListenerEvents {
sender: Listener
}

interface ControllerEvents {
interface MainServerEvents {
close?: { force: boolean }
}

Expand Down Expand Up @@ -106,6 +109,8 @@ export async function initServer({
dryRun: boolean;
}) {

Controller.handleSettings(settings);

if (checkServerConfig(settings)[0] !== true)
throw "ServerConfig did not pass validator";

Expand All @@ -118,6 +123,10 @@ export async function initServer({

startup.eventer.emit("serverOpen", startup.servers, startup.hosts, !!settingshttps, dryRun);
startup.printWelcomeMessage();
startup.disposer.add(() => {

});
Controller.handleServer(startup);

if (dryRun) console.log("DRY RUN: No further processing is likely to happen");

Expand All @@ -138,7 +147,7 @@ export class MainServer {
hosts: string[]
isHttps: boolean
events = new Subject<ListenerEvents>();
command = new Subject<ControllerEvents>();
command = new Subject<MainServerEvents>();
disposer = new Subscription();
debugOutput: Writable;

Expand Down Expand Up @@ -293,6 +302,8 @@ export class MainServer {
let { iface } = e.sender;
if (e.close) {
this.debug(4, "server %s closed", iface);
if (this.servers.every(e => e.closed))
this.disposer.unsubscribe();
} else if (e.listening) {
this.debug(1, "server %s listening", iface);
} else if (e.request) {
Expand Down Expand Up @@ -378,6 +389,9 @@ export class MainServer {

static handleAdminRoute(state: StateObject) {
switch (state.path[2]) {
case "controller":
Controller.handleController(state);
break;
case "authenticate":
handleAuthRoute(state);
break;
Expand All @@ -395,7 +409,7 @@ export class Listener {
constructor(
public debugOutput: Writable,
public events: Subject<ListenerEvents>,
public command: Observable<ControllerEvents>,
public command: Observable<MainServerEvents>,
public iface: string,
public server: https.Server | http.Server
) {
Expand Down Expand Up @@ -436,14 +450,14 @@ export class Listener {

false && this.server.on("clientError", (error, socket) => {
this.events.next({ sender: this, error: { error, socket, type: "connection" } });
})
});

this.server.on("close", () => {
this.closing = true;
this.closed = true;
this.events.next({ sender: this, close: true });
if (this.sockets.length > 0)
console.log("BUG: Listener#sockets.length > 0 -- please report");
process.nextTick(() => {
this.closed = true;
this.events.next({ sender: this, close: true });
});
});
this.wss.on("connection", (client, request) => {
false && client.on("error", (error) => {
Expand All @@ -461,6 +475,8 @@ export class Listener {
close() {
this.closing = true;
this.server.close();
//this is a hack which should release the listener address
this.server.emit("close");
}
sockets: Socket[] = [];
/** same as close but also destroys all open sockets */
Expand All @@ -472,3 +488,63 @@ export class Listener {
this.sockets = [];
}
}
let instance: Controller;
let timestamp: number = 0;
let expectedControllers: string = "";
interface ControllerEvents {
restart: boolean;
getSettings: boolean;
putSettings: ServerConfigSchema;
timestamp: number;
signature: string;
publicKeyHash: string;
}
export class Controller {
static timestamp: number;
static handleSettings(settings: ServerConfig) {
let safeJSON = (key, val) => {
if (key === "__proto__" || key === "constructor") return undefined;
else return val;
}
if (!checkController.check(JSON.parse(JSON.stringify(settings.controllers, safeJSON), safeJSON))) {
Controller.handleServer = (startup) => { };
Controller.handleController = (state) => { state.throw(500); return Promise.resolve(); }
return false;
} else {
expectedControllers = JSON.stringify(settings.controllers, safeJSON);
return true;
}
}
static handleServer(startup: MainServer) {

}
static async handleController(state: StateObject) {
if (state.req.method !== "POST" || state.url.pathname !== "/")
return state.throw(400);

await state.recieveBody(false, () => { });

let cont: ServerConfig_Controller = {} as any;
let json: ControllerEvents = {} as any;

if (json.timestamp <= timestamp) return state.throw(403);

timestamp = json.timestamp

let restart = !!json.restart;
let settings = !!json.putSettings;

let allowed = (!restart || cont.allowRestart) && (!settings || cont.allowSave);

if (!allowed) return state.throw(403);


}
constructor(public main: MainServer) {

}
}
// This isn't ready yet
Controller.handleSettings = () => true;
Controller.handleServer = () => { };
Controller.handleController = async (state) => { state.throw(500); };
16 changes: 6 additions & 10 deletions src/state-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class StateObject {
startTime: [number, number];
timestamp: string;

body: string = "";
body: Buffer[] = [];
json: any | undefined;


Expand Down Expand Up @@ -311,13 +311,13 @@ export class StateObject {
let chunks: Buffer[] = [];
this._req.on("data", chunk => {
if (typeof chunk === "string") {
chunks.push(Buffer.from(chunk));
if (chunk.length) chunks.push(Buffer.from(chunk));
} else {
chunks.push(chunk);
if (chunk.byteLength) chunks.push(chunk);
}
});
this._req.on("end", () => {
this.body = Buffer.concat(chunks).toString("utf8");
this.body = chunks; // Buffer.concat(chunks).toString("utf8");

if (this.body.length === 0 || !parseJSON) return resolve();

Expand All @@ -332,8 +332,8 @@ export class StateObject {
: errorCB;

this.json = catchHandler
? tryParseJSON<any>(this.body, catchHandler)
: tryParseJSON(this.body);
? tryParseJSON<any>(Buffer.concat(this.body).toString("utf8"), catchHandler)
: tryParseJSON(Buffer.concat(this.body).toString("utf8"));
resolve();
});
});
Expand Down Expand Up @@ -397,7 +397,6 @@ export class StateObject {
"utf8"
);
}
// private debugLog: typeof DebugLog = StateObject.DebugLogger("STATE ");
}


Expand All @@ -423,13 +422,10 @@ function getTreeOptions(event: RequestEvent, result: PathResolverResult) {
indexExts: [],
},
};
// console.log(state.ancestry);
ancestry.forEach(e => {
// console.log(e);
e.$options &&
e.$options.forEach(f => {
if (f.$element === "auth" || f.$element === "putsaver" || f.$element === "index") {
// console.log(f);
Object.keys(f).forEach(k => {
if (f[k] === undefined) return;
options[f.$element][k] = f[k];
Expand Down

0 comments on commit bcdcbab

Please sign in to comment.