diff --git a/src/dynamic/lang/go.rs b/src/dynamic/lang/go.rs index 535e0ebe..250afb4f 100644 --- a/src/dynamic/lang/go.rs +++ b/src/dynamic/lang/go.rs @@ -140,6 +140,18 @@ fn go_string_literal(s: &str) -> String { format!("\"{escaped}\"") } +fn go_identifier_expr(name: &str) -> Option { + let mut chars = name.chars(); + let first = chars.next()?; + if !(first == '_' || first.is_ascii_alphabetic()) { + return None; + } + if !chars.all(|c| c == '_' || c.is_ascii_alphanumeric()) { + return None; + } + Some(format!("entry.{name}")) +} + /// Sorted, deduped tab-prefixed import lines covering the driver's /// `fmt` + `os` plus everything in [`SHIM_IMPORTS`]. fn chain_step_imports() -> String { @@ -2613,6 +2625,96 @@ fn emit_graphql_resolver_harness( ) -> HarnessSource { let shim = probe_shim(); let go_mod = generate_go_mod_for_spec(GoShape::Generic, spec); + let handler_expr = go_identifier_expr(handler).unwrap_or_else(|| "nil".to_owned()); + let use_gqlgen_runtime = spec + .framework + .as_ref() + .map(|binding| binding.adapter == "graphql-gqlgen") + .unwrap_or(false); + let runtime_imports = if use_gqlgen_runtime { + r#" "bytes" + "encoding/json" + "net/http/httptest" + + "github.com/99designs/gqlgen/graphql" + gqlhandler "github.com/99designs/gqlgen/graphql/handler" + "github.com/vektah/gqlparser/v2" + "github.com/vektah/gqlparser/v2/ast" + "github.com/vektah/gqlparser/v2/gqlerror" +"# + } else { + "" + }; + let runtime_call = if use_gqlgen_runtime { + "\tif nyxTryGqlgenHandler(cb, payload) {\n\t\treturn\n\t}\n" + } else { + "" + }; + let runtime_helpers = if use_gqlgen_runtime { + format!( + r##" +type nyxExecutableSchema struct {{ + schema *ast.Schema + resolver reflect.Value + payload string + field string +}} + +func (s *nyxExecutableSchema) Schema() *ast.Schema {{ + return s.schema +}} + +func (s *nyxExecutableSchema) Complexity(typeName, fieldName string, childComplexity int, args map[string]interface{{}}) (int, bool) {{ + return 1, true +}} + +func (s *nyxExecutableSchema) Exec(ctx context.Context) graphql.ResponseHandler {{ + return func(ctx context.Context) *graphql.Response {{ + value, err := nyxInvokeResolverValue(s.resolver, s.payload) + if err != nil {{ + return &graphql.Response{{Errors: gqlerror.List{{gqlerror.Errorf(err.Error())}}}} + }} + data, err := json.Marshal(map[string]interface{{}}{{s.field: fmt.Sprint(value)}}) + if err != nil {{ + return &graphql.Response{{Errors: gqlerror.List{{gqlerror.Errorf(err.Error())}}}} + }} + return &graphql.Response{{Data: json.RawMessage(data)}} + }} +}} + +func nyxTryGqlgenHandler(cb reflect.Value, payload string) bool {{ + schema, err := gqlparser.LoadSchema(&ast.Source{{ + Name: "nyx.graphql", + Input: "schema {{ query: Query }}\ntype Query {{ {field}(id: String, input: String): String }}", + }}) + if err != nil {{ + fmt.Fprintf(os.Stderr, "NYX_GQLGEN_SCHEMA_FALLBACK: %v\n", err) + return false + }} + server := gqlhandler.NewDefaultServer(&nyxExecutableSchema{{ + schema: schema, resolver: cb, payload: payload, field: "{field}", + }}) + body, _ := json.Marshal(map[string]interface{{}}{{ + "query": "query($value: String) {{ {field}(id: $value, input: $value) }}", + "variables": map[string]interface{{}}{{"value": payload}}, + }}) + req := httptest.NewRequest("POST", "/query", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + server.ServeHTTP(rec, req) + if rec.Code < 200 || rec.Code >= 300 {{ + fmt.Fprintf(os.Stderr, "NYX_GQLGEN_HANDLER_FALLBACK: status=%d body=%s\n", rec.Code, rec.Body.String()) + return false + }} + fmt.Print(rec.Body.String()) + return true +}} +"##, + field = field + ) + } else { + String::new() + }; let source = format!( r##"// Nyx dynamic harness — GraphQL resolver (Phase 21 / Track M.3). package main @@ -2622,6 +2724,7 @@ import ( "fmt" "os" "reflect" +{runtime_imports} "nyx-harness/entry" ) @@ -2640,37 +2743,67 @@ func main() {{ payload := nyxPayload() fmt.Println("__NYX_GRAPHQL_RESOLVER__: " + "{type_name}" + "." + "{field}") fmt.Println("__NYX_SINK_HIT__") - cb, ok := entry.NyxResolvers["{handler}"] - if !ok {{ + cb := reflect.ValueOf({handler_expr}) + if !cb.IsValid() || cb.Kind() != reflect.Func {{ fmt.Fprintln(os.Stderr, "NYX_RESOLVER_NOT_FOUND: " + "{handler}") os.Exit(78) }} - v := reflect.ValueOf(cb) - args := make([]reflect.Value, v.Type().NumIn()) - for i := 0; i < v.Type().NumIn(); i++ {{ - want := v.Type().In(i) - if want.Kind() == reflect.String {{ - args[i] = reflect.ValueOf(payload) - }} else if want.String() == "context.Context" {{ - args[i] = reflect.ValueOf(context.Background()) - }} else {{ - args[i] = reflect.Zero(want) - }} - }} +{runtime_call} defer func() {{ if r := recover(); r != nil {{ fmt.Fprintf(os.Stderr, "NYX_EXCEPTION: panic: %v\n", r) }} }}() - out := v.Call(args) - if len(out) > 0 {{ - fmt.Println(out[0].Interface()) + value, err := nyxInvokeResolverValue(cb, payload) + if err != nil {{ + fmt.Fprintf(os.Stderr, "NYX_EXCEPTION: %v\n", err) + return + }} + if value != nil {{ + fmt.Println(value) }} }} + +func nyxInvokeResolverValue(v reflect.Value, payload string) (interface{{}}, error) {{ + contextType := reflect.TypeOf((*context.Context)(nil)).Elem() + errorType := reflect.TypeOf((*error)(nil)).Elem() + args := make([]reflect.Value, v.Type().NumIn()) + for i := 0; i < v.Type().NumIn(); i++ {{ + want := v.Type().In(i) + if want.Kind() == reflect.String {{ + args[i] = reflect.ValueOf(payload) + }} else if want.Implements(contextType) {{ + args[i] = reflect.ValueOf(context.Background()) + }} else if contextType.AssignableTo(want) {{ + args[i] = reflect.ValueOf(context.Background()) + }} else {{ + args[i] = reflect.Zero(want) + }} + }} + out := v.Call(args) + var value interface{{}} + for _, item := range out {{ + if item.Type().Implements(errorType) {{ + if (item.Kind() == reflect.Interface || item.Kind() == reflect.Pointer) && !item.IsNil() {{ + return nil, item.Interface().(error) + }} + continue + }} + if value == nil && item.IsValid() {{ + value = item.Interface() + }} + }} + return value, nil +}} +{runtime_helpers} "##, handler = handler, + handler_expr = handler_expr, type_name = type_name, field = field, + runtime_imports = runtime_imports, + runtime_call = runtime_call, + runtime_helpers = runtime_helpers, ); HarnessSource { source, diff --git a/src/dynamic/lang/java.rs b/src/dynamic/lang/java.rs index 0e94131c..75df026c 100644 --- a/src/dynamic/lang/java.rs +++ b/src/dynamic/lang/java.rs @@ -4687,9 +4687,6 @@ public class NyxHarness {{ System.exit(78); }} m.setAccessible(true); - if (nyxTrySpringHandlerInterceptor(instance, m, payload)) {{ - return; - }} Class[] params = m.getParameterTypes(); Object[] mArgs = new Object[params.length]; for (int i = 0; i < params.length; i++) {{ @@ -4810,6 +4807,9 @@ public class NyxHarness {{ System.exit(78); }} m.setAccessible(true); + if (nyxTrySpringHandlerExecutionChain(instance, m, payload)) {{ + return; + }} Class[] params = m.getParameterTypes(); Object[] mArgs = new Object[params.length]; for (int i = 0; i < params.length; i++) {{ @@ -4835,6 +4835,57 @@ public class NyxHarness {{ return ""; }} + static boolean nyxTrySpringHandlerExecutionChain(Object instance, Method m, String payload) {{ + if (!m.getName().equals("preHandle") || m.getParameterTypes().length < 3) {{ + return false; + }} + try {{ + Class chainClass = Class.forName("org.springframework.web.servlet.HandlerExecutionChain"); + Class interceptorClass = Class.forName("org.springframework.web.servlet.HandlerInterceptor"); + if (!interceptorClass.isAssignableFrom(instance.getClass())) {{ + return false; + }} + Object interceptors = java.lang.reflect.Array.newInstance(interceptorClass, 1); + java.lang.reflect.Array.set(interceptors, 0, instance); + Object chain = chainClass + .getConstructor(Object.class, interceptors.getClass()) + .newInstance(new Object(), interceptors); + Method getInterceptors = chainClass.getMethod("getInterceptors"); + Object chainInterceptors = getInterceptors.invoke(chain); + int count = chainInterceptors == null ? 0 : java.lang.reflect.Array.getLength(chainInterceptors); + if (count == 0) {{ + return false; + }} + Object request = null; + Object response = null; + for (Class p : m.getParameterTypes()) {{ + String name = p.getName(); + if (request == null && name.endsWith("HttpServletRequest")) {{ + request = nyxServletProxy(p, payload); + }} else if (response == null && name.endsWith("HttpServletResponse")) {{ + response = nyxServletProxy(p, payload); + }} + }} + if (request == null || response == null) {{ + return false; + }} + Object interceptor = java.lang.reflect.Array.get(chainInterceptors, 0); + Method preHandle = interceptor.getClass().getMethod( + "preHandle", + m.getParameterTypes()[0], + m.getParameterTypes()[1], + m.getParameterTypes()[2] + ); + preHandle.invoke(interceptor, request, response, new Object()); + return true; + }} catch (ClassNotFoundException missingSpring) {{ + return false; + }} catch (Throwable e) {{ + System.err.println("NYX_SPRING_CHAIN_FALLBACK: " + e.getClass().getName() + ": " + e.getMessage()); + return false; + }} + }} + static boolean nyxTrySpringHandlerInterceptor(Object instance, Method m, String payload) {{ Class[] params = m.getParameterTypes(); if (params.length < 3 || !m.getName().equals("preHandle")) {{ diff --git a/src/dynamic/lang/python.rs b/src/dynamic/lang/python.rs index ddbf1385..9c7e21cb 100644 --- a/src/dynamic/lang/python.rs +++ b/src/dynamic/lang/python.rs @@ -1536,8 +1536,64 @@ def _nyx_try_celery_eager(task, body): print(f"NYX_CELERY_EAGER_FALLBACK: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True) return False +def _nyx_try_celery_registered_task(handler_name, task, body): + try: + from celery import current_app + except Exception: + return False + try: + app = getattr(task, "app", None) or current_app + if app is None or not hasattr(app, "tasks"): + return False + if hasattr(app, "conf"): + try: + app.conf.task_always_eager = True + app.conf.task_eager_propagates = False + except Exception: + pass + candidates = [ + handler_name, + getattr(task, "name", None), + getattr(_entry_mod, "__name__", "") + "." + handler_name, + ] + registered = None + for name in candidates: + if not name: + continue + try: + registered = app.tasks.get(name) + except Exception: + registered = None + if registered is not None: + break + if registered is None: + suffix = "." + handler_name + try: + for task_name, candidate in app.tasks.items(): + if str(task_name).endswith(suffix): + registered = candidate + break + except Exception: + registered = None + if registered is None: + return False + if hasattr(registered, "signature"): + sig = registered.signature(args=(body,)) + result = sig.apply(throw=False) + else: + result = registered.apply(args=(body,), throw=False) + value = getattr(result, "result", None) + if value is not None: + print(str(value), flush=True) + return True + except SystemExit: + raise + except Exception as _e: + print(f"NYX_CELERY_REGISTRY_FALLBACK: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True) + return False + try: - if not _nyx_try_celery_eager(_h, payload): + if not _nyx_try_celery_registered_task({handler:?}, _h, payload) and not _nyx_try_celery_eager(_h, payload): _result = _h(payload) if _result is not None: try: @@ -1842,7 +1898,54 @@ try: print(f"NYX_DJANGO_MIDDLEWARE_FALLBACK: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True) return False - if not _nyx_try_django_middleware(_h, payload): + def _nyx_try_django_handler_chain(factory, body): + try: + import types + from django.conf import settings + module_name = "_nyx_phase21_middleware" + module = types.ModuleType(module_name) + setattr(module, "NyxMiddleware", factory) + module.urlpatterns = [] + sys.modules[module_name] = module + middleware_path = module_name + ".NyxMiddleware" + if not settings.configured: + settings.configure( + DEFAULT_CHARSET="utf-8", + SECRET_KEY="nyx-dynamic-harness", + ROOT_URLCONF=module_name, + ALLOWED_HOSTS=["testserver", "localhost", "127.0.0.1"], + INSTALLED_APPS=[], + MIDDLEWARE=[middleware_path], + ) + else: + try: + settings.MIDDLEWARE = [middleware_path] + settings.ROOT_URLCONF = module_name + except Exception: + return False + import django + django.setup() + from django.core.handlers.base import BaseHandler + from django.test import RequestFactory + request = RequestFactory().post("/nyx", data={{"q": body}}) + request._body = str(body).encode("utf-8", "replace") + handler = BaseHandler() + handler.load_middleware() + response = handler.get_response(request) + body_bytes = getattr(response, "content", None) + if body_bytes: + try: + print(body_bytes.decode("utf-8", "replace"), flush=True) + except Exception: + print(str(body_bytes), flush=True) + return True + except SystemExit: + raise + except Exception as _e: + print(f"NYX_DJANGO_HANDLER_CHAIN_FALLBACK: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True) + return False + + if not _nyx_try_django_handler_chain(_h, payload) and not _nyx_try_django_middleware(_h, payload): _req = _NyxRequest(payload) # Try class-shaped middleware (instantiate with a get_response stub). try: diff --git a/src/dynamic/lang/ruby.rs b/src/dynamic/lang/ruby.rs index b1ac939c..47a377be 100644 --- a/src/dynamic/lang/ruby.rs +++ b/src/dynamic/lang/ruby.rs @@ -765,7 +765,12 @@ if Object.const_defined?({handler:?}) require 'sidekiq/testing' if cls.respond_to?(:perform_async) Sidekiq::Testing.inline! do - cls.perform_async($nyx_payload) + begin + require 'sidekiq/client' + Sidekiq::Client.push('class' => cls, 'args' => [$nyx_payload]) + rescue LoadError, StandardError + cls.perform_async($nyx_payload) + end end exit 0 end diff --git a/tests/phase21_corpus.rs b/tests/phase21_corpus.rs index 0014230f..ca9ced59 100644 --- a/tests/phase21_corpus.rs +++ b/tests/phase21_corpus.rs @@ -680,6 +680,9 @@ fn scheduled_job_python_harness_carries_sentinel_and_handler() { assert!(h.source.contains("__NYX_SCHEDULED_JOB__")); assert!(h.source.contains("\"tick\"")); assert!(h.source.contains("*/5 * * * *")); + assert!(h.source.contains("_nyx_try_celery_registered_task")); + assert!(h.source.contains("current_app")); + assert!(h.source.contains("app.tasks")); assert!(h.source.contains("_nyx_try_celery_eager")); assert!(h.source.contains("task.apply")); } @@ -714,6 +717,10 @@ fn scheduled_job_java_harness_carries_sentinel_and_handler() { assert!(h.source.contains("\"execute\"")); assert!(h.source.contains("nyxTryQuartz")); assert!(h.source.contains("org.quartz.JobBuilder")); + assert!( + !h.source + .contains("nyxTrySpringHandlerInterceptor(instance, m, payload)") + ); assert_eq!(h.command, vec!["java", "-cp", ".:lib/*", "NyxHarness"]); } @@ -729,6 +736,7 @@ fn scheduled_job_ruby_harness_carries_sentinel_and_handler() { assert!(h.source.contains("__NYX_SCHEDULED_JOB__")); assert!(h.source.contains("TickWorker")); assert!(h.source.contains("sidekiq/testing")); + assert!(h.source.contains("Sidekiq::Client.push")); assert!(h.source.contains("perform_async")); } @@ -873,7 +881,34 @@ fn graphql_resolver_go_harness_carries_sentinel_and_field() { let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_GRAPHQL_RESOLVER__")); assert!(h.source.contains("ResolveUser")); - assert!(h.source.contains("entry.NyxResolvers")); + assert!(h.source.contains("reflect.ValueOf(entry.ResolveUser)")); + assert!(!h.source.contains("entry.NyxResolvers")); +} + +#[test] +fn graphql_resolver_go_gqlgen_harness_uses_handler_runtime() { + let spec = framework_bound_spec( + Lang::Go, + EvEntryKind::GraphQLResolver { + type_name: "Query".into(), + field: "user".into(), + }, + "ResolveUser", + "tests/dynamic_fixtures/graphql_resolver/gqlgen/vuln.go", + "graphql-gqlgen", + ); + let h = lang::emit(&spec).expect("emit ok"); + assert!( + h.source + .contains("github.com/99designs/gqlgen/graphql/handler") + ); + assert!(h.source.contains("gqlhandler.NewDefaultServer")); + assert!(h.source.contains("httptest.NewRecorder")); + assert!(h.source.contains("nyxExecutableSchema")); + assert!(h.source.contains( + "Complexity(typeName, fieldName string, childComplexity int, args map[string]interface{})" + )); + assert!(!h.source.contains("entry.NyxResolvers")); } #[test] @@ -945,6 +980,9 @@ fn middleware_python_harness_carries_sentinel_and_handler() { let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_MIDDLEWARE__")); assert!(h.source.contains("\"audit\"")); + assert!(h.source.contains("_nyx_try_django_handler_chain")); + assert!(h.source.contains("BaseHandler")); + assert!(h.source.contains("handler.load_middleware")); assert!(h.source.contains("_nyx_try_django_middleware")); assert!(h.source.contains("RequestFactory")); } @@ -979,6 +1017,8 @@ fn middleware_java_harness_carries_sentinel_and_handler() { let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_MIDDLEWARE__")); assert!(h.source.contains("\"preHandle\"")); + assert!(h.source.contains("nyxTrySpringHandlerExecutionChain")); + assert!(h.source.contains("HandlerExecutionChain")); assert!(h.source.contains("nyxTrySpringHandlerInterceptor")); assert!(h.source.contains("HttpServletRequest")); }