new plugins system

This commit is contained in:
KingKDot
2026-04-15 20:16:12 -05:00
parent 7367923ac5
commit fbe43e18c0
6 changed files with 349 additions and 174 deletions
@@ -5,7 +5,7 @@ package plugins
import ( import (
"errors" "errors"
"fmt" "fmt"
"sync" "runtime"
"syscall" "syscall"
"unsafe" "unsafe"
) )
@@ -15,59 +15,96 @@ func loadNativePlugin(data []byte) (NativePlugin, error) {
return nil, errors.New("empty plugin binary") return nil, errors.New("empty plugin binary")
} }
mm, err := LoadMemoryModule(data) type initResult struct {
if err != nil { dp *dllPlugin
return nil, fmt.Errorf("pe load: %w", err) err error
} }
ch := make(chan initResult, 1)
if err := mm.CallEntryPoint(dllProcessAttach); err != nil { go func() {
mm.Free() runtime.LockOSThread()
return nil, fmt.Errorf("DllMain init: %w", err)
}
onLoad, err := mm.GetExport("PluginOnLoad") mm, err := LoadMemoryModule(data)
if err != nil { if err != nil {
mm.Free() ch <- initResult{err: fmt.Errorf("pe load: %w", err)}
return nil, err runtime.UnlockOSThread()
} return
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
}
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" onLoad, err := mm.GetExport("PluginOnLoad")
if getRuntimeAddr, err := mm.GetExport("PluginGetRuntime"); err == nil { if err != nil {
ret, _, _ := syscall.SyscallN(getRuntimeAddr) mm.Free()
if ret != 0 { ch <- initResult{err: err}
var buf [32]byte runtime.UnlockOSThread()
for i := range buf { return
b := *(*byte)(unsafe.Pointer(ret + uintptr(i))) }
if b == 0 { onEvent, err := mm.GetExport("PluginOnEvent")
runtime = string(buf[:i]) if err != nil {
break 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{ dp := &dllPlugin{
mem: mm, mem: mm,
onLoadAddr: onLoad, onLoadAddr: onLoad,
onEventAddr: onEvent, onEventAddr: onEvent,
onUnloadAddr: onUnload, onUnloadAddr: onUnload,
setCallbackAddr: setCallback, setCallbackAddr: setCallback,
pluginRuntime: runtime, 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 { type dllPlugin struct {
@@ -78,76 +115,94 @@ type dllPlugin struct {
setCallbackAddr uintptr setCallbackAddr uintptr
callbackHandle uintptr // prevent GC of the callback closure callbackHandle uintptr // prevent GC of the callback closure
pluginRuntime string // "go", "c", "cpp", etc. 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 { func (p *dllPlugin) Load(send func(string, []byte), hostInfo []byte) error {
p.mu.Lock() return p.runOnWorker(func() error {
defer p.mu.Unlock() // 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. if p.setCallbackAddr != 0 {
cb := syscall.NewCallback(func(eventPtr, eventLen, payloadPtr, payloadLen uintptr) uintptr { syscall.SyscallN(p.setCallbackAddr, cb)
event := make([]byte, eventLen)
if eventLen > 0 {
copy(event, unsafe.Slice((*byte)(unsafe.Pointer(eventPtr)), eventLen))
} }
payload := make([]byte, payloadLen)
if payloadLen > 0 { var infoPtr uintptr
copy(payload, unsafe.Slice((*byte)(unsafe.Pointer(payloadPtr)), payloadLen)) infoLen := uintptr(len(hostInfo))
if len(hostInfo) > 0 {
infoPtr = uintptr(unsafe.Pointer(&hostInfo[0]))
} }
send(string(event), payload) ret, _, _ := syscall.SyscallN(p.onLoadAddr, infoPtr, infoLen, cb)
return 0 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 { func (p *dllPlugin) Event(event string, payload []byte) error {
p.mu.Lock()
defer p.mu.Unlock()
eventBytes := []byte(event) eventBytes := []byte(event)
var eventPtr, payloadPtr uintptr payloadCopy := make([]byte, len(payload))
eventLen := uintptr(len(eventBytes)) copy(payloadCopy, payload)
payloadLen := uintptr(len(payload))
if len(eventBytes) > 0 {
eventPtr = uintptr(unsafe.Pointer(&eventBytes[0]))
}
if len(payload) > 0 {
payloadPtr = uintptr(unsafe.Pointer(&payload[0]))
}
ret, _, _ := syscall.SyscallN(p.onEventAddr, eventPtr, eventLen, payloadPtr, payloadLen) go func() {
if int32(ret) != 0 { runtime.LockOSThread()
return errors.New("PluginOnEvent returned non-zero") 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 return nil
} }
func (p *dllPlugin) Unload() { func (p *dllPlugin) Unload() {
p.mu.Lock() _ = p.runOnWorker(func() error {
defer p.mu.Unlock() if p.onUnloadAddr != 0 {
if p.onUnloadAddr != 0 { syscall.SyscallN(p.onUnloadAddr)
syscall.SyscallN(p.onUnloadAddr) }
} return nil
})
} }
func (p *dllPlugin) Close() error { func (p *dllPlugin) Close() error {
p.Unload() p.Unload()
close(p.workCh)
if p.pluginRuntime != "go" { if p.pluginRuntime != "go" {
if p.mem != nil { if p.mem != nil {
p.mem.Free() p.mem.Free()
@@ -11,6 +11,8 @@ import (
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
func currentTEB() uintptr
const ( const (
imageDOSSignature = 0x5A4D // "MZ" imageDOSSignature = 0x5A4D // "MZ"
imageNTSignature = 0x00004550 // "PE\0\0" imageNTSignature = 0x00004550 // "PE\0\0"
@@ -123,14 +125,18 @@ type imageTLSDirectory64 struct {
} }
type MemoryModule struct { type MemoryModule struct {
base uintptr base uintptr
size uintptr size uintptr
entryPoint uintptr entryPoint uintptr
exports map[string]uintptr exports map[string]uintptr
importDLLs []windows.Handle importDLLs []windows.Handle
initialized bool initialized bool
tlsIndex uint32 tlsIndex uint32
tlsData uintptr 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) { func LoadMemoryModule(data []byte) (*MemoryModule, error) {
@@ -293,8 +299,8 @@ func (mm *MemoryModule) Free() {
if mm.tlsIndex != 0xFFFFFFFF { if mm.tlsIndex != 0xFFFFFFFF {
if mm.tlsData != 0 { if mm.tlsData != 0 {
_ = windows.VirtualFree(mm.tlsData, 0, windows.MEM_RELEASE) _ = windows.VirtualFree(mm.tlsData, 0, windows.MEM_RELEASE)
mm.tlsData = 0
} }
procTlsFree.Call(uintptr(mm.tlsIndex))
} }
for _, h := range mm.importDLLs { for _, h := range mm.importDLLs {
_ = windows.FreeLibrary(h) _ = windows.FreeLibrary(h)
@@ -431,30 +437,78 @@ func (mm *MemoryModule) setupTLS(reason uint32) {
tls := (*imageTLSDirectory64)(unsafe.Pointer(mm.base + uintptr(tlsDir.VirtualAddress))) 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) dataSize := uintptr(tls.EndAddressOfRawData - tls.StartAddressOfRawData)
totalSize := dataSize + uintptr(tls.SizeOfZeroFill) totalSize := dataSize + uintptr(tls.SizeOfZeroFill)
var tlsData uintptr
if totalSize > 0 { 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) windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE)
if err == nil && tlsData != 0 { if err != nil || tlsData == 0 {
if dataSize > 0 { return
copyMem(tlsData, (*byte)(unsafe.Pointer(uintptr(tls.StartAddressOfRawData))), dataSize) }
} if dataSize > 0 {
procTlsSetValue.Call(uintptr(mm.tlsIndex), tlsData) copyMem(tlsData, (*byte)(unsafe.Pointer(uintptr(tls.StartAddressOfRawData))), dataSize)
mm.tlsData = tlsData
} }
} }
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 { if tls.AddressOfCallBacks != 0 {
cbAddr := uintptr(tls.AddressOfCallBacks) cbAddr := uintptr(tls.AddressOfCallBacks)
for { 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 { func peString(addr uintptr) string {
var buf []byte var buf []byte
for i := 0; i < 4096; i++ { // safety limit for i := 0; i < 4096; i++ { // safety limit
@@ -480,45 +589,42 @@ func peString(addr uintptr) string {
return string(buf) return string(buf)
} }
var ( func copyMem(dst uintptr, src *byte, size uintptr) {
modKernel32 = windows.NewLazySystemDLL("kernel32.dll") for i := uintptr(0); i < size; i++ {
procGetProcAddr = modKernel32.NewProc("GetProcAddress") *(*byte)(unsafe.Pointer(dst + i)) = *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(src)) + i))
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)
} }
return r, nil
} }
func sectionProtection(chars uint32) uint32 { func sectionProtection(chars uint32) uint32 {
exec := chars&imageSCNMemExecute != 0 r := chars&imageSCNMemRead != 0
read := chars&imageSCNMemRead != 0 w := chars&imageSCNMemWrite != 0
write := chars&imageSCNMemWrite != 0 x := chars&imageSCNMemExecute != 0
switch { switch {
case exec && write: case x && w:
return windows.PAGE_EXECUTE_READWRITE return windows.PAGE_EXECUTE_READWRITE
case exec && read: case x && r:
return windows.PAGE_EXECUTE_READ return windows.PAGE_EXECUTE_READ
case exec: case x:
return windows.PAGE_EXECUTE return windows.PAGE_EXECUTE
case write: case w:
return windows.PAGE_READWRITE return windows.PAGE_READWRITE
case read: case r:
return windows.PAGE_READONLY return windows.PAGE_READONLY
default: default:
return windows.PAGE_NOACCESS return windows.PAGE_NOACCESS
} }
} }
func copyMem(dst uintptr, src *byte, size uintptr) { var (
dstSlice := unsafe.Slice((*byte)(unsafe.Pointer(dst)), size) modKernel32 = windows.NewLazySystemDLL("kernel32.dll")
srcSlice := unsafe.Slice(src, size) procGetProcAddr = modKernel32.NewProc("GetProcAddress")
copy(dstSlice, srcSlice) )
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
} }
@@ -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
+1 -2
View File
@@ -39,7 +39,7 @@ import { isAuthorizedAgentRequest } from "./server/agent-auth";
import { generateBuildMutex, sanitizeMutex, sanitizeOutputName } from "./server/build-utils"; import { generateBuildMutex, sanitizeMutex, sanitizeOutputName } from "./server/build-utils";
import { detectUploadOs, normalizeClientOs, type DeployOs } from "./server/deploy-utils"; import { detectUploadOs, normalizeClientOs, type DeployOs } from "./server/deploy-utils";
import { CORS_HEADERS } from "./server/http-security"; 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 { sanitizePluginId } from "./server/plugin-utils";
import { dispatchAutoScriptsForConnection } from "./server/auto-script-dispatch"; import { dispatchAutoScriptsForConnection } from "./server/auto-script-dispatch";
import { dispatchAutoDeploysForConnection } from "./server/auto-deploy-dispatch"; import { dispatchAutoDeploysForConnection } from "./server/auto-deploy-dispatch";
@@ -421,7 +421,6 @@ async function startServer() {
enqueuePluginEvent: notificationPluginHandlers.enqueuePluginEvent, enqueuePluginEvent: notificationPluginHandlers.enqueuePluginEvent,
drainPluginUIEvents: notificationPluginHandlers.drainPluginUIEvents, drainPluginUIEvents: notificationPluginHandlers.drainPluginUIEvents,
secureHeaders, secureHeaders,
securePluginHeaders,
mimeType, mimeType,
}, },
fileShare: { fileShare: {
+1 -1
View File
@@ -11,7 +11,7 @@ export function securePluginHeaders() {
return { return {
...SECURITY_HEADERS, ...SECURITY_HEADERS,
"X-Frame-Options": "SAMEORIGIN", "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'",
}; };
} }
@@ -46,7 +46,6 @@ type PluginRouteDeps = {
enqueuePluginEvent: (clientId: string, pluginId: string, event: string, payload: any) => void; enqueuePluginEvent: (clientId: string, pluginId: string, event: string, payload: any) => void;
drainPluginUIEvents: (clientId: string, pluginId: string) => Array<{ event: string; payload: any }>; drainPluginUIEvents: (clientId: string, pluginId: string) => Array<{ event: string; payload: any }>;
secureHeaders: (contentType?: string) => Record<string, string>; secureHeaders: (contentType?: string) => Record<string, string>;
securePluginHeaders: () => Record<string, string>;
mimeType: (path: string) => string; mimeType: (path: string) => string;
}; };
@@ -715,7 +714,6 @@ export async function handlePluginRoutes(
const raw = await file.text(); const raw = await file.text();
const baseTag = `<base href="/plugins/${pluginId}/assets/" />`; const baseTag = `<base href="/plugins/${pluginId}/assets/" />`;
const bridgeTag = `<script src="/assets/plugin-bridge.js"></script>`;
let injected = raw; let injected = raw;
const headMatch = raw.match(/<head[^>]*>/i); const headMatch = raw.match(/<head[^>]*>/i);
@@ -723,16 +721,8 @@ export async function handlePluginRoutes(
injected = raw.replace(headMatch[0], `${headMatch[0]}\n ${baseTag}`); injected = raw.replace(headMatch[0], `${headMatch[0]}\n ${baseTag}`);
} }
if (injected.includes("</head>")) {
injected = injected.replace("</head>", ` ${bridgeTag}\n </head>`);
} else if (injected.includes("</body>")) {
injected = injected.replace("</body>", ` ${bridgeTag}\n</body>`);
} else {
injected = `${bridgeTag}\n${injected}`;
}
return new Response(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 clientId = url.searchParams.get("clientId") || "";
const bridgeToken = uuidv4();
const origin = url.origin; const htmlFile = path.join(deps.PLUGIN_ROOT, pluginId, "assets", `${pluginId}.html`);
const iframeSrc = `/plugins/${pluginId}/frame?clientId=${encodeURIComponent(clientId)}&token=${encodeURIComponent(bridgeToken)}&origin=${encodeURIComponent(origin)}`; const file = Bun.file(htmlFile);
let pluginBody = "";
let pluginHeadExtras = "";
if (await file.exists()) {
const raw = await file.text();
const bodyMatch = raw.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
pluginBody = bodyMatch ? bodyMatch[1] : raw;
const headMatch = raw.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
if (headMatch) {
const headContent = headMatch[1];
const linkTags = headContent.match(/<link[^>]*>/gi) || [];
const styleTags = headContent.match(/<style[\s\S]*?<\/style>/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 = `<!doctype html> const html = `<!doctype html>
<html lang="en"> <html lang="en">
@@ -771,27 +785,19 @@ export async function handlePluginRoutes(
/> />
<link rel="stylesheet" href="/assets/main.css" /> <link rel="stylesheet" href="/assets/main.css" />
<link rel="stylesheet" href="/assets/custom.css" /> <link rel="stylesheet" href="/assets/custom.css" />
${pluginHeadExtras}
</head> </head>
<body class="min-h-screen bg-slate-950 text-slate-100"> <body class="min-h-screen bg-slate-950 text-slate-100">
<header id="top-nav"></header> <header id="top-nav"></header>
<main class="px-5 py-6"> <main class="px-5 py-6">
<div class="max-w-6xl mx-auto"> <div class="max-w-6xl mx-auto">
<div class="rounded-2xl border border-slate-800 bg-slate-900/50 overflow-hidden"> <div id="plugin-container" class="rounded-2xl border border-slate-800 bg-slate-900/50 overflow-hidden p-4">
<iframe ${pluginBody}
id="plugin-frame"
src="${iframeSrc}"
sandbox="allow-scripts allow-same-origin"
class="w-full h-[calc(100vh-220px)] bg-slate-950"
></iframe>
</div> </div>
</div> </div>
</main> </main>
<div
id="plugin-host"
data-bridge-token="${bridgeToken}"
></div>
<script type="module" src="/assets/nav.js"></script> <script type="module" src="/assets/nav.js"></script>
<script src="/assets/plugin-host.js"></script> ${hasPluginJs ? `<script src="/plugins/${pluginId}/assets/${pluginId}.js"></script>` : ""}
</body> </body>
</html>`; </html>`;