import { URL } from "url" ; import { MiniflareError } from "@miniflare/shared" ; export type RouterErrorCode = "ERR_QUERY_STRING" | "ERR_INFIX_WILDCARD" ; export class RouterError extends MiniflareError < RouterErrorCode > { } export interface Route { target : string ; route : string ; protocol ? : string ; allowHostnamePrefix : boolean ; hostname : string ; path : string ; allowPathSuffix : boolean ; } const A_MORE_SPECIFIC = - 1 ; const B_MORE_SPECIFIC = 1 ; export class Router { routes : Route [ ] = [ ] ; update ( allRoutes ) { const newRoutes = [ ] ; for ( const [ target , routes ] of allRoutes ) { for ( const route of routes ) { const hasProtocol = /^[a-z0-9+\-.]+:\/\//i . test ( route ) ; let urlInput = route ; if ( ! hasProtocol ) urlInput = `https://${ urlInput }` ; const url = new URL ( urlInput ) ; const protocol = hasProtocol ? url . protocol : undefined ; const allowHostnamePrefix = url . hostname . startsWith ( "*" ) ; const anyHostname = url . hostname === "*" ; if ( allowHostnamePrefix && ! anyHostname ) { url . hostname = url . hostname . substring ( 1 ) ; } const allowPathSuffix = url . pathname . endsWith ( "*" ) ; if ( allowPathSuffix ) { url . pathname = url . pathname . substring ( 0 , url . pathname . length - 1 ) ; } if ( url . search ) { throw new RouterError ( "ERR_QUERY_STRING" , `Route "${ route }" for "${ target }" contains a query string. This is not allowed.` ) ; } if ( url . toString ( ) . includes ( "*" ) && ! anyHostname ) { throw new RouterError ( "ERR_INFIX_WILDCARD" , `Route "${ route }" for "${ target }" contains an infix wildcard. This is not allowed.` ) ; } newRoutes . push ( { target , route , protocol , allowHostnamePrefix , hostname : anyHostname ? "" : url . hostname , path : url . pathname , allowPathSuffix , } ) ; } } newRoutes . sort ( ( a , b ) => { const aHasProtocol = a . protocol !== undefined ; const bHasProtocol = b . protocol !== undefined ; if ( aHasProtocol && ! bHasProtocol ) return A_MORE_SPECIFIC ; if ( ! aHasProtocol && bHasProtocol ) return B_MORE_SPECIFIC ; if ( ! a . allowHostnamePrefix && b . allowHostnamePrefix ) return A_MORE_SPECIFIC ; if ( a . allowHostnamePrefix && ! b . allowHostnamePrefix ) return B_MORE_SPECIFIC ; if ( ! a . allowPathSuffix && b . allowPathSuffix ) return A_MORE_SPECIFIC ; if ( a . allowPathSuffix && ! b . allowPathSuffix ) return B_MORE_SPECIFIC ; const aPathSegments = a . path . split ( "/" ) ; const bPathSegments = b . path . split ( "/" ) ; const aLastSegmentEmpty = aPathSegments [ aPathSegments . length - 1 ] === "" ; const bLastSegmentEmpty = bPathSegments [ bPathSegments . length - 1 ] === "" ; if ( aLastSegmentEmpty && ! bLastSegmentEmpty ) return B_MORE_SPECIFIC ; if ( ! aLastSegmentEmpty && bLastSegmentEmpty ) return A_MORE_SPECIFIC ; if ( aPathSegments . length !== bPathSegments . length ) return bPathSegments . length - aPathSegments . length ; if ( a . path . length !== b . path . length ) return b . path . length - a . path . length ; return b . hostname . length - a . hostname . length ; } ) ; this . routes = newRoutes ; } match ( url ) { for ( const route of this . routes ) { if ( route . protocol && route . protocol !== url . protocol ) continue ; if ( route . allowHostnamePrefix ) { if ( ! url . hostname . endsWith ( route . hostname ) ) continue ; } else { if ( url . hostname !== route . hostname ) continue ; } const path = url . pathname + url . search ; if ( route . allowPathSuffix ) { if ( ! path . startsWith ( route . path ) ) continue ; } else { if ( path !== route . path ) continue ; } return route . target ; } return null ; } }