2024-03-16 22:29:24 +08:00
import { deleteCookie , getCookie , setCookie } from "hono/helper.ts" ;
import { cors } from "hono/middleware.ts" ;
2024-08-07 19:27:25 +08:00
import { type Context , Hono , validator } from "hono/mod.ts" ;
2024-07-30 23:24:17 +08:00
import type { AssetBundle } from "$lib/asset_bundle/bundle.ts" ;
2024-08-07 02:11:38 +08:00
import type {
EndpointRequest ,
EndpointResponse ,
FileMeta ,
} from "@silverbulletmd/silverbullet/types" ;
import type { ShellRequest } from "@silverbulletmd/silverbullet/type/rpc" ;
2024-08-07 19:27:25 +08:00
import { SpaceServer } from "./space_server.ts" ;
2024-07-30 23:24:17 +08:00
import type { KvPrimitives } from "$lib/data/kv_primitives.ts" ;
2024-02-09 04:00:45 +08:00
import { PrefixedKvPrimitives } from "$lib/data/prefixed_kv_primitives.ts" ;
import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts" ;
import { parse } from "$common/markdown_parser/parse_tree.ts" ;
2024-01-28 22:08:35 +08:00
import { renderMarkdownToHtml } from "../plugs/markdown/markdown_render.ts" ;
2024-08-07 02:11:38 +08:00
import {
2024-08-20 15:38:56 +08:00
decodePageURI ,
2024-08-07 02:11:38 +08:00
looksLikePathWithExtension ,
parsePageRef ,
} from "@silverbulletmd/silverbullet/lib/page_ref" ;
2024-02-09 04:00:45 +08:00
import { base64Encode } from "$lib/crypto.ts" ;
2024-11-15 23:51:18 +08:00
import { LockoutTimer } from "./lockout.ts" ;
2023-12-11 19:11:56 +08:00
const authenticationExpirySeconds = 60 * 60 * 24 * 7 ; // 1 week
2022-10-10 20:50:21 +08:00
export type ServerOptions = {
2022-12-04 13:24:06 +08:00
hostname : string ;
2022-10-10 20:50:21 +08:00
port : number ;
2023-05-24 02:53:53 +08:00
clientAssetBundle : AssetBundle ;
2023-12-10 20:23:42 +08:00
plugAssetBundle : AssetBundle ;
2023-12-14 00:52:56 +08:00
baseKvPrimitives : KvPrimitives ;
2023-05-24 02:53:53 +08:00
certFile? : string ;
keyFile? : string ;
2023-12-10 20:23:42 +08:00
2024-08-07 19:27:25 +08:00
// Enable username/password auth
auth ? : { user : string ; pass : string } ;
// Additional API auth token
authToken? : string ;
pagesPath : string ;
shellBackend : string ;
syncOnly : boolean ;
readOnly : boolean ;
enableSpaceScript : boolean ;
2022-10-10 20:50:21 +08:00
} ;
export class HttpServer {
abortController? : AbortController ;
2023-05-24 02:53:53 +08:00
clientAssetBundle : AssetBundle ;
2023-12-10 20:23:42 +08:00
plugAssetBundle : AssetBundle ;
hostname : string ;
port : number ;
2024-01-14 01:07:02 +08:00
app : Hono ;
2023-12-10 20:23:42 +08:00
keyFile : string | undefined ;
certFile : string | undefined ;
2024-08-07 19:27:25 +08:00
// Available after start()
spaceServer ! : SpaceServer ;
2023-12-14 00:52:56 +08:00
baseKvPrimitives : KvPrimitives ;
2023-12-10 20:23:42 +08:00
2024-08-07 19:27:25 +08:00
constructor ( private options : ServerOptions ) {
2024-01-14 01:07:02 +08:00
this . app = new Hono ( ) ;
2023-12-10 20:23:42 +08:00
this . clientAssetBundle = options . clientAssetBundle ;
this . plugAssetBundle = options . plugAssetBundle ;
2022-12-04 13:24:06 +08:00
this . hostname = options . hostname ;
2022-10-10 20:50:21 +08:00
this . port = options . port ;
2023-12-10 20:23:42 +08:00
this . keyFile = options . keyFile ;
this . certFile = options . certFile ;
this . baseKvPrimitives = options . baseKvPrimitives ;
2023-05-24 02:53:53 +08:00
}
2022-10-10 20:50:21 +08:00
2023-05-24 02:53:53 +08:00
// Replaces some template variables in index.html in a rather ad-hoc manner, but YOLO
2024-01-29 16:43:56 +08:00
async renderHtmlPage (
spaceServer : SpaceServer ,
pageName : string ,
c : Context ,
) : Promise < Response > {
2024-01-28 22:08:35 +08:00
let html = "" ;
2024-01-29 16:43:56 +08:00
let lastModified = utcDateString ( Date . now ( ) ) ;
2024-01-29 02:24:59 +08:00
if ( ! spaceServer . auth ) {
// Only attempt server-side rendering when this site is not protected by auth
try {
2024-01-29 16:43:56 +08:00
const { data , meta } = await spaceServer . spacePrimitives . readFile (
2024-01-29 02:24:59 +08:00
` ${ pageName } .md ` ,
) ;
2024-01-29 16:43:56 +08:00
lastModified = utcDateString ( meta . lastModified ) ;
if ( c . req . header ( "If-Modified-Since" ) === lastModified ) {
// Not modified, empty body status 304
return c . body ( null , 304 ) ;
}
2024-01-29 02:24:59 +08:00
const text = new TextDecoder ( ) . decode ( data ) ;
const tree = parse ( extendedMarkdownLanguage , text ) ;
html = renderMarkdownToHtml ( tree ) ;
} catch ( e : any ) {
if ( e . message !== "Not found" ) {
console . error ( "Error server-side rendering page" , e ) ;
}
2024-01-28 22:08:35 +08:00
}
}
2024-01-29 02:24:59 +08:00
// TODO: Replace this with a proper template engine
2024-01-29 16:43:56 +08:00
html = this . clientAssetBundle . readTextFileSync ( ".client/index.html" )
2024-01-28 22:08:35 +08:00
. replace (
2023-05-24 02:53:53 +08:00
"{{SPACE_PATH}}" ,
2023-12-17 18:46:18 +08:00
spaceServer . pagesPath . replaceAll ( "\\" , "\\\\" ) ,
2024-01-28 22:08:35 +08:00
)
2024-01-28 22:51:02 +08:00
. replace (
"{{DESCRIPTION}}" ,
JSON . stringify ( stripHtml ( html ) . substring ( 0 , 255 ) ) ,
)
2024-01-28 22:08:35 +08:00
. replace (
"{{TITLE}}" ,
pageName ,
) . replace (
2023-08-30 23:25:54 +08:00
"{{SYNC_ONLY}}" ,
2023-12-17 18:46:18 +08:00
spaceServer . syncOnly ? "true" : "false" ,
2024-02-06 23:51:04 +08:00
) . replace (
"{{ENABLE_SPACE_SCRIPT}}" ,
spaceServer . enableSpaceScript ? "true" : "false" ,
2024-01-28 22:08:35 +08:00
) . replace (
2024-01-27 00:05:10 +08:00
"{{READ_ONLY}}" ,
spaceServer . readOnly ? "true" : "false" ,
2024-01-28 22:08:35 +08:00
) . replace (
"{{CONTENT}}" ,
html ,
2023-08-30 03:17:29 +08:00
) ;
2024-01-29 16:43:56 +08:00
return c . html (
html ,
200 ,
{
"Last-Modified" : lastModified ,
} ,
) ;
2022-10-10 20:50:21 +08:00
}
2024-08-07 19:27:25 +08:00
async start() {
2023-07-06 22:47:50 +08:00
// Serve static files (javascript, css, html)
2024-01-14 01:07:02 +08:00
this . serveStatic ( ) ;
2024-07-27 01:12:10 +08:00
this . serveCustomEndpoints ( ) ;
2024-01-14 01:07:02 +08:00
this . addAuth ( ) ;
this . addFsRoutes ( ) ;
2022-10-10 20:50:21 +08:00
2024-08-07 19:27:25 +08:00
// Boot space server
this . spaceServer = new SpaceServer (
this . options ,
this . plugAssetBundle ,
new PrefixedKvPrimitives ( this . baseKvPrimitives , [ "*" ] ) , // * for backwards compatibility reasons
) ;
await this . spaceServer . init ( ) ;
2023-07-06 22:47:50 +08:00
// Fallback, serve the UI index.html
2024-08-07 19:27:25 +08:00
this . app . use ( "*" , ( c ) = > {
2024-01-28 22:08:35 +08:00
const url = new URL ( c . req . url ) ;
2024-08-20 15:38:56 +08:00
const pageName = decodePageURI ( url . pathname . slice ( 1 ) ) ;
2024-08-07 19:27:25 +08:00
return this . renderHtmlPage ( this . spaceServer , pageName , c ) ;
2023-07-06 22:47:50 +08:00
} ) ;
2023-06-14 02:47:05 +08:00
2022-10-10 20:50:21 +08:00
this . abortController = new AbortController ( ) ;
2023-05-24 02:53:53 +08:00
const listenOptions : any = {
2022-12-15 03:32:26 +08:00
hostname : this.hostname ,
port : this.port ,
signal : this.abortController.signal ,
2023-05-24 02:53:53 +08:00
} ;
2023-12-10 20:23:42 +08:00
if ( this . keyFile ) {
listenOptions . key = Deno . readTextFileSync ( this . keyFile ) ;
2023-05-24 02:53:53 +08:00
}
2023-12-10 20:23:42 +08:00
if ( this . certFile ) {
listenOptions . cert = Deno . readTextFileSync ( this . certFile ) ;
2023-05-24 02:53:53 +08:00
}
2024-01-14 01:07:02 +08:00
// Start the actual server
Deno . serve ( listenOptions , this . app . fetch ) ;
2022-12-15 03:32:26 +08:00
const visibleHostname = this . hostname === "0.0.0.0"
? "localhost"
: this . hostname ;
2022-10-10 20:50:21 +08:00
console . log (
2023-01-16 23:45:55 +08:00
` SilverBullet is now running: http:// ${ visibleHostname } : ${ this . port } ` ,
2022-10-10 20:50:21 +08:00
) ;
2022-11-26 21:15:38 +08:00
}
2024-07-27 01:12:10 +08:00
// Custom endpoints can be defined in the server
serveCustomEndpoints() {
this . app . use ( "/_/*" , async ( ctx ) = > {
const req = ctx . req ;
const url = new URL ( req . url ) ;
2024-08-07 19:27:25 +08:00
if ( ! this . spaceServer . serverSystem ) {
2024-07-27 01:12:10 +08:00
return ctx . text ( "No server system available" , 500 ) ;
}
try {
2024-07-27 15:26:55 +08:00
const path = url . pathname . slice ( 2 ) ; // Remove the /_
2024-08-07 19:27:25 +08:00
const responses : EndpointResponse [ ] = await this . spaceServer
. serverSystem
2024-07-27 15:26:55 +08:00
. eventHook . dispatchEvent ( ` http:request: ${ path } ` , {
2024-07-27 01:12:10 +08:00
fullPath : url.pathname ,
2024-07-27 15:26:55 +08:00
path ,
2024-07-27 01:12:10 +08:00
method : req.method ,
body : await req . text ( ) ,
query : Object.fromEntries (
url . searchParams . entries ( ) ,
) ,
headers : req.header ( ) ,
} as EndpointRequest ) ;
if ( responses . length === 0 ) {
2024-07-27 15:26:55 +08:00
return ctx . text (
"No custom endpoint handler is handling this path" ,
404 ,
) ;
2024-07-27 01:12:10 +08:00
} else if ( responses . length > 1 ) {
2024-07-27 15:26:55 +08:00
return ctx . text (
"Multiple endpoint handlers are handling this path, this is not supported" ,
500 ,
) ;
2024-07-27 01:12:10 +08:00
}
const response = responses [ 0 ] ;
if ( response . headers ) {
for (
const [ key , value ] of Object . entries (
response . headers ,
)
) {
ctx . header ( key , value ) ;
}
}
ctx . status ( response . status || 200 ) ;
if ( typeof response . body === "string" ) {
return ctx . text ( response . body ) ;
} else if ( response . body instanceof Uint8Array ) {
return ctx . body ( response . body ) ;
} else {
return ctx . json ( response . body ) ;
}
} catch ( e : any ) {
console . error ( "HTTP endpoint error" , e ) ;
return ctx . text ( e . message , 500 ) ;
}
} ) ;
}
2024-01-14 01:07:02 +08:00
serveStatic() {
2024-08-07 19:27:25 +08:00
this . app . use ( "*" , ( c , next ) : Promise < void | Response > = > {
2024-01-14 01:07:02 +08:00
const req = c . req ;
const url = new URL ( req . url ) ;
// console.log("URL", url);
2023-07-06 22:47:50 +08:00
if (
2024-01-14 01:07:02 +08:00
url . pathname === "/"
2023-07-06 22:47:50 +08:00
) {
2024-01-14 01:07:02 +08:00
// Serve the UI (index.html)
2024-08-15 04:27:55 +08:00
let indexPage = "index" ;
try {
indexPage = parsePageRef ( this . spaceServer . config ? . indexPage ! ) . page ;
} catch ( e : any ) {
console . error ( "Error parsing index page from config" , e ) ;
}
2024-08-07 19:27:25 +08:00
return this . renderHtmlPage ( this . spaceServer , indexPage , c ) ;
2023-07-06 22:47:50 +08:00
}
2024-01-14 01:07:02 +08:00
try {
const assetName = url . pathname . slice ( 1 ) ;
if ( ! this . clientAssetBundle . has ( assetName ) ) {
return next ( ) ;
}
if (
this . clientAssetBundle . has ( assetName ) &&
req . header ( "If-Modified-Since" ) ===
utcDateString ( this . clientAssetBundle . getMtime ( assetName ) ) &&
assetName !== "service_worker.js"
) {
2024-08-07 19:27:25 +08:00
return Promise . resolve ( c . body ( null , 304 ) ) ;
2024-01-14 01:07:02 +08:00
}
c . status ( 200 ) ;
c . header ( "Content-type" , this . clientAssetBundle . getMimeType ( assetName ) ) ;
let data : Uint8Array | string = this . clientAssetBundle . readFileSync (
assetName ,
2023-12-17 18:46:18 +08:00
) ;
2024-01-14 01:07:02 +08:00
c . header ( "Content-length" , "" + data . length ) ;
if ( assetName !== "service_worker.js" ) {
c . header (
"Last-Modified" ,
utcDateString ( this . clientAssetBundle . getMtime ( assetName ) ) ,
2023-12-17 18:46:18 +08:00
) ;
}
2024-01-14 01:07:02 +08:00
if ( req . method === "GET" ) {
if ( assetName === "service_worker.js" ) {
2024-01-29 16:43:56 +08:00
c . header ( "Cache-Control" , "no-cache" ) ;
2024-01-14 01:07:02 +08:00
const textData = new TextDecoder ( ) . decode ( data ) ;
// console.log(
// "Swapping out config hash in service worker",
// );
data = textData . replaceAll (
"{{CONFIG_HASH}}" ,
base64Encode (
JSON . stringify ( [
2024-08-07 19:27:25 +08:00
this . spaceServer . syncOnly ,
this . spaceServer . readOnly ,
this . spaceServer . enableSpaceScript ,
2024-01-14 01:07:02 +08:00
] ) ,
) ,
) ;
}
2024-08-07 19:27:25 +08:00
return Promise . resolve ( c . body ( data ) ) ;
2024-01-14 01:07:02 +08:00
} // else e.g. HEAD, OPTIONS, don't send body
} catch {
return next ( ) ;
2023-07-06 22:47:50 +08:00
}
2024-08-07 19:27:25 +08:00
return Promise . resolve ( ) ;
2024-01-14 01:07:02 +08:00
} ) ;
2023-07-06 22:47:50 +08:00
}
2024-01-14 01:07:02 +08:00
private addAuth() {
2022-12-22 18:21:12 +08:00
const excludedPaths = [
"/manifest.json" ,
"/favicon.png" ,
"/logo.png" ,
"/.auth" ,
] ;
2024-11-15 23:51:18 +08:00
const lockoutTimer = new LockoutTimer ( ) ;
2023-07-02 17:25:32 +08:00
2024-06-22 18:45:23 +08:00
// TODO: This should probably be a POST request
2024-07-30 21:17:34 +08:00
this . app . get ( "/.logout" , ( c ) = > {
2024-01-14 01:07:02 +08:00
const url = new URL ( c . req . url ) ;
2024-06-22 18:45:23 +08:00
deleteCookie ( c , authCookieName ( url . host ) ) ;
2024-10-28 21:09:32 +08:00
deleteCookie ( c , "refreshLogin" ) ;
2024-06-22 18:45:23 +08:00
return c . redirect ( "/.auth" ) ;
} ) ;
2024-07-30 21:17:34 +08:00
this . app . get ( "/.auth" , ( c ) = > {
2024-06-22 18:45:23 +08:00
const html = this . clientAssetBundle . readTextFileSync ( ".client/auth.html" ) ;
return c . html ( html ) ;
} ) . post (
validator ( "form" , ( value , c ) = > {
const username = value [ "username" ] ;
const password = value [ "password" ] ;
2024-10-28 21:09:32 +08:00
const rememberMe = value [ "rememberMe" ] ;
2024-06-22 18:45:23 +08:00
if (
! username || typeof username !== "string" ||
2024-10-28 21:09:32 +08:00
! password || typeof password !== "string" ||
( rememberMe && typeof rememberMe !== "string" )
2024-06-22 18:45:23 +08:00
) {
return c . redirect ( "/.auth?error=0" ) ;
}
2024-10-28 21:09:32 +08:00
return { username , password , rememberMe } ;
2024-06-22 18:45:23 +08:00
} ) ,
async ( c ) = > {
const req = c . req ;
const url = new URL ( c . req . url ) ;
2024-10-28 21:09:32 +08:00
const { username , password , rememberMe } = req . valid ( "form" ) ;
2024-06-22 18:45:23 +08:00
const {
user : expectedUser ,
pass : expectedPassword ,
2024-08-07 19:27:25 +08:00
} = this . spaceServer . auth ! ;
2024-06-22 18:45:23 +08:00
2024-11-15 23:51:18 +08:00
if ( lockoutTimer . isLocked ( ) ) {
console . error ( "Authentication locked out, redirecting to auth page." ) ;
return c . redirect ( "/.auth?error=2" ) ;
}
2024-01-14 01:07:02 +08:00
if ( username === expectedUser && password === expectedPassword ) {
// Generate a JWT and set it as a cookie
2024-10-28 21:09:32 +08:00
const jwt = rememberMe
? await this . spaceServer . jwtIssuer . createJWT ( { username } )
: await this . spaceServer . jwtIssuer . createJWT (
{ username } ,
authenticationExpirySeconds ,
) ;
2024-01-14 01:07:02 +08:00
console . log ( "Successful auth" ) ;
2024-10-28 21:09:32 +08:00
const inAWeek = new Date (
Date . now ( ) + authenticationExpirySeconds * 1000 ,
) ;
2024-06-22 18:45:23 +08:00
setCookie ( c , authCookieName ( url . host ) , jwt , {
2024-10-28 21:09:32 +08:00
expires : inAWeek ,
2024-01-14 01:07:02 +08:00
// sameSite: "Strict",
// httpOnly: true,
} ) ;
2024-10-28 21:09:32 +08:00
if ( rememberMe ) {
setCookie ( c , "refreshLogin" , "true" , { expires : inAWeek } ) ;
}
2024-06-22 18:45:23 +08:00
const values = await c . req . parseBody ( ) ;
const from = values [ "from" ] ;
return c . redirect ( typeof from === "string" ? from : "/" ) ;
2023-07-02 17:25:32 +08:00
} else {
2024-06-08 19:44:41 +08:00
console . error ( "Authentication failed, redirecting to auth page." ) ;
2024-11-15 23:51:18 +08:00
lockoutTimer . addCount ( ) ;
2024-10-28 21:09:32 +08:00
return c . redirect ( "/.auth?error=1" ) ;
2023-07-02 17:25:32 +08:00
}
2024-06-22 18:45:23 +08:00
} ,
2024-07-30 21:17:34 +08:00
) . all ( ( c ) = > {
2024-06-22 18:45:23 +08:00
return c . redirect ( "/.auth" ) ;
2023-07-02 17:25:32 +08:00
} ) ;
2023-12-10 20:23:42 +08:00
// Check auth
2024-01-14 01:07:02 +08:00
this . app . use ( "*" , async ( c , next ) = > {
const req = c . req ;
2024-08-07 19:27:25 +08:00
if ( ! this . spaceServer . auth && ! this . spaceServer . authToken ) {
2023-12-10 20:23:42 +08:00
// Auth disabled in this config, skip
return next ( ) ;
}
2024-01-14 01:07:02 +08:00
const url = new URL ( req . url ) ;
const host = url . host ;
2024-06-22 18:45:23 +08:00
const redirectToAuth = ( ) = > {
// Try filtering api paths
if ( req . path . startsWith ( "/." ) || req . path . endsWith ( ".md" ) ) {
2024-09-10 00:36:54 +08:00
return c . redirect ( "/.auth" , 401 ) ;
2024-06-22 18:45:23 +08:00
} else {
2024-09-10 00:36:54 +08:00
return c . redirect ( ` /.auth?from= ${ req . path } ` , 401 ) ;
2024-06-22 18:45:23 +08:00
}
} ;
2024-01-14 01:07:02 +08:00
if ( ! excludedPaths . includes ( url . pathname ) ) {
const authCookie = getCookie ( c , authCookieName ( host ) ) ;
2023-12-14 00:52:56 +08:00
2024-08-07 19:27:25 +08:00
if ( ! authCookie && this . spaceServer . authToken ) {
2023-12-14 00:52:56 +08:00
// Attempt Bearer Authorization based authentication
2024-01-14 01:07:02 +08:00
const authHeader = req . header ( "Authorization" ) ;
2023-12-14 00:52:56 +08:00
if ( authHeader && authHeader . startsWith ( "Bearer " ) ) {
const authToken = authHeader . slice ( "Bearer " . length ) ;
2024-08-07 19:27:25 +08:00
if ( authToken === this . spaceServer . authToken ) {
2023-12-14 00:52:56 +08:00
// All good, let's proceed
2024-10-28 21:09:32 +08:00
this . refreshLogin ( c , host ) ;
2023-12-14 00:52:56 +08:00
return next ( ) ;
} else {
console . log (
"Unauthorized token access, redirecting to auth page" ,
) ;
2024-01-14 01:07:02 +08:00
return c . text ( "Unauthorized" , 401 ) ;
2023-12-14 00:52:56 +08:00
}
}
}
2024-01-14 01:07:02 +08:00
if ( ! authCookie ) {
2023-12-11 20:53:08 +08:00
console . log ( "Unauthorized access, redirecting to auth page" ) ;
2024-06-22 18:45:23 +08:00
return redirectToAuth ( ) ;
2022-12-22 18:21:12 +08:00
}
2024-08-07 19:27:25 +08:00
const { user : expectedUser } = this . spaceServer . auth ! ;
2023-12-11 19:11:56 +08:00
try {
2024-08-07 19:27:25 +08:00
const verifiedJwt = await this . spaceServer . jwtIssuer
. verifyAndDecodeJWT (
authCookie ,
) ;
2023-12-11 19:11:56 +08:00
if ( verifiedJwt . username !== expectedUser ) {
throw new Error ( "Username mismatch" ) ;
}
} catch ( e : any ) {
console . error (
"Error verifying JWT, redirecting to auth page" ,
e . message ,
) ;
2024-06-22 18:45:23 +08:00
return redirectToAuth ( ) ;
2023-12-10 20:23:42 +08:00
}
}
2024-10-28 21:09:32 +08:00
this . refreshLogin ( c , host ) ;
2023-12-14 00:52:56 +08:00
return next ( ) ;
2023-12-10 20:23:42 +08:00
} ) ;
2022-10-18 02:35:38 +08:00
}
2024-10-28 21:09:32 +08:00
private refreshLogin ( c : Context , host : string ) {
if ( getCookie ( c , "refreshLogin" ) ) {
const inAWeek = new Date (
Date . now ( ) + authenticationExpirySeconds * 1000 ,
) ;
const jwt = getCookie ( c , authCookieName ( host ) ) ;
if ( jwt ) {
setCookie ( c , authCookieName ( host ) , jwt , {
expires : inAWeek ,
// sameSite: "Strict",
// httpOnly: true,
} ) ;
setCookie ( c , "refreshLogin" , "true" , { expires : inAWeek } ) ;
}
}
}
2024-01-14 01:07:02 +08:00
private addFsRoutes() {
this . app . use (
"*" ,
cors ( {
origin : "*" ,
allowHeaders : [ "*" ] ,
exposeHeaders : [ "*" ] ,
allowMethods : [ "GET" , "POST" , "PUT" , "DELETE" , "HEAD" , "OPTIONS" ] ,
} ) ,
) ;
2023-07-06 22:47:50 +08:00
2022-10-18 02:35:38 +08:00
// File list
2024-06-22 18:45:23 +08:00
this . app . get ( "/index.json" , async ( c ) = > {
const req = c . req ;
if ( req . header ( "X-Sync-Mode" ) ) {
// Only handle direct requests for a JSON representation of the file list
2024-08-07 19:27:25 +08:00
const files = await this . spaceServer . spacePrimitives . fetchFileList ( ) ;
2024-06-22 18:45:23 +08:00
return c . json ( files , 200 , {
2024-08-07 19:27:25 +08:00
"X-Space-Path" : this . spaceServer . pagesPath ,
2024-06-22 18:45:23 +08:00
} ) ;
} else {
// Otherwise, redirect to the UI
// The reason to do this is to handle authentication systems like Authelia nicely
return c . redirect ( "/" ) ;
}
} ) ;
2022-10-18 02:35:38 +08:00
2024-08-04 14:24:38 +08:00
// Simple ping health endpoint
this . app . get ( "/.ping" , ( c ) = > {
return c . text ( "OK" , 200 , {
"Cache-Control" : "no-cache" ,
} ) ;
} ) ;
2024-01-14 01:07:02 +08:00
// RPC shell
this . app . post ( "/.rpc/shell" , async ( c ) = > {
const req = c . req ;
const body = await req . json ( ) ;
2023-05-24 02:53:53 +08:00
try {
2024-01-14 01:07:02 +08:00
const shellCommand : ShellRequest = body ;
2024-08-07 19:27:25 +08:00
const shellResponse = await this . spaceServer . shellBackend . handle (
2024-01-14 01:07:02 +08:00
shellCommand ,
) ;
return c . json ( shellResponse ) ;
} catch ( e : any ) {
console . log ( "Shell error" , e ) ;
return c . text ( e . message , 500 ) ;
}
} ) ;
// RPC syscall
2024-01-15 23:43:12 +08:00
this . app . post ( "/.rpc/:plugName/:syscall" , async ( c ) = > {
2024-01-14 01:07:02 +08:00
const req = c . req ;
const syscall = req . param ( "syscall" ) ! ;
2024-01-15 23:43:12 +08:00
const plugName = req . param ( "plugName" ) ! ;
2024-01-14 01:07:02 +08:00
const body = await req . json ( ) ;
try {
2024-08-07 19:27:25 +08:00
if ( this . spaceServer . syncOnly ) {
2024-01-14 01:07:02 +08:00
return c . text ( "Sync only mode, no syscalls allowed" , 400 ) ;
}
const args : string [ ] = body ;
try {
2024-08-07 19:27:25 +08:00
const result = await this . spaceServer . system ! . syscall (
2024-01-21 02:16:07 +08:00
{ plug : plugName === "_" ? undefined : plugName } ,
2024-01-15 23:43:12 +08:00
syscall ,
args ,
) ;
2024-01-14 01:07:02 +08:00
return c . json ( {
result : result ,
} ) ;
} catch ( e : any ) {
return c . json ( {
error : e.message ,
} , 500 ) ;
2023-05-24 02:53:53 +08:00
}
} catch ( e : any ) {
console . log ( "Error" , e ) ;
2024-01-14 01:07:02 +08:00
return c . text ( e . message , 500 ) ;
2023-05-24 02:53:53 +08:00
}
} ) ;
2024-07-12 04:37:58 +08:00
const filePathRegex = "/:path{[^!].*\\.[a-zA-Z0-9]+}" ;
2024-03-24 03:02:16 +08:00
const mdExt = ".md" ;
2024-01-14 01:07:02 +08:00
2024-06-22 18:45:23 +08:00
this . app . get ( filePathRegex , async ( c ) = > {
const req = c . req ;
const name = req . param ( "path" ) ! ;
console . log ( "Requested file" , name ) ;
if (
name . endsWith ( mdExt ) &&
// This header signififies the requests comes directly from the http_space_primitives client (not the browser)
! req . header ( "X-Sync-Mode" ) &&
// This Accept header is used by federation to still work with CORS
req . header ( "Accept" ) !==
"application/octet-stream" &&
req . header ( "sec-fetch-mode" ) !== "cors"
) {
// It can happen that during a sync, authentication expires, this may result in a redirect to the login page and then back to this particular file. This particular file may be an .md file, which isn't great to show so we're redirecting to the associated SB UI page.
console . warn (
"Request was without X-Sync-Mode nor a CORS request, redirecting to page" ,
2024-01-14 01:07:02 +08:00
) ;
2024-07-13 19:51:49 +08:00
return c . redirect ( ` / ${ name . slice ( 0 , - mdExt . length ) } ` ) ;
2024-06-22 18:45:23 +08:00
}
if ( name . startsWith ( "." ) ) {
// Don't expose hidden files
return c . notFound ( ) ;
}
// Handle federated links through a simple redirect, only used for attachments loads with service workers disabled
if ( name . startsWith ( "!" ) ) {
let url = name . slice ( 1 ) ;
console . log ( "Handling this as a federated link" , url ) ;
if ( url . startsWith ( "localhost" ) ) {
url = ` http:// ${ url } ` ;
} else {
url = ` https:// ${ url } ` ;
2024-01-14 01:07:02 +08:00
}
2024-06-22 18:45:23 +08:00
try {
const req = await fetch ( url ) ;
// Override X-Permssion header to always be "ro"
const newHeaders = new Headers ( ) ;
for ( const [ key , value ] of req . headers . entries ( ) ) {
newHeaders . set ( key , value ) ;
2023-07-06 22:47:50 +08:00
}
2024-06-22 18:45:23 +08:00
newHeaders . set ( "X-Permission" , "ro" ) ;
return new Response ( req . body , {
status : req.status ,
headers : newHeaders ,
} ) ;
} catch ( e : any ) {
console . error ( "Error fetching federated link" , e ) ;
return c . text ( e . message , 500 ) ;
2024-01-14 01:07:02 +08:00
}
2024-06-22 18:45:23 +08:00
}
try {
if ( req . header ( "X-Get-Meta" ) ) {
// Getting meta via GET request
2024-08-07 19:27:25 +08:00
const fileData = await this . spaceServer . spacePrimitives . getFileMeta (
2024-06-22 18:45:23 +08:00
name ,
2024-03-24 03:02:16 +08:00
) ;
2024-06-22 18:45:23 +08:00
return c . text ( "" , 200 , this . fileMetaToHeaders ( fileData ) ) ;
2024-03-24 03:02:16 +08:00
}
2024-08-07 19:27:25 +08:00
const fileData = await this . spaceServer . spacePrimitives . readFile ( name ) ;
2024-06-22 18:45:23 +08:00
const lastModifiedHeader = new Date ( fileData . meta . lastModified )
. toUTCString ( ) ;
if (
req . header ( "If-Modified-Since" ) === lastModifiedHeader
) {
return c . body ( null , 304 ) ;
2024-01-14 01:07:02 +08:00
}
2024-06-22 18:45:23 +08:00
return c . body ( fileData . data , 200 , {
. . . this . fileMetaToHeaders ( fileData . meta ) ,
"Last-Modified" : lastModifiedHeader ,
} ) ;
} catch ( e : any ) {
console . error ( "Error GETting file" , name , e . message ) ;
return c . notFound ( ) ;
}
} ) . put (
2024-01-14 01:07:02 +08:00
async ( c ) = > {
const req = c . req ;
const name = req . param ( "path" ) ! ;
2024-08-07 19:27:25 +08:00
if ( this . spaceServer . readOnly ) {
2024-01-27 00:05:10 +08:00
return c . text ( "Read only mode, no writes allowed" , 405 ) ;
}
console . log ( "Writing file" , name ) ;
2023-07-06 22:47:50 +08:00
if ( name . startsWith ( "." ) ) {
// Don't expose hidden files
2024-01-14 01:07:02 +08:00
return c . text ( "Forbidden" , 403 ) ;
2023-07-06 22:47:50 +08:00
}
2024-01-14 01:07:02 +08:00
const body = await req . arrayBuffer ( ) ;
2022-10-18 02:35:38 +08:00
try {
2024-08-07 19:27:25 +08:00
const meta = await this . spaceServer . spacePrimitives . writeFile (
2024-01-14 01:07:02 +08:00
name ,
new Uint8Array ( body ) ,
) ;
return c . text ( "OK" , 200 , this . fileMetaToHeaders ( meta ) ) ;
} catch ( err ) {
console . error ( "Write failed" , err ) ;
return c . text ( "Write failed" , 500 ) ;
2022-10-18 02:35:38 +08:00
}
2024-01-14 01:07:02 +08:00
} ,
) . delete ( async ( c ) = > {
const req = c . req ;
const name = req . param ( "path" ) ! ;
2024-08-07 19:27:25 +08:00
if ( this . spaceServer . readOnly ) {
2024-01-27 00:05:10 +08:00
return c . text ( "Read only mode, no writes allowed" , 405 ) ;
}
2024-01-14 01:07:02 +08:00
console . log ( "Deleting file" , name ) ;
if ( name . startsWith ( "." ) ) {
// Don't expose hidden files
return c . text ( "Forbidden" , 403 ) ;
}
try {
2024-08-07 19:27:25 +08:00
await this . spaceServer . spacePrimitives . deleteFile ( name ) ;
2024-01-14 01:07:02 +08:00
return c . text ( "OK" ) ;
} catch ( e : any ) {
console . error ( "Error deleting attachment" , e ) ;
return c . text ( e . message , 500 ) ;
}
} ) . options ( ) ;
2023-08-24 01:08:21 +08:00
2023-11-16 20:55:02 +08:00
// Federation proxy
2024-01-14 01:07:02 +08:00
const proxyPathRegex = "/:uri{!.+}" ;
this . app . all (
2023-11-16 20:55:02 +08:00
proxyPathRegex ,
2024-01-14 01:07:02 +08:00
async ( c , next ) = > {
const req = c . req ;
2024-08-07 19:27:25 +08:00
if ( this . spaceServer . readOnly ) {
2024-01-29 16:43:56 +08:00
return c . text ( "Read only mode, no federation proxy allowed" , 405 ) ;
}
2024-01-14 01:07:02 +08:00
let url = req . param ( "uri" ) ! . slice ( 1 ) ;
if ( ! req . header ( "X-Proxy-Request" ) ) {
2023-11-16 20:55:02 +08:00
// Direct browser request, not explicity fetch proxy request
2024-07-13 19:51:49 +08:00
if ( ! looksLikePathWithExtension ( url ) ) {
2023-11-16 20:55:02 +08:00
console . log ( "Directly loading federation page via URL:" , url ) ;
// This is not a direct file reference so LIKELY a page request, fall through and load the SB UI
return next ( ) ;
2023-08-24 02:00:40 +08:00
}
}
2023-11-16 20:55:02 +08:00
if ( url . startsWith ( "localhost" ) ) {
url = ` http:// ${ url } ` ;
} else {
url = ` https:// ${ url } ` ;
}
try {
const safeRequestHeaders = new Headers ( ) ;
for (
const headerName of [ "Authorization" , "Accept" , "Content-Type" ]
) {
2024-01-14 01:07:02 +08:00
if ( req . header ( headerName ) ) {
2023-11-16 20:55:02 +08:00
safeRequestHeaders . set (
headerName ,
2024-01-14 01:07:02 +08:00
req . header ( headerName ) ! ,
2023-11-16 20:55:02 +08:00
) ;
}
}
2024-01-14 01:07:02 +08:00
const body = await req . arrayBuffer ( ) ;
const fetchReq = await fetch ( url , {
method : req.method ,
2023-11-16 20:55:02 +08:00
headers : safeRequestHeaders ,
2024-01-14 01:07:02 +08:00
body : body.byteLength > 0 ? body : undefined ,
2023-11-16 20:55:02 +08:00
} ) ;
2024-01-14 01:07:02 +08:00
const responseHeaders : Record < string , any > = { } ;
for ( const [ key , value ] of fetchReq . headers . entries ( ) ) {
responseHeaders [ key ] = value ;
}
return c . body ( fetchReq . body , fetchReq . status , responseHeaders ) ;
2023-11-16 20:55:02 +08:00
} catch ( e : any ) {
console . error ( "Error fetching federated link" , e ) ;
2024-01-14 01:07:02 +08:00
return c . text ( e . message , 500 ) ;
2023-11-16 20:55:02 +08:00
}
} ,
) ;
2023-07-06 22:47:50 +08:00
}
2024-01-14 01:07:02 +08:00
private fileMetaToHeaders ( fileMeta : FileMeta ) {
return {
"Content-Type" : fileMeta . contentType ,
"X-Last-Modified" : "" + fileMeta . lastModified ,
"X-Created" : "" + fileMeta . created ,
"Cache-Control" : "no-cache" ,
"X-Permission" : fileMeta . perm ,
"X-Content-Length" : "" + fileMeta . size ,
} ;
2022-10-10 20:50:21 +08:00
}
2023-05-24 02:53:53 +08:00
stop() {
2022-10-10 20:50:21 +08:00
if ( this . abortController ) {
this . abortController . abort ( ) ;
console . log ( "stopped server" ) ;
}
}
}
2023-05-24 02:53:53 +08:00
function utcDateString ( mtime : number ) : string {
return new Date ( mtime ) . toUTCString ( ) ;
}
2023-08-17 18:43:08 +08:00
function authCookieName ( host : string ) {
2024-01-14 01:07:02 +08:00
return ` auth_ ${ host . replaceAll ( /\W/g , "_" ) } ` ;
2023-08-17 18:43:08 +08:00
}
2024-01-28 22:51:02 +08:00
function stripHtml ( html : string ) : string {
const regex = /<[^>]*>/g ;
return html . replace ( regex , "" ) ;
}