2024-08-27 16:40:11 +02:00
import type { Context } from "@hono/hono" ;
2024-07-11 12:56:28 +02:00
import { createMiddleware } from "@hono/hono/factory" ;
2024-08-27 16:40:11 +02:00
import type { OpenAPIHono } from "@hono/zod-openapi" ;
2024-06-27 01:11:39 +02:00
import { getLogger } from "@logtape/logtape" ;
2024-06-14 10:03:51 +02:00
import { extractParams , verifySolution } from "altcha-lib" ;
2024-05-22 02:59:03 +02:00
import chalk from "chalk" ;
2024-06-14 10:03:51 +02:00
import { eq } from "drizzle-orm" ;
2024-04-14 12:36:25 +02:00
import {
anyOf ,
caseInsensitive ,
charIn ,
2024-08-26 19:27:40 +02:00
charNotIn ,
2024-04-14 12:36:25 +02:00
createRegExp ,
digit ,
exactly ,
2024-05-13 04:27:40 +02:00
global ,
2024-05-12 03:27:28 +02:00
letter ,
2024-05-13 04:27:40 +02:00
maybe ,
2024-08-26 19:27:40 +02:00
not ,
2024-05-12 03:27:28 +02:00
oneOrMore ,
2024-04-14 12:36:25 +02:00
} from "magic-regexp" ;
2024-05-06 09:16:33 +02:00
import { parse } from "qs" ;
import type { z } from "zod" ;
import { fromZodError } from "zod-validation-error" ;
2024-06-29 05:50:56 +02:00
import type { Application } from "~/classes/functions/application" ;
import { type AuthData , getFromHeader } from "~/classes/functions/user" ;
2024-06-14 10:03:51 +02:00
import { db } from "~/drizzle/db" ;
import { Challenges } from "~/drizzle/schema" ;
2024-08-19 14:43:54 +02:00
import { config } from "~/packages/config-manager/index" ;
2024-05-29 02:59:49 +02:00
import type { User } from "~/packages/database-interface/user" ;
2024-08-27 17:20:36 +02:00
import type { ApiRouteMetadata , HonoEnv , HttpVerb } from "~/types/api" ;
2023-10-16 05:51:29 +02:00
2024-06-13 04:26:43 +02:00
export const applyConfig = ( routeMeta : ApiRouteMetadata ) = > {
2024-04-07 07:30:49 +02:00
const newMeta = routeMeta ;
2023-10-16 05:51:29 +02:00
2024-04-07 07:30:49 +02:00
// Apply ratelimits from config
newMeta . ratelimits . duration *= config . ratelimits . duration_coeff ;
newMeta . ratelimits . max *= config . ratelimits . max_coeff ;
2023-10-16 05:51:29 +02:00
2024-05-16 04:37:25 +02:00
if ( config . ratelimits . custom [ routeMeta . route ] ) {
newMeta . ratelimits = config . ratelimits . custom [ routeMeta . route ] ;
2024-04-07 07:30:49 +02:00
}
2023-10-16 05:51:29 +02:00
2024-04-07 07:30:49 +02:00
return newMeta ;
2023-10-16 05:51:29 +02:00
} ;
2024-03-10 23:48:14 +01:00
2024-08-27 17:20:36 +02:00
export const apiRoute = ( fn : ( app : OpenAPIHono < HonoEnv > ) = > void ) = > fn ;
2024-08-19 20:06:38 +02:00
2024-04-14 12:36:25 +02:00
export const idValidator = createRegExp (
anyOf ( digit , charIn ( "ABCDEF" ) ) . times ( 8 ) ,
exactly ( "-" ) ,
anyOf ( digit , charIn ( "ABCDEF" ) ) . times ( 4 ) ,
exactly ( "-" ) ,
exactly ( "7" ) ,
anyOf ( digit , charIn ( "ABCDEF" ) ) . times ( 3 ) ,
exactly ( "-" ) ,
anyOf ( "8" , "9" , "A" , "B" ) . times ( 1 ) ,
anyOf ( digit , charIn ( "ABCDEF" ) ) . times ( 3 ) ,
exactly ( "-" ) ,
anyOf ( digit , charIn ( "ABCDEF" ) ) . times ( 12 ) ,
[ caseInsensitive ] ,
) ;
2024-05-06 09:16:33 +02:00
2024-05-12 03:27:28 +02:00
export const emojiValidator = createRegExp (
// A-Z a-z 0-9 _ -
2024-08-26 19:27:40 +02:00
oneOrMore ( letter . or ( digit ) . or ( charIn ( "_-" ) ) ) ,
2024-05-13 07:14:37 +02:00
[ caseInsensitive , global ] ,
2024-05-12 03:27:28 +02:00
) ;
2024-05-13 03:07:55 +02:00
export const emojiValidatorWithColons = createRegExp (
exactly ( ":" ) ,
2024-08-26 19:27:40 +02:00
oneOrMore ( letter . or ( digit ) . or ( charIn ( "_-" ) ) ) ,
2024-05-13 03:07:55 +02:00
exactly ( ":" ) ,
2024-05-13 07:14:37 +02:00
[ caseInsensitive , global ] ,
2024-05-13 03:07:55 +02:00
) ;
2024-08-26 19:27:40 +02:00
export const emojiValidatorWithIdentifiers = createRegExp (
exactly (
exactly ( not . letter . or ( not . digit ) . or ( charNotIn ( "_-" ) ) ) . times ( 1 ) ,
oneOrMore ( letter . or ( digit ) . or ( charIn ( "_-" ) ) ) . groupedAs ( "shortcode" ) ,
exactly ( not . letter . or ( not . digit ) . or ( charNotIn ( "_-" ) ) ) . times ( 1 ) ,
) ,
[ caseInsensitive , global ] ,
) ;
2024-05-13 04:27:40 +02:00
export const mentionValidator = createRegExp (
exactly ( "@" ) ,
oneOrMore ( anyOf ( letter . lowercase , digit , charIn ( "-" ) ) ) . groupedAs (
"username" ,
) ,
maybe (
exactly ( "@" ) ,
oneOrMore ( anyOf ( letter , digit , charIn ( "_-.:" ) ) ) . groupedAs ( "domain" ) ,
) ,
[ global ] ,
) ;
2024-06-14 10:03:51 +02:00
export const userAddressValidator = createRegExp (
maybe ( "@" ) ,
oneOrMore ( anyOf ( letter . lowercase , digit , charIn ( "-" ) ) ) . groupedAs (
"username" ,
) ,
maybe (
exactly ( "@" ) ,
oneOrMore ( anyOf ( letter , digit , charIn ( "_-.:" ) ) ) . groupedAs ( "domain" ) ,
) ,
[ global ] ,
) ;
2024-05-13 04:27:40 +02:00
export const webfingerMention = createRegExp (
exactly ( "acct:" ) ,
oneOrMore ( anyOf ( letter , digit , charIn ( "-" ) ) ) . groupedAs ( "username" ) ,
maybe (
exactly ( "@" ) ,
oneOrMore ( anyOf ( letter , digit , charIn ( "_-.:" ) ) ) . groupedAs ( "domain" ) ,
) ,
[ ] ,
) ;
2024-07-16 23:29:20 +02:00
export const parseUserAddress = ( address : string ) = > {
let output = address ;
// Remove leading @ if it exists
if ( output . startsWith ( "@" ) ) {
output = output . slice ( 1 ) ;
}
const [ username , domain ] = output . split ( "@" ) ;
return { username , domain } ;
} ;
2024-05-06 09:16:33 +02:00
export const handleZodError = (
result :
| { success : true ; data? : object }
| { success : false ; error : z.ZodError < z.AnyZodObject > ; data? : object } ,
2024-08-19 21:03:59 +02:00
context : Context ,
) : Response | undefined = > {
2024-05-06 09:16:33 +02:00
if ( ! result . success ) {
2024-08-19 21:03:59 +02:00
return context . json (
{
error : fromZodError ( result . error ) . message ,
} ,
422 ,
) ;
2024-05-06 09:16:33 +02:00
}
} ;
2024-06-13 04:26:43 +02:00
const checkPermissions = (
auth : AuthData | null ,
permissionData : ApiRouteMetadata [ "permissions" ] ,
context : Context ,
2024-08-19 21:03:59 +02:00
) : Response | undefined = > {
2024-06-13 04:26:43 +02:00
const userPerms = auth ? . user
? auth . user . getAllPermissions ( )
: config . permissions . anonymous ;
const requiredPerms =
permissionData ? . methodOverrides ? . [ context . req . method as HttpVerb ] ? ?
permissionData ? . required ? ?
[ ] ;
if ( ! requiredPerms . every ( ( perm ) = > userPerms . includes ( perm ) ) ) {
const missingPerms = requiredPerms . filter (
( perm ) = > ! userPerms . includes ( perm ) ,
) ;
2024-08-19 21:03:59 +02:00
return context . json (
{
error : ` You do not have the required permissions to access this route. Missing: ${ missingPerms . join ( ", " ) } ` ,
} ,
2024-06-13 04:26:43 +02:00
403 ,
) ;
}
} ;
const checkRouteNeedsAuth = (
auth : AuthData | null ,
authData : ApiRouteMetadata [ "auth" ] ,
context : Context ,
2024-08-19 21:03:59 +02:00
) :
| Response
| {
user : User | null ;
token : string | null ;
application : Application | null ;
} = > {
2024-06-13 04:26:43 +02:00
if ( auth ? . user ) {
return {
user : auth.user as User ,
token : auth.token as string ,
application : auth.application as Application | null ,
} ;
}
2024-06-14 10:03:51 +02:00
if (
authData . required ||
authData . methodOverrides ? . [ context . req . method as HttpVerb ]
) {
2024-08-19 21:03:59 +02:00
return context . json (
{
error : "This route requires authentication." ,
} ,
2024-06-13 04:26:43 +02:00
401 ,
) ;
}
return {
user : null ,
token : null ,
application : null ,
} ;
} ;
2024-06-14 10:03:51 +02:00
export const checkRouteNeedsChallenge = async (
challengeData : ApiRouteMetadata [ "challenge" ] ,
context : Context ,
2024-08-19 21:03:59 +02:00
) : Promise < true | Response > = > {
2024-06-14 10:03:51 +02:00
if ( ! challengeData ) {
return true ;
}
const challengeSolution = context . req . header ( "X-Challenge-Solution" ) ;
if ( ! challengeSolution ) {
2024-08-19 21:03:59 +02:00
return context . json (
{
error : "This route requires a challenge solution to be sent to it via the X-Challenge-Solution header. Please check the documentation for more information." ,
} ,
2024-06-14 10:03:51 +02:00
401 ,
) ;
}
const { challenge_id } = extractParams ( challengeSolution ) ;
if ( ! challenge_id ) {
2024-08-19 21:03:59 +02:00
return context . json (
{
error : "The challenge solution provided is invalid." ,
} ,
2024-06-14 10:03:51 +02:00
401 ,
) ;
}
const challenge = await db . query . Challenges . findFirst ( {
where : ( c , { eq } ) = > eq ( c . id , challenge_id ) ,
} ) ;
if ( ! challenge ) {
2024-08-19 21:03:59 +02:00
return context . json (
{
error : "The challenge solution provided is invalid." ,
} ,
2024-06-14 10:03:51 +02:00
401 ,
) ;
}
if ( new Date ( challenge . expiresAt ) < new Date ( ) ) {
2024-08-19 21:03:59 +02:00
return context . json (
{
error : "The challenge provided has expired." ,
} ,
2024-06-14 10:03:51 +02:00
401 ,
) ;
}
const isValid = await verifySolution (
challengeSolution ,
config . validation . challenges . key ,
) ;
if ( ! isValid ) {
2024-08-19 21:03:59 +02:00
return context . json (
{
error : "The challenge solution provided is incorrect." ,
} ,
2024-06-14 10:03:51 +02:00
401 ,
) ;
}
// Expire the challenge
await db
. update ( Challenges )
. set ( { expiresAt : new Date ( ) . toISOString ( ) } )
. where ( eq ( Challenges . id , challenge_id ) ) ;
return true ;
} ;
2024-06-08 06:57:29 +02:00
export const auth = (
2024-06-13 04:26:43 +02:00
authData : ApiRouteMetadata [ "auth" ] ,
permissionData? : ApiRouteMetadata [ "permissions" ] ,
2024-06-14 10:03:51 +02:00
challengeData? : ApiRouteMetadata [ "challenge" ] ,
2024-06-13 04:26:43 +02:00
) = >
2024-08-27 17:20:36 +02:00
createMiddleware < HonoEnv > ( async ( context , next ) = > {
const header = context . req . header ( "Authorization" ) ;
const auth = header ? await getFromHeader ( header ) : null ;
2024-06-13 04:26:43 +02:00
2024-08-19 21:03:59 +02:00
// Only exists for type casting, as otherwise weird errors happen with Hono
const fakeResponse = context . json ( { } ) ;
2024-06-13 04:26:43 +02:00
// Permissions check
if ( permissionData ) {
const permissionCheck = checkPermissions (
auth ,
permissionData ,
context ,
) ;
if ( permissionCheck ) {
2024-08-19 21:03:59 +02:00
return permissionCheck as typeof fakeResponse ;
2024-06-13 04:26:43 +02:00
}
}
2024-06-16 08:27:31 +02:00
if ( challengeData && config . validation . challenges . enabled ) {
2024-06-14 10:03:51 +02:00
const challengeCheck = await checkRouteNeedsChallenge (
challengeData ,
context ,
) ;
if ( challengeCheck !== true ) {
2024-08-19 21:03:59 +02:00
return challengeCheck as typeof fakeResponse ;
2024-06-14 10:03:51 +02:00
}
}
2024-08-27 17:20:36 +02:00
const authCheck = checkRouteNeedsAuth ( auth , authData , context ) as
2024-08-19 21:03:59 +02:00
| typeof fakeResponse
| {
user : User | null ;
token : string | null ;
application : Application | null ;
} ;
2024-08-27 17:20:36 +02:00
if ( authCheck instanceof Response ) {
return authCheck ;
}
context . set ( "auth" , authCheck ) ;
await next ( ) ;
2024-06-13 04:26:43 +02:00
} ) ;
2024-06-13 06:16:59 +02:00
// Helper function to parse form data
async function parseFormData ( context : Context ) {
const formData = await context . req . formData ( ) ;
const urlparams = new URLSearchParams ( ) ;
const files = new Map < string , File > ( ) ;
for ( const [ key , value ] of [ . . . formData . entries ( ) ] ) {
if ( Array . isArray ( value ) ) {
for ( const val of value ) {
urlparams . append ( key , val ) ;
2024-06-08 06:57:29 +02:00
}
2024-06-13 06:16:59 +02:00
} else if ( value instanceof File ) {
if ( ! files . has ( key ) ) {
files . set ( key , value ) ;
2024-05-08 13:16:16 +02:00
}
2024-06-13 06:16:59 +02:00
} else {
urlparams . append ( key , String ( value ) ) ;
}
}
2024-05-08 13:16:16 +02:00
2024-06-13 06:16:59 +02:00
const parsed = parse ( urlparams . toString ( ) , {
parseArrays : true ,
interpretNumericEntities : true ,
} ) ;
2024-05-08 13:16:16 +02:00
2024-06-13 06:16:59 +02:00
return {
parsed ,
files ,
} ;
}
2024-05-08 13:16:16 +02:00
2024-06-13 06:16:59 +02:00
// Helper function to parse urlencoded data
async function parseUrlEncoded ( context : Context ) {
const parsed = parse ( await context . req . text ( ) , {
parseArrays : true ,
interpretNumericEntities : true ,
2024-05-06 09:16:33 +02:00
} ) ;
2024-06-13 06:16:59 +02:00
return parsed ;
}
2024-05-06 09:16:33 +02:00
export const qsQuery = ( ) = > {
return createMiddleware ( async ( context , next ) = > {
const parsed = parse ( context . req . query ( ) , {
parseArrays : true ,
interpretNumericEntities : true ,
} ) ;
2024-06-29 09:33:19 +02:00
// @ts-expect-error Very bad hack
2024-05-06 09:16:33 +02:00
context . req . query = ( ) = > parsed ;
2024-06-13 06:16:59 +02:00
2024-06-29 09:33:19 +02:00
// @ts-expect-error I'm so sorry for this
2024-05-06 10:19:42 +02:00
context . req . queries = ( ) = > parsed ;
2024-05-06 09:16:33 +02:00
await next ( ) ;
} ) ;
} ;
2024-05-06 10:31:12 +02:00
2024-06-13 06:16:59 +02:00
export const setContextFormDataToObject = (
context : Context ,
setTo : object ,
) : Context = > {
2024-07-11 12:56:28 +02:00
context . req . bodyCache . json = setTo ;
context . req . parseBody = async ( ) = > context . req . bodyCache . json ;
context . req . json = async ( ) = > context . req . bodyCache . json ;
2024-06-13 06:16:59 +02:00
return context ;
} ;
/ *
* Middleware to magically unfuck forms
* Add it to random Hono routes and hope it works
* @returns
* /
2024-05-06 10:31:12 +02:00
export const jsonOrForm = ( ) = > {
return createMiddleware ( async ( context , next ) = > {
const contentType = context . req . header ( "content-type" ) ;
if ( contentType ? . includes ( "application/json" ) ) {
2024-06-13 06:16:59 +02:00
setContextFormDataToObject ( context , await context . req . json ( ) ) ;
2024-05-06 10:31:12 +02:00
} else if ( contentType ? . includes ( "application/x-www-form-urlencoded" ) ) {
2024-06-13 06:16:59 +02:00
const parsed = await parseUrlEncoded ( context ) ;
2024-05-06 10:31:12 +02:00
2024-06-13 06:16:59 +02:00
setContextFormDataToObject ( context , parsed ) ;
2024-07-11 12:56:28 +02:00
context . req . raw . headers . set ( "Content-Type" , "application/json" ) ;
2024-05-12 03:27:28 +02:00
} else if ( contentType ? . includes ( "multipart/form-data" ) ) {
2024-06-13 06:16:59 +02:00
const { parsed , files } = await parseFormData ( context ) ;
2024-05-12 03:27:28 +02:00
2024-06-13 06:16:59 +02:00
setContextFormDataToObject ( context , {
2024-05-12 03:27:28 +02:00
. . . parsed ,
. . . Object . fromEntries ( files ) ,
2024-06-13 06:16:59 +02:00
} ) ;
2024-07-11 12:56:28 +02:00
context . req . raw . headers . set ( "Content-Type" , "application/json" ) ;
} else if ( ! contentType ) {
setContextFormDataToObject ( context , { } ) ;
context . req . raw . headers . set ( "Content-Type" , "application/json" ) ;
2024-05-06 10:31:12 +02:00
}
2024-06-08 05:31:17 +02:00
2024-05-06 10:31:12 +02:00
await next ( ) ;
} ) ;
} ;
2024-05-22 02:59:03 +02:00
2024-06-27 04:14:12 +02:00
export const debugRequest = async ( req : Request ) = > {
2024-09-04 22:59:39 +02:00
const body = await req . text ( ) ;
2024-06-27 01:11:39 +02:00
const logger = getLogger ( "server" ) ;
const urlAndMethod = ` ${ chalk . green ( req . method ) } ${ chalk . blue ( req . url ) } ` ;
const hash = ` ${ chalk . bold ( "Hash" ) } : ${ chalk . yellow (
new Bun . SHA256 ( ) . update ( body ) . digest ( "hex" ) ,
) } ` ;
const headers = ` ${ chalk . bold ( "Headers" ) } : \ n ${ Array . from (
req . headers . entries ( ) ,
)
. map ( ( [ key , value ] ) = > ` - ${ chalk . cyan ( key ) } : ${ chalk . white ( value ) } ` )
. join ( "\n" ) } ` ;
const bodyLog = ` ${ chalk . bold ( "Body" ) } : ${ chalk . gray ( body ) } ` ;
if ( config . logging . log_requests_verbose ) {
logger . debug ` ${ urlAndMethod } \ n ${ hash } \ n ${ headers } \ n ${ bodyLog } ` ;
} else {
logger . debug ` ${ urlAndMethod } ` ;
}
2024-05-22 02:59:03 +02:00
} ;
2024-09-04 22:52:43 +02:00
export const debugResponse = async ( res : Response ) = > {
const body = await res . clone ( ) . text ( ) ;
const logger = getLogger ( "server" ) ;
const status = ` ${ chalk . bold ( "Status" ) } : ${ chalk . green ( res . status ) } ` ;
const headers = ` ${ chalk . bold ( "Headers" ) } : \ n ${ Array . from (
res . headers . entries ( ) ,
)
. map ( ( [ key , value ] ) = > ` - ${ chalk . cyan ( key ) } : ${ chalk . white ( value ) } ` )
. join ( "\n" ) } ` ;
const bodyLog = ` ${ chalk . bold ( "Body" ) } : ${ chalk . gray ( body ) } ` ;
if ( config . logging . log_requests_verbose ) {
logger . debug ` ${ status } \ n ${ headers } \ n ${ bodyLog } ` ;
} else {
logger . debug ` ${ status } ` ;
}
} ;