package main /*#include <stdint.h>#include <stdlib.h> typedef struct { void* ptr; size_t len;} cliproxy_buffer; typedef struct { uint32_t abi_version; void* host_ctx; void* call; void* free_buffer;} cliproxy_host_api; typedef int (*cliproxy_plugin_call_fn)(char*, uint8_t*, size_t, cliproxy_buffer*);typedef void (*cliproxy_plugin_free_fn)(void*, size_t);typedef void (*cliproxy_plugin_shutdown_fn)(void); typedef struct { uint32_t abi_version; cliproxy_plugin_call_fn call; cliproxy_plugin_free_fn free_buffer; cliproxy_plugin_shutdown_fn shutdown;} cliproxy_plugin_api; extern int cliproxyPluginCall(char*, uint8_t*, size_t, cliproxy_buffer*);extern void cliproxyPluginFree(void*, size_t);extern void cliproxyPluginShutdown(void);*/import "C" import ( "encoding/json" "fmt" "net/http" "strings" "sync" "unsafe" "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi" "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi" "gopkg.in/yaml.v3") var state = pluginState{ config: pluginConfig{MaxConcurrency: 2, RejectKeyword: "blocked"}, active: make(map[string]struct{}),} type pluginState struct { mu sync.Mutex config pluginConfig active map[string]struct{}} type pluginConfig struct { MaxConcurrency int `yaml:"max_concurrency"` RejectKeyword string `yaml:"reject_keyword"`} type envelope struct { OK bool `json:"ok"` Result json.RawMessage `json:"result,omitempty"` Error *envelopeError `json:"error,omitempty"`} type envelopeError struct { Code string `json:"code"` Message string `json:"message"`} type lifecycleRequest struct { ConfigYAML []byte `json:"config_yaml"` SchemaVersion uint32 `json:"schema_version"`} type registration struct { SchemaVersion uint32 `json:"schema_version"` Metadata pluginapi.Metadata `json:"metadata"` Capabilities registrationCapability `json:"capabilities"`} type registrationCapability struct { RequestInterceptor bool `json:"request_interceptor"` RequestLifecyclePlugin bool `json:"request_lifecycle_plugin"`} func main() {} //export cliproxy_plugin_initfunc cliproxy_plugin_init(_ *C.cliproxy_host_api, plugin *C.cliproxy_plugin_api) C.int { if plugin == nil { return 1 } plugin.abi_version = C.uint32_t(pluginabi.ABIVersion) plugin.call = C.cliproxy_plugin_call_fn(C.cliproxyPluginCall) plugin.free_buffer = C.cliproxy_plugin_free_fn(C.cliproxyPluginFree) plugin.shutdown = C.cliproxy_plugin_shutdown_fn(C.cliproxyPluginShutdown) return 0} //export cliproxyPluginCallfunc cliproxyPluginCall(method *C.char, request *C.uint8_t, requestLen C.size_t, response *C.cliproxy_buffer) C.int { if response != nil { response.ptr = nil response.len = 0 } if method == nil { writeResponse(response, errorEnvelope("invalid_method", "method is required")) return 1 } var requestBytes []byte if request != nil && requestLen > 0 { requestBytes = C.GoBytes(unsafe.Pointer(request), C.int(requestLen)) } raw, errHandle := handleMethod(C.GoString(method), requestBytes) if errHandle != nil { writeResponse(response, errorEnvelope("plugin_error", errHandle.Error())) return 1 } writeResponse(response, raw) return 0} //export cliproxyPluginFreefunc cliproxyPluginFree(ptr unsafe.Pointer, len C.size_t) { if ptr != nil { C.free(ptr) } _ = len} //export cliproxyPluginShutdownfunc cliproxyPluginShutdown() { state.mu.Lock() defer state.mu.Unlock() state.active = make(map[string]struct{})} func handleMethod(method string, request []byte) ([]byte, error) { switch method { case pluginabi.MethodPluginRegister, pluginabi.MethodPluginReconfigure: if errConfigure := configure(request); errConfigure != nil { return nil, errConfigure } return okEnvelope(pluginRegistration()) case pluginabi.MethodRequestInterceptBefore: return interceptBeforeAuth(request) case pluginabi.MethodRequestInterceptAfter: return passThroughRequest(request) case pluginabi.MethodRequestComplete: return completeRequest(request) default: return errorEnvelope("unknown_method", "unknown method: "+method), nil }} func configure(raw []byte) error { var req lifecycleRequest if len(raw) > 0 { if errUnmarshal := json.Unmarshal(raw, &req); errUnmarshal != nil { return errUnmarshal } } if req.SchemaVersion < 2 { return fmt.Errorf("request lifecycle plugin requires host schema version 2 or newer") } cfg := pluginConfig{MaxConcurrency: 2, RejectKeyword: "blocked"} if len(req.ConfigYAML) > 0 { if errUnmarshal := yaml.Unmarshal(req.ConfigYAML, &cfg); errUnmarshal != nil { return errUnmarshal } } if cfg.MaxConcurrency < 1 { return fmt.Errorf("max_concurrency must be greater than zero") } cfg.RejectKeyword = strings.TrimSpace(cfg.RejectKeyword) state.mu.Lock() defer state.mu.Unlock() state.config = cfg return nil} func pluginRegistration() registration { return registration{ SchemaVersion: pluginabi.SchemaVersion, Metadata: pluginapi.Metadata{ Name: "request-lifecycle", Version: "0.1.0", Author: "router-for-me", GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI", Logo: "https://raw.githubusercontent.com/router-for-me/CLIProxyAPI/main/docs/logo.png", ConfigFields: []pluginapi.ConfigField{ { Name: "max_concurrency", Type: pluginapi.ConfigFieldTypeInteger, Description: "Maximum number of intercepted requests allowed in flight.", }, { Name: "reject_keyword", Type: pluginapi.ConfigFieldTypeString, Description: "Terminates requests whose raw JSON body contains this keyword.", }, }, }, Capabilities: registrationCapability{ RequestInterceptor: true, RequestLifecyclePlugin: true, }, }} func interceptBeforeAuth(raw []byte) ([]byte, error) { var req pluginapi.RequestInterceptRequest if errUnmarshal := json.Unmarshal(raw, &req); errUnmarshal != nil { return nil, errUnmarshal } if req.RequestID == "" { return nil, fmt.Errorf("request ID is required") } state.mu.Lock() defer state.mu.Unlock() if _, exists := state.active[req.RequestID]; exists { return okEnvelope(pluginapi.RequestInterceptResponse{Headers: req.Headers, Body: req.Body}) } if state.config.RejectKeyword != "" && strings.Contains(string(req.Body), state.config.RejectKeyword) { return terminatedResponse(http.StatusForbidden, "request blocked by plugin policy", nil) } if len(state.active) >= state.config.MaxConcurrency { return terminatedResponse(http.StatusTooManyRequests, "plugin concurrency limit reached", http.Header{"Retry-After": {"1"}}) } state.active[req.RequestID] = struct{}{} return okEnvelope(pluginapi.RequestInterceptResponse{Headers: req.Headers, Body: req.Body})} func passThroughRequest(raw []byte) ([]byte, error) { var req pluginapi.RequestInterceptRequest if errUnmarshal := json.Unmarshal(raw, &req); errUnmarshal != nil { return nil, errUnmarshal } return okEnvelope(pluginapi.RequestInterceptResponse{Headers: req.Headers, Body: req.Body})} func terminatedResponse(statusCode int, message string, headers http.Header) ([]byte, error) { body, errMarshal := json.Marshal(map[string]any{ "error": map[string]any{ "type": "plugin_request_rejected", "message": message, }, }) if errMarshal != nil { return nil, errMarshal } if headers == nil { headers = make(http.Header) } headers.Set("Content-Type", "application/json") return okEnvelope(pluginapi.RequestInterceptResponse{ Terminate: true, StatusCode: statusCode, ResponseHeaders: headers, ResponseBody: body, })} func completeRequest(raw []byte) ([]byte, error) { var completion pluginapi.RequestCompletion if errUnmarshal := json.Unmarshal(raw, &completion); errUnmarshal != nil { return nil, errUnmarshal } state.mu.Lock() defer state.mu.Unlock() delete(state.active, completion.RequestID) return okEnvelope(struct{}{})} func okEnvelope(v any) ([]byte, error) { raw, errMarshal := json.Marshal(v) if errMarshal != nil { return nil, errMarshal } return json.Marshal(envelope{OK: true, Result: raw})} func errorEnvelope(code, message string) []byte { raw, errMarshal := json.Marshal(envelope{OK: false, Error: &envelopeError{Code: code, Message: message}}) if errMarshal != nil { return []byte(`{"ok":false,"error":{"code":"plugin_error","message":"encode error"}}`) } return raw} func writeResponse(response *C.cliproxy_buffer, raw []byte) { if response == nil || len(raw) == 0 { return } ptr := C.CBytes(raw) if ptr == nil { return } response.ptr = ptr response.len = C.size_t(len(raw))}
package main
/*
#include <stdint.h>
#include <stdlib.h>
typedef struct {
void* ptr;
size_t len;
} cliproxy_buffer;
typedef struct {
uint32_t abi_version;
void* host_ctx;
void* call;
void* free_buffer;
} cliproxy_host_api;
typedef int (*cliproxy_plugin_call_fn)(char*, uint8_t*, size_t, cliproxy_buffer*);
typedef void (*cliproxy_plugin_free_fn)(void*, size_t);
typedef void (*cliproxy_plugin_shutdown_fn)(void);
typedef struct {
uint32_t abi_version;
cliproxy_plugin_call_fn call;
cliproxy_plugin_free_fn free_buffer;
cliproxy_plugin_shutdown_fn shutdown;
} cliproxy_plugin_api;
extern int cliproxyPluginCall(char*, uint8_t*, size_t, cliproxy_buffer*);
extern void cliproxyPluginFree(void*, size_t);
extern void cliproxyPluginShutdown(void);
*/
import "C"
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
"unsafe"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"gopkg.in/yaml.v3"
)
var state = pluginState{
config: pluginConfig{MaxConcurrency: 2, RejectKeyword: "blocked"},
active: make(map[string]struct{}),
}
type pluginState struct {
mu sync.Mutex
config pluginConfig
active map[string]struct{}
}
type pluginConfig struct {
MaxConcurrency int `yaml:"max_concurrency"`
RejectKeyword string `yaml:"reject_keyword"`
}
type envelope struct {
OK bool `json:"ok"`
Result json.RawMessage `json:"result,omitempty"`
Error *envelopeError `json:"error,omitempty"`
}
type envelopeError struct {
Code string `json:"code"`
Message string `json:"message"`
}
type lifecycleRequest struct {
ConfigYAML []byte `json:"config_yaml"`
SchemaVersion uint32 `json:"schema_version"`
}
type registration struct {
SchemaVersion uint32 `json:"schema_version"`
Metadata pluginapi.Metadata `json:"metadata"`
Capabilities registrationCapability `json:"capabilities"`
}
type registrationCapability struct {
RequestInterceptor bool `json:"request_interceptor"`
RequestLifecyclePlugin bool `json:"request_lifecycle_plugin"`
}
func main() {}
//export cliproxy_plugin_init
func cliproxy_plugin_init(_ *C.cliproxy_host_api, plugin *C.cliproxy_plugin_api) C.int {
if plugin == nil {
return 1
}
plugin.abi_version = C.uint32_t(pluginabi.ABIVersion)
plugin.call = C.cliproxy_plugin_call_fn(C.cliproxyPluginCall)
plugin.free_buffer = C.cliproxy_plugin_free_fn(C.cliproxyPluginFree)
plugin.shutdown = C.cliproxy_plugin_shutdown_fn(C.cliproxyPluginShutdown)
return 0
}
//export cliproxyPluginCall
func cliproxyPluginCall(method *C.char, request *C.uint8_t, requestLen C.size_t, response *C.cliproxy_buffer) C.int {
if response != nil {
response.ptr = nil
response.len = 0
}
if method == nil {
writeResponse(response, errorEnvelope("invalid_method", "method is required"))
return 1
}
var requestBytes []byte
if request != nil && requestLen > 0 {
requestBytes = C.GoBytes(unsafe.Pointer(request), C.int(requestLen))
}
raw, errHandle := handleMethod(C.GoString(method), requestBytes)
if errHandle != nil {
writeResponse(response, errorEnvelope("plugin_error", errHandle.Error()))
return 1
}
writeResponse(response, raw)
return 0
}
//export cliproxyPluginFree
func cliproxyPluginFree(ptr unsafe.Pointer, len C.size_t) {
if ptr != nil {
C.free(ptr)
}
_ = len
}
//export cliproxyPluginShutdown
func cliproxyPluginShutdown() {
state.mu.Lock()
defer state.mu.Unlock()
state.active = make(map[string]struct{})
}
func handleMethod(method string, request []byte) ([]byte, error) {
switch method {
case pluginabi.MethodPluginRegister, pluginabi.MethodPluginReconfigure:
if errConfigure := configure(request); errConfigure != nil {
return nil, errConfigure
}
return okEnvelope(pluginRegistration())
case pluginabi.MethodRequestInterceptBefore:
return interceptBeforeAuth(request)
case pluginabi.MethodRequestInterceptAfter:
return passThroughRequest(request)
case pluginabi.MethodRequestComplete:
return completeRequest(request)
default:
return errorEnvelope("unknown_method", "unknown method: "+method), nil
}
}
func configure(raw []byte) error {
var req lifecycleRequest
if len(raw) > 0 {
if errUnmarshal := json.Unmarshal(raw, &req); errUnmarshal != nil {
return errUnmarshal
}
}
if req.SchemaVersion < 2 {
return fmt.Errorf("request lifecycle plugin requires host schema version 2 or newer")
}
cfg := pluginConfig{MaxConcurrency: 2, RejectKeyword: "blocked"}
if len(req.ConfigYAML) > 0 {
if errUnmarshal := yaml.Unmarshal(req.ConfigYAML, &cfg); errUnmarshal != nil {
return errUnmarshal
}
}
if cfg.MaxConcurrency < 1 {
return fmt.Errorf("max_concurrency must be greater than zero")
}
cfg.RejectKeyword = strings.TrimSpace(cfg.RejectKeyword)
state.mu.Lock()
defer state.mu.Unlock()
state.config = cfg
return nil
}
func pluginRegistration() registration {
return registration{
SchemaVersion: pluginabi.SchemaVersion,
Metadata: pluginapi.Metadata{
Name: "request-lifecycle",
Version: "0.1.0",
Author: "router-for-me",
GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI",
Logo: "https://raw.githubusercontent.com/router-for-me/CLIProxyAPI/main/docs/logo.png",
ConfigFields: []pluginapi.ConfigField{
{
Name: "max_concurrency",
Type: pluginapi.ConfigFieldTypeInteger,
Description: "Maximum number of intercepted requests allowed in flight.",
},
{
Name: "reject_keyword",
Type: pluginapi.ConfigFieldTypeString,
Description: "Terminates requests whose raw JSON body contains this keyword.",
},
},
},
Capabilities: registrationCapability{
RequestInterceptor: true,
RequestLifecyclePlugin: true,
},
}
}
func interceptBeforeAuth(raw []byte) ([]byte, error) {
var req pluginapi.RequestInterceptRequest
if errUnmarshal := json.Unmarshal(raw, &req); errUnmarshal != nil {
return nil, errUnmarshal
}
if req.RequestID == "" {
return nil, fmt.Errorf("request ID is required")
}
state.mu.Lock()
defer state.mu.Unlock()
if _, exists := state.active[req.RequestID]; exists {
return okEnvelope(pluginapi.RequestInterceptResponse{Headers: req.Headers, Body: req.Body})
}
if state.config.RejectKeyword != "" && strings.Contains(string(req.Body), state.config.RejectKeyword) {
return terminatedResponse(http.StatusForbidden, "request blocked by plugin policy", nil)
}
if len(state.active) >= state.config.MaxConcurrency {
return terminatedResponse(http.StatusTooManyRequests, "plugin concurrency limit reached", http.Header{"Retry-After": {"1"}})
}
state.active[req.RequestID] = struct{}{}
return okEnvelope(pluginapi.RequestInterceptResponse{Headers: req.Headers, Body: req.Body})
}
func passThroughRequest(raw []byte) ([]byte, error) {
var req pluginapi.RequestInterceptRequest
if errUnmarshal := json.Unmarshal(raw, &req); errUnmarshal != nil {
return nil, errUnmarshal
}
return okEnvelope(pluginapi.RequestInterceptResponse{Headers: req.Headers, Body: req.Body})
}
func terminatedResponse(statusCode int, message string, headers http.Header) ([]byte, error) {
body, errMarshal := json.Marshal(map[string]any{
"error": map[string]any{
"type": "plugin_request_rejected",
"message": message,
},
})
if errMarshal != nil {
return nil, errMarshal
}
if headers == nil {
headers = make(http.Header)
}
headers.Set("Content-Type", "application/json")
return okEnvelope(pluginapi.RequestInterceptResponse{
Terminate: true,
StatusCode: statusCode,
ResponseHeaders: headers,
ResponseBody: body,
})
}
func completeRequest(raw []byte) ([]byte, error) {
var completion pluginapi.RequestCompletion
if errUnmarshal := json.Unmarshal(raw, &completion); errUnmarshal != nil {
return nil, errUnmarshal
}
state.mu.Lock()
defer state.mu.Unlock()
delete(state.active, completion.RequestID)
return okEnvelope(struct{}{})
}
func okEnvelope(v any) ([]byte, error) {
raw, errMarshal := json.Marshal(v)
if errMarshal != nil {
return nil, errMarshal
}
return json.Marshal(envelope{OK: true, Result: raw})
}
func errorEnvelope(code, message string) []byte {
raw, errMarshal := json.Marshal(envelope{OK: false, Error: &envelopeError{Code: code, Message: message}})
if errMarshal != nil {
return []byte(`{"ok":false,"error":{"code":"plugin_error","message":"encode error"}}`)
}
return raw
}
func writeResponse(response *C.cliproxy_buffer, raw []byte) {
if response == nil || len(raw) == 0 {
return
}
ptr := C.CBytes(raw)
if ptr == nil {
return
}
response.ptr = ptr
response.len = C.size_t(len(raw))
}