Skip to content

Commit

Permalink
Add 404 page, schema defaults, and fix file uploads
Browse files Browse the repository at this point in the history
  • Loading branch information
Arlen22 committed Dec 4, 2020
1 parent 9c9adfa commit 67e0eea
Show file tree
Hide file tree
Showing 14 changed files with 198 additions and 190 deletions.
36 changes: 0 additions & 36 deletions https/certificates.js

This file was deleted.

9 changes: 0 additions & 9 deletions https/https.json

This file was deleted.

8 changes: 0 additions & 8 deletions https/jsconfig.json

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tiddlyserver",
"version": "2.2.0-alpha-7",
"version": "2.2.0-alpha-8",
"description": "Many TiddlyWiki, One Port",
"homepage": "https://arlen22.github.io/tiddlyserver/",
"repository": "github:Arlen22/TiddlyServer",
Expand Down
1 change: 0 additions & 1 deletion scripts/printdocs.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ let doc = {
mkdir: "Whether clients may create new directories and datafolders inside existing directories served by TiddlyWiki",
putsaver: "Whether clients may use the put saver, allowing any file served within the tree (not assets) to be overwritten, not just TiddlyWiki files. The put saver cannot save to data folders regardless of this setting.",
registerNotice: "Whether login attempts for a public/private key pair which has not been registered will be logged at debug level 2 with the full public key which can be copied into an authAccounts entry. Turn this off if you get spammed with login attempts.",
transfer: "Allows clients to use a custom TiddlyServer feature which relays a connection between two clients. ",
upload: "Whether clients may upload files to directories (not data folders).",
websockets: "Whether clients may open websocket connections.",
writeErrors: "Whether to write status 500 errors to the browser, possibly including stack traces."
Expand Down
54 changes: 3 additions & 51 deletions src/server/auth-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,11 @@ const TIDDLY_SERVER_AUTH_COOKIE: string = "TiddlyServerAuth";

/** Handles the /admin/authenticate route */
export const handleAuthRoute = (state: StateObject) => {
// let state = new StateObject(req)
if (state.req.method === "GET" || state.req.method === "HEAD") {
if (state.req.method === "GET" || state.req.method === "HEAD")
return handleHEADorGETFileServe(state);
}
if (state.req.method !== "POST") return state.throw(405);
if (state.req.method !== "POST")
return state.throw(405);
switch (state.path[3]) {
// case "transfer":
// return handleTransfer(state);
// case "initpin":
// return handleInitPin(state);
// case "initshared":
// return handleInitShared(state);
case "login":
return handleLogin(state);
case "logout":
Expand Down Expand Up @@ -146,47 +139,6 @@ const handleLogin = async (state: StateObject) => {
}
};

const getRandomPin = async (): Promise<string> => {
// Wait for libsodium.ready
await ready;
let randomPin = randombytes_buf(8);
let pin = "";
while (!pin || pko[pin]) {
pin = to_hex((randomPin = crypto_generichash(8, randomPin, undefined, "uint8array")));
}
pko[pin] = { step: 1, cancelTimeout: removePendingPinTimeout(pin) };
return pin;
};

const handleInitPin = (state: StateObject) => {
if (!state.allow.transfer) {
state.throwReason(403, "Access Denied");
} else if (Object.keys(pko).length > state.settings.maxTransferRequests) {
state.throwReason(509, "Too many transfer requests in progress");
} else {
state.respond(200).json({ initPin: getRandomPin() });
}
return;
};

let sharedKeyList: Record<string, string> = {};
const setSharedKey = async (key: string) => {
const pin = await getRandomPin();
if (!sharedKeyList[key]) sharedKeyList[key] = pin;
return sharedKeyList[key];
};

const handleInitShared = (state: StateObject) => {
if (!state.allow.transfer) {
state.throwReason(403, "Access Denied");
} else if (Object.keys(pko).length > 1000) {
state.throwReason(509, "Too many transfer requests in progress");
} else {
state.respond(200).json({ initPin: setSharedKey(state.path[4]) });
}
return;
};

const handleLogout = (state: StateObject) => {
state.setHeader("Set-Cookie", getSetCookie(TIDDLY_SERVER_AUTH_COOKIE, "", false, 0));
state.respond(200).empty();
Expand Down
13 changes: 10 additions & 3 deletions src/server/generate-directory-listing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ const updatedPutSaver =
"data:text/vnd.tiddler,%7B%22title%22%3A%22%24%3A%2Fcore%2Fmodules%2Fsavers%2Fput.js%22%2C%22text%22%3A%22%2F*%5C%5C%5Cntitle%3A%20%24%3A%2Fcore%2Fmodules%2Fsavers%2Fput.js%5Cntype%3A%20application%2Fjavascript%5Cnmodule-type%3A%20saver%5Cn%5CnSaves%20wiki%20by%20performing%20a%20PUT%20request%20to%20the%20server%5Cn%5CnWorks%20with%20any%20server%20which%20accepts%20a%20PUT%20request%5Cnto%20the%20current%20URL%2C%20such%20as%20a%20WebDAV%20server.%5Cn%5Cn%5C%5C*%2F%5Cn(function()%7B%5Cn%5Cn%2F*jslint%20node%3A%20true%2C%20browser%3A%20true%20*%2F%5Cn%2F*global%20%24tw%3A%20false%20*%2F%5Cn%5C%22use%20strict%5C%22%3B%5Cn%5Cn%2F*%5CnSelect%20the%20appropriate%20saver%20module%20and%20set%20it%20up%5Cn*%2F%5Cnvar%20PutSaver%20%3D%20function(wiki)%20%7B%5Cn%5Ctthis.wiki%20%3D%20wiki%3B%5Cn%5Ctvar%20self%20%3D%20this%3B%5Cn%5Ctvar%20uri%20%3D%20this.uri()%3B%5Cn%5Ct%2F%2F%20Async%20server%20probe.%20Until%20probe%20finishes%2C%20save%20will%20fail%20fast%5Cn%5Ct%2F%2F%20See%20also%20https%3A%2F%2Fgithub.com%2FJermolene%2FTiddlyWiki5%2Fissues%2F2276%5Cn%5Ct%24tw.utils.httpRequest(%7B%5Cn%5Ct%5Cturl%3A%20uri%2C%5Cn%5Ct%5Cttype%3A%20%5C%22OPTIONS%5C%22%2C%5Cn%5Ct%5Ctcallback%3A%20function(err%2C%20data%2C%20xhr)%20%7B%5Cn%5Ct%5Ct%5Ct%2F%2F%20Check%20DAV%20header%20http%3A%2F%2Fwww.webdav.org%2Fspecs%2Frfc2518.html%23rfc.section.9.1%5Cn%5Ct%5Ct%5Ctif(!err)%20%7B%5Cn%5Ct%5Ct%5Ct%5Ctself.serverAcceptsPuts%20%3D%20xhr.status%20%3D%3D%3D%20200%20%26%26%20!!xhr.getResponseHeader(%5C%22dav%5C%22)%3B%5Cn%5Ct%5Ct%5Ct%7D%5Cn%5Ct%5Ct%7D%5Cn%5Ct%7D)%3B%5Cn%5Ct%2F%2F%20Retrieve%20ETag%20if%20available%5Cn%5Ct%24tw.utils.httpRequest(%7B%5Cn%5Ct%5Cturl%3A%20uri%2C%5Cn%5Ct%5Cttype%3A%20%5C%22HEAD%5C%22%2C%5Cn%5Ct%5Ctcallback%3A%20function(err%2C%20data%2C%20xhr)%20%7B%5Cn%5Ct%5Ct%5Ctif(!err)%20%7B%5Cn%5Ct%5Ct%5Ct%5Ctself.etag%20%3D%20xhr.getResponseHeader(%5C%22ETag%5C%22)%3B%5Cn%5Ct%5Ct%5Ct%7D%5Cn%5Ct%5Ct%7D%5Cn%5Ct%7D)%3B%5Cn%7D%3B%5Cn%5CnPutSaver.prototype.uri%20%3D%20function()%20%7B%5Cn%5Ctreturn%20document.location.toString().split(%5C%22%23%5C%22)%5B0%5D%3B%5Cn%7D%3B%5Cn%5Cn%2F%2F%20TODO%3A%20in%20case%20of%20edit%20conflict%5Cn%2F%2F%20Prompt%3A%20Do%20you%20want%20to%20save%20over%20this%3F%20Y%2FN%5Cn%2F%2F%20Merging%20would%20be%20ideal%2C%20and%20may%20be%20possible%20using%20future%20generic%20merge%20flow%5CnPutSaver.prototype.save%20%3D%20function(text%2C%20method%2C%20callback)%20%7B%5Cn%5Ctif(!this.serverAcceptsPuts)%20%7B%5Cn%5Ct%5Ctreturn%20false%3B%5Cn%5Ct%7D%5Cn%5Ctvar%20self%20%3D%20this%3B%5Cn%5Ctvar%20headers%20%3D%20%7B%20%5C%22Content-Type%5C%22%3A%20%5C%22text%2Fhtml%3Bcharset%3DUTF-8%5C%22%20%7D%3B%5Cn%5Ctif(this.etag)%20%7B%5Cn%5Ct%5Ctheaders%5B%5C%22If-Match%5C%22%5D%20%3D%20this.etag%3B%5Cn%5Ct%7D%5Cn%5Ct%24tw.utils.httpRequest(%7B%5Cn%5Ct%5Cturl%3A%20this.uri()%2C%5Cn%5Ct%5Cttype%3A%20%5C%22PUT%5C%22%2C%5Cn%5Ct%5Ctheaders%3A%20headers%2C%5Cn%5Ct%5Ctdata%3A%20text%2C%5Cn%5Ct%5Ctcallback%3A%20function(err%2C%20data%2C%20xhr)%20%7B%5Cn%5Ct%5Ct%5Ctif(err)%20%7B%5Cn%5Ct%5Ct%5Ct%5Ctcallback(err)%3B%5Cn%5Ct%5Ct%5Ct%7D%20if(xhr.status%20%3D%3D%3D%20200%20%7C%7C%20xhr.status%20%3D%3D%3D%20201)%20%7B%5Cn%5Ct%5Ct%5Ct%5Ctself.etag%20%3D%20xhr.getResponseHeader(%5C%22ETag%5C%22)%3B%5Cn%5Ct%5Ct%5Ct%5Ctcallback(null)%3B%20%2F%2F%20success%5Cn%5Ct%5Ct%5Ct%7D%20else%20if(xhr.status%20%3D%3D%3D%20412)%20%7B%20%2F%2F%20edit%20conflict%5Cn%5Ct%5Ct%5Ct%5Ctvar%20message%20%3D%20%24tw.language.getString(%5C%22Error%2FEditConflict%5C%22)%3B%5Cn%5Ct%5Ct%5Ct%5Ctcallback(message)%3B%5Cn%5Ct%5Ct%5Ct%7D%20else%20%7B%5Cn%5Ct%5Ct%5Ct%5Ctcallback(xhr.responseText)%3B%20%2F%2F%20fail%5Cn%5Ct%5Ct%5Ct%7D%5Cn%5Ct%5Ct%7D%5Cn%5Ct%7D)%3B%5Cn%5Ctreturn%20true%3B%5Cn%7D%3B%5Cn%5Cn%2F*%5CnInformation%20about%20this%20saver%5Cn*%2F%5CnPutSaver.prototype.info%20%3D%20%7B%5Cn%5Ctname%3A%20%5C%22put%5C%22%2C%5Cn%5Ctpriority%3A%202000%2C%5Cn%5Ctcapabilities%3A%20%5B%5C%22save%5C%22%2C%20%5C%22autosave%5C%22%5D%5Cn%7D%3B%5Cn%5Cn%2F*%5CnStatic%20method%20that%20returns%20true%20if%20this%20saver%20is%20capable%20of%20working%5Cn*%2F%5Cnexports.canSave%20%3D%20function(wiki)%20%7B%5Cn%5Ctreturn%20%2F%5Ehttps%3F%3A%2F.test(location.protocol)%3B%5Cn%7D%3B%5Cn%5Cn%2F*%5CnCreate%20an%20instance%20of%20this%20saver%5Cn*%2F%5Cnexports.create%20%3D%20function(wiki)%20%7B%5Cn%5Ctreturn%20new%20PutSaver(wiki)%3B%5Cn%7D%3B%5Cn%5Cn%7D)()%3B%5Cn%22%2C%22type%22%3A%22application%2Fjavascript%22%2C%22module-type%22%3A%22saver%22%7D";
const info = require("../package.json");

const ErrorText = {
403: "Access Denied. You do not have permission to access this resource.",
404: "Not Found. This path does not lead to a resource."
}

export function generateDirectoryListing(directory: DirectoryIndexListing, options: DirectoryIndexOptions) {
let isError = directory.type === 403 || directory.type === 404;

function listEntries(entries: DirectoryIndexListing["entries"]) {
return entries
.slice()
Expand Down Expand Up @@ -66,9 +73,9 @@ ${
: `<p><a href="/admin/authenticate/login.html?redirect=${encodeURIComponent(ourPath)}">Login</a></p>`
}
${pathArr.length > 0 ? `<p><a href="${parentPath}">Parent Directory: ${parentPath}</a></p>` : ``}
<ul>${listEntries(directory.entries)}</ul>
${isError ? `<p>Error ${directory.type} ${ErrorText[directory.type]}</p>` : `<ul>${listEntries(directory.entries)}</ul>`}
${
options.upload
(options.upload && !isError)
? `<p>
<form action="?formtype=upload" method="post" enctype="multipart/form-data" name="upload">
<label>Upload file</label>
Expand All @@ -81,7 +88,7 @@ ${
: ""
}
${
options.mkdir
(options.mkdir && !isError)
? `<p>
<form action="?formtype=mkdir" method="post" enctype="multipart/form-data" name="mkdir">
<label>Create Directory</label>
Expand Down
48 changes: 33 additions & 15 deletions src/server/interface-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ abstract class TypeCheck<T> {
return this;
}
/** Chainable method to set schema default */
public defaultData(def: NonNullable<T>) {
public defaultData(def: Partial<NonNullable<T>>) {
this.defData = def;
return this;
}
Expand Down Expand Up @@ -471,6 +471,7 @@ class CheckObject<T extends {}> extends TypeCheck<T> {
throw new Error("unionKey not found in checkermap " + k);
});
}

getSchema(reffer) {
const properties = {};
this.required.forEach(k => {
Expand Down Expand Up @@ -677,7 +678,7 @@ type treeType = {

type refsType = {
"GroupChild": () => TypeCheck<Config.PathElement | Config.GroupElement>
"TreeOptions": () => TypeCheck<Config.Options_Auth | Config.Options_Index | Config.Options_Putsaver>
"TreeOptions": () => TypeCheck<Config.Options_Auth | Config.Options_Index | Config.Options_Putsaver | Config.Options_Upload>
"AccessOptions": () => TypeCheck<ServerConfig_AccessOptions>
"AuthAccountsValue": () => TypeCheck<ServerConfig_AuthAccountsValue>
"ServerConfig": () => TypeCheck<ServerConfig>
Expand All @@ -700,7 +701,10 @@ function getSchemaGroupChild(): () => TypeCheck<any> {
indexPath: checkString().describe("Path of a file to serve as the directory index.")
},
["$element"]
).describe("This is a group element in an " + (array ? "array" : "object") + ". It is used for grouping folders under a mount point. " + (array ? "The key property is the mount point" : "The key is the mount point"));
)
.describe("This is a group element in an " + (array ? "array" : "object") + ". It is used for grouping folders under a mount point. " + (array ? "The key property is the mount point" : "The key is the mount point"))
.defaultData({ $element: "group", $children: {}, key: array ? "" : undefined as any })
;
};
const folderElement: {
(array: true): TypeCheck<Schema.ArrayFolderElement>;
Expand All @@ -719,8 +723,11 @@ function getSchemaGroupChild(): () => TypeCheck<any> {
},
["$element"]
).describe("This is a folder element. The path property specifies the file system path to serve. Any path may be specified, not just a folder. The key is the mount point for this item. If this is in an Array, the key property will be used or the path basename.")
.defaultData({ $element: "folder", path: "", key: array ? "" : undefined as any })
};
const folderString = () => checkString().describe("A string specifying the file or folder to mount here. If this is in an array the basename will be used, in an object the key will be used.");
const folderString = () => checkString()
.describe("A string specifying the file or folder to mount here. If this is in an array the basename will be used, in an object the key will be used.")
.defaultData("");

const SchemaGroupChild = () => checkUnionArray([
checkRecord<string, string | object>(checkStringRegex(/^[^$]+$/), checkUnion(
Expand All @@ -743,7 +750,9 @@ function getSchemaGroupChild(): () => TypeCheck<any> {
folderString(),
)
)
], false).describe("This can be an array or object containing tree items. An array cannot use the group shorthand because there is no way to specify the mount point. You can use the advanced element syntax ({\"$element\":\"group\", \"key\":\"mount-point\", \"$children\": Children }) instead.");
], false)
.describe("This can be an array or object containing tree items. An array cannot use the group shorthand because there is no way to specify the mount point. You can use the advanced element syntax ({\"$element\":\"group\", \"key\":\"mount-point\", \"$children\": Children }) instead.")
.defaultData({});

const schemaRef = {
"ArrayFolder": folderElement(true),
Expand Down Expand Up @@ -806,13 +815,22 @@ function getServerConfig() {

),
"TreeOptions": () => checkUnion(
checkObject<Config.Options_Auth, "$element">("",
{ $element: checkStringEnum(["auth"]).describe("Only allow requests using these authAccounts. Option elements affect the group they belong to and all children under that. Each property in an auth element replaces the key from parent auth elements.\n\nAnonymous requests are ALWAYS denied if an auth list applies to the requested path.\n\nNote that this does not change server authentication procedures. Data folders are always given the authenticated username regardless of whether there are auth elements in the tree.") },
{
authError: checkNumberEnum([403, 404] as const).describe("Which error code to return for unauthorized (or anonymous) requests\n\n - 403 Access Denied: Client is not granted permission to access this resouce.\n - 404 Not Found: Client is told that the resource does not exist."),
authList: checkUnion(checkArray(checkString()), checkNull()).describe("Array of keys from authAccounts object that can access this resource. `null` allows all requests, including anonymous."),
},
["$element"]
checkUnion.cu(
checkObject<Config.Options_Auth, "$element">("",
{ $element: checkStringEnum(["auth"]).describe("Only allow requests using these authAccounts. Option elements affect the group they belong to and all children under that. Each property in an auth element replaces the key from parent auth elements.\n\nAnonymous requests are ALWAYS denied if an auth list applies to the requested path.\n\nNote that this does not change server authentication procedures. Data folders are always given the authenticated username regardless of whether there are auth elements in the tree.") },
{
authError: checkNumberEnum([403, 404] as const).describe("Which error code to return for unauthorized (or anonymous) requests\n\n - 403 Access Denied: Client is not granted permission to access this resouce.\n - 404 Not Found: Client is told that the resource does not exist."),
authList: checkUnion(checkArray(checkString()), checkNull()).describe("Array of keys from authAccounts object that can access this resource. `null` allows all requests, including anonymous."),
},
["$element"]
).defaultData({ $element: "auth" }),
checkObject<Config.Options_Upload, "$element">("",
{ $element: checkStringEnum(["upload"]).describe("Options related to uploads. Option elements affect the group they belong to and all children under that. Each property in an auth element replaces the key from parent auth elements.\n\nAnonymous requests are ALWAYS denied if an auth list applies to the requested path.\n\nNote that this does not change server authentication procedures. Data folders are always given the authenticated username regardless of whether there are auth elements in the tree.") },
{
maxFileSize: checkNumber().describe("Max file size allowed for upload in bytes")
},
["$element"]
).defaultData({ $element: "upload" })
),
checkUnion.cu(
checkObject<Config.Options_Putsaver, "$element">("",
Expand All @@ -821,7 +839,7 @@ function getServerConfig() {
},
putsaverOptional(),
["$element"]
),
).defaultData({ $element: "putsaver" }),
checkObject<Config.Options_Index, "$element">("",
{
$element: checkStringEnum(["index"] as const).describe("Options related to the directory index. If you want to specify an index file for a group, use the indexPath attribute on the group element"),
Expand All @@ -833,7 +851,7 @@ function getServerConfig() {
indexFile: checkArray(checkString()).describe('Look for index files named exactly this or with one of the defaultExts added. For example, a defaultFile of ["index"] and a defaultExts of ["htm","",html"] would look for ["index.htm","index","index.html"] in that order. \n\nOnly applies to folder elements, but may be set on a group element to apply to all child folder elements. An empty array disables this feature. To use a .hidden file, put the full filename here, and set indexExts to [""].'),
},
["$element"]
)
).defaultData({ $element: "index" })
)
),
"AccessOptions": () => checkObject<ServerConfig_AccessOptions>(
Expand Down Expand Up @@ -934,7 +952,7 @@ function getServerConfig() {
}/* as TypeCheckItems<NonNullable<ServerConfigSchema["bindInfo"]>> */),
authAccounts: checkRecord(checkString(), checkRef<refsType>()("AuthAccountsValue", "expected AuthAccountsValue")).describe("This is the auth accounts settings related to logins. The keys of this object are the authAccount specifed in the authList under the tree"),
putsaver: checkObject<ServerConfig["putsaver"], never>("", {}, putsaverOptional()).describe("Settings related to the put saver"),
directoryIndex: checkObject<Defined<ServerConfigSchema["directoryIndex"]>>("", {
directoryIndex: checkObject<Defined<ServerConfigSchema["directoryIndex"]>, never>("", {}, {
defaultType: checkStringEnum(["html", "json"] as const)
.describe("default format for the directory index"),
icons: checkRecord(
Expand Down
Loading

0 comments on commit 67e0eea

Please sign in to comment.