@@ -46,6 +46,7 @@ import (
4646 "sync"
4747 "time"
4848
49+ abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/jsonschema"
4950 "github.com/gobwas/ws"
5051 "github.com/gobwas/ws/wsutil"
5152)
@@ -151,7 +152,7 @@ type CustomEvent struct {
151152
152153type CustomCommand struct {
153154 Name string `json:"name"`
154- Expression string `json:"expression"`
155+ Expression string `json:"expression,omitempty "`
155156 ParamsSchema map [string ]any `json:"paramsSchema,omitempty"`
156157 ResultSchema map [string ]any `json:"resultSchema,omitempty"`
157158}
@@ -217,6 +218,10 @@ type ModCDPClient struct {
217218 pending map [int64 ]chan map [string ]any
218219 handlers map [string ][]Handler
219220 cdpHandlers map [string ][]func (CDPEvent )
221+ commandParamsSchemas map [string ]map [string ]any
222+ commandResultSchemas map [string ]map [string ]any
223+ eventSchemas map [string ]map [string ]any
224+ schemaMu sync.RWMutex
220225 handlersMu sync.Mutex
221226 targetSessions map [string ]string
222227 sessionTargets map [string ]map [string ]any
@@ -273,14 +278,19 @@ func New(opts Options) *ModCDPClient {
273278 if opts .WSConnectErrorSettleTimeoutMS == 0 {
274279 opts .WSConnectErrorSettleTimeoutMS = DefaultWSConnectErrorSettleTimeoutMS
275280 }
276- return & ModCDPClient {
277- opts : opts ,
278- pending : map [int64 ]chan map [string ]any {},
279- handlers : map [string ][]Handler {},
280- cdpHandlers : map [string ][]func (CDPEvent ){},
281- targetSessions : map [string ]string {},
282- sessionTargets : map [string ]map [string ]any {},
283- }
281+ client := & ModCDPClient {
282+ opts : opts ,
283+ pending : map [int64 ]chan map [string ]any {},
284+ handlers : map [string ][]Handler {},
285+ cdpHandlers : map [string ][]func (CDPEvent ){},
286+ commandParamsSchemas : map [string ]map [string ]any {},
287+ commandResultSchemas : map [string ]map [string ]any {},
288+ eventSchemas : map [string ]map [string ]any {},
289+ targetSessions : map [string ]string {},
290+ sessionTargets : map [string ]map [string ]any {},
291+ }
292+ client .hydrateCustomSurface ()
293+ return client
284294}
285295
286296func (c * ModCDPClient ) Connect () error {
@@ -436,11 +446,171 @@ func (c *ModCDPClient) Connect() error {
436446 return nil
437447}
438448
449+ func normalizeModCDPName (name string ) (string , error ) {
450+ normalized := strings .TrimSpace (name )
451+ if normalized == "" {
452+ return "" , fmt .Errorf ("name must be a non-empty string" )
453+ }
454+ if strings .Count (normalized , "." ) != 1 {
455+ return "" , fmt .Errorf ("name must be in Domain.method form" )
456+ }
457+ parts := strings .Split (normalized , "." )
458+ if parts [0 ] == "" || parts [1 ] == "" {
459+ return "" , fmt .Errorf ("name must be in Domain.method form" )
460+ }
461+ return normalized , nil
462+ }
463+
464+ func cloneSchema (schema map [string ]any ) map [string ]any {
465+ if schema == nil {
466+ return nil
467+ }
468+ normalized , _ := abxjsonschema .Normalize (schema ).(map [string ]any )
469+ if normalized == nil {
470+ return nil
471+ }
472+ return normalized
473+ }
474+
475+ func (c * ModCDPClient ) hydrateCustomSurface () {
476+ c .schemaMu .Lock ()
477+ defer c .schemaMu .Unlock ()
478+ for _ , command := range c .opts .CustomCommands {
479+ if command .Name == "" {
480+ continue
481+ }
482+ name , err := normalizeModCDPName (command .Name )
483+ if err != nil {
484+ continue
485+ }
486+ if schema := cloneSchema (command .ParamsSchema ); schema != nil {
487+ c .commandParamsSchemas [name ] = schema
488+ }
489+ if schema := cloneSchema (command .ResultSchema ); schema != nil {
490+ c .commandResultSchemas [name ] = schema
491+ }
492+ }
493+ for _ , event := range c .opts .CustomEvents {
494+ if event .Name == "" {
495+ continue
496+ }
497+ name , err := normalizeModCDPName (event .Name )
498+ if err != nil {
499+ continue
500+ }
501+ if schema := cloneSchema (event .EventSchema ); schema != nil {
502+ c .eventSchemas [name ] = schema
503+ }
504+ }
505+ }
506+
507+ func (c * ModCDPClient ) registerCustomCommandParams (params map [string ]any ) (string , bool , error ) {
508+ rawName , _ := params ["name" ].(string )
509+ name , err := normalizeModCDPName (rawName )
510+ if err != nil {
511+ return "" , false , err
512+ }
513+ c .schemaMu .Lock ()
514+ defer c .schemaMu .Unlock ()
515+ if rawSchema , ok := params ["paramsSchema" ].(map [string ]any ); ok {
516+ if schema := cloneSchema (rawSchema ); schema != nil {
517+ c .commandParamsSchemas [name ] = schema
518+ }
519+ }
520+ if rawSchema , ok := params ["resultSchema" ].(map [string ]any ); ok {
521+ if schema := cloneSchema (rawSchema ); schema != nil {
522+ c .commandResultSchemas [name ] = schema
523+ }
524+ }
525+ expression , _ := params ["expression" ].(string )
526+ return name , expression != "" , nil
527+ }
528+
529+ func (c * ModCDPClient ) registerCustomEventParams (params map [string ]any ) (string , error ) {
530+ rawName , _ := params ["name" ].(string )
531+ name , err := normalizeModCDPName (rawName )
532+ if err != nil {
533+ return "" , err
534+ }
535+ c .schemaMu .Lock ()
536+ defer c .schemaMu .Unlock ()
537+ if rawSchema , ok := params ["eventSchema" ].(map [string ]any ); ok {
538+ if schema := cloneSchema (rawSchema ); schema != nil {
539+ c .eventSchemas [name ] = schema
540+ }
541+ }
542+ return name , nil
543+ }
544+
545+ func (c * ModCDPClient ) validateCommandParams (method string , params map [string ]any ) error {
546+ c .schemaMu .RLock ()
547+ schema := c .commandParamsSchemas [method ]
548+ c .schemaMu .RUnlock ()
549+ if schema == nil {
550+ return nil
551+ }
552+ if err := abxjsonschema .Validate (schema , params ); err != nil {
553+ return fmt .Errorf ("%s params did not match paramsSchema: %w" , method , err )
554+ }
555+ return nil
556+ }
557+
558+ func (c * ModCDPClient ) validateCommandResult (method string , result any ) error {
559+ c .schemaMu .RLock ()
560+ schema := c .commandResultSchemas [method ]
561+ c .schemaMu .RUnlock ()
562+ if schema == nil {
563+ return nil
564+ }
565+ if err := abxjsonschema .Validate (schema , result ); err != nil {
566+ return fmt .Errorf ("%s result did not match resultSchema: %w" , method , err )
567+ }
568+ return nil
569+ }
570+
571+ func (c * ModCDPClient ) validateEventData (event string , data any ) (any , bool ) {
572+ c .schemaMu .RLock ()
573+ schema := c .eventSchemas [event ]
574+ c .schemaMu .RUnlock ()
575+ if schema == nil {
576+ return data , true
577+ }
578+ if err := abxjsonschema .Validate (schema , data ); err != nil {
579+ fmt .Fprintf (os .Stderr , "[ModCDPClient] %s event did not match eventSchema: %v\n " , event , err )
580+ return nil , false
581+ }
582+ return data , true
583+ }
584+
439585func (c * ModCDPClient ) Send (method string , params map [string ]any ) (any , error ) {
440586 startedAt := time .Now ().UnixMilli ()
441587 if params == nil {
442588 params = map [string ]any {}
443589 }
590+ if method == "Mod.addCustomCommand" {
591+ name , hasExpression , err := c .registerCustomCommandParams (params )
592+ if err != nil {
593+ return nil , err
594+ }
595+ if ! hasExpression {
596+ completedAt := time .Now ().UnixMilli ()
597+ c .LastCommandTiming = map [string ]any {
598+ "method" : method ,
599+ "target" : "client" ,
600+ "started_at" : startedAt ,
601+ "completed_at" : completedAt ,
602+ "duration_ms" : completedAt - startedAt ,
603+ }
604+ return map [string ]any {"name" : name , "registered" : true }, nil
605+ }
606+ } else if method == "Mod.addCustomEvent" {
607+ if _ , err := c .registerCustomEventParams (params ); err != nil {
608+ return nil , err
609+ }
610+ }
611+ if err := c .validateCommandParams (method , params ); err != nil {
612+ return nil , err
613+ }
444614 command , err := wrapCommandIfNeeded (method , params , c .opts .Routes , c .ExtSessionID )
445615 if err != nil {
446616 return nil , err
@@ -454,7 +624,13 @@ func (c *ModCDPClient) Send(method string, params map[string]any) (any, error) {
454624 "completed_at" : completedAt ,
455625 "duration_ms" : completedAt - startedAt ,
456626 }
457- return result , err
627+ if err != nil {
628+ return nil , err
629+ }
630+ if err := c .validateCommandResult (method , result ); err != nil {
631+ return nil , err
632+ }
633+ return result , nil
458634}
459635
460636func (c * ModCDPClient ) SendRaw (method string , params map [string ]any , sessionID ... string ) (map [string ]any , error ) {
@@ -926,16 +1102,20 @@ func (c *ModCDPClient) reader() {
9261102 params , _ := msg ["params" ].(map [string ]any )
9271103 bindingName , _ := params ["name" ].(string )
9281104 if event , data , ok := unwrapEventIfNeeded (method , params , sessionID , c .ExtSessionID ); ok {
1105+ validatedData , valid := c .validateEventData (event , data )
1106+ if ! valid {
1107+ continue
1108+ }
9291109 c .handlersMu .Lock ()
9301110 hs := append ([]Handler (nil ), c .handlers [event ]... )
9311111 cdpHandlers := append ([]func (CDPEvent ){}, c .cdpHandlers ["*" ]... )
9321112 cdpHandlers = append (cdpHandlers , c .cdpHandlers [event ]... )
9331113 c .handlersMu .Unlock ()
9341114 for _ , h := range hs {
935- go h (data )
1115+ go h (validatedData )
9361116 }
9371117 if bindingName == upstreamEventBindingName {
938- dataMap , _ := data .(map [string ]any )
1118+ dataMap , _ := validatedData .(map [string ]any )
9391119 cdpEvent := CDPEvent {Method : event , Params : dataMap , CDPSessionID : sessionID , SessionID : sessionID }
9401120 for _ , h := range cdpHandlers {
9411121 go h (cdpEvent )
@@ -945,16 +1125,24 @@ func (c *ModCDPClient) reader() {
9451125 continue
9461126 }
9471127 if method != "" {
1128+ validatedParams , valid := c .validateEventData (method , params )
1129+ if ! valid {
1130+ continue
1131+ }
1132+ validatedParamsMap , _ := validatedParams .(map [string ]any )
1133+ if validatedParamsMap == nil {
1134+ validatedParamsMap = map [string ]any {}
1135+ }
9481136 c .handlersMu .Lock ()
9491137 hs := append ([]Handler (nil ), c .handlers [method ]... )
9501138 cdpHandlers := append ([]func (CDPEvent ){}, c .cdpHandlers ["*" ]... )
9511139 cdpHandlers = append (cdpHandlers , c .cdpHandlers [method ]... )
9521140 c .handlersMu .Unlock ()
9531141 for _ , h := range hs {
954- go h (params )
1142+ go h (validatedParams )
9551143 }
9561144 if len (cdpHandlers ) > 0 {
957- event := CDPEvent {Method : method , Params : params , CDPSessionID : sessionID , SessionID : sessionID }
1145+ event := CDPEvent {Method : method , Params : validatedParamsMap , CDPSessionID : sessionID , SessionID : sessionID }
9581146 for _ , h := range cdpHandlers {
9591147 go h (event )
9601148 }
0 commit comments