From 1990864b1701ced8aa8b5e235186e7ed8856bee3 Mon Sep 17 00:00:00 2001 From: Mathieu Ancelin Date: Fri, 8 Jul 2022 17:05:16 +0200 Subject: [PATCH] Format code before release --- manual/src/main/paradox/code/openapi.json | 1480 +------------- otoroshi/app/cluster/cluster.scala | 258 +-- .../controllers/BackOfficeController.scala | 114 +- .../app/controllers/HealthController.scala | 14 +- .../adminapi/ClusterController.scala | 39 +- otoroshi/app/env/Env.scala | 40 +- otoroshi/app/gateway/handlers.scala | 3 +- otoroshi/app/models/descriptor.scala | 18 +- otoroshi/app/next/models/route.scala | 37 +- otoroshi/app/next/plugins/graphql.scala | 182 +- otoroshi/app/next/proxy/engine.scala | 54 +- otoroshi/app/next/proxy/errors.scala | 4 +- otoroshi/app/next/proxy/request.scala | 49 +- otoroshi/app/next/proxy/zones.scala | 172 +- otoroshi/app/script/requesthandler.scala | 98 +- otoroshi/app/ssl/ssl.scala | 2 +- .../drivers/inmemory/InMemoryDataStores.scala | 15 +- otoroshi/app/storage/storage.scala | 74 +- .../stores/KvAuthConfigsDataStore.scala | 2 +- otoroshi/app/tcp/tcp.scala | 2 +- otoroshi/app/utils/httpclient.scala | 39 +- otoroshi/conf/schemas/openapi-cfg.json | 253 --- otoroshi/conf/schemas/openapi.json | 1482 +------------- otoroshi/javascript/src/pages/MetricsPage.js | 184 +- .../src/pages/RouteDesigner/Designer.js | 308 +-- .../src/pages/RouteDesigner/Graph.js | 31 +- .../src/pages/RouteDesigner/GraphQLForm.js | 906 +++++---- .../src/pages/RouteDesigner/Informations.js | 8 +- .../src/pages/RouteDesigner/MocksDesigner.js | 1759 +++++++++-------- .../pages/RouteDesigner/RouteComposition.js | 38 +- .../src/pages/RouteDesigner/TryIt.js | 246 +-- .../src/pages/RouteDesigner/form.js | 2 +- .../src/pages/RouteDesigner/index.js | 31 +- otoroshi/public/openapi.json | 1482 +------------- 34 files changed, 2785 insertions(+), 6641 deletions(-) diff --git a/manual/src/main/paradox/code/openapi.json b/manual/src/main/paradox/code/openapi.json index 35ad6817c1..832b9332dd 100644 --- a/manual/src/main/paradox/code/openapi.json +++ b/manual/src/main/paradox/code/openapi.json @@ -17232,60 +17232,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.CorsSettings" : { - "description" : "Settings for CORS support", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Whether or not cors is enabled", - "type" : "boolean" - }, - "allowCredentials" : { - "description" : "Allow to pass credentials", - "type" : "boolean" - }, - "maxAge" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "number" - } ], - "description" : "Cors max age" - }, - "allowMethods" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "The cors allowed methods" - }, - "allowHeaders" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "The cors allowed headers" - }, - "excludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "The cors excluded patterns" - }, - "exposeHeaders" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "The cors exposed header" - }, - "allowOrigin" : { - "description" : "The cors allowed origin", - "type" : "string" - } - } - }, "otoroshi.plugins.apikeys.BiscuitConf" : { "description" : "Configuration for the biscuit plugin", "type" : "object", @@ -17611,21 +17557,6 @@ "$ref" : "#/components/schemas/otoroshi.models.WebAuthnOtoroshiAdmin" } }, - "otoroshi.models.RegionMatch" : { - "description" : "Match a target if in the same region", - "type" : "object", - "properties" : { - "type" : { - "description" : "the kind of predicate", - "type" : "string", - "enum" : [ "AlwaysMatch", "NetworkLocationMatch", "GeolocationMatch" ] - }, - "region" : { - "description" : "Region name", - "type" : "string" - } - } - }, "otoroshi.next.plugins.Cors" : { "description" : "Plugin to use cors", "type" : "object", @@ -17887,35 +17818,6 @@ } } }, - "otoroshi.models.RefJwtVerifier" : { - "description" : "Reference to a jwt verifier", - "type" : "object", - "properties" : { - "excludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Verifier excluded paths" - }, - "ids" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Verifiers ids" - }, - "type" : { - "description" : "the kind of verifier", - "type" : "string", - "enum" : [ "global", "local", "ref" ] - }, - "enabled" : { - "description" : "Verifier enabled", - "type" : "boolean" - } - } - }, "otoroshi.next.plugins.ViolationsException" : { "description" : "???", "type" : "object", @@ -17941,20 +17843,7 @@ "otoroshi.next.plugins.AuthModule" : { "description" : "Plugin to use auth. modules", "type" : "object", - "properties" : { - "module" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Id of the auth. module" - }, - "pass_with_apikey" : { - "description" : "let the request pass if an apikey is present", - "type" : "boolean" - } - } + "properties" : { } }, "otoroshi.plugins.clientcert.HasClientCertValidator" : { "description" : "Plugin that validates client certificates", @@ -17967,17 +17856,6 @@ "$ref" : "#/components/schemas/otoroshi.script.Script" } }, - "otoroshi.models.WeightedBestResponseTime" : { - "description" : "Loadbalancing policy that route to best response time targets with a weight", - "type" : "object", - "properties" : { - "ratio" : { - "format" : "double", - "description" : "Weight ratio", - "type" : "number" - } - } - }, "otoroshi.next.plugins.AdditionalHeadersIn" : { "description" : "Plugin that add headers on a request", "type" : "object", @@ -18061,67 +17939,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.CustomTimeouts" : { - "description" : "Settings for custom timeouts for a specific path", - "type" : "object", - "properties" : { - "path" : { - "description" : "path on which this configuration works", - "type" : "string" - }, - "callAndStreamTimeout" : { - "format" : "int64", - "description" : "Specify how long each call should last at most in milliseconds (hard timeout, connection will be closed after that duration)", - "type" : "integer" - }, - "callTimeout" : { - "format" : "int64", - "description" : "Specify how long each call should last at most in milliseconds (soft timeout as it's enforced by the circuit breaker)", - "type" : "integer" - }, - "idleTimeout" : { - "format" : "int64", - "description" : "Timeout on idle connection", - "type" : "integer" - }, - "globalTimeout" : { - "format" : "int64", - "description" : "Specify how long the global call (with retries) should last at most in milliseconds", - "type" : "integer" - }, - "connectionTimeout" : { - "format" : "int64", - "description" : "Timeout at connection", - "type" : "integer" - } - } - }, - "otoroshi.models.BasicAuthConstraints" : { - "description" : "Settings to extract apikey from a basic auth header like", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Constraint enabled", - "type" : "boolean" - }, - "headerName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name to get client_id:client_secret base64 encoded" - }, - "queryName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Query param name to get client_id:client_secret base64 encoded" - } - } - }, "otoroshi.models.Transform" : { "description" : "jwt token transformation policy settings", "type" : "object", @@ -18358,21 +18175,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.InfraProviderMatch" : { - "description" : "Match a target if in the same infrastructure", - "type" : "object", - "properties" : { - "provider" : { - "description" : "provider name", - "type" : "string" - }, - "type" : { - "description" : "the kind of predicate", - "type" : "string", - "enum" : [ "AlwaysMatch", "NetworkLocationMatch", "GeolocationMatch" ] - } - } - }, "otoroshi.models.Exporter" : { "oneOf" : [ { "$ref" : "#/components/schemas/otoroshi.events.KafkaConfig" @@ -19983,37 +19785,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.NetworkLocationMatch" : { - "description" : "Match a target if in the same network location", - "type" : "object", - "properties" : { - "rack" : { - "description" : "Rack name", - "type" : "string" - }, - "provider" : { - "description" : "Provider name", - "type" : "string" - }, - "dataCenter" : { - "description" : "Datacenter name", - "type" : "string" - }, - "zone" : { - "description" : "Zone name", - "type" : "string" - }, - "type" : { - "description" : "the kind of predicate", - "type" : "string", - "enum" : [ "AlwaysMatch", "NetworkLocationMatch", "GeolocationMatch" ] - }, - "region" : { - "description" : "Region name", - "type" : "string" - } - } - }, "otoroshi.script.PluginType" : { "type" : "string", "enum" : [ "app", "transformer", "validator", "preroute", "sink", "listener", "job", "exporter" ], @@ -20330,36 +20101,6 @@ "type" : "string", "description" : "the id of a service prefixed by 'service_'" }, - "otoroshi.models.SecComHeaders" : { - "description" : "Header names for the otoroshi exchange protocol", - "type" : "object", - "properties" : { - "claimRequestName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name where the info token will be" - }, - "stateRequestName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name where the validation token will be" - }, - "stateResponseName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name where the validation token respondewill be" - } - } - }, "otoroshi.auth.SAMLCredentials" : { "description" : "Used to sign, encrypt assertions and sign SAML documents", "type" : "object", @@ -20549,463 +20290,48 @@ } } }, - "otoroshi.models.ServiceDescriptor" : { - "description" : "The otoroshi model for a service (handles routing)", + "otoroshi.next.plugins.UdpTunnel" : { + "description" : "Plugin to have udp tunnels over websockets", + "type" : "object", + "properties" : { } + }, + "otoroshi.plugins.izanami.IzanamiCanaryConfig" : { + "description" : "Configuration for IzanamiCanary", "type" : "object", "properties" : { - "buildMode" : { - "description" : "Display a construction page when a user try to use the service", - "type" : "boolean" - }, - "hosts" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Possible hosts for the service" - }, - "privateApp" : { - "description" : "When enabled, user will be allowed to use the service (UI) only if they are registered users of the private apps domain", - "type" : "boolean" - }, - "localScheme" : { - "description" : "The scheme used localy, mainly http", + "izanamiClientId" : { + "description" : "Izanami client id", "type" : "string" }, - "authConfigRef" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "A reference to a global auth module config" - }, - "issueCertCA" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "CA for cert issuance" - }, - "root" : { - "description" : "Otoroshi will append this root to any target choosen. If the specified root is '/api/foo', then a request to https://yyyyyyy/bar will actually hit https://xxxxxxxxx/api/foo/bar", + "configId" : { + "description" : "Id of the target izanami configuration", "type" : "string" }, - "name" : { - "description" : "The name of your service. Only for debug and human readability purposes", + "experimentId" : { + "description" : "Id of the target izanami experiment", "type" : "string" }, - "additionalHeaders" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Specify headers that will be added to each client request. Useful to add authentication" + "mtls" : { + "description" : "Izanami server tls config", + "$ref" : "#/components/schemas/otoroshi.utils.http.MtlsConfig" }, - "domain" : { - "description" : "The domain on which the service is available.", + "izanamiUrl" : { + "description" : "Izanami server url", "type" : "string" }, - "clientConfig" : { - "description" : "Http client settings", - "$ref" : "#/components/schemas/otoroshi.models.ClientConfig" + "timeout" : { + "description" : "Timeout when talking to the izanami server", + "type" : "number" + }, + "izanamiClientSecret" : { + "description" : "Izanami client secret", + "type" : "string" }, - "matchingRoot" : { + "routeConfig" : { "oneOf" : [ { "$ref" : "#/components/schemas/Null" }, { - "type" : "string" - } ], - "description" : "The root path on which the service is available" - }, - "forceHttps" : { - "description" : "Will force redirection to https:// if not present", - "type" : "boolean" - }, - "localHost" : { - "description" : "The host used localy, mainly localhost:xxxx", - "type" : "string" - }, - "sendOtoroshiHeadersBack" : { - "description" : "When enabled, Otoroshi will send headers to consumer like request id, client latency, overhead, etc ...", - "type" : "boolean" - }, - "healthCheck" : { - "description" : "Healthcheck settings", - "$ref" : "#/components/schemas/otoroshi.models.HealthCheck" - }, - "strictlyPrivate" : { - "description" : "When strictly private, private app session will not pass apikey filters", - "type" : "boolean" - }, - "detectApiKeySooner" : { - "description" : "Detect if an apikey is present but do not fail if not", - "type" : "boolean" - }, - "allowHttp10" : { - "description" : "Allow HTTP/1.0 requests", - "type" : "boolean" - }, - "subdomain" : { - "description" : "The subdomain on which the service is available", - "type" : "string" - }, - "paths" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Matching paths on request" - }, - "stripPath" : { - "description" : "Strip matching path in the forwarded request path", - "type" : "boolean" - }, - "secComAlgoChallengeOtoToBack" : { - "description" : "Algorithm to sign challenge token to the backend", - "$ref" : "#/components/schemas/otoroshi.models.AlgoSettings" - }, - "apiKeyConstraints" : { - "description" : "Routing and extraction constraints for the apikeyh", - "$ref" : "#/components/schemas/otoroshi.models.ApiKeyConstraints" - }, - "env" : { - "description" : "The line on which the service is available. Based on that value, the name of the line will be appended to the subdomain. For line prod, nothing will be appended. For example, if the subdomain is 'foo' and line is 'preprod', then the exposed service will be available at 'foo.preprod.mydomain'", - "type" : "string" - }, - "xForwardedHeaders" : { - "description" : "Send X-Forwarded-* headers", - "type" : "boolean" - }, - "transformerRefs" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Enabled transformer plugins" - }, - "enabled" : { - "description" : "Activate or deactivate your service. Once disabled, users will get an error page saying the service does not exist", - "type" : "boolean" - }, - "gzip" : { - "description" : "GZIP settings", - "$ref" : "#/components/schemas/otoroshi.utils.gzip.GzipConfig" - }, - "sendInfoToken" : { - "description" : "Should otoroshi send info token", - "type" : "boolean" - }, - "tcpUdpTunneling" : { - "description" : "Enabled TCP/UDP tunneling through websocket connection", - "type" : "boolean" - }, - "removeHeadersOut" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Remove headers on client response" - }, - "useAkkaHttpClient" : { - "description" : "Use akka http client for this service", - "type" : "boolean" - }, - "maintenanceMode" : { - "description" : "Display a maintainance page when a user try to use the service", - "type" : "boolean" - }, - "id" : { - "description" : "A unique random string to identify your service", - "type" : "string" - }, - "removeHeadersIn" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Remove headers on client request" - }, - "logAnalyticsOnServer" : { - "description" : "Log analytics event on the server", - "type" : "boolean" - }, - "secComAlgoInfoToken" : { - "description" : "Algorithm to verify/sign challenge token coming from/to the backend", - "$ref" : "#/components/schemas/otoroshi.models.AlgoSettings" - }, - "userFacing" : { - "description" : "The fact that this service will be seen by users and cannot be impacted by the Snow Monkey", - "type" : "boolean" - }, - "transformerConfig" : { - "description" : "Transformer plugins configuration", - "type" : "object" - }, - "clientValidatorRef" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "A reference to validation authority" - }, - "securityExcludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Exclude some paths" - }, - "ipFiltering" : { - "description" : "Ip filtering settings", - "$ref" : "#/components/schemas/otoroshi.models.IpFiltering" - }, - "targets" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/otoroshi.models.Target" - }, - "description" : "The list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures" - }, - "redirection" : { - "description" : "Redirection settings", - "$ref" : "#/components/schemas/otoroshi.models.RedirectionSettings" - }, - "tags" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Entity tags" - }, - "restrictions" : { - "description" : "Restriction settings", - "$ref" : "#/components/schemas/otoroshi.models.Restrictions" - }, - "overrideHost" : { - "description" : "Host header will be overriden with Host of the target", - "type" : "boolean" - }, - "accessValidator" : { - "description" : "Service access validatiors", - "$ref" : "#/components/schemas/otoroshi.script.AccessValidatorRef" - }, - "sendStateChallenge" : { - "description" : "Should otoroshi send challenge token", - "type" : "boolean" - }, - "chaosConfig" : { - "description" : "Chaos engineering settings", - "$ref" : "#/components/schemas/otoroshi.models.ChaosConfig" - }, - "secComInfoTokenVersion" : { - "description" : "Version of the info token", - "$ref" : "#/components/schemas/otoroshi.models.SecComInfoTokenVersion" - }, - "additionalHeadersOut" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Specify headers that will be added to each client response" - }, - "secComHeaders" : { - "description" : "Header names for sec. com. protocol", - "$ref" : "#/components/schemas/otoroshi.models.SecComHeaders" - }, - "matchingHeaders" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Specify headers that MUST be present on client request to route it. Useful to implement versioning" - }, - "secComAlgoChallengeBackToOto" : { - "description" : "Algorithm to verify challenge token coming from the backend", - "$ref" : "#/components/schemas/otoroshi.models.AlgoSettings" - }, - "secComUseSameAlgo" : { - "description" : "Use the same algo for info token, challenge token signing, challenge token verification", - "type" : "boolean" - }, - "useNewWSClient" : { - "description" : "Use akka http client for this service on websocket calls", - "type" : "boolean" - }, - "secComExcludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "URI patterns excluded from secured communications" - }, - "redirectToLocal" : { - "description" : "If you work locally with Otoroshi, you may want to use that feature to redirect one particuliar service to a local host. For example, you can relocate https://foo.preprod.bar.com to http://localhost:8080 to make some tests", - "type" : "boolean" - }, - "enforceSecureCommunication" : { - "description" : "When enabled, Otoroshi will try to exchange headers with backend service to ensure no one else can use the service from outside", - "type" : "boolean" - }, - "missingOnlyHeadersOut" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Add header on client response if they are not present" - }, - "secComSettings" : { - "description" : "Sec. com. settings", - "$ref" : "#/components/schemas/otoroshi.models.AlgoSettings" - }, - "handleLegacyDomain" : { - "description" : "Use 'domain', 'subdomain', 'env' and 'matchingRoot' for routing in addition to hosts, or just use hosts.", - "type" : "boolean" - }, - "canary" : { - "description" : "Canary settings", - "$ref" : "#/components/schemas/otoroshi.models.Canary" - }, - "_loc" : { - "description" : "Entity location", - "$ref" : "#/components/schemas/otoroshi.models.EntityLocation" - }, - "plugins" : { - "description" : "Plugins enabled for this service. will replace separate plugins fields in a near future", - "$ref" : "#/components/schemas/otoroshi.script.plugins.Plugins" - }, - "secComTtl" : { - "description" : "TTL for the info token", - "type" : "number" - }, - "description" : { - "description" : "Entity description", - "type" : "string" - }, - "secComVersion" : { - "description" : "Version of the sec. com.", - "$ref" : "#/components/schemas/otoroshi.models.SecComVersion" - }, - "preRouting" : { - "description" : "Pre routing plugin settings", - "$ref" : "#/components/schemas/otoroshi.script.PreRoutingRef" - }, - "groups" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Each service descriptor is attached to groups. A group can have one or more services. Each API key is linked to a group and allow access to every service in the group" - }, - "readOnly" : { - "description" : "Service only accepts GET, HEAD and OPTIONS requests", - "type" : "boolean" - }, - "privatePatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "If you define a public pattern that is a little bit too much, you can make some of public URL private again" - }, - "targetsLoadBalancing" : { - "description" : "Loadbalancing strategy", - "$ref" : "#/components/schemas/otoroshi.models.LoadBalancing" - }, - "cors" : { - "description" : "CORS settings", - "$ref" : "#/components/schemas/otoroshi.models.CorsSettings" - }, - "metadata" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Just a bunch of random properties" - }, - "publicPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "By default, every services are private only and you'll need an API key to access it. However, if you want to expose a public UI, you can define one or more public patterns (regex) to allow access to anybody. For example if you want to allow anybody on any URL, just use '/.*'" - }, - "api" : { - "description" : "Api exposition settings", - "$ref" : "#/components/schemas/otoroshi.models.ApiDescriptor" - }, - "missingOnlyHeadersIn" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Add header on client request if they are not present" - }, - "issueCert" : { - "description" : "Flag to automatically issue a cert for this service", - "type" : "boolean" - }, - "headersVerification" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Specify headers that will be verified after routing." - }, - "jwtVerifier" : { - "description" : "JWT verifiers settings", - "$ref" : "#/components/schemas/otoroshi.models.JwtVerifier" - }, - "letsEncrypt" : { - "description" : "Flag to automatically issue a let's encrypt (ACME) cert for this service", - "type" : "boolean" - } - } - }, - "otoroshi.next.plugins.UdpTunnel" : { - "description" : "Plugin to have udp tunnels over websockets", - "type" : "object", - "properties" : { } - }, - "otoroshi.plugins.izanami.IzanamiCanaryConfig" : { - "description" : "Configuration for IzanamiCanary", - "type" : "object", - "properties" : { - "izanamiClientId" : { - "description" : "Izanami client id", - "type" : "string" - }, - "configId" : { - "description" : "Id of the target izanami configuration", - "type" : "string" - }, - "experimentId" : { - "description" : "Id of the target izanami experiment", - "type" : "string" - }, - "mtls" : { - "description" : "Izanami server tls config", - "$ref" : "#/components/schemas/otoroshi.utils.http.MtlsConfig" - }, - "izanamiUrl" : { - "description" : "Izanami server url", - "type" : "string" - }, - "timeout" : { - "description" : "Timeout when talking to the izanami server", - "type" : "number" - }, - "izanamiClientSecret" : { - "description" : "Izanami client secret", - "type" : "string" - }, - "routeConfig" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "object" + "type" : "object" } ], "description" : "The actual routing config" } @@ -21456,32 +20782,6 @@ } } }, - "otoroshi.models.Canary" : { - "description" : "Settings for canary routing", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Use canary mode for this service", - "type" : "boolean" - }, - "traffic" : { - "format" : "double", - "description" : "Ratio of traffic that will be sent to canary targets.", - "type" : "number" - }, - "targets" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/otoroshi.models.Target" - }, - "description" : "The list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures" - }, - "root" : { - "description" : "Otoroshi will append this root to any target choosen. If the specified root is '/api/foo', then a request to https://yyyyyyy/bar will actually hit https://xxxxxxxxx/api/foo/bar", - "type" : "string" - } - } - }, "otoroshi.next.models.NgRouteDomainAndPathWrapper" : { "description" : "Internal api", "type" : "object", @@ -22127,47 +21427,21 @@ "responses" : { "type" : "array", "items" : { - "$ref" : "#/components/schemas/otoroshi.next.plugins.MockResponse" - }, - "description" : "Possible responses" - }, - "pass_through" : { - "description" : "Pass the call if no mocked response found", - "type" : "boolean" - }, - "form_data" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "$ref" : "#/components/schemas/otoroshi.next.plugins.MockFormData" - } ], - "description" : "???" - } - } - }, - "otoroshi.models.ClientIdAuthConstraints" : { - "description" : "Settings to extract apikey (using client_id only) from a header or query param", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Constraint enabled", - "type" : "boolean" + "$ref" : "#/components/schemas/otoroshi.next.plugins.MockResponse" + }, + "description" : "Possible responses" }, - "headerName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name to find client_id" + "pass_through" : { + "description" : "Pass the call if no mocked response found", + "type" : "boolean" }, - "queryName" : { + "form_data" : { "oneOf" : [ { "$ref" : "#/components/schemas/Null" }, { - "type" : "string" + "$ref" : "#/components/schemas/otoroshi.next.plugins.MockFormData" } ], - "description" : "Query param name to find client_id" + "description" : "???" } } }, @@ -22212,85 +21486,6 @@ } } }, - "otoroshi.models.ClientConfig" : { - "description" : "Settings for the http client when http request is forwarded", - "type" : "object", - "properties" : { - "connectionTimeout" : { - "format" : "int64", - "description" : "Timeout at connection", - "type" : "integer" - }, - "useCircuitBreaker" : { - "description" : "Use a circuit breaker to avoid cascading failure when calling chains of services. Highly recommended !", - "type" : "boolean" - }, - "retryInitialDelay" : { - "format" : "int64", - "description" : "Specify the delay between two retries. Each retry, the delay is multiplied by the backoff factor", - "type" : "integer" - }, - "cacheConnectionSettings" : { - "description" : "Cached connection settings", - "$ref" : "#/components/schemas/otoroshi.utils.http.CacheConnectionSettings" - }, - "proxy" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "$ref" : "#/components/schemas/play.api.libs.ws.WSProxyServer" - } ], - "description" : "Web proxy settings for http client" - }, - "callTimeout" : { - "format" : "int64", - "description" : "Specify how long each call should last at most in milliseconds (soft timeout as it's enforced by the circuit breaker)", - "type" : "integer" - }, - "callAndStreamTimeout" : { - "format" : "int64", - "description" : "Specify how long each call should last at most in milliseconds (hard timeout, connection will be closed after that duration)", - "type" : "integer" - }, - "globalTimeout" : { - "format" : "int64", - "description" : "Specify how long the global call (with retries) should last at most in milliseconds", - "type" : "integer" - }, - "maxErrors" : { - "format" : "int32", - "description" : "Specify how many errors can pass before opening the circuit breaker", - "type" : "integer" - }, - "retries" : { - "format" : "int32", - "description" : "Specify how many times the client will try to fetch the result of the request after an error before giving up.", - "type" : "integer" - }, - "backoffFactor" : { - "format" : "int64", - "description" : "Specify the factor to multiply the delay for each retry", - "type" : "integer" - }, - "customTimeouts" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/otoroshi.models.CustomTimeouts" - }, - "description" : "Custom timeouts per path" - }, - "idleTimeout" : { - "format" : "int64", - "description" : "Timeout on idle connection", - "type" : "integer" - }, - "sampleInterval" : { - "format" : "int64", - "description" : "Specify the sliding window time for the circuit breaker in milliseconds, after this time, error count will be reseted", - "type" : "integer" - } - } - }, "otoroshi.next.plugins.NgBadResponse" : { "description" : "Configuration for SnowMonkeyChaos", "type" : "object", @@ -22322,32 +21517,6 @@ "type" : "object", "description" : "Service live stats" }, - "otoroshi.models.ApiKeyConstraints" : { - "description" : "Settings used to extract apikeys from http requests and routing traffic", - "type" : "object", - "properties" : { - "customHeadersAuth" : { - "description" : "Settings to extract apikey from custom headers", - "$ref" : "#/components/schemas/otoroshi.models.CustomHeadersAuthConstraints" - }, - "routing" : { - "description" : "Routing settings for this apikey", - "$ref" : "#/components/schemas/otoroshi.models.ApiKeyRouteMatcher" - }, - "clientIdAuth" : { - "description" : "Settings to extract client_id only apikey", - "$ref" : "#/components/schemas/otoroshi.models.ClientIdAuthConstraints" - }, - "jwtAuth" : { - "description" : "Settings to extract apikey from jwt token", - "$ref" : "#/components/schemas/otoroshi.models.JwtAuthConstraints" - }, - "basicAuth" : { - "description" : "Settings to extract basic auth style apikey", - "$ref" : "#/components/schemas/otoroshi.models.BasicAuthConstraints" - } - } - }, "otoroshi.plugins.metrics.ServiceMetrics" : { "description" : "Plugin to collect service metrics", "type" : "object", @@ -23236,27 +22405,6 @@ } } }, - "otoroshi.models.GeoPositionRadius" : { - "description" : "Geolocation radius", - "type" : "object", - "properties" : { - "latitude" : { - "format" : "double", - "description" : "Latitude of the position", - "type" : "number" - }, - "longitude" : { - "format" : "double", - "description" : "Longitude of the position", - "type" : "number" - }, - "radius" : { - "format" : "double", - "description" : "Radius of the circle in meters", - "type" : "number" - } - } - }, "otoroshi.models.InCookie" : { "description" : "JWT token location (cookie)", "type" : "object", @@ -23387,90 +22535,6 @@ "type" : "object", "description" : "" }, - "otoroshi.models.LocalJwtVerifier" : { - "description" : "Local jwt verifier (deprecated)", - "type" : "object", - "properties" : { - "excludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Verifier excluded paths" - }, - "algoSettings" : { - "description" : "Algo settings", - "$ref" : "#/components/schemas/otoroshi.models.AlgoSettings" - }, - "source" : { - "description" : "Token source", - "$ref" : "#/components/schemas/otoroshi.models.JwtTokenLocation" - }, - "type" : { - "description" : "the kind of verifier", - "type" : "string", - "enum" : [ "global", "local", "ref" ] - }, - "strict" : { - "description" : "Strict token verification", - "type" : "boolean" - }, - "strategy" : { - "description" : "Token strategy", - "$ref" : "#/components/schemas/otoroshi.models.VerifierStrategy" - }, - "enabled" : { - "description" : "Verifier enabled", - "type" : "boolean" - } - } - }, - "otoroshi.utils.gzip.GzipConfig" : { - "description" : "Settings for gzip support", - "type" : "object", - "properties" : { - "compressionLevel" : { - "format" : "int32", - "description" : "Compression level (0 - 9)", - "type" : "integer" - }, - "blackList" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "blocklisted content types" - }, - "chunkedThreshold" : { - "format" : "int32", - "description" : "Chunk size", - "type" : "integer" - }, - "excludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Excluded paths" - }, - "bufferSize" : { - "format" : "int32", - "description" : "Buffer size in bytes", - "type" : "integer" - }, - "whiteList" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "allow listed content types" - }, - "enabled" : { - "description" : "Gzip enabled", - "type" : "boolean" - } - } - }, "otoroshi.plugins.useragent.UserAgentExtractor" : { "description" : "Plugin that extract user-agent related infos", "type" : "object", @@ -23734,61 +22798,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.JwtAuthConstraints" : { - "description" : "Settings to extract apikey from a jwt token", - "type" : "object", - "properties" : { - "keyPairSigned" : { - "description" : "The jwt token is signed by a keypair from a cert found from its id in apikey meta. 'jwt-sign-keypair'", - "type" : "boolean" - }, - "cookieName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Cookie name to extract jwt token" - }, - "queryName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Query param name to extract jwt token" - }, - "headerName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name to extract jwt token" - }, - "secretSigned" : { - "description" : "Jwt token signed with the client_secret", - "type" : "boolean" - }, - "maxJwtLifespanSecs" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "integer", - "format" : "int64" - } ], - "description" : "Check if token does not have a long lifespan" - }, - "enabled" : { - "description" : "Constraint enabled", - "type" : "boolean" - }, - "includeRequestAttributes" : { - "description" : "Jwt token should include verb and path", - "type" : "boolean" - } - } - }, "otoroshi.plugins.apikeys.ClientCredentialFlowExtractor" : { "description" : "Internal api", "type" : "object", @@ -23847,34 +22856,6 @@ "type" : "string", "description" : "the id of a group prefixed by 'group_'" }, - "otoroshi.script.AccessValidatorRef" : { - "description" : "References to access validation plugins", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Access validator plugins enabled", - "type" : "boolean" - }, - "excludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Excluded paths" - }, - "refs" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Enabled plugins" - }, - "config" : { - "description" : "Access validator plugins configuration", - "type" : "object" - } - } - }, "otoroshi.ssl.pki.models.GenCertResponse" : { "description" : "Response for a certificate generation operation", "type" : "object", @@ -24104,20 +23085,6 @@ "$ref" : "#/components/schemas/otoroshi.events.AlertEvent" } }, - "otoroshi.models.HealthCheck" : { - "description" : "Healthcheck settings for a service", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Whether or not healthcheck is enabled on the current service descriptor", - "type" : "boolean" - }, - "url" : { - "description" : "The URL to check", - "type" : "string" - } - } - }, "otoroshi.plugins.core.apikeys.ClientIdApikeyExtractor" : { "description" : "Internal api", "type" : "object", @@ -24282,21 +23249,6 @@ } } }, - "otoroshi.models.ZoneMatch" : { - "description" : "Match a target if in the same zone", - "type" : "object", - "properties" : { - "zone" : { - "description" : "Zone name", - "type" : "string" - }, - "type" : { - "description" : "the kind of predicate", - "type" : "string", - "enum" : [ "AlwaysMatch", "NetworkLocationMatch", "GeolocationMatch" ] - } - } - }, "otoroshi.next.models.NgPlugins" : { "description" : "A set of NgPluginInstance", "type" : "object", @@ -24414,22 +23366,8 @@ "type" : "object", "description" : "Is certificate valid", "properties" : { - "valid" : { - "type" : "boolean" - } - } - }, - "otoroshi.models.RestrictionPath" : { - "description" : "Represent an http request on which restrictions will apply", - "type" : "object", - "properties" : { - "method" : { - "description" : "Method of the http request", - "type" : "string" - }, - "path" : { - "description" : "Path of the http request", - "type" : "string" + "valid" : { + "type" : "boolean" } } }, @@ -24533,24 +23471,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.GeolocationMatch" : { - "description" : "Match a target if in the same geo location radius", - "type" : "object", - "properties" : { - "positions" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/otoroshi.models.GeoPositionRadius" - }, - "description" : "Possible positions" - }, - "type" : { - "description" : "the kind of predicate", - "type" : "string", - "enum" : [ "AlwaysMatch", "NetworkLocationMatch", "GeolocationMatch" ] - } - } - }, "BulkResponseBody" : { "type" : "array", "items" : { @@ -24648,60 +23568,6 @@ } } }, - "otoroshi.models.Target" : { - "description" : "A target model for a service (destination for forwarded requests)", - "type" : "object", - "properties" : { - "tags" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Tags for this target" - }, - "host" : { - "description" : "The host on which the HTTP call will be forwarded. Can be a domain name, or an IP address. Can also have a port", - "type" : "string" - }, - "weight" : { - "format" : "int32", - "description" : "The weight of the target when choosing", - "type" : "integer" - }, - "metadata" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Metadata for this target" - }, - "protocol" : { - "description" : "Protocol for the target", - "type" : "string", - "enum" : [ "HTTP/1.0", "HTTP/1.1", "HTTP/2.0" ] - }, - "predicate" : { - "description" : "Predicate to choose this target", - "$ref" : "#/components/schemas/otoroshi.models.TargetPredicate" - }, - "ipAddress" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Target ip address. Usefull to make manual DNS resolution without breaking SNI" - }, - "mtlsConfig" : { - "description" : "TLS settings to contact this target", - "$ref" : "#/components/schemas/otoroshi.utils.http.MtlsConfig" - }, - "scheme" : { - "description" : "The protocol used for communication. Can be http or https", - "type" : "string" - } - } - }, "otoroshi.next.plugins.Redirection" : { "description" : "Plugin to perform redirections", "type" : "object", @@ -24849,21 +23715,6 @@ } } }, - "otoroshi.utils.http.CacheConnectionSettings" : { - "description" : "???", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "???", - "type" : "boolean" - }, - "queueSize" : { - "format" : "int32", - "description" : "???", - "type" : "integer" - } - } - }, "otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookSidecarInjector" : { "description" : "Internal api", "type" : "object", @@ -24911,25 +23762,6 @@ } } }, - "otoroshi.models.RedirectionSettings" : { - "description" : "Settings for routing redirection", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Whether or not redirection is enabled", - "type" : "boolean" - }, - "code" : { - "format" : "int32", - "description" : "The http redirect code", - "type" : "integer" - }, - "to" : { - "description" : "The location for redirection", - "type" : "string" - } - } - }, "otoroshi.next.plugins.NgOtoroshiChallengeKeys" : { "description" : "Configuration for OtoroshiChallenge", "type" : "object", @@ -25206,21 +24038,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.RackMatch" : { - "description" : "Match a target if in the same rack", - "type" : "object", - "properties" : { - "rack" : { - "description" : "Rack name", - "type" : "string" - }, - "type" : { - "description" : "the kind of predicate", - "type" : "string", - "enum" : [ "AlwaysMatch", "NetworkLocationMatch", "GeolocationMatch" ] - } - } - }, "otoroshi.plugins.oidc.ThirdPartyApiKeyConfig" : { "description" : "Internal api", "type" : "object", @@ -25645,24 +24462,6 @@ "type" : "object", "description" : "Alert trail event" }, - "otoroshi.models.ApiDescriptor" : { - "description" : "Represent if a service exposes an API with an optional url to an openapi descriptor", - "type" : "object", - "properties" : { - "exposeApi" : { - "description" : "Is this an API", - "type" : "boolean" - }, - "openApiDescriptorUrl" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "openapi descriptor url" - } - } - }, "otoroshi.auth.SessionCookieValues" : { "description" : "The configuration for session cookie", "type" : "object", @@ -25814,75 +24613,6 @@ } } }, - "otoroshi.models.ApiKeyRouteMatcher" : { - "description" : "Routing settings based on apikeys metadata and tags", - "type" : "object", - "properties" : { - "oneTagIn" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "outing if one tag presents in apikey" - }, - "noneMetaKeysIn" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Routing if none meta keys presents in apikey" - }, - "oneMetaIn" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Routing if one meta presents in apikey" - }, - "oneMetaKeyIn" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Routing if one meta key presents in apikey" - }, - "allMetaKeysIn" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Routing if all meta keys presents in apikey" - }, - "noneTagIn" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Routing if none tags presents in apikey" - }, - "allTagsIn" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Routing if all tags presents in apikey" - }, - "allMetaIn" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Routing if all meta presents in apikey" - }, - "noneMetaIn" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Routing if none meta presents in apikey" - } - } - }, "otoroshi.next.plugins.SOAPAction" : { "description" : "Plugin to call SOAP service", "type" : "object", @@ -26298,14 +25028,6 @@ "description" : "The root path of the backend or the full rewrite path", "type" : "string" }, - "health_check" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "$ref" : "#/components/schemas/otoroshi.models.HealthCheck" - } ], - "description" : "Healthcheck config og the backend" - }, "client" : { "description" : "Client config. of the backend", "$ref" : "#/components/schemas/otoroshi.next.models.NgClientConfig" @@ -26386,34 +25108,6 @@ "$ref" : "#/components/schemas/PluginDescription" } }, - "otoroshi.script.PreRoutingRef" : { - "description" : "References to pre-routing plugins", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "pre-routing plugins enabled", - "type" : "boolean" - }, - "excludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Excluded paths" - }, - "refs" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Enabled plugins" - }, - "config" : { - "description" : "pre-routing plugins configuration", - "type" : "object" - } - } - }, "otoroshi.plugins.biscuit.BiscuitConfig" : { "description" : "Internal api", "type" : "object", @@ -26778,32 +25472,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.CustomHeadersAuthConstraints" : { - "description" : "Settings to extract apikey from a custom headers", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Constraint enabled", - "type" : "boolean" - }, - "clientIdHeaderName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name to find client_id" - }, - "clientSecretHeaderName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name to find client_secret" - } - } - }, "otoroshi.models.TlsSettings" : { "description" : "Global TLS settings. The default domain that will be picked if no certificate matches the current request", "type" : "object", @@ -27101,41 +25769,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.Restrictions" : { - "description" : "Http requests restrictions for a service or an apikey", - "type" : "object", - "properties" : { - "forbidden" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/otoroshi.models.RestrictionPath" - }, - "description" : "Forbidden paths (return 403)" - }, - "allowed" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/otoroshi.models.RestrictionPath" - }, - "description" : "Allowed paths" - }, - "notFound" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/otoroshi.models.RestrictionPath" - }, - "description" : "Not found paths (return 404)" - }, - "allowLast" : { - "description" : "Evalute allowed paths after everything else", - "type" : "boolean" - }, - "enabled" : { - "description" : "Restrictions enabled", - "type" : "boolean" - } - } - }, "otoroshi.next.plugins.NgLargeRequestFaultConfig" : { "description" : "Configuration for SnowMonkeyChaos", "type" : "object", @@ -27380,26 +26013,6 @@ } } }, - "otoroshi.models.IpFiltering" : { - "description" : "Settings for ip address filtering for a service or globally", - "type" : "object", - "properties" : { - "whitelist" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Whitelisted IP addresses" - }, - "blacklist" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Blacklisted IP addresses" - } - } - }, "otoroshi.plugins.quotas.ServiceQuotas" : { "description" : "Internal api", "type" : "object", @@ -28308,21 +26921,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.DataCenterMatch" : { - "description" : "Match a target if in the same datacenter", - "type" : "object", - "properties" : { - "type" : { - "description" : "the kind of predicate", - "type" : "string", - "enum" : [ "AlwaysMatch", "NetworkLocationMatch", "GeolocationMatch" ] - }, - "dc" : { - "description" : "DC name", - "type" : "string" - } - } - }, "otoroshi.models.JwtTokenLocation" : { "oneOf" : [ { "$ref" : "#/components/schemas/otoroshi.models.InCookie" diff --git a/otoroshi/app/cluster/cluster.scala b/otoroshi/app/cluster/cluster.scala index 5a38b87032..7fc524f5e0 100644 --- a/otoroshi/app/cluster/cluster.scala +++ b/otoroshi/app/cluster/cluster.scala @@ -168,76 +168,78 @@ case class WorkerConfig( ) case class LeaderConfig( - name: String = s"otoroshi-leader-${IdGenerator.token(16)}", - urls: Seq[String] = Seq.empty, - host: String = "otoroshi-api.oto.tools", - clientId: String = "admin-api-apikey-id", - clientSecret: String = "admin-api-apikey-secret", - groupingBy: Int = 50, - cacheStateFor: Long = 4000, - stateDumpPath: Option[String] = None + name: String = s"otoroshi-leader-${IdGenerator.token(16)}", + urls: Seq[String] = Seq.empty, + host: String = "otoroshi-api.oto.tools", + clientId: String = "admin-api-apikey-id", + clientSecret: String = "admin-api-apikey-secret", + groupingBy: Int = 50, + cacheStateFor: Long = 4000, + stateDumpPath: Option[String] = None ) case class InstanceLocation( - provider: String, - zone: String, - region: String, - datacenter: String, - rack: String + provider: String, + zone: String, + region: String, + datacenter: String, + rack: String ) { - def desc: String = s"provider: '${provider}', region: '${region}', zone: '${zone}', datacenter: '${datacenter}', rack: '${rack}''" + def desc: String = + s"provider: '${provider}', region: '${region}', zone: '${zone}', datacenter: '${datacenter}', rack: '${rack}''" def json: JsValue = Json.obj( - "provider" -> provider, - "zone" -> zone, - "region" -> region, + "provider" -> provider, + "zone" -> zone, + "region" -> region, "datacenter" -> datacenter, - "rack" -> rack, + "rack" -> rack ) } case class InstanceExposition( - urls: Seq[String], - hostname: String, - ipAddress: Option[String], - clientId: Option[String], - clientSecret: Option[String], - tls: Option[MtlsConfig], + urls: Seq[String], + hostname: String, + ipAddress: Option[String], + clientId: Option[String], + clientSecret: Option[String], + tls: Option[MtlsConfig] ) { - def json: JsValue = Json.obj( - "urls" -> urls, - "hostname" -> hostname, - ) - .applyOnWithOpt(clientId) { - case (obj, cid) => obj ++ Json.obj("clientId" -> cid) - } - .applyOnWithOpt(clientSecret) { - case (obj, cid) => obj ++ Json.obj("clientSecret" -> cid) - } - .applyOnWithOpt(ipAddress) { - case (obj, cid) => obj ++ Json.obj("ipAddress" -> cid) - } - .applyOnWithOpt(tls) { - case (obj, cid) => obj ++ Json.obj("tls" -> cid.json) - } + def json: JsValue = Json + .obj( + "urls" -> urls, + "hostname" -> hostname + ) + .applyOnWithOpt(clientId) { case (obj, cid) => + obj ++ Json.obj("clientId" -> cid) + } + .applyOnWithOpt(clientSecret) { case (obj, cid) => + obj ++ Json.obj("clientSecret" -> cid) + } + .applyOnWithOpt(ipAddress) { case (obj, cid) => + obj ++ Json.obj("ipAddress" -> cid) + } + .applyOnWithOpt(tls) { case (obj, cid) => + obj ++ Json.obj("tls" -> cid.json) + } } case class RelayRouting( - enabled: Boolean, - leaderOnly: Boolean, - location: InstanceLocation, - exposition: InstanceExposition + enabled: Boolean, + leaderOnly: Boolean, + location: InstanceLocation, + exposition: InstanceExposition ) { def json: JsValue = Json.obj( - "enabled" -> enabled, + "enabled" -> enabled, "leaderOnly" -> leaderOnly, - "location" -> location.json, - "exposition" -> exposition.json, + "location" -> location.json, + "exposition" -> exposition.json ) } object RelayRouting { - val logger = Logger("otoroshi-relay-routing") - val default = RelayRouting( + val logger = Logger("otoroshi-relay-routing") + val default = RelayRouting( enabled = false, leaderOnly = false, location = InstanceLocation( @@ -245,7 +247,7 @@ object RelayRouting { zone = "local", region = "local", datacenter = "local", - rack = "local", + rack = "local" ), exposition = InstanceExposition( urls = Seq.empty, @@ -253,7 +255,7 @@ object RelayRouting { clientId = None, clientSecret = None, ipAddress = None, - tls = None, + tls = None ) ) def parse(json: String): Option[RelayRouting] = Try { @@ -261,12 +263,12 @@ object RelayRouting { RelayRouting( enabled = value.select("enabled").asOpt[Boolean].getOrElse(false), leaderOnly = value.select("leaderOnly").asOpt[Boolean].getOrElse(false), - location = InstanceLocation( + location = InstanceLocation( provider = value.select("location").select("provider").asOpt[String].getOrElse("local"), zone = value.select("location").select("zone").asOpt[String].getOrElse("local"), region = value.select("location").select("region").asOpt[String].getOrElse("local"), datacenter = value.select("location").select("datacenter").asOpt[String].getOrElse("local"), - rack = value.select("location").select("rack").asOpt[String].getOrElse("local"), + rack = value.select("location").select("rack").asOpt[String].getOrElse("local") ), exposition = InstanceExposition( urls = value.select("exposition").select("urls").asOpt[Seq[String]].getOrElse(default.exposition.urls), @@ -274,29 +276,29 @@ object RelayRouting { clientId = value.select("exposition").select("clientId").asOpt[String].filter(_.nonEmpty), clientSecret = value.select("exposition").select("clientSecret").asOpt[String].filter(_.nonEmpty), ipAddress = value.select("exposition").select("ipAddress").asOpt[String].filter(_.nonEmpty), - tls = value.select("exposition").select("tls").asOpt[JsValue].flatMap(v => MtlsConfig.format.reads(v).asOpt), - ), + tls = value.select("exposition").select("tls").asOpt[JsValue].flatMap(v => MtlsConfig.format.reads(v).asOpt) + ) ) } match { - case Failure(e) => None + case Failure(e) => None case Success(value) => value.some } } case class ClusterConfig( - mode: ClusterMode = ClusterMode.Off, - compression: Int = -1, - proxy: Option[WSProxyServer], - mtlsConfig: MtlsConfig, - streamed: Boolean, - relay: RelayRouting, - // autoUpdateState: Boolean, - retryDelay: Long, - retryFactor: Long, - leader: LeaderConfig = LeaderConfig(), - worker: WorkerConfig = WorkerConfig() + mode: ClusterMode = ClusterMode.Off, + compression: Int = -1, + proxy: Option[WSProxyServer], + mtlsConfig: MtlsConfig, + streamed: Boolean, + relay: RelayRouting, + // autoUpdateState: Boolean, + retryDelay: Long, + retryFactor: Long, + leader: LeaderConfig = LeaderConfig(), + worker: WorkerConfig = WorkerConfig() ) { - def id: String = ClusterConfig.clusterNodeId + def id: String = ClusterConfig.clusterNodeId def name: String = if (mode.isOff) "standalone" else (if (mode.isLeader) leader.name else worker.name) def gzip(): Flow[ByteString, ByteString, NotUsed] = if (compression == -1) Flow.apply[ByteString] else Compression.gzip(compression) @@ -318,38 +320,48 @@ object ClusterConfig { relay = RelayRouting( enabled = configuration.getOptionalWithFileSupport[Boolean]("relay.enabled").getOrElse(false), leaderOnly = configuration.getOptionalWithFileSupport[Boolean]("relay.leaderOnly").getOrElse(false), - location = InstanceLocation( + location = InstanceLocation( provider = configuration.getOptionalWithFileSupport[String]("relay.location.provider").getOrElse("local"), zone = configuration.getOptionalWithFileSupport[String]("relay.location.zone").getOrElse("local"), region = configuration.getOptionalWithFileSupport[String]("relay.location.region").getOrElse("local"), datacenter = configuration.getOptionalWithFileSupport[String]("relay.location.datacenter").getOrElse("local"), - rack = configuration.getOptionalWithFileSupport[String]("relay.location.rack").getOrElse("local"), + rack = configuration.getOptionalWithFileSupport[String]("relay.location.rack").getOrElse("local") ), exposition = InstanceExposition( urls = configuration.getOptionalWithFileSupport[String]("relay.exposition.url").map(v => Seq(v)).orElse { - configuration.getOptionalWithFileSupport[String]("relay.exposition.urlsStr") + configuration + .getOptionalWithFileSupport[String]("relay.exposition.urlsStr") .map(v => v.split(",").toSeq.map(_.trim)) .orElse( configuration.getOptionalWithFileSupport[Seq[String]]("relay.exposition.urls") - ).filter(_.nonEmpty) - } getOrElse(Seq.empty), - hostname = configuration.getOptionalWithFileSupport[String]("relay.exposition.hostname").getOrElse("otoroshi-api.oto.tools"), + ) + .filter(_.nonEmpty) + } getOrElse (Seq.empty), + hostname = configuration + .getOptionalWithFileSupport[String]("relay.exposition.hostname") + .getOrElse("otoroshi-api.oto.tools"), clientId = configuration.getOptionalWithFileSupport[String]("relay.exposition.clientId"), clientSecret = configuration.getOptionalWithFileSupport[String]("relay.exposition.clientSecret"), ipAddress = configuration.getOptionalWithFileSupport[String]("relay.exposition.ipAddress"), tls = { - val enabled = configuration.getOptionalWithFileSupport[Boolean]("relay.exposition.tls.mtls").getOrElse(false) + val enabled = + configuration.getOptionalWithFileSupport[Boolean]("relay.exposition.tls.mtls").getOrElse(false) if (enabled) { - val loose = configuration.getOptionalWithFileSupport[Boolean]("relay.exposition.tls.loose").getOrElse(false) - val trustAll = configuration.getOptionalWithFileSupport[Boolean]("relay.exposition.tls.loose").getOrElse(false) - val certs = configuration.getOptionalWithFileSupport[Seq[String]]("relay.exposition.tls.certs").getOrElse(Seq.empty) - val trustedCerts = configuration.getOptionalWithFileSupport[Seq[String]]("relay.exposition.tls.trustedCerts").getOrElse(Seq.empty) + val loose = + configuration.getOptionalWithFileSupport[Boolean]("relay.exposition.tls.loose").getOrElse(false) + val trustAll = + configuration.getOptionalWithFileSupport[Boolean]("relay.exposition.tls.loose").getOrElse(false) + val certs = + configuration.getOptionalWithFileSupport[Seq[String]]("relay.exposition.tls.certs").getOrElse(Seq.empty) + val trustedCerts = configuration + .getOptionalWithFileSupport[Seq[String]]("relay.exposition.tls.trustedCerts") + .getOrElse(Seq.empty) MtlsConfig( certs = certs, trustedCerts = trustedCerts, mtls = enabled, loose = loose, - trustAll = trustAll, + trustAll = trustAll ).some } else { None @@ -444,25 +456,25 @@ case class StatsView( ) case class MemberView( - id: String, - name: String, - location: String, - lastSeen: DateTime, - timeout: Duration, - memberType: ClusterMode, - relay: RelayRouting, - stats: JsObject = Json.obj() + id: String, + name: String, + location: String, + lastSeen: DateTime, + timeout: Duration, + memberType: ClusterMode, + relay: RelayRouting, + stats: JsObject = Json.obj() ) { def asJson: JsValue = Json.obj( - "id" -> id, + "id" -> id, "name" -> name, "location" -> location, "lastSeen" -> lastSeen.getMillis, "timeout" -> timeout.toMillis, "type" -> memberType.name, "stats" -> stats, - "relay" -> relay.json, + "relay" -> relay.json ) def statsView: StatsView = { StatsView( @@ -505,21 +517,39 @@ object MemberView { relay = RelayRouting( enabled = true, leaderOnly = false, - location = InstanceLocation( + location = InstanceLocation( provider = value.select("relay").select("location").select("provider").asOpt[String].getOrElse("local"), zone = value.select("relay").select("location").select("zone").asOpt[String].getOrElse("local"), region = value.select("relay").select("location").select("region").asOpt[String].getOrElse("local"), - datacenter = value.select("relay").select("location").select("datacenter").asOpt[String].getOrElse("local"), - rack = value.select("relay").select("location").select("rack").asOpt[String].getOrElse("local"), + datacenter = + value.select("relay").select("location").select("datacenter").asOpt[String].getOrElse("local"), + rack = value.select("relay").select("location").select("rack").asOpt[String].getOrElse("local") ), exposition = InstanceExposition( - urls = value.select("relay").select("exposition").select("urls").asOpt[Seq[String]].getOrElse(Seq(s"${env.rootScheme}${env.adminApiExposedHost}")), - hostname = value.select("relay").select("exposition").select("hostname").asOpt[String].getOrElse(env.adminApiExposedHost), + urls = value + .select("relay") + .select("exposition") + .select("urls") + .asOpt[Seq[String]] + .getOrElse(Seq(s"${env.rootScheme}${env.adminApiExposedHost}")), + hostname = value + .select("relay") + .select("exposition") + .select("hostname") + .asOpt[String] + .getOrElse(env.adminApiExposedHost), clientId = value.select("relay").select("exposition").select("clientId").asOpt[String].filter(_.nonEmpty), - clientSecret = value.select("relay").select("exposition").select("clientSecret").asOpt[String].filter(_.nonEmpty), - ipAddress = value.select("relay").select("exposition").select("ipAddress").asOpt[String].filter(_.nonEmpty), - tls = value.select("relay").select("exposition").select("tls").asOpt[JsValue].flatMap(v => MtlsConfig.format.reads(v).asOpt), - ), + clientSecret = + value.select("relay").select("exposition").select("clientSecret").asOpt[String].filter(_.nonEmpty), + ipAddress = + value.select("relay").select("exposition").select("ipAddress").asOpt[String].filter(_.nonEmpty), + tls = value + .select("relay") + .select("exposition") + .select("tls") + .asOpt[JsValue] + .flatMap(v => MtlsConfig.format.reads(v).asOpt) + ) ) ) ) @@ -771,9 +801,9 @@ class RedisClusterStateDataStore(redisLike: RedisClientMasterSlaves, env: Env) e object ClusterAgent { - val OtoroshiWorkerIdHeader = "Otoroshi-Worker-Id" - val OtoroshiWorkerNameHeader = "Otoroshi-Worker-Name" - val OtoroshiWorkerLocationHeader = "Otoroshi-Worker-Location" + val OtoroshiWorkerIdHeader = "Otoroshi-Worker-Id" + val OtoroshiWorkerNameHeader = "Otoroshi-Worker-Name" + val OtoroshiWorkerLocationHeader = "Otoroshi-Worker-Location" val OtoroshiWorkerRelayRoutingHeader = "Otoroshi-Worker-Relay-Routing" def apply(config: ClusterConfig, env: Env) = new ClusterAgent(config, env) @@ -1469,13 +1499,13 @@ class ClusterAgent(config: ClusterConfig, env: Env) { val request = env.MtlsWs .url(otoroshiUrl + s"/api/cluster/state?budget=${config.worker.state.timeout}", config.mtlsConfig) .withHttpHeaders( - "Host" -> config.leader.host, - "Accept" -> "application/x-ndjson", + "Host" -> config.leader.host, + "Accept" -> "application/x-ndjson", // "Accept-Encoding" -> "gzip", - ClusterAgent.OtoroshiWorkerIdHeader -> ClusterConfig.clusterNodeId, - ClusterAgent.OtoroshiWorkerNameHeader -> config.worker.name, - ClusterAgent.OtoroshiWorkerLocationHeader -> s"$hostAddress:${env.exposedHttpPort}/${env.exposedHttpsPort}", - ClusterAgent.OtoroshiWorkerRelayRoutingHeader -> env.clusterConfig.relay.json.stringify, + ClusterAgent.OtoroshiWorkerIdHeader -> ClusterConfig.clusterNodeId, + ClusterAgent.OtoroshiWorkerNameHeader -> config.worker.name, + ClusterAgent.OtoroshiWorkerLocationHeader -> s"$hostAddress:${env.exposedHttpPort}/${env.exposedHttpsPort}", + ClusterAgent.OtoroshiWorkerRelayRoutingHeader -> env.clusterConfig.relay.json.stringify ) .withAuth(config.leader.clientId, config.leader.clientSecret, WSAuthScheme.BASIC) .withRequestTimeout(Duration(config.worker.state.timeout, TimeUnit.MILLISECONDS)) @@ -1693,13 +1723,13 @@ class ClusterAgent(config: ClusterConfig, env: Env) { val request = env.MtlsWs .url(otoroshiUrl + s"/api/cluster/quotas?budget=${config.worker.quotas.timeout}", config.mtlsConfig) .withHttpHeaders( - "Host" -> config.leader.host, - "Content-Type" -> "application/x-ndjson", + "Host" -> config.leader.host, + "Content-Type" -> "application/x-ndjson", // "Content-Encoding" -> "gzip", - ClusterAgent.OtoroshiWorkerIdHeader -> ClusterConfig.clusterNodeId, - ClusterAgent.OtoroshiWorkerNameHeader -> config.worker.name, - ClusterAgent.OtoroshiWorkerLocationHeader -> s"$hostAddress:${env.exposedHttpPort}/${env.exposedHttpsPort}", - ClusterAgent.OtoroshiWorkerRelayRoutingHeader -> env.clusterConfig.relay.json.stringify, + ClusterAgent.OtoroshiWorkerIdHeader -> ClusterConfig.clusterNodeId, + ClusterAgent.OtoroshiWorkerNameHeader -> config.worker.name, + ClusterAgent.OtoroshiWorkerLocationHeader -> s"$hostAddress:${env.exposedHttpPort}/${env.exposedHttpsPort}", + ClusterAgent.OtoroshiWorkerRelayRoutingHeader -> env.clusterConfig.relay.json.stringify ) .withAuth(config.leader.clientId, config.leader.clientSecret, WSAuthScheme.BASIC) .withRequestTimeout(Duration(config.worker.quotas.timeout, TimeUnit.MILLISECONDS)) diff --git a/otoroshi/app/controllers/BackOfficeController.scala b/otoroshi/app/controllers/BackOfficeController.scala index e2773fd848..79d83845b4 100644 --- a/otoroshi/app/controllers/BackOfficeController.scala +++ b/otoroshi/app/controllers/BackOfficeController.scala @@ -46,19 +46,19 @@ import scala.concurrent.{ExecutionContext, Future} import scala.util.Try case class ServiceLike(entity: EntityLocationSupport, groups: Seq[String]) extends EntityLocationSupport { - def id: String = internalId - override def internalId: String = entity.internalId - override def json: JsValue = entity.json - override def theName: String = entity.theName - override def theDescription: String = entity.theDescription - override def theTags: Seq[String] = entity.theTags + def id: String = internalId + override def internalId: String = entity.internalId + override def json: JsValue = entity.json + override def theName: String = entity.theName + override def theDescription: String = entity.theDescription + override def theTags: Seq[String] = entity.theTags override def theMetadata: Map[String, String] = entity.theMetadata - override def location: EntityLocation = entity.location + override def location: EntityLocation = entity.location } object ServiceLike { - def fromService(service: ServiceDescriptor): ServiceLike = ServiceLike(service, service.groups) - def fromRoute(service: NgRoute): ServiceLike = ServiceLike(service, service.groups) + def fromService(service: ServiceDescriptor): ServiceLike = ServiceLike(service, service.groups) + def fromRoute(service: NgRoute): ServiceLike = ServiceLike(service, service.groups) def fromRouteComposition(service: NgService): ServiceLike = ServiceLike(service, service.groups) } @@ -818,7 +818,7 @@ class BackOfficeController( tags = Seq.empty, metadata = Map.empty, sessionCookieValues = sessionCookieValues, - clientSideSessionEnabled = true, + clientSideSessionEnabled = true ).asJson ) ) @@ -838,7 +838,7 @@ class BackOfficeController( tags = Seq.empty, metadata = Map.empty, sessionCookieValues = sessionCookieValues, - clientSideSessionEnabled = true, + clientSideSessionEnabled = true ) val body = Json.parse(resp.body) val issuer = (body \ "issuer").asOpt[String].getOrElse("http://localhost:8082/") @@ -907,7 +907,7 @@ class BackOfficeController( tags = Seq.empty, metadata = Map.empty, sessionCookieValues = sessionCookieValues, - clientSideSessionEnabled = true, + clientSideSessionEnabled = true ).asJson ) } @@ -924,7 +924,7 @@ class BackOfficeController( tags = Seq.empty, metadata = Map.empty, sessionCookieValues = sessionCookieValues, - clientSideSessionEnabled = true, + clientSideSessionEnabled = true ).asJson ) } @@ -1602,13 +1602,15 @@ class BackOfficeController( def findServiceLike(serviceId: String): Future[Option[ServiceLike]] = { env.datastores.serviceDescriptorDataStore.findById(serviceId) flatMap { case Some(service) => ServiceLike.fromService(service).some.vfuture - case None => env.datastores.routeDataStore.findById(serviceId) flatMap { - case Some(service) => ServiceLike.fromRoute(service).some.vfuture - case None => env.datastores.servicesDataStore.findById(serviceId) map { - case Some(service) => ServiceLike.fromRouteComposition(service).some - case None => None + case None => + env.datastores.routeDataStore.findById(serviceId) flatMap { + case Some(service) => ServiceLike.fromRoute(service).some.vfuture + case None => + env.datastores.servicesDataStore.findById(serviceId) map { + case Some(service) => ServiceLike.fromRouteComposition(service).some + case None => None + } } - } } } @@ -1723,8 +1725,8 @@ class BackOfficeController( } def graphqlProxy() = BackOfficeActionAuth.async(sourceBodyParser) { ctx => - val url = ctx.request.queryString.get("url").map(_.last).get - val host = Uri(url).authority.host.toString() + val url = ctx.request.queryString.get("url").map(_.last).get + val host = Uri(url).authority.host.toString() val headers = (ctx.request.headers.toSimpleMap ++ Map("Host" -> host)).toSeq val builder = env.Ws @@ -1744,7 +1746,8 @@ class BackOfficeController( .execute() .fast .map { res => - Results.Status(res.status)(res.body) + Results + .Status(res.status)(res.body) .withHeaders(res.headers.mapValues(_.last).toSeq.filterNot(_._1 == "Content-Type"): _*) .as(res.contentType) } @@ -1752,7 +1755,7 @@ class BackOfficeController( def routeEntries(routeId: String) = BackOfficeActionAuth.async { ctx => env.datastores.routeDataStore.findById(routeId) flatMap { - case None => NotFound(Json.obj("error" -> "route not found")).future + case None => NotFound(Json.obj("error" -> "route not found")).future case Some(route) => val isSecured = route.plugins.slots.exists(p => p.plugin.contains("ForceHttpsTraffic")) @@ -1761,8 +1764,7 @@ class BackOfficeController( .withScheme(if (isSecured) "https" else "http") .withPort(if (isSecured) env.exposedHttpsPortInt else env.exposedHttpPortInt) .toString() - }))) - .future + }))).future } } @@ -1770,52 +1772,57 @@ class BackOfficeController( val schema = ctx.request.body.select("schema").asOpt[String].getOrElse("{}") sangria.parser.QueryParser.parse(schema) match { - case scala.util.Failure(exception) => BadRequest(Json.obj("error" -> exception.getMessage)).future + case scala.util.Failure(exception) => BadRequest(Json.obj("error" -> exception.getMessage)).future case scala.util.Success(astDocument) => - val generatedSchema = sangria.schema.Schema.buildFromAst(astDocument) - val res = GraphQLFormats.astDocumentToJson(generatedSchema) + val generatedSchema = sangria.schema.Schema.buildFromAst(astDocument) + val res = GraphQLFormats.astDocumentToJson(generatedSchema) - Ok(Json.obj("types" -> res)).future + Ok(Json.obj("types" -> res)).future } } def jsonToGraphqlSchema() = BackOfficeActionAuth.async(parse.json) { ctx => import sangria.schema.Schema - val types = ctx.request.body.select("types") - .as[JsArray] - .value - .map(GraphQLFormats.objectTypeDefinitionFmt.reads) - .flatMap { - case JsSuccess(v, _) => Some(v) - case JsError(_) => None - } + val types = ctx.request.body + .select("types") + .as[JsArray] + .value + .map(GraphQLFormats.objectTypeDefinitionFmt.reads) + .flatMap { + case JsSuccess(v, _) => Some(v) + case JsError(_) => None + } val schema = ctx.request.body.select("schema").asOpt[String].getOrElse("{}") Try { sangria.parser.QueryParser.parse(schema) match { - case scala.util.Failure(exception) => BadRequest(Json.obj("error" -> exception.getMessage)).future + case scala.util.Failure(exception) => BadRequest(Json.obj("error" -> exception.getMessage)).future case scala.util.Success(astDocument) => val generatedSchema = Schema.buildFromAst(astDocument) - val document = generatedSchema.toAst + val document = generatedSchema.toAst val newDocument = document.copy( definitions = document.definitions.flatMap { - case _: sangria.ast.TypeDefinition => None + case _: sangria.ast.TypeDefinition => None case _: sangria.ast.InterfaceTypeDefinition => None - case v => Some(v) + case v => Some(v) } ++ types ) - Ok(Json.obj( - "schema" -> Schema.buildFromAst(newDocument).renderPretty - )).future + Ok( + Json.obj( + "schema" -> Schema.buildFromAst(newDocument).renderPretty + ) + ).future } - } recover { - case e => BadRequest(Json.obj( - "error" -> e.getMessage - )).future + } recover { case e => + BadRequest( + Json.obj( + "error" -> e.getMessage + ) + ).future } get } @@ -1823,14 +1830,3 @@ class BackOfficeController( Ok(Yaml.write(ctx.request.body)).as("application/yaml") } } - - - - - - - - - - - diff --git a/otoroshi/app/controllers/HealthController.scala b/otoroshi/app/controllers/HealthController.scala index 8c732416c0..be93ce0f7d 100644 --- a/otoroshi/app/controllers/HealthController.scala +++ b/otoroshi/app/controllers/HealthController.scala @@ -13,7 +13,8 @@ import otoroshi.utils.syntax.implicits._ import scala.concurrent.Future -class HealthController(cc: ControllerComponents, BackOfficeActionAuth: BackOfficeActionAuth)(implicit env: Env) extends AbstractController(cc) { +class HealthController(cc: ControllerComponents, BackOfficeActionAuth: BackOfficeActionAuth)(implicit env: Env) + extends AbstractController(cc) { implicit lazy val ec = env.otoroshiExecutionContext implicit lazy val mat = env.otoroshiMaterializer @@ -159,7 +160,12 @@ class HealthController(cc: ControllerComponents, BackOfficeActionAuth: BackOffic } } - def fetchMetrics(format: Option[String], acceptsJson: Boolean, acceptsProm: Boolean, filter: Option[String]): Result = { + def fetchMetrics( + format: Option[String], + acceptsJson: Boolean, + acceptsProm: Boolean, + filter: Option[String] + ): Result = { if (format.contains("old_json") || format.contains("old")) { Ok(env.metrics.jsonExport(filter)).as("application/json") } else if (format.contains("json")) { @@ -176,8 +182,8 @@ class HealthController(cc: ControllerComponents, BackOfficeActionAuth: BackOffic } def processMetrics() = Action.async { req => - val format = req.getQueryString("format") - val filter = req.getQueryString("filter") + val format = req.getQueryString("format") + val filter = req.getQueryString("filter") val acceptsJson = req.accepts("application/json") val acceptsProm = req.accepts("application/prometheus") if (env.metricsEnabled) { diff --git a/otoroshi/app/controllers/adminapi/ClusterController.scala b/otoroshi/app/controllers/adminapi/ClusterController.scala index 0a9c482b0e..5313cab21d 100644 --- a/otoroshi/app/controllers/adminapi/ClusterController.scala +++ b/otoroshi/app/controllers/adminapi/ClusterController.scala @@ -10,7 +10,7 @@ import otoroshi.actions.ApiAction import otoroshi.cluster._ import otoroshi.env.Env import otoroshi.models.{PrivateAppsUser, RightsChecker} -import otoroshi.next.proxy.{RelayRoutingRequest, ProxyEngine} +import otoroshi.next.proxy.{ProxyEngine, RelayRoutingRequest} import otoroshi.script.RequestHandler import otoroshi.security.IdGenerator import otoroshi.utils.syntax.implicits._ @@ -352,7 +352,9 @@ class ClusterController(ApiAction: ApiAction, cc: ControllerComponents)(implicit .map { name => env.datastores.clusterStateDataStore.registerMember( MemberView( - id = ctx.request.headers.get(ClusterAgent.OtoroshiWorkerIdHeader).getOrElse(s"tmpnode_${IdGenerator.uuid}"), + id = ctx.request.headers + .get(ClusterAgent.OtoroshiWorkerIdHeader) + .getOrElse(s"tmpnode_${IdGenerator.uuid}"), name = name, memberType = ClusterMode.Worker, location = @@ -451,7 +453,9 @@ class ClusterController(ApiAction: ApiAction, cc: ControllerComponents)(implicit ctx.request.headers.get(ClusterAgent.OtoroshiWorkerNameHeader).map { name => env.datastores.clusterStateDataStore.registerMember( MemberView( - id = ctx.request.headers.get(ClusterAgent.OtoroshiWorkerIdHeader).getOrElse(s"tmpnode_${IdGenerator.uuid}"), + id = ctx.request.headers + .get(ClusterAgent.OtoroshiWorkerIdHeader) + .getOrElse(s"tmpnode_${IdGenerator.uuid}"), name = name, memberType = ClusterMode.Worker, location = ctx.request.headers.get(ClusterAgent.OtoroshiWorkerLocationHeader).getOrElse("--"), @@ -640,30 +644,35 @@ class ClusterController(ApiAction: ApiAction, cc: ControllerComponents)(implicit ctx.checkRights(RightsChecker.SuperAdminOnly) { env.clusterConfig.mode match { case Off => NotFound(Json.obj("error" -> "Cluster API not available")).future - case _ => { - val engine = env.scriptManager.getAnyScript[RequestHandler](s"cp:${classOf[ProxyEngine].getName}").right.get - val cookies = ctx.request.headers.get("Otoroshi-Relay-Routing-Cookies").map(c => Cookies.decodeCookieHeader(c)).getOrElse(Seq.empty[Cookie]) - val certs = ctx.request.headers.headers.filter(_._1.startsWith("Otoroshi-Relay-Routing-Certs-")) + case _ => { + val engine = env.scriptManager.getAnyScript[RequestHandler](s"cp:${classOf[ProxyEngine].getName}").right.get + val cookies = ctx.request.headers + .get("Otoroshi-Relay-Routing-Cookies") + .map(c => Cookies.decodeCookieHeader(c)) + .getOrElse(Seq.empty[Cookie]) + val certs = ctx.request.headers.headers + .filter(_._1.startsWith("Otoroshi-Relay-Routing-Certs-")) .map { case (key, value) => (key.replace("Otoroshi-Relay-Routing-Certs-", "").toInt, value) } .sortWith((a, b) => a._1.compareTo(b._1) < 0) - .map { - case (_, value) => value.trim.toCertificate - }.applyOn { seq => + .map { case (_, value) => + value.trim.toCertificate + } + .applyOn { seq => if (seq.isEmpty) { None } else { seq.some } } - val request = new RelayRoutingRequest(ctx.request, Cookies(cookies), certs) - val routeName = ctx.request.headers.get("Otoroshi-Relay-Routing-Route-Name").getOrElse("--") + val request = new RelayRoutingRequest(ctx.request, Cookies(cookies), certs) + val routeName = ctx.request.headers.get("Otoroshi-Relay-Routing-Route-Name").getOrElse("--") val callerName = ctx.request.headers.get("Otoroshi-Relay-Routing-Caller-Name").getOrElse("--") RelayRouting.logger.debug(s"routing relay call to '${routeName}' from '${callerName}'") engine.handle(request, _ => Results.InternalServerError("bad default routing").vfuture).map { resp => resp.copy( header = resp.header.copy( - headers = resp.header.headers.map { - case (key, value) => (s"Otoroshi-Relay-Routing-Response-Header-$key", value) + headers = resp.header.headers.map { case (key, value) => + (s"Otoroshi-Relay-Routing-Response-Header-$key", value) } ) ) @@ -672,4 +681,4 @@ class ClusterController(ApiAction: ApiAction, cc: ControllerComponents)(implicit } } } -} \ No newline at end of file +} diff --git a/otoroshi/app/env/Env.scala b/otoroshi/app/env/Env.scala index 54eb6a318f..b111f5cfa4 100644 --- a/otoroshi/app/env/Env.scala +++ b/otoroshi/app/env/Env.scala @@ -26,7 +26,20 @@ import org.mindrot.jbcrypt.BCrypt import org.slf4j.LoggerFactory import otoroshi.events.{OtoroshiEventsActorSupervizer, StartExporters} import otoroshi.jobs.updates.Version -import otoroshi.models.{EntityLocation, OtoroshiAdminType, SimpleOtoroshiAdmin, Team, TeamAccess, TeamId, Tenant, TenantAccess, TenantId, UserRight, UserRights, WebAuthnOtoroshiAdmin} +import otoroshi.models.{ + EntityLocation, + OtoroshiAdminType, + SimpleOtoroshiAdmin, + Team, + TeamAccess, + TeamId, + Tenant, + TenantAccess, + TenantId, + UserRight, + UserRights, + WebAuthnOtoroshiAdmin +} import otoroshi.next.proxy.NgProxyState import otoroshi.openapi.{ClassGraphScanner, FormsGenerator, OpenApiGenerator, OpenapiToJson} import otoroshi.script.{AccessValidatorRef, JobManager, Script, ScriptCompiler, ScriptManager} @@ -408,21 +421,24 @@ class Env( lazy val isProd: Boolean = !isDev lazy val number: Int = configuration.getOptionalWithFileSupport[Int]("app.instance.number").getOrElse(0) lazy val name: String = configuration.getOptionalWithFileSupport[String]("app.instance.name").getOrElse("otoroshi") - lazy val title: String = configuration.getOptionalWithFileSupport[String]("app.instance.title").map { - case v if v.startsWith("ReplaceAll(") => v.substring(11, v.length) - case v => v - }.getOrElse("Otoroshi") + lazy val title: String = configuration + .getOptionalWithFileSupport[String]("app.instance.title") + .map { + case v if v.startsWith("ReplaceAll(") => v.substring(11, v.length) + case v => v + } + .getOrElse("Otoroshi") // lazy val rack: String = configuration.getOptionalWithFileSupport[String]("app.instance.rack").getOrElse("local") // lazy val infraProvider: String = // configuration.getOptionalWithFileSupport[String]("app.instance.provider").getOrElse("local") // lazy val dataCenter: String = configuration.getOptionalWithFileSupport[String]("app.instance.dc").getOrElse("local") // lazy val zone: String = configuration.getOptionalWithFileSupport[String]("app.instance.zone").getOrElse("local") // lazy val region: String = configuration.getOptionalWithFileSupport[String]("app.instance.region").getOrElse("local") - lazy val rack: String = clusterConfig.relay.location.rack - lazy val infraProvider: String = clusterConfig.relay.location.provider - lazy val dataCenter: String = clusterConfig.relay.location.datacenter - lazy val zone: String = clusterConfig.relay.location.zone - lazy val region: String = clusterConfig.relay.location.region + lazy val rack: String = clusterConfig.relay.location.rack + lazy val infraProvider: String = clusterConfig.relay.location.provider + lazy val dataCenter: String = clusterConfig.relay.location.datacenter + lazy val zone: String = clusterConfig.relay.location.zone + lazy val region: String = clusterConfig.relay.location.region lazy val liveJs: Boolean = configuration .getOptionalWithFileSupport[String]("app.env") .filter(_ == "dev") @@ -730,8 +746,8 @@ class Env( "OTOROSHI_ADMIN_API_SECRET", "used to access otoroshi admin api" ) - ).collect { - case Some(mess) => s" - $mess" + ).collect { case Some(mess) => + s" - $mess" } if (!clusterConfig.mode.isWorker && values.nonEmpty) { diff --git a/otoroshi/app/gateway/handlers.scala b/otoroshi/app/gateway/handlers.scala index bf12551eb6..702ea339f3 100644 --- a/otoroshi/app/gateway/handlers.scala +++ b/otoroshi/app/gateway/handlers.scala @@ -351,7 +351,8 @@ class GatewayRequestHandler( case env.backOfficeHost if env.exposeAdminDashboard => super.routeRequest(request) case env.privateAppsHost => super.routeRequest(request) - case env.adminApiHost if !env.exposeAdminApi && relativeUri.startsWith("/api/cluster/") => super.routeRequest(request) + case env.adminApiHost if !env.exposeAdminApi && relativeUri.startsWith("/api/cluster/") => + super.routeRequest(request) case h if env.adminApiExposedDomains.contains(h) && relativeUri.startsWith("/.well-known/jwks.json") => Some(jwks()) diff --git a/otoroshi/app/models/descriptor.scala b/otoroshi/app/models/descriptor.scala index d134e82cfa..5166c973a5 100644 --- a/otoroshi/app/models/descriptor.scala +++ b/otoroshi/app/models/descriptor.scala @@ -367,9 +367,9 @@ trait TargetPredicate { } object TargetPredicate { - val AlwaysMatch = otoroshi.models.AlwaysMatch - val GeolocationMatch = otoroshi.models.GeolocationMatch - val NetworkLocationMatch = otoroshi.models.NetworkLocationMatch + val AlwaysMatch = otoroshi.models.AlwaysMatch + val GeolocationMatch = otoroshi.models.GeolocationMatch + val NetworkLocationMatch = otoroshi.models.NetworkLocationMatch val format: Format[TargetPredicate] = new Format[TargetPredicate] { override def writes(o: TargetPredicate): JsValue = o.toJson override def reads(json: JsValue): JsResult[TargetPredicate] = { @@ -496,10 +496,16 @@ case class NetworkLocationMatch( "rack" -> rack ) override def matches(reqId: String, req: RequestHeader, attrs: TypedMap)(implicit env: Env): Boolean = { - otoroshi.utils.RegexPool(provider.trim.toLowerCase).matches(env.clusterConfig.relay.location.provider.trim.toLowerCase) && - otoroshi.utils.RegexPool(region.trim.toLowerCase).matches(env.clusterConfig.relay.location.region.trim.toLowerCase) && + otoroshi.utils + .RegexPool(provider.trim.toLowerCase) + .matches(env.clusterConfig.relay.location.provider.trim.toLowerCase) && + otoroshi.utils + .RegexPool(region.trim.toLowerCase) + .matches(env.clusterConfig.relay.location.region.trim.toLowerCase) && otoroshi.utils.RegexPool(zone.trim.toLowerCase).matches(env.clusterConfig.relay.location.zone.trim.toLowerCase) && - otoroshi.utils.RegexPool(dataCenter.trim.toLowerCase).matches(env.clusterConfig.relay.location.datacenter.trim.toLowerCase) && + otoroshi.utils + .RegexPool(dataCenter.trim.toLowerCase) + .matches(env.clusterConfig.relay.location.datacenter.trim.toLowerCase) && otoroshi.utils.RegexPool(rack.trim.toLowerCase).matches(env.clusterConfig.relay.location.rack.trim.toLowerCase) } } diff --git a/otoroshi/app/next/models/route.scala b/otoroshi/app/next/models/route.scala index 97586e2246..7ea04a11da 100644 --- a/otoroshi/app/next/models/route.scala +++ b/otoroshi/app/next/models/route.scala @@ -186,16 +186,33 @@ case class NgRoute( lazy val openapiUrl: Option[String] = metadata.get("otoroshi-core-openapi-url").filter(_.nonEmpty) lazy val originalRouteId: Option[String] = metadata.get("otoroshi-core-original-route-id").filter(_.nonEmpty) - lazy val deploymentProviders: Seq[String] = metadata.get("otoroshi-deployment-providers").filter(_.nonEmpty).map(_.split(",").map(_.trim).toSeq).getOrElse(Seq.empty) - lazy val hasDeploymentProviders: Boolean = deploymentProviders.nonEmpty - lazy val deploymentRegions: Seq[String] = metadata.get("otoroshi-deployment-regions").filter(_.nonEmpty).map(_.split(",").map(_.trim).toSeq).getOrElse(Seq.empty) - lazy val hasDeploymentRegions: Boolean = deploymentRegions.nonEmpty - lazy val deploymentZones: Seq[String] = metadata.get("otoroshi-deployment-zones").filter(_.nonEmpty).map(_.split(",").map(_.trim).toSeq).getOrElse(Seq.empty) - lazy val hasDeploymentZones: Boolean = deploymentZones.nonEmpty - lazy val deploymentDatacenters: Seq[String] = metadata.get("otoroshi-deployment-dcs").filter(_.nonEmpty).map(_.split(",").map(_.trim).toSeq).getOrElse(Seq.empty) - lazy val hasDeploymentDatacenters: Boolean = deploymentDatacenters.nonEmpty - lazy val deploymentRacks: Seq[String] = metadata.get("otoroshi-deployment-racks").filter(_.nonEmpty).map(_.split(",").map(_.trim).toSeq).getOrElse(Seq.empty) - lazy val hasDeploymentRacks: Boolean = deploymentRacks.nonEmpty + lazy val deploymentProviders: Seq[String] = metadata + .get("otoroshi-deployment-providers") + .filter(_.nonEmpty) + .map(_.split(",").map(_.trim).toSeq) + .getOrElse(Seq.empty) + lazy val hasDeploymentProviders: Boolean = deploymentProviders.nonEmpty + lazy val deploymentRegions: Seq[String] = metadata + .get("otoroshi-deployment-regions") + .filter(_.nonEmpty) + .map(_.split(",").map(_.trim).toSeq) + .getOrElse(Seq.empty) + lazy val hasDeploymentRegions: Boolean = deploymentRegions.nonEmpty + lazy val deploymentZones: Seq[String] = metadata + .get("otoroshi-deployment-zones") + .filter(_.nonEmpty) + .map(_.split(",").map(_.trim).toSeq) + .getOrElse(Seq.empty) + lazy val hasDeploymentZones: Boolean = deploymentZones.nonEmpty + lazy val deploymentDatacenters: Seq[String] = + metadata.get("otoroshi-deployment-dcs").filter(_.nonEmpty).map(_.split(",").map(_.trim).toSeq).getOrElse(Seq.empty) + lazy val hasDeploymentDatacenters: Boolean = deploymentDatacenters.nonEmpty + lazy val deploymentRacks: Seq[String] = metadata + .get("otoroshi-deployment-racks") + .filter(_.nonEmpty) + .map(_.split(",").map(_.trim).toSeq) + .getOrElse(Seq.empty) + lazy val hasDeploymentRacks: Boolean = deploymentRacks.nonEmpty lazy val legacy: ServiceDescriptor = serviceDescriptor lazy val serviceDescriptor: ServiceDescriptor = { diff --git a/otoroshi/app/next/plugins/graphql.scala b/otoroshi/app/next/plugins/graphql.scala index af014e403c..5d1647f5bc 100644 --- a/otoroshi/app/next/plugins/graphql.scala +++ b/otoroshi/app/next/plugins/graphql.scala @@ -21,7 +21,24 @@ import sangria.execution.deferred.DeferredResolver import sangria.execution.{ExceptionHandler, Executor, HandledException, QueryReducer} import sangria.marshalling.playJson._ import sangria.parser.QueryParser -import sangria.schema.{Action, Argument, AstDirectiveContext, AstSchemaBuilder, BooleanType, Directive, DirectiveResolver, FieldResolver, InstanceCheck, IntType, IntrospectionSchemaBuilder, ListInputType, OptionInputType, ResolverBasedAstSchemaBuilder, Schema, StringType} +import sangria.schema.{ + Action, + Argument, + AstDirectiveContext, + AstSchemaBuilder, + BooleanType, + Directive, + DirectiveResolver, + FieldResolver, + InstanceCheck, + IntType, + IntrospectionSchemaBuilder, + ListInputType, + OptionInputType, + ResolverBasedAstSchemaBuilder, + Schema, + StringType +} import sangria.validation.{QueryValidator, ValueCoercionViolation, Violation} import scala.concurrent.duration.{DurationLong, FiniteDuration, MILLISECONDS} @@ -30,10 +47,10 @@ import scala.jdk.CollectionConverters._ import scala.util._ import scala.util.control.NoStackTrace -case object TooComplexQueryError extends Exception("Query is too expensive.") with NoStackTrace -case class ViolationsException(errors: Seq[String]) extends Exception with NoStackTrace -case class GraphlCallException(message: String) extends Exception(message) -case class AuthorisationException(message: String) extends Exception(message) +case object TooComplexQueryError extends Exception("Query is too expensive.") with NoStackTrace +case class ViolationsException(errors: Seq[String]) extends Exception with NoStackTrace +case class GraphlCallException(message: String) extends Exception(message) +case class AuthorisationException(message: String) extends Exception(message) case class MissingMockResponsesException(message: String) extends Exception(message) case class MockResponseNotFoundException(message: String) extends Exception(message) @@ -443,8 +460,10 @@ class GraphQLBackend extends NgBackendCall { permissionResponse(authorized, c) } - def authorizeDirectiveResolver(c: AstDirectiveContext[Unit], ctx: NgbBackendCallContext) - (implicit env: Env, ec: ExecutionContext): Action[Unit, Any] = { + def authorizeDirectiveResolver(c: AstDirectiveContext[Unit], ctx: NgbBackendCallContext)(implicit + env: Env, + ec: ExecutionContext + ): Action[Unit, Any] = { val context = buildContext(ctx) val value: String = c.arg(valueArg) val path: String = c.arg(pathArg) @@ -572,52 +591,52 @@ class GraphQLBackend extends NgBackendCall { def mockDirectiveResolver(c: AstDirectiveContext[Unit], rawConfig: Option[JsObject])(implicit env: Env) = { rawConfig match { - case None => throw MissingMockResponsesException("Missing mock response plugin") + case None => throw MissingMockResponsesException("Missing mock response plugin") case Some(config) => MockResponsesConfig.format.reads(config) match { - case JsSuccess(value, _) => - val url = replaceTermsInUrl(c) - - NgTreeRouter - .build( - value.responses.map(resp => { - val r = NgFakeRoute.routeFromPath(s"oto.tools${resp.path}") - r.copy( - metadata = Map("mock" -> resp.json.stringify), - frontend = r.frontend.copy(exact = true) - ) + case JsSuccess(value, _) => + val url = replaceTermsInUrl(c) + + NgTreeRouter + .build( + value.responses.map(resp => { + val r = NgFakeRoute.routeFromPath(s"oto.tools${resp.path}") + r.copy( + metadata = Map("mock" -> resp.json.stringify), + frontend = r.frontend.copy(exact = true) + ) + }) + ) + .find("oto.tools", url) + .filter(_.noMoreSegments) + .flatMap { c => + if (c.routes.headOption.nonEmpty) + Some(c) + else + None + } + .map(r => { + val route = r.routes.headOption.get + val response = Json.parse(route.metadata("mock")).as[MockResponse](MockResponse.format) + + Json.parse(response.body) match { + case JsArray(value) => + val res = sliceArrayWithArgs(value, c) + res.map { + case v: JsObject => v + case JsString(v) => v + case JsNumber(v) => v + case JsBoolean(v) => v + case v => v.toString() + } + case v => v + } }) - ) - .find("oto.tools", url) - .filter(_.noMoreSegments) - .flatMap { c => - if (c.routes.headOption.nonEmpty) - Some(c) - else - None - } - .map(r => { - val route = r.routes.headOption.get - val response = Json.parse(route.metadata("mock")).as[MockResponse](MockResponse.format) - - Json.parse(response.body) match { - case JsArray(value) => - val res = sliceArrayWithArgs(value, c) - res.map { - case v: JsObject => v - case JsString(v) => v - case JsNumber(v) => v - case JsBoolean(v) => v - case v => v.toString() - } - case v => v + .getOrElse { + throw MockResponseNotFoundException("Mock response not found for this uri") } - }) - .getOrElse { - throw MockResponseNotFoundException("Mock response not found for this uri") - } - case JsError(_) => throw MissingMockResponsesException("Missing mock response plugin") - } + case JsError(_) => throw MissingMockResponsesException("Missing mock response plugin") + } } } @@ -653,7 +672,7 @@ class GraphQLBackend extends NgBackendCall { case v => v.toString() })) } - case _ => Map.empty // TODO - throw exception or whatever + case _ => Map.empty // TODO - throw exception or whatever }) ++ paramsArgs GlobalExpressionLanguage.apply( @@ -674,7 +693,7 @@ class GraphQLBackend extends NgBackendCall { ctx: NgbBackendCallContext, delegates: () => Future[Either[NgProxyEngineError, BackendCallResponse]] )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Action[Unit, Any] = { - val url = replaceQueryParams(c) + val url = replaceQueryParams(c) val graphqlQuery = env.scriptManager.getAnyScript[GraphQLQuery](s"cp:${classOf[GraphQLQuery].getName}").right.get graphqlQuery @@ -740,22 +759,22 @@ class GraphQLBackend extends NgBackendCall { val containsMockDirective = o.directives.exists(d => d.name == "mock") o.copy( directives = o.directives - .filter(d => (containsMockDirective && d.name != "rest") || !containsMockDirective) - .sortWith { case (a, b) => - val containsA = - List("permission", "allpermissions", "onePermissionsOf", "authorize", "mock").contains(a.name) - val containsB = - List("permission", "allpermissions", "onePermissionsOf", "authorize", "mock").contains(b.name) - if (containsA && containsB) - true - else if (containsA && !containsB) - true - else - false - }, + .filter(d => (containsMockDirective && d.name != "rest") || !containsMockDirective) + .sortWith { case (a, b) => + val containsA = + List("permission", "allpermissions", "onePermissionsOf", "authorize", "mock").contains(a.name) + val containsB = + List("permission", "allpermissions", "onePermissionsOf", "authorize", "mock").contains(b.name) + if (containsA && containsB) + true + else if (containsA && !containsB) + true + else + false + }, fields = o.fields .map(field => { - val fieldContainsMockDirective = field.directives.exists(d => d.name == "mock") + val fieldContainsMockDirective = field.directives.exists(d => d.name == "mock") val limitAndOffsetValues: Vector[InputValueDefinition] = field.directives.flatMap(directive => { if (directive.arguments.exists(a => a.name == "paginate")) { val limit = field.arguments.exists(a => a.name == "limit") @@ -784,17 +803,19 @@ class GraphQLBackend extends NgBackendCall { directives = field.directives .filter(d => (fieldContainsMockDirective && d.name != "rest") || !fieldContainsMockDirective) .sortWith { case (a, b) => - val containsA = - List("permission", "allpermissions", "onePermissionsOf", "authorize", "mock").contains(a.name) - val containsB = - List("permission", "allpermissions", "onePermissionsOf", "authorize", "mock").contains(b.name) - if (containsA && containsB) - true - else if (containsA && !containsB) - true - else - false - } + val containsA = + List("permission", "allpermissions", "onePermissionsOf", "authorize", "mock") + .contains(a.name) + val containsB = + List("permission", "allpermissions", "onePermissionsOf", "authorize", "mock") + .contains(b.name) + if (containsA && containsB) + true + else if (containsA && !containsB) + true + else + false + } ) }) .groupBy(_.name) @@ -892,7 +913,7 @@ class GraphQLBackend extends NgBackendCall { arguments = jsonDataArg :: jsonPathArg :: paginateArg :: Nil, locations = directivesLocations ) - val mockDirective = Directive( + val mockDirective = Directive( "mock", arguments = urlArg :: Nil, locations = directivesLocations @@ -915,8 +936,11 @@ class GraphQLBackend extends NgBackendCall { DirectiveResolver(soapDirective, resolve = c => soapDirectiveResolver(c, ctx, delegates, body)), /*DirectiveResolver(OtoroshiRouteDirective, resolve = OtoroshiRouteDirectiveResolver),*/ DirectiveResolver(jsonDirective, resolve = c => jsonDirectiveResolver(c, config)), - DirectiveResolver(mockDirective, resolve = c => mockDirectiveResolver(c, - ctx.route.plugins.getPluginByClass[MockResponses].map(_.config.raw))), + DirectiveResolver( + mockDirective, + resolve = + c => mockDirectiveResolver(c, ctx.route.plugins.getPluginByClass[MockResponses].map(_.config.raw)) + ), FieldResolver.defaultInput[Unit, JsValue] ) diff --git a/otoroshi/app/next/proxy/engine.scala b/otoroshi/app/next/proxy/engine.scala index 86ff822fe7..4617b39dfe 100644 --- a/otoroshi/app/next/proxy/engine.scala +++ b/otoroshi/app/next/proxy/engine.scala @@ -428,7 +428,9 @@ class ProxyEngine() extends RequestHandler { report.markDurations() closeCurrentRequest(env) attrs.get(Keys.RouteKey).foreach { route => - attrs.get(Keys.ContextualPluginsKey).foreach(ctxplgs => callPluginsAfterRequestCallback(snowflake, request, route, ctxplgs)) + attrs + .get(Keys.ContextualPluginsKey) + .foreach(ctxplgs => callPluginsAfterRequestCallback(snowflake, request, route, ctxplgs)) handleHighOverhead(request, route.some) if (tryIt) { tryItId.foreach(id => env.proxyState.addReport(id, report)) @@ -621,22 +623,26 @@ class ProxyEngine() extends RequestHandler { report: NgExecutionReport, globalConfig: GlobalConfig, attrs: TypedMap, - mat: Materializer): FEither[NgProxyEngineError, Done] = { + mat: Materializer + ): FEither[NgProxyEngineError, Done] = { if (env.clusterConfig.relay.enabled) { - val location = env.clusterConfig.relay.location - val matchRack: Boolean = if (route.hasDeploymentRacks) route.deploymentRacks.contains(location.rack) else true - val matchDatacenter: Boolean = if (route.hasDeploymentDatacenters) route.deploymentDatacenters.contains(location.datacenter) else true - val matchZone: Boolean = if (route.hasDeploymentZones) route.deploymentZones.contains(location.zone) else true - val matchRegion: Boolean = if (route.hasDeploymentRegions) route.deploymentRegions.contains(location.region) else true - val matchProvider: Boolean = if (route.hasDeploymentProviders) route.deploymentProviders.contains(location.provider) else true - val matching: Boolean = matchRack && matchDatacenter && matchZone && matchRegion && matchProvider + val location = env.clusterConfig.relay.location + val matchRack: Boolean = if (route.hasDeploymentRacks) route.deploymentRacks.contains(location.rack) else true + val matchDatacenter: Boolean = + if (route.hasDeploymentDatacenters) route.deploymentDatacenters.contains(location.datacenter) else true + val matchZone: Boolean = if (route.hasDeploymentZones) route.deploymentZones.contains(location.zone) else true + val matchRegion: Boolean = + if (route.hasDeploymentRegions) route.deploymentRegions.contains(location.region) else true + val matchProvider: Boolean = + if (route.hasDeploymentProviders) route.deploymentProviders.contains(location.provider) else true + val matching: Boolean = matchRack && matchDatacenter && matchZone && matchRegion && matchProvider if (matching) { FEither.right(Done) } else { // Here, choose zone leader and forward the call FEither(env.datastores.clusterStateDataStore.getMembers().flatMap { members => val possibleLeaders = new PossibleLeaders(members, route) - val leader = possibleLeaders.chooseNext(reqCounter) + val leader = possibleLeaders.chooseNext(reqCounter) leader.call(req, body) }) } @@ -2888,19 +2894,21 @@ class ProxyEngine() extends RequestHandler { sameSite = c.sameSite ) case c => { - val sameSite: Option[Cookie.SameSite] = rawResponse.headers.get("Set-Cookie").orElse(rawResponse.headers.get("set-cookie")).flatMap { values => // legit - values - .find { sc => - sc.startsWith(s"${c.name}=${c.value}") - } - .flatMap { sc => - sc.split(";") - .map(_.trim) - .find(p => p.toLowerCase.startsWith("samesite=")) - .map(_.replace("samesite=", "").replace("SameSite=", "")) - .flatMap(Cookie.SameSite.parse) - } - } + val sameSite: Option[Cookie.SameSite] = + rawResponse.headers.get("Set-Cookie").orElse(rawResponse.headers.get("set-cookie")).flatMap { + values => // legit + values + .find { sc => + sc.startsWith(s"${c.name}=${c.value}") + } + .flatMap { sc => + sc.split(";") + .map(_.trim) + .find(p => p.toLowerCase.startsWith("samesite=")) + .map(_.replace("samesite=", "").replace("SameSite=", "")) + .flatMap(Cookie.SameSite.parse) + } + } Cookie( name = c.name, value = c.value, diff --git a/otoroshi/app/next/proxy/errors.scala b/otoroshi/app/next/proxy/errors.scala index cb9e74dbb2..55a28e901a 100644 --- a/otoroshi/app/next/proxy/errors.scala +++ b/otoroshi/app/next/proxy/errors.scala @@ -6,10 +6,10 @@ import play.api.mvc.Result import scala.concurrent.{ExecutionContext, Future} -trait NgProxyEngineError { +trait NgProxyEngineError { def asResult()(implicit ec: ExecutionContext, env: Env): Future[Result] } -object NgProxyEngineError { +object NgProxyEngineError { case class NgResultProxyEngineError(result: Result) extends NgProxyEngineError { override def asResult()(implicit ec: ExecutionContext, env: Env): Future[Result] = result.vfuture } diff --git a/otoroshi/app/next/proxy/request.scala b/otoroshi/app/next/proxy/request.scala index a81884b380..84f1b1d672 100644 --- a/otoroshi/app/next/proxy/request.scala +++ b/otoroshi/app/next/proxy/request.scala @@ -10,35 +10,40 @@ import play.api.mvc.request.{Cell, RemoteConnection, RequestAttrKey, RequestTarg import java.net.{InetAddress, URI} import java.security.cert.X509Certificate -class RelayRoutingRequest(req: Request[Source[ByteString, _]], cookies: Cookies, certs: Option[Seq[X509Certificate]]) extends Request[Source[ByteString, _]] { - - lazy val version = req.version - lazy val reqId = req.headers.get("Otoroshi-Relay-Routing-Id").get.toLong - lazy val method = req.headers.get("Otoroshi-Relay-Routing-Method").get - lazy val body = req.body - lazy val _remoteAddr = req.headers.get("Otoroshi-Relay-Routing-Remote-Addr").get +class RelayRoutingRequest(req: Request[Source[ByteString, _]], cookies: Cookies, certs: Option[Seq[X509Certificate]]) + extends Request[Source[ByteString, _]] { + + lazy val version = req.version + lazy val reqId = req.headers.get("Otoroshi-Relay-Routing-Id").get.toLong + lazy val method = req.headers.get("Otoroshi-Relay-Routing-Method").get + lazy val body = req.body + lazy val _remoteAddr = req.headers.get("Otoroshi-Relay-Routing-Remote-Addr").get lazy val _remoteAddrInet = InetAddress.getByName(_remoteAddr) - lazy val _remoteSecured = req.headers.get("Otoroshi-Relay-Routing-Secured").get.toBoolean - lazy val _remoteHasBody = req.headers.get("Otoroshi-Relay-Routing-Has-Body").get.toBoolean - lazy val _remoteUriStr = req.headers.get("Otoroshi-Relay-Routing-Uri").get - lazy val attrs = TypedMap.apply( - RequestAttrKey.Id -> reqId, + lazy val _remoteSecured = req.headers.get("Otoroshi-Relay-Routing-Secured").get.toBoolean + lazy val _remoteHasBody = req.headers.get("Otoroshi-Relay-Routing-Has-Body").get.toBoolean + lazy val _remoteUriStr = req.headers.get("Otoroshi-Relay-Routing-Uri").get + lazy val attrs = TypedMap.apply( + RequestAttrKey.Id -> reqId, RequestAttrKey.Cookies -> Cell(cookies) ) - lazy val headers: Headers = Headers( + lazy val headers: Headers = Headers( req.headers.toSimpleMap.toSeq .filterNot(_._1 == "Otoroshi-Relay-Routing-Cookies") .filter(_._1.startsWith("Otoroshi-Relay-Routing-Header-")) - .map(v => (v._1.replace("Otoroshi-Relay-Routing-Header-", ""), v._2)) - : _*) + .map(v => (v._1.replace("Otoroshi-Relay-Routing-Header-", ""), v._2)): _* + ) lazy val connection: RemoteConnection = new RelayRoutingRemoteConnection(_remoteAddrInet, _remoteSecured, certs) - lazy val target: RequestTarget = new RelayRoutingRequestTarget(_remoteUriStr) + lazy val target: RequestTarget = new RelayRoutingRequestTarget(_remoteUriStr) } -class RelayRoutingRemoteConnection(_remoteAddrInet: InetAddress, _remoteSecured: Boolean, certs: Option[Seq[X509Certificate]]) extends RemoteConnection { - override def remoteAddress: InetAddress = _remoteAddrInet - override def secure: Boolean = _remoteSecured +class RelayRoutingRemoteConnection( + _remoteAddrInet: InetAddress, + _remoteSecured: Boolean, + certs: Option[Seq[X509Certificate]] +) extends RemoteConnection { + override def remoteAddress: InetAddress = _remoteAddrInet + override def secure: Boolean = _remoteSecured override def clientCertificateChain: Option[Seq[X509Certificate]] = certs } @@ -47,8 +52,8 @@ class RelayRoutingRequestTarget(_remoteUriStr: String) extends RequestTarget { private lazy val _remoteUri = Uri(_remoteUriStr) private lazy val _remoteURI = URI.create(_remoteUriStr) - override def uri: URI = _remoteURI - override def uriString: String = _remoteUriStr - override def path: String = _remoteUri.path.toString() + override def uri: URI = _remoteURI + override def uriString: String = _remoteUriStr + override def path: String = _remoteUri.path.toString() override def queryMap: Map[String, Seq[String]] = _remoteUri.query().toMultiMap } diff --git a/otoroshi/app/next/proxy/zones.scala b/otoroshi/app/next/proxy/zones.scala index 200570e161..a563787470 100644 --- a/otoroshi/app/next/proxy/zones.scala +++ b/otoroshi/app/next/proxy/zones.scala @@ -24,44 +24,47 @@ import scala.concurrent.duration.DurationLong import scala.concurrent.{ExecutionContext, Future} /** - -java \ - -Dhttp.port=8080 \ - -Dhttps.port=8443 \ - -Dotoroshi.cluster.mode=leader \ - -Dapp.adminPassword=password \ - -Dapp.storage=file \ - -Dotoroshi.cluster.relayRouting.enabled=true \ - -Dotoroshi.cluster.relayRouting.location.region=zone1 \ - -Dotoroshi.cluster.relayRouting.exposition.hostname=otoroshi-api-zone1.oto.tools \ - -Dotoroshi.cluster.relayRouting.exposition.url=http://otoroshi-api-zone1.oto.tools:8080 \ - -jar otoroshi.jar - - -java \ - -Dhttp.port=8081 \ - -Dhttps.port=8444 \ - -Dotoroshi.cluster.mode=worker \ - -Dotoroshi.cluster.relayRouting.enabled=true \ - -Dotoroshi.cluster.relayRouting.location.region=zone2 \ - -Dotoroshi.cluster.relayRouting.exposition.hostname=otoroshi-api-zone2.oto.tools \ - -Dotoroshi.cluster.relayRouting.exposition.url=http://otoroshi-api-zone2.oto.tools:8081 \ - -jar otoroshi.jar - + * java \ + * -Dhttp.port=8080 \ + * -Dhttps.port=8443 \ + * -Dotoroshi.cluster.mode=leader \ + * -Dapp.adminPassword=password \ + * -Dapp.storage=file \ + * -Dotoroshi.cluster.relayRouting.enabled=true \ + * -Dotoroshi.cluster.relayRouting.location.region=zone1 \ + * -Dotoroshi.cluster.relayRouting.exposition.hostname=otoroshi-api-zone1.oto.tools \ + * -Dotoroshi.cluster.relayRouting.exposition.url=http://otoroshi-api-zone1.oto.tools:8080 \ + * -jar otoroshi.jar + * + * java \ + * -Dhttp.port=8081 \ + * -Dhttps.port=8444 \ + * -Dotoroshi.cluster.mode=worker \ + * -Dotoroshi.cluster.relayRouting.enabled=true \ + * -Dotoroshi.cluster.relayRouting.location.region=zone2 \ + * -Dotoroshi.cluster.relayRouting.exposition.hostname=otoroshi-api-zone2.oto.tools \ + * -Dotoroshi.cluster.relayRouting.exposition.url=http://otoroshi-api-zone2.oto.tools:8081 \ + * -jar otoroshi.jar */ class RelayRoutingResult(resp: WSResponse) extends NgProxyEngineError { override def asResult()(implicit ec: ExecutionContext, env: Env): Future[Result] = { - val cl = resp.headers.getIgnoreCase("Content-Length").map(_.last).map(_.toLong) - val ct = resp.headers.getIgnoreCase("Content-Type").map(_.last) - val setCookie = resp.headers.get("Otoroshi-Relay-Routing-Response-Header-Set-Cookie").map(vs => vs.flatMap(v => Cookies.decodeSetCookieHeader(v))).getOrElse(Seq.empty[Cookie]) + val cl = resp.headers.getIgnoreCase("Content-Length").map(_.last).map(_.toLong) + val ct = resp.headers.getIgnoreCase("Content-Type").map(_.last) + val setCookie = resp.headers + .get("Otoroshi-Relay-Routing-Response-Header-Set-Cookie") + .map(vs => vs.flatMap(v => Cookies.decodeSetCookieHeader(v))) + .getOrElse(Seq.empty[Cookie]) val headers: Seq[(String, String)] = resp.headers .filterNot(_._1 == "Otoroshi-Relay-Routing-Response-Header-Set-Cookie") - .filter(_._1.startsWith("Otoroshi-Relay-Routing-Response-Header-")).map { - case (key, values) => (key.replace("Otoroshi-Relay-Routing-Response-Header-", ""), values.last) - }.toSeq + .filter(_._1.startsWith("Otoroshi-Relay-Routing-Response-Header-")) + .map { case (key, values) => + (key.replace("Otoroshi-Relay-Routing-Response-Header-", ""), values.last) + } + .toSeq Results - .Status(resp.status).sendEntity(HttpEntity.Streamed(resp.bodyAsSource, cl, ct)) + .Status(resp.status) + .sendEntity(HttpEntity.Streamed(resp.bodyAsSource, cl, ct)) .withHeaders(headers: _*) .applyOnIf(setCookie.nonEmpty)(_.withCookies(setCookie: _*)) .vfuture @@ -69,25 +72,32 @@ class RelayRoutingResult(resp: WSResponse) extends NgProxyEngineError { } case class SelectedLeader(member: MemberView, route: NgRoute, counter: AtomicInteger) { - def call(req: RequestHeader, body: Source[ByteString, _])(implicit ec: ExecutionContext, env: Env, report: NgExecutionReport): Future[Either[NgProxyEngineError, Done]] = { + def call(req: RequestHeader, body: Source[ByteString, _])(implicit + ec: ExecutionContext, + env: Env, + report: NgExecutionReport + ): Future[Either[NgProxyEngineError, Done]] = { implicit val sched = env.otoroshiScheduler Retry.retry( times = env.clusterConfig.worker.retries, delay = env.clusterConfig.retryDelay, factor = env.clusterConfig.retryDelay, - ctx = "forwarding call through a relay node", + ctx = "forwarding call through a relay node" ) { attempt => - val useLeader = env.clusterConfig.mode.isWorker && env.clusterConfig.relay.leaderOnly - val urls = if (useLeader) env.clusterConfig.leader.urls else member.relay.exposition.urls - val index = counter.get() % (if (urls.nonEmpty) urls.size else 1) - val url = urls.sortWith((m1, m2) => m1.compareTo(m2) < 0).apply(index) - val clientId: String = if (useLeader) env.clusterConfig.leader.clientId else member.relay.exposition.clientId.getOrElse(env.backOfficeApiKeyClientId) - val clientSecret: String = env.proxyState.apikey(clientId).map(_.clientSecret).getOrElse("secret") - val ct: Option[String] = req.headers.toSimpleMap.getIgnoreCase("Content-Type") - val cl: Option[String] = req.headers.toSimpleMap.getIgnoreCase("Content-Length") - val host: String = if (useLeader) env.clusterConfig.leader.host else member.relay.exposition.hostname - val ipAddress: Option[String] = if (useLeader) None else member.relay.exposition.ipAddress - val mtlsConfig: MtlsConfig = if (useLeader) env.clusterConfig.mtlsConfig else member.relay.exposition.tls.getOrElse(MtlsConfig()) + val useLeader = env.clusterConfig.mode.isWorker && env.clusterConfig.relay.leaderOnly + val urls = if (useLeader) env.clusterConfig.leader.urls else member.relay.exposition.urls + val index = counter.get() % (if (urls.nonEmpty) urls.size else 1) + val url = urls.sortWith((m1, m2) => m1.compareTo(m2) < 0).apply(index) + val clientId: String = + if (useLeader) env.clusterConfig.leader.clientId + else member.relay.exposition.clientId.getOrElse(env.backOfficeApiKeyClientId) + val clientSecret: String = env.proxyState.apikey(clientId).map(_.clientSecret).getOrElse("secret") + val ct: Option[String] = req.headers.toSimpleMap.getIgnoreCase("Content-Type") + val cl: Option[String] = req.headers.toSimpleMap.getIgnoreCase("Content-Length") + val host: String = if (useLeader) env.clusterConfig.leader.host else member.relay.exposition.hostname + val ipAddress: Option[String] = if (useLeader) None else member.relay.exposition.ipAddress + val mtlsConfig: MtlsConfig = + if (useLeader) env.clusterConfig.mtlsConfig else member.relay.exposition.tls.getOrElse(MtlsConfig()) val headers: Seq[(String, String)] = (Seq( ("Host" -> host), ("Otoroshi-Client-Id", clientId), @@ -101,33 +111,39 @@ case class SelectedLeader(member: MemberView, route: NgRoute, counter: AtomicInt ("Otoroshi-Relay-Routing-Route-Id", route.id), ("Otoroshi-Relay-Routing-Route-Name", route.name), ("Otoroshi-Relay-Routing-Caller-Id", env.clusterConfig.id), - ("Otoroshi-Relay-Routing-Caller-Name", env.clusterConfig.name), - ) ++ req.headers.toSimpleMap.toSeq.map { - case (key, value) => (s"Otoroshi-Relay-Routing-Header-${key}", value) - }).applyOnWithOpt(ct) { - case (seq, cty) => seq :+ ("Content-Type", cty) - }.applyOnWithOpt(cl) { - case (seq, clt) => seq :+ ("Content-Length", clt) - }.applyOnWithOpt(req.clientCertificateChain) { - case (seq, certs) => seq ++ certs.zipWithIndex.map { case (c, idx) => (s"Otoroshi-Relay-Routing-Certs-${idx}" -> c.encoded) } + ("Otoroshi-Relay-Routing-Caller-Name", env.clusterConfig.name) + ) ++ req.headers.toSimpleMap.toSeq.map { case (key, value) => + (s"Otoroshi-Relay-Routing-Header-${key}", value) + }).applyOnWithOpt(ct) { case (seq, cty) => + seq :+ ("Content-Type", cty) + }.applyOnWithOpt(cl) { case (seq, clt) => + seq :+ ("Content-Length", clt) + }.applyOnWithOpt(req.clientCertificateChain) { case (seq, certs) => + seq ++ certs.zipWithIndex.map { case (c, idx) => (s"Otoroshi-Relay-Routing-Certs-${idx}" -> c.encoded) } }.applyOnIf(req.cookies.nonEmpty) { seq => seq :+ ("Otoroshi-Relay-Routing-Cookies", Cookies.encodeCookieHeader(req.cookies.toSeq)) } - val uriStr = s"$url/api/cluster/relay" - val uri = Uri(uriStr) + val uriStr = s"$url/api/cluster/relay" + val uri = Uri(uriStr) if (useLeader) { - RelayRouting.logger.debug(s"forwarding call to '${route.name}' through local leader '${uriStr}' (attempt ${attempt})") + RelayRouting.logger.debug( + s"forwarding call to '${route.name}' through local leader '${uriStr}' (attempt ${attempt})" + ) } else { RelayRouting.logger.debug(s"forwarding call to '${route.name}' through relay '${uriStr}' (attempt ${attempt})") } - env.Ws.akkaUrlWithTarget(uriStr, Target( - host = uri.authority.toString(), - scheme = uri.scheme, - protocol = HttpProtocols.`HTTP/1.1`, - predicate = TargetPredicate.AlwaysMatch, - ipAddress = ipAddress, - mtlsConfig = mtlsConfig - )) + env.Ws + .akkaUrlWithTarget( + uriStr, + Target( + host = uri.authority.toString(), + scheme = uri.scheme, + protocol = HttpProtocols.`HTTP/1.1`, + predicate = TargetPredicate.AlwaysMatch, + ipAddress = ipAddress, + mtlsConfig = mtlsConfig + ) + ) .withMethod("POST") .withRequestTimeout(route.backend.client.globalTimeout.milliseconds) .withBody(body) @@ -144,16 +160,20 @@ class PossibleLeaders(members: Seq[MemberView], route: NgRoute) { def chooseNext(counter: AtomicInteger)(implicit env: Env): SelectedLeader = { val useLeader = env.clusterConfig.mode.isWorker && env.clusterConfig.relay.leaderOnly if (useLeader) { - SelectedLeader(MemberView( - id = "local-leader", - name = "local-leader", - location = env.clusterConfig.relay.exposition.urls.headOption.getOrElse("127.0.0.1"), - lastSeen = DateTime.now(), - timeout = 10.seconds, - memberType = ClusterMode.Leader, - relay = env.clusterConfig.relay, - stats = Json.obj(), - ), route, counter) + SelectedLeader( + MemberView( + id = "local-leader", + name = "local-leader", + location = env.clusterConfig.relay.exposition.urls.headOption.getOrElse("127.0.0.1"), + lastSeen = DateTime.now(), + timeout = 10.seconds, + memberType = ClusterMode.Leader, + relay = env.clusterConfig.relay, + stats = Json.obj() + ), + route, + counter + ) } else { val selectedMembers = members .filter { member => @@ -192,9 +212,9 @@ class PossibleLeaders(members: Seq[MemberView], route: NgRoute) { } } - val index = counter.get() % (if (selectedMembers.nonEmpty) selectedMembers.size else 1) + val index = counter.get() % (if (selectedMembers.nonEmpty) selectedMembers.size else 1) val member = selectedMembers.sortWith((m1, m2) => m1.id.compareTo(m2.id) < 0).apply(index) SelectedLeader(member, route, counter) } } -} \ No newline at end of file +} diff --git a/otoroshi/app/script/requesthandler.scala b/otoroshi/app/script/requesthandler.scala index 664966e7ba..2582daae8f 100644 --- a/otoroshi/app/script/requesthandler.scala +++ b/otoroshi/app/script/requesthandler.scala @@ -84,32 +84,32 @@ class ForwardTrafficHandler extends RequestHandler { domains.get(request.theDomain) match { case None => defaultRouting(request) case Some(obj) => { - val start = System.currentTimeMillis() - val baseUrl = obj.select("baseUrl").asString - val secret = obj.select("secret").asString - val service = obj.select("service").asObject - val serviceId = service.select("id").asString - val serviceName = service.select("name").asString + val start = System.currentTimeMillis() + val baseUrl = obj.select("baseUrl").asString + val secret = obj.select("secret").asString + val service = obj.select("service").asObject + val serviceId = service.select("id").asString + val serviceName = service.select("name").asString val issuer = obj.select("jwtIssuer").asOpt[String].getOrElse(env.Headers.OtoroshiIssuer) val stateHeaderName = obj.select("stateHeaderName").asOpt[String].getOrElse(env.Headers.OtoroshiState) val claimHeaderName = obj.select("claimHeaderName").asOpt[String].getOrElse(env.Headers.OtoroshiClaim) - val date = DateTime.now() - val reqId = UUID.randomUUID().toString - val alg = Algorithm.HMAC512(secret) - val token = JWT.create().withIssuer(issuer).sign(alg) - val path = request.thePath - val baseUri = Uri(baseUrl) - val host = baseUri.authority.host.toString() - val headers = request.headers.toSimpleMap.toSeq + val date = DateTime.now() + val reqId = UUID.randomUUID().toString + val alg = Algorithm.HMAC512(secret) + val token = JWT.create().withIssuer(issuer).sign(alg) + val path = request.thePath + val baseUri = Uri(baseUrl) + val host = baseUri.authority.host.toString() + val headers = request.headers.toSimpleMap.toSeq // .filterNot(_._1.toLowerCase == "content-type") .filterNot(_._1.toLowerCase == "timeout-access") .filterNot(_._1.toLowerCase == "tls-session-info") .filterNot(_._1.toLowerCase == "host") ++ Seq( (stateHeaderName -> reqId), (claimHeaderName -> token), - ("Host" -> host) + ("Host" -> host) ) - val cookies = request.cookies.toSeq.map { c => + val cookies = request.cookies.toSeq.map { c => WSCookieWithSameSite( name = c.name, value = c.value, @@ -121,8 +121,8 @@ class ForwardTrafficHandler extends RequestHandler { sameSite = c.sameSite ) } - val overhead = System.currentTimeMillis() - start - var builder = env.gatewayClient + val overhead = System.currentTimeMillis() - start + var builder = env.gatewayClient .akkaUrl(s"$baseUrl$path") .withHttpHeaders(headers: _*) .withCookies(cookies: _*) @@ -136,30 +136,33 @@ class ForwardTrafficHandler extends RequestHandler { builder .stream() .map { resp => - val duration = System.currentTimeMillis() - start - val ctypeOut = resp.headers.get("Content-Type").orElse(resp.headers.get("content-type")).map(_.last) - val clenOut = resp.headers.get("Content-Length").orElse(resp.headers.get("content-length")).map(_.last).map(_.toLong) - val headersOut = resp.headers.mapValues(_.last) - .filterNot { - case (key, _) => key.toLowerCase == "content-length" + val duration = System.currentTimeMillis() - start + val ctypeOut = resp.headers.get("Content-Type").orElse(resp.headers.get("content-type")).map(_.last) + val clenOut = + resp.headers.get("Content-Length").orElse(resp.headers.get("content-length")).map(_.last).map(_.toLong) + val headersOut = resp.headers + .mapValues(_.last) + .filterNot { case (key, _) => + key.toLowerCase == "content-length" } - .filterNot { - case (key, _) => key.toLowerCase == "content-type" + .filterNot { case (key, _) => + key.toLowerCase == "content-type" } .toSeq - val transferEncoding = resp.headers.get("Transfer-Encoding").orElse(resp.headers.get("transfer-encoding")).map(_.last) - val hasChunkedHeader = transferEncoding.exists(h => h.toLowerCase().contains("chunked")) + val transferEncoding = + resp.headers.get("Transfer-Encoding").orElse(resp.headers.get("transfer-encoding")).map(_.last) + val hasChunkedHeader = transferEncoding.exists(h => h.toLowerCase().contains("chunked")) val isChunked: Boolean = resp.isChunked() match { // don't know if actualy legit ... - case Some(chunked) => chunked - case None if !env.emptyContentLengthIsChunked => + case Some(chunked) => chunked + case None if !env.emptyContentLengthIsChunked => hasChunkedHeader // false - case None if env.emptyContentLengthIsChunked && hasChunkedHeader => + case None if env.emptyContentLengthIsChunked && hasChunkedHeader => true - case None if env.emptyContentLengthIsChunked && !hasChunkedHeader && clenOut.isEmpty => + case None if env.emptyContentLengthIsChunked && !hasChunkedHeader && clenOut.isEmpty => true - case _ => false + case _ => false } - val cookiesOut = resp.cookies.map { + val cookiesOut = resp.cookies.map { case c: WSCookieWithSameSite => Cookie( name = c.name, @@ -172,19 +175,20 @@ class ForwardTrafficHandler extends RequestHandler { sameSite = c.sameSite ) case c => { - val sameSite: Option[Cookie.SameSite] = resp.headers.get("Set-Cookie").orElse(resp.headers.get("set-cookie")).flatMap { values => // legit - values - .find { sc => - sc.startsWith(s"${c.name}=${c.value}") - } - .flatMap { sc => - sc.split(";") - .map(_.trim) - .find(p => p.toLowerCase.startsWith("samesite=")) - .map(_.replace("samesite=", "").replace("SameSite=", "")) - .flatMap(Cookie.SameSite.parse) - } - } + val sameSite: Option[Cookie.SameSite] = + resp.headers.get("Set-Cookie").orElse(resp.headers.get("set-cookie")).flatMap { values => // legit + values + .find { sc => + sc.startsWith(s"${c.name}=${c.value}") + } + .flatMap { sc => + sc.split(";") + .map(_.trim) + .find(p => p.toLowerCase.startsWith("samesite=")) + .map(_.replace("samesite=", "").replace("SameSite=", "")) + .flatMap(Cookie.SameSite.parse) + } + } Cookie( name = c.name, value = c.value, diff --git a/otoroshi/app/ssl/ssl.scala b/otoroshi/app/ssl/ssl.scala index f3cb3d4de9..13631b7437 100644 --- a/otoroshi/app/ssl/ssl.scala +++ b/otoroshi/app/ssl/ssl.scala @@ -2272,7 +2272,7 @@ class KvClientCertificateValidationDataStore(redisCli: RedisLike, env: Env) override def fmt: Format[ClientCertificateValidator] = ClientCertificateValidator.fmt override def redisLike(implicit env: Env): RedisLike = redisCli - override def key(id: String): String = s"${env.storageRoot}:certificates:validators:$id" + override def key(id: String): String = s"${env.storageRoot}:certificates:validators:$id" override def extractId(value: ClientCertificateValidator): String = value.id } diff --git a/otoroshi/app/storage/drivers/inmemory/InMemoryDataStores.scala b/otoroshi/app/storage/drivers/inmemory/InMemoryDataStores.scala index da5b027855..4ad9598bed 100644 --- a/otoroshi/app/storage/drivers/inmemory/InMemoryDataStores.scala +++ b/otoroshi/app/storage/drivers/inmemory/InMemoryDataStores.scala @@ -14,7 +14,16 @@ import otoroshi.events.{AlertDataStore, AuditDataStore, HealthCheckDataStore} import otoroshi.gateway.{InMemoryRequestsDataStore, RequestsDataStore} import otoroshi.models._ import otoroshi.models.{SimpleAdminDataStore, WebAuthnAdminDataStore} -import otoroshi.next.models.{KvNgRouteDataStore, KvNgServiceDataStore, KvStoredNgBackendDataStore, KvStoredNgTargetDataStore, NgRouteDataStore, NgServiceDataStore, StoredNgBackendDataStore, StoredNgTargetDataStore} +import otoroshi.next.models.{ + KvNgRouteDataStore, + KvNgServiceDataStore, + KvStoredNgBackendDataStore, + KvStoredNgTargetDataStore, + NgRouteDataStore, + NgServiceDataStore, + StoredNgBackendDataStore, + StoredNgTargetDataStore +} import otoroshi.script.{KvScriptDataStore, ScriptDataStore} import otoroshi.storage.stores._ import otoroshi.storage.{DataStoreHealth, DataStores, RawDataStore, SwappableRedisLikeMetricsWrapper} @@ -54,12 +63,12 @@ class InMemoryDataStores( val _optimized = configuration.getOptionalWithFileSupport[Boolean]("app.inmemory.optimized").getOrElse(false) val _modern = configuration.getOptionalWithFileSupport[Boolean]("app.inmemory.modern").getOrElse(false) // lazy val redis = new SwappableInMemoryRedis(_optimized, env, actorSystem) - lazy val _redis = if (_modern) { + lazy val _redis = if (_modern) { new ModernSwappableInMemoryRedis(_optimized, env, actorSystem) } else { new SwappableInMemoryRedis(_optimized, env, actorSystem) } - lazy val redis = if (env.isDev) new SwappableRedisLikeMetricsWrapper(_redis, env) else _redis + lazy val redis = if (env.isDev) new SwappableRedisLikeMetricsWrapper(_redis, env) else _redis lazy val persistence = persistenceKind match { case PersistenceKind.HttpPersistenceKind => new HttpPersistence(this, env) diff --git a/otoroshi/app/storage/storage.scala b/otoroshi/app/storage/storage.scala index d54d365275..d2a084f360 100644 --- a/otoroshi/app/storage/storage.scala +++ b/otoroshi/app/storage/storage.scala @@ -106,7 +106,7 @@ trait BasicStore[T] { def key(id: String): String // def keyStr(id: String): String = key(id).key def extractId(value: T): String - def extractKey(value: T): String = key(extractId(value)) + def extractKey(value: T): String = key(extractId(value)) def findAll(force: Boolean = false)(implicit ec: ExecutionContext, env: Env): Future[Seq[T]] //def findAllByKeys(ids: Seq[Key], force: Boolean = false)(implicit ec: ExecutionContext, env: Env): Future[Seq[T]] = // findAllById(ids.map(_.key), force) @@ -255,7 +255,7 @@ trait OptimizedRedisLike { case _ if key.startsWith(ds.tenantDataStore.key("")) => "tenant".some case _ if key.startsWith(ds.tcpServiceDataStore.key("")) => "tcp-service".some case _ if key.startsWith(ds.globalConfigDataStore.key("")) => "global-config".some - case _ => None + case _ => None } } } @@ -515,8 +515,8 @@ trait MetricsWrapper { private val logger = Logger("otoroshi-metrics-wrapper") - private val opsKey = "otoroshi.core.storage.ops" - private val opsReadKey = s"$opsKey.read" + private val opsKey = "otoroshi.core.storage.ops" + private val opsReadKey = s"$opsKey.read" private val opsWriteKey = s"$opsKey.write" logger.warn("Metrics wrapper is enabled !") @@ -693,7 +693,10 @@ class RedisLikeMetricsWrapper(redis: RedisLike, val env: Env) extends RedisLike } } -class SwappableRedisLikeMetricsWrapper(redis: RedisLike with SwappableRedis, val env: Env) extends RedisLike with MetricsWrapper with SwappableRedis { +class SwappableRedisLikeMetricsWrapper(redis: RedisLike with SwappableRedis, val env: Env) + extends RedisLike + with MetricsWrapper + with SwappableRedis { private val incropt = new IncrOptimizer(200, 10000) @@ -719,20 +722,20 @@ class SwappableRedisLikeMetricsWrapper(redis: RedisLike with SwappableRedis, val redis.mget(keys: _*) } override def set( - key: String, - value: String, - exSeconds: Option[Long] = None, - pxMilliseconds: Option[Long] = None - ): Future[Boolean] = { + key: String, + value: String, + exSeconds: Option[Long] = None, + pxMilliseconds: Option[Long] = None + ): Future[Boolean] = { countWrite(key, "set") redis.set(key, value, exSeconds, pxMilliseconds) } override def setBS( - key: String, - value: ByteString, - exSeconds: Option[Long] = None, - pxMilliseconds: Option[Long] = None - ): Future[Boolean] = { + key: String, + value: ByteString, + exSeconds: Option[Long] = None, + pxMilliseconds: Option[Long] = None + ): Future[Boolean] = { countWrite(key, "set") redis.setBS(key, value, exSeconds, pxMilliseconds) } @@ -849,8 +852,8 @@ class SwappableRedisLikeMetricsWrapper(redis: RedisLike with SwappableRedis, val redis.scard(key) } override def setnxBS(key: String, value: ByteString, ttl: Option[Long])(implicit - ec: ExecutionContext, - env: Env + ec: ExecutionContext, + env: Env ): Future[Boolean] = { countWrite(key, "setnx") redis.setnxBS(key, value, ttl) @@ -859,11 +862,18 @@ class SwappableRedisLikeMetricsWrapper(redis: RedisLike with SwappableRedis, val override def swap(memory: Memory, strategy: SwapStrategy): Unit = redis.swap(memory, strategy) } -case class IncrOptimizerItem(ops: Int, time: Int, last: AtomicLong, incr: AtomicLong, current: AtomicLong, curOps: AtomicInteger) { +case class IncrOptimizerItem( + ops: Int, + time: Int, + last: AtomicLong, + incr: AtomicLong, + current: AtomicLong, + curOps: AtomicInteger +) { def setCurrent(value: Long): Unit = current.set(value) def incrBy(increment: Long)(f: Long => Future[Long])(implicit ec: ExecutionContext): Future[Long] = { - val elapsed = (System.currentTimeMillis() - last.get()) - val tooMuchOps = curOps.incrementAndGet() > ops + val elapsed = System.currentTimeMillis() - last.get() + val tooMuchOps = curOps.incrementAndGet() > ops val tooMuchTime = elapsed > time if (tooMuchOps || tooMuchTime) { val total = incr.get() + increment @@ -886,15 +896,23 @@ class IncrOptimizer(ops: Int, time: Int) { private val cache = new TrieMap[String, IncrOptimizerItem]() def incrBy(key: String, increment: Long)(f: Long => Future[Long])(implicit ec: ExecutionContext): Future[Long] = { cache.get(key) match { - case None => f(increment).map { r => - val item = IncrOptimizerItem(ops, time, new AtomicLong(System.currentTimeMillis()), new AtomicLong(0L), new AtomicLong(r), new AtomicInteger(0)) - cache.putIfAbsent(key, item) match { - case None => - cache.get(key).foreach(i => i.setCurrent(r)) // when already there ....not sure about it ! - r - case Some(_) => r + case None => + f(increment).map { r => + val item = IncrOptimizerItem( + ops, + time, + new AtomicLong(System.currentTimeMillis()), + new AtomicLong(0L), + new AtomicLong(r), + new AtomicInteger(0) + ) + cache.putIfAbsent(key, item) match { + case None => + cache.get(key).foreach(i => i.setCurrent(r)) // when already there ....not sure about it ! + r + case Some(_) => r + } } - } case Some(item) => item.incrBy(increment)(f) } } diff --git a/otoroshi/app/storage/stores/KvAuthConfigsDataStore.scala b/otoroshi/app/storage/stores/KvAuthConfigsDataStore.scala index ed69e6ca35..03e7b4456e 100644 --- a/otoroshi/app/storage/stores/KvAuthConfigsDataStore.scala +++ b/otoroshi/app/storage/stores/KvAuthConfigsDataStore.scala @@ -17,7 +17,7 @@ class KvAuthConfigsDataStore(redisCli: RedisLike, _env: Env) override def redisLike(implicit env: Env): RedisLike = redisCli override def fmt: Format[AuthModuleConfig] = AuthModuleConfig._fmt - override def key(id: String): String = s"${_env.storageRoot}:auth:configs:${id}" + override def key(id: String): String = s"${_env.storageRoot}:auth:configs:${id}" override def extractId(value: AuthModuleConfig): String = value.id override def generateLoginToken( diff --git a/otoroshi/app/tcp/tcp.scala b/otoroshi/app/tcp/tcp.scala index 7a27e2c16c..1df34cc279 100644 --- a/otoroshi/app/tcp/tcp.scala +++ b/otoroshi/app/tcp/tcp.scala @@ -1012,7 +1012,7 @@ class KvTcpServiceDataStoreDataStore(redisCli: RedisLike, env: Env) override def fmt: Format[TcpService] = TcpService.fmt override def redisLike(implicit env: Env): RedisLike = redisCli - override def key(id: String): String = s"${env.storageRoot}:tcp:services:$id" + override def key(id: String): String = s"${env.storageRoot}:tcp:services:$id" override def extractId(value: TcpService): String = value.id } diff --git a/otoroshi/app/utils/httpclient.scala b/otoroshi/app/utils/httpclient.scala index 3a75701ef6..ead46153fc 100644 --- a/otoroshi/app/utils/httpclient.scala +++ b/otoroshi/app/utils/httpclient.scala @@ -1298,25 +1298,28 @@ case class AkkaWsClientRequest( } .flatMap { // case ParsingResult.Ok(header, _) => Option(header.asInstanceOf[`Content-Type`].contentType) - case ParsingResult.Ok(header, _) => header match { - case `Content-Type`(contentType) => contentType.some - case RawHeader(_, value) if value.contains(",") => value.split(",").headOption.map(_.trim).map(v => `Content-Type`.parseFromValueString(v)) match { - case Some(Left(errs)) => { - ClientConfig.logger.error(s"Error while parsing request content-type: ${errs}") - None - } - case Some(Right(`Content-Type`(contentType))) => contentType.some - case None => None - } - case RawHeader(_, value) => `Content-Type`.parseFromValueString(value) match { - case Left(errs) => { - ClientConfig.logger.error(s"Error while parsing request content-type: ${errs}") - None - } - case Right(`Content-Type`(contentType) ) => contentType.some + case ParsingResult.Ok(header, _) => + header match { + case `Content-Type`(contentType) => contentType.some + case RawHeader(_, value) if value.contains(",") => + value.split(",").headOption.map(_.trim).map(v => `Content-Type`.parseFromValueString(v)) match { + case Some(Left(errs)) => { + ClientConfig.logger.error(s"Error while parsing request content-type: ${errs}") + None + } + case Some(Right(`Content-Type`(contentType))) => contentType.some + case None => None + } + case RawHeader(_, value) => + `Content-Type`.parseFromValueString(value) match { + case Left(errs) => { + ClientConfig.logger.error(s"Error while parsing request content-type: ${errs}") + None + } + case Right(`Content-Type`(contentType)) => contentType.some + } + case _ => None } - case _ => None - } case _ => None } } diff --git a/otoroshi/conf/schemas/openapi-cfg.json b/otoroshi/conf/schemas/openapi-cfg.json index 80e2ed7b26..75976ede18 100644 --- a/otoroshi/conf/schemas/openapi-cfg.json +++ b/otoroshi/conf/schemas/openapi-cfg.json @@ -65,26 +65,15 @@ "entity_description.otoroshi.events.PulsarConfig": "Settings for connection to a pulsar cluster", "entity_description.otoroshi.events.StatsdConfig": "Settings for connection to a statsd agent", "entity_description.otoroshi.gateway.CircuitBreakersHolder": "Internal component to hold current circuit breakers and their configuration", - "entity_description.otoroshi.models.ApiDescriptor": "Represent if a service exposes an API with an optional url to an openapi descriptor", "entity_description.otoroshi.models.ApiKey": "An otoroshi apikey that can allow you to access some services", - "entity_description.otoroshi.models.ApiKeyConstraints": "Settings used to extract apikeys from http requests and routing traffic", "entity_description.otoroshi.models.ApiKeyRotation": "Settings for automatic apikey rotation with grace period", - "entity_description.otoroshi.models.ApiKeyRouteMatcher": "Routing settings based on apikeys metadata and tags", "entity_description.otoroshi.models.AutoCert": "Settings to generate certificates on the fly", "entity_description.otoroshi.models.BackOfficeUser": "User session for otoroshi-ui admins", "entity_description.otoroshi.models.BadResponse": "Settings for a bad response return (chaos engineering)", "entity_description.otoroshi.models.BadResponsesFaultConfig": "List of bad response settings", - "entity_description.otoroshi.models.BasicAuthConstraints": "Settings to extract apikey from a basic auth header like", - "entity_description.otoroshi.models.Canary": "Settings for canary routing", "entity_description.otoroshi.models.ChaosConfig": "Settings to enable chaos engineering for a service", "entity_description.otoroshi.models.CleverCloudSettings": "Settings for connection to the clever-cloud api", - "entity_description.otoroshi.models.ClientConfig": "Settings for the http client when http request is forwarded", - "entity_description.otoroshi.models.ClientIdAuthConstraints": "Settings to extract apikey (using client_id only) from a header or query param", "entity_description.otoroshi.models.ConsoleSettings": "Settings to export to console", - "entity_description.otoroshi.models.CorsSettings": "Settings for CORS support", - "entity_description.otoroshi.models.CustomHeadersAuthConstraints": "Settings to extract apikey from a custom headers", - "entity_description.otoroshi.models.CustomTimeouts": "Settings for custom timeouts for a specific path", - "entity_description.otoroshi.models.DataCenterMatch": "Match a target if in the same datacenter", "entity_description.otoroshi.models.DataExporterConfig": "Data exporter settings", "entity_description.otoroshi.models.DataExporterConfigFiltering": "Filter events to export", "entity_description.otoroshi.models.DefaultTemplates": "The template that will be merged with newly created entities", @@ -96,34 +85,26 @@ "entity_description.otoroshi.models.ErrorTemplate": "Service descriptor error template", "entity_description.otoroshi.models.ExporterRef": "Ref to an exporter id", "entity_description.otoroshi.models.FileSettings": "Settings to export to a file", - "entity_description.otoroshi.models.GeoPositionRadius": "Geolocation radius", - "entity_description.otoroshi.models.GeolocationMatch": "Match a target if in the same geo location radius", "entity_description.otoroshi.models.GlobalConfig": "The global config (dynamic) for otoroshi", "entity_description.otoroshi.models.GlobalJwtVerifier": "Otoroshi model for JWT token verifier", "entity_description.otoroshi.models.GlobalScripts": "Settings to apply plugins globally", "entity_description.otoroshi.models.GoReplayFileSettings": "Settings to export to a file", "entity_description.otoroshi.models.GoReplayS3Settings": "Settings to export to a S3 bucket", "entity_description.otoroshi.models.HSAlgoSettings": "Settings to use HMAC-SHA signing algorithm", - "entity_description.otoroshi.models.HealthCheck": "Healthcheck settings for a service", "entity_description.otoroshi.models.InCookie": "JWT token location (cookie)", "entity_description.otoroshi.models.InHeader": "JWT token location (header)", "entity_description.otoroshi.models.InQueryParam": "JWT token location (query param)", "entity_description.otoroshi.models.IndexSettings": "Elasticseach indexation settings", "entity_description.otoroshi.models.IndexSettingsInterval": "Elasticseach indexation interval", - "entity_description.otoroshi.models.InfraProviderMatch": "Match a target if in the same infrastructure", - "entity_description.otoroshi.models.IpFiltering": "Settings for ip address filtering for a service or globally", "entity_description.otoroshi.models.IpStackGeolocationSettings": "Settings for connection to IpStack", "entity_description.otoroshi.models.JWKSAlgoSettings": "Settings to use keypair from JWKS for verification", - "entity_description.otoroshi.models.JwtAuthConstraints": "Settings to extract apikey from a jwt token", "entity_description.otoroshi.models.KidAlgoSettings": "Settings to find keypair based on header kid for verification", "entity_description.otoroshi.models.LargeRequestFaultConfig": "Settings for a large request fault (chaos engineering)", "entity_description.otoroshi.models.LargeResponseFaultConfig": "Settings for a large response fault (chaos engineering)", "entity_description.otoroshi.models.LatencyInjectionFaultConfig": "Settings for a latency injection fault (chaos engineering)", - "entity_description.otoroshi.models.LocalJwtVerifier": "Local jwt verifier (deprecated)", "entity_description.otoroshi.models.MappingSettings": "Settings to transform a jwt token", "entity_description.otoroshi.models.MaxmindGeolocationSettings": "Settings for connection to a maxmind db", "entity_description.otoroshi.models.MetricsSettings": "Settings to export to otoroshi metrics", - "entity_description.otoroshi.models.NetworkLocationMatch": "Match a target if in the same network location", "entity_description.otoroshi.models.Outage": "A snowmonkey outage model", "entity_description.otoroshi.models.PassThrough": "jwt token validation policicy that just validate the token", "entity_description.otoroshi.models.PrivateAppsUser": "User session for private apps", @@ -131,21 +112,12 @@ "entity_description.otoroshi.models.QuotasAlmostExceededSettings": "Settings for apikey quotas alerts", "entity_description.otoroshi.models.RSAKPAlgoSettings": "Settings to use RSA signing algorithm from a certificate keypair", "entity_description.otoroshi.models.RSAlgoSettings": "Settings to use RSA signing algorithm", - "entity_description.otoroshi.models.RackMatch": "Match a target if in the same rack", - "entity_description.otoroshi.models.RedirectionSettings": "Settings for routing redirection", - "entity_description.otoroshi.models.RefJwtVerifier": "Reference to a jwt verifier", - "entity_description.otoroshi.models.RegionMatch": "Match a target if in the same region", "entity_description.otoroshi.models.RemainingQuotas": "Remaining quotas for an apikey", - "entity_description.otoroshi.models.RestrictionPath": "Represent an http request on which restrictions will apply", - "entity_description.otoroshi.models.Restrictions": "Http requests restrictions for a service or an apikey", "entity_description.otoroshi.models.S3ExporterSettings": "Settings to export to a S3 bucket", - "entity_description.otoroshi.models.SecComHeaders": "Header names for the otoroshi exchange protocol", - "entity_description.otoroshi.models.ServiceDescriptor": "The otoroshi model for a service (handles routing)", "entity_description.otoroshi.models.ServiceGroup": "The otoroshi model for a group of services", "entity_description.otoroshi.models.Sign": "jwt token re-sign policy settings", "entity_description.otoroshi.models.SimpleOtoroshiAdmin": "An otoroshi admin user", "entity_description.otoroshi.models.SnowMonkeyConfig": "Settings for the snow monkey (chaos engineering)", - "entity_description.otoroshi.models.Target": "A target model for a service (destination for forwarded requests)", "entity_description.otoroshi.models.Team": "An otoroshi model for a team of users in the organization (otoroshi-ui)", "entity_description.otoroshi.models.TeamAccess": "Access rights for teams", "entity_description.otoroshi.models.Tenant": "An otoroshi model for an organization (otoroshi-ui)", @@ -159,8 +131,6 @@ "entity_description.otoroshi.models.VerificationSettings": "jwt token verification settings", "entity_description.otoroshi.models.WebAuthnOtoroshiAdmin": "An otoroshi admin user that uses webauthn at login", "entity_description.otoroshi.models.Webhook": "Settings for webhook call", - "entity_description.otoroshi.models.WeightedBestResponseTime": "Loadbalancing policy that route to best response time targets with a weight", - "entity_description.otoroshi.models.ZoneMatch": "Match a target if in the same zone", "entity_description.otoroshi.next.models.GraphQLFormats": "???", "entity_description.otoroshi.next.models.NgBackend": "A backend representation with it's targets, load balancing and general settings", "entity_description.otoroshi.next.models.NgCacheConnectionSettings": "The settings for http cached connection at host level", @@ -442,9 +412,7 @@ "entity_description.otoroshi.plugins.workflow.WorkflowEndpoint": "Experimental plugin", "entity_description.otoroshi.plugins.workflow.WorkflowJob": "Experimental plugin", "entity_description.otoroshi.script.AccessContext": "Context for AccessValidation plugins", - "entity_description.otoroshi.script.AccessValidatorRef": "References to access validation plugins", "entity_description.otoroshi.script.PreRoutingContext": "Context for preroutes plugins", - "entity_description.otoroshi.script.PreRoutingRef": "References to pre-routing plugins", "entity_description.otoroshi.script.Script": "An otoroshi plugins stored as scala code in the otoroshi datastore", "entity_description.otoroshi.script.plugins.Plugins": "Settings for plugins (of any kind)", "entity_description.otoroshi.ssl.Cert": "The otoroshi model for X509 certificates", @@ -462,8 +430,6 @@ "entity_description.otoroshi.utils.ConcurrentMutableTypedMap": "A concurrent map with typed keys", "entity_description.otoroshi.utils.JsonPathValidator": "Validator based on JsonPath", "entity_description.otoroshi.utils.TypedMap": "A map with typed keys", - "entity_description.otoroshi.utils.gzip.GzipConfig": "Settings for gzip support", - "entity_description.otoroshi.utils.http.CacheConnectionSettings": "???", "entity_description.otoroshi.utils.http.MtlsConfig": "???", "entity_description.otoroshi.utils.letsencrypt.LetsEncryptSettings": "Settings for connection to a let's encrypt (or ACME) server", "entity_description.otoroshi.utils.mailer.ConsoleMailerSettings": "Settings for the console mailer", @@ -1347,8 +1313,6 @@ "otoroshi.events.StatsdConfig.datadog": "Datadog agent", "otoroshi.events.StatsdConfig.host": "The host of the StatsD agent", "otoroshi.events.StatsdConfig.port": "The port of the StatsD agent", - "otoroshi.models.ApiDescriptor.exposeApi": "Is this an API", - "otoroshi.models.ApiDescriptor.openApiDescriptorUrl": "openapi descriptor url", "otoroshi.models.ApiKey.allowClientIdOnly": "This apikey can be used juste with the client_id value", "otoroshi.models.ApiKey.authorizedEntities": "The group/service ids (prefixed by group_ or service_ on which the key is authorized", "otoroshi.models.ApiKey.clientId": "The unique id of the Api Key. Usually 16 random alpha numerical characters, but can be anything", @@ -1367,24 +1331,10 @@ "otoroshi.models.ApiKey.tags": "Apikey tags", "otoroshi.models.ApiKey.throttlingQuota": "Authorized number of calls per second, measured on 10 seconds", "otoroshi.models.ApiKey.validUntil": "Date until when the apikey is valid", - "otoroshi.models.ApiKeyConstraints.basicAuth": "Settings to extract basic auth style apikey", - "otoroshi.models.ApiKeyConstraints.clientIdAuth": "Settings to extract client_id only apikey", - "otoroshi.models.ApiKeyConstraints.customHeadersAuth": "Settings to extract apikey from custom headers", - "otoroshi.models.ApiKeyConstraints.jwtAuth": "Settings to extract apikey from jwt token", - "otoroshi.models.ApiKeyConstraints.routing": "Routing settings for this apikey", "otoroshi.models.ApiKeyRotation.enabled": "Rotation enabled", "otoroshi.models.ApiKeyRotation.gracePeriod": "period (in hours) during which both secrets works", "otoroshi.models.ApiKeyRotation.nextSecret": "Next client_secret value", "otoroshi.models.ApiKeyRotation.rotationEvery": "Rotate every n hours", - "otoroshi.models.ApiKeyRouteMatcher.allMetaIn": "Routing if all meta presents in apikey", - "otoroshi.models.ApiKeyRouteMatcher.allMetaKeysIn": "Routing if all meta keys presents in apikey", - "otoroshi.models.ApiKeyRouteMatcher.allTagsIn": "Routing if all tags presents in apikey", - "otoroshi.models.ApiKeyRouteMatcher.noneMetaIn": "Routing if none meta presents in apikey", - "otoroshi.models.ApiKeyRouteMatcher.noneMetaKeysIn": "Routing if none meta keys presents in apikey", - "otoroshi.models.ApiKeyRouteMatcher.noneTagIn": "Routing if none tags presents in apikey", - "otoroshi.models.ApiKeyRouteMatcher.oneMetaIn": "Routing if one meta presents in apikey", - "otoroshi.models.ApiKeyRouteMatcher.oneMetaKeyIn": "Routing if one meta key presents in apikey", - "otoroshi.models.ApiKeyRouteMatcher.oneTagIn": "outing if one tag presents in apikey", "otoroshi.models.AutoCert.allowed": "allowed domains", "otoroshi.models.AutoCert.caRef": "Generate cert from the following CA", "otoroshi.models.AutoCert.enabled": "Enable auto cert", @@ -1409,13 +1359,6 @@ "otoroshi.models.BadResponse.status": "The HTTP status for the response", "otoroshi.models.BadResponsesFaultConfig.ratio": "The percentage of requests affected by this fault. Value should be between 0.0 and 1.0", "otoroshi.models.BadResponsesFaultConfig.responses": "The possibles responses", - "otoroshi.models.BasicAuthConstraints.enabled": "Constraint enabled", - "otoroshi.models.BasicAuthConstraints.headerName": "Header name to get client_id:client_secret base64 encoded", - "otoroshi.models.BasicAuthConstraints.queryName": "Query param name to get client_id:client_secret base64 encoded", - "otoroshi.models.Canary.enabled": "Use canary mode for this service", - "otoroshi.models.Canary.root": "Otoroshi will append this root to any target choosen. If the specified root is '/api/foo', then a request to https://yyyyyyy/bar will actually hit https://xxxxxxxxx/api/foo/bar", - "otoroshi.models.Canary.targets": "The list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures", - "otoroshi.models.Canary.traffic": "Ratio of traffic that will be sent to canary targets.", "otoroshi.models.ChaosConfig.badResponsesFaultConfig": "Settings for bad responses", "otoroshi.models.ChaosConfig.enabled": "Whether or not this config is enabled", "otoroshi.models.ChaosConfig.largeRequestFaultConfig": "Settings for large requests", @@ -1426,41 +1369,6 @@ "otoroshi.models.CleverCloudSettings.orgaId": "Clever-Cloud organization id", "otoroshi.models.CleverCloudSettings.secret": "Clever-Cloud oauth secret", "otoroshi.models.CleverCloudSettings.token": "Clever-Cloud oauth token", - "otoroshi.models.ClientConfig.backoffFactor": "Specify the factor to multiply the delay for each retry", - "otoroshi.models.ClientConfig.cacheConnectionSettings": "Cached connection settings", - "otoroshi.models.ClientConfig.callAndStreamTimeout": "Specify how long each call should last at most in milliseconds (hard timeout, connection will be closed after that duration)", - "otoroshi.models.ClientConfig.callTimeout": "Specify how long each call should last at most in milliseconds (soft timeout as it's enforced by the circuit breaker)", - "otoroshi.models.ClientConfig.connectionTimeout": "Timeout at connection", - "otoroshi.models.ClientConfig.customTimeouts": "Custom timeouts per path", - "otoroshi.models.ClientConfig.globalTimeout": "Specify how long the global call (with retries) should last at most in milliseconds", - "otoroshi.models.ClientConfig.idleTimeout": "Timeout on idle connection", - "otoroshi.models.ClientConfig.maxErrors": "Specify how many errors can pass before opening the circuit breaker", - "otoroshi.models.ClientConfig.proxy": "Web proxy settings for http client", - "otoroshi.models.ClientConfig.retries": "Specify how many times the client will try to fetch the result of the request after an error before giving up.", - "otoroshi.models.ClientConfig.retryInitialDelay": "Specify the delay between two retries. Each retry, the delay is multiplied by the backoff factor", - "otoroshi.models.ClientConfig.sampleInterval": "Specify the sliding window time for the circuit breaker in milliseconds, after this time, error count will be reseted", - "otoroshi.models.ClientConfig.useCircuitBreaker": "Use a circuit breaker to avoid cascading failure when calling chains of services. Highly recommended !", - "otoroshi.models.ClientIdAuthConstraints.enabled": "Constraint enabled", - "otoroshi.models.ClientIdAuthConstraints.headerName": "Header name to find client_id", - "otoroshi.models.ClientIdAuthConstraints.queryName": "Query param name to find client_id", - "otoroshi.models.CorsSettings.allowCredentials": "Allow to pass credentials", - "otoroshi.models.CorsSettings.allowHeaders": "The cors allowed headers", - "otoroshi.models.CorsSettings.allowMethods": "The cors allowed methods", - "otoroshi.models.CorsSettings.allowOrigin": "The cors allowed origin", - "otoroshi.models.CorsSettings.enabled": "Whether or not cors is enabled", - "otoroshi.models.CorsSettings.excludedPatterns": "The cors excluded patterns", - "otoroshi.models.CorsSettings.exposeHeaders": "The cors exposed header", - "otoroshi.models.CorsSettings.maxAge": "Cors max age", - "otoroshi.models.CustomHeadersAuthConstraints.clientIdHeaderName": "Header name to find client_id", - "otoroshi.models.CustomHeadersAuthConstraints.clientSecretHeaderName": "Header name to find client_secret", - "otoroshi.models.CustomHeadersAuthConstraints.enabled": "Constraint enabled", - "otoroshi.models.CustomTimeouts.callAndStreamTimeout": "Specify how long each call should last at most in milliseconds (hard timeout, connection will be closed after that duration)", - "otoroshi.models.CustomTimeouts.callTimeout": "Specify how long each call should last at most in milliseconds (soft timeout as it's enforced by the circuit breaker)", - "otoroshi.models.CustomTimeouts.connectionTimeout": "Timeout at connection", - "otoroshi.models.CustomTimeouts.globalTimeout": "Specify how long the global call (with retries) should last at most in milliseconds", - "otoroshi.models.CustomTimeouts.idleTimeout": "Timeout on idle connection", - "otoroshi.models.CustomTimeouts.path": "path on which this configuration works", - "otoroshi.models.DataCenterMatch.dc": "DC name", "otoroshi.models.DataExporterConfig.bufferSize": "Number of events in buffer", "otoroshi.models.DataExporterConfig.config": "Exporter config", "otoroshi.models.DataExporterConfig.desc": "Description", @@ -1525,10 +1433,6 @@ "otoroshi.models.ExporterRef.ref": "Reference to an exporter", "otoroshi.models.FileSettings.maxFileSize": "Max file size for rolling", "otoroshi.models.FileSettings.path": "File path", - "otoroshi.models.GeoPositionRadius.latitude": "Latitude of the position", - "otoroshi.models.GeoPositionRadius.longitude": "Longitude of the position", - "otoroshi.models.GeoPositionRadius.radius": "Radius of the circle in meters", - "otoroshi.models.GeolocationMatch.positions": "Possible positions", "otoroshi.models.GlobalConfig.alertsEmails": "Email addresses that will receive all Otoroshi alert events", "otoroshi.models.GlobalConfig.alertsWebhooks": "Webhook that will receive all Otoroshi alert events", "otoroshi.models.GlobalConfig.analyticsWebhooks": "Webhook that will receive all internal Otoroshi events", @@ -1612,17 +1516,12 @@ "otoroshi.models.HSAlgoSettings.base64": "The secret is base64 encoded", "otoroshi.models.HSAlgoSettings.secret": "HMAC secret", "otoroshi.models.HSAlgoSettings.size": "Size for SHA function", - "otoroshi.models.HealthCheck.enabled": "Whether or not healthcheck is enabled on the current service descriptor", - "otoroshi.models.HealthCheck.url": "The URL to check", "otoroshi.models.InCookie.name": "Cookie name", "otoroshi.models.InHeader.name": "Header name", "otoroshi.models.InHeader.remove": "Remove from value", "otoroshi.models.InQueryParam.name": "Query param name", "otoroshi.models.IndexSettings.clientSide": "Enable index splitting on client side", "otoroshi.models.IndexSettings.interval": "Index splitting interval", - "otoroshi.models.InfraProviderMatch.provider": "provider name", - "otoroshi.models.IpFiltering.blacklist": "Blacklisted IP addresses", - "otoroshi.models.IpFiltering.whitelist": "Whitelisted IP addresses", "otoroshi.models.IpStackGeolocationSettings.apikey": "IpStack apikey", "otoroshi.models.IpStackGeolocationSettings.enabled": "enable ipstack geolocation", "otoroshi.models.IpStackGeolocationSettings.timeout": "API call timeout", @@ -1633,14 +1532,6 @@ "otoroshi.models.JWKSAlgoSettings.tlsConfig": "TLS config", "otoroshi.models.JWKSAlgoSettings.ttl": "Cache ttl", "otoroshi.models.JWKSAlgoSettings.url": "JWKS url", - "otoroshi.models.JwtAuthConstraints.cookieName": "Cookie name to extract jwt token", - "otoroshi.models.JwtAuthConstraints.enabled": "Constraint enabled", - "otoroshi.models.JwtAuthConstraints.headerName": "Header name to extract jwt token", - "otoroshi.models.JwtAuthConstraints.includeRequestAttributes": "Jwt token should include verb and path", - "otoroshi.models.JwtAuthConstraints.keyPairSigned": "The jwt token is signed by a keypair from a cert found from its id in apikey meta. 'jwt-sign-keypair'", - "otoroshi.models.JwtAuthConstraints.maxJwtLifespanSecs": "Check if token does not have a long lifespan", - "otoroshi.models.JwtAuthConstraints.queryName": "Query param name to extract jwt token", - "otoroshi.models.JwtAuthConstraints.secretSigned": "Jwt token signed with the client_secret", "otoroshi.models.KidAlgoSettings.onlyExposedCerts": "Use only exposed certs", "otoroshi.models.LargeRequestFaultConfig.additionalRequestSize": "The size added to the request body in bytes. Added payload will be spaces only.", "otoroshi.models.LargeRequestFaultConfig.ratio": "The percentage of requests affected by this fault. Value should be between 0.0 and 1.0", @@ -1649,23 +1540,12 @@ "otoroshi.models.LatencyInjectionFaultConfig.from": "The start range of latency added to the request", "otoroshi.models.LatencyInjectionFaultConfig.ratio": "The percentage of requests affected by this fault. Value should be between 0.0 and 1.0", "otoroshi.models.LatencyInjectionFaultConfig.to": "The end range of latency added to the request", - "otoroshi.models.LocalJwtVerifier.algoSettings": "Algo settings", - "otoroshi.models.LocalJwtVerifier.enabled": "Verifier enabled", - "otoroshi.models.LocalJwtVerifier.excludedPatterns": "Verifier excluded paths", - "otoroshi.models.LocalJwtVerifier.source": "Token source", - "otoroshi.models.LocalJwtVerifier.strategy": "Token strategy", - "otoroshi.models.LocalJwtVerifier.strict": "Strict token verification", "otoroshi.models.MappingSettings.map": "Change values", "otoroshi.models.MappingSettings.remove": "Remove some token claims", "otoroshi.models.MappingSettings.values": "Add values", "otoroshi.models.MaxmindGeolocationSettings.enabled": "Geolocation using maxmind db enabled", "otoroshi.models.MaxmindGeolocationSettings.path": "Maxmlind db file path", "otoroshi.models.MetricsSettings.labels": "Exported labels for prometheus", - "otoroshi.models.NetworkLocationMatch.dataCenter": "Datacenter name", - "otoroshi.models.NetworkLocationMatch.provider": "Provider name", - "otoroshi.models.NetworkLocationMatch.rack": "Rack name", - "otoroshi.models.NetworkLocationMatch.region": "Region name", - "otoroshi.models.NetworkLocationMatch.zone": "Zone name", "otoroshi.models.Outage.descriptorId": "Service descriptor id", "otoroshi.models.Outage.descriptorName": "Service descriptor name", "otoroshi.models.Outage.duration": "Outage duration", @@ -1702,14 +1582,6 @@ "otoroshi.models.RSAlgoSettings.privateKey": "Private key (for signing)", "otoroshi.models.RSAlgoSettings.publicKey": "Public key (for verification)", "otoroshi.models.RSAlgoSettings.size": "SHA function size", - "otoroshi.models.RackMatch.rack": "Rack name", - "otoroshi.models.RedirectionSettings.code": "The http redirect code", - "otoroshi.models.RedirectionSettings.enabled": "Whether or not redirection is enabled", - "otoroshi.models.RedirectionSettings.to": "The location for redirection", - "otoroshi.models.RefJwtVerifier.enabled": "Verifier enabled", - "otoroshi.models.RefJwtVerifier.excludedPatterns": "Verifier excluded paths", - "otoroshi.models.RefJwtVerifier.ids": "Verifiers ids", - "otoroshi.models.RegionMatch.region": "Region name", "otoroshi.models.RemainingQuotas.authorizedCallsPerDay": "Number of authorized call per day", "otoroshi.models.RemainingQuotas.authorizedCallsPerMonth": "Number of authorized call per month", "otoroshi.models.RemainingQuotas.authorizedCallsPerSec": "Number of authorized call per second", @@ -1719,102 +1591,8 @@ "otoroshi.models.RemainingQuotas.remainingCallsPerDay": "Remaining number of call per day", "otoroshi.models.RemainingQuotas.remainingCallsPerMonth": "Remaining number of call per month", "otoroshi.models.RemainingQuotas.remainingCallsPerSec": "Remaining number of call per second", - "otoroshi.models.RestrictionPath.method": "Method of the http request", - "otoroshi.models.RestrictionPath.path": "Path of the http request", - "otoroshi.models.Restrictions.allowLast": "Evalute allowed paths after everything else", - "otoroshi.models.Restrictions.allowed": "Allowed paths", - "otoroshi.models.Restrictions.enabled": "Restrictions enabled", - "otoroshi.models.Restrictions.forbidden": "Forbidden paths (return 403)", - "otoroshi.models.Restrictions.notFound": "Not found paths (return 404)", "otoroshi.models.S3ExporterSettings.config": "Exporter settings", "otoroshi.models.S3ExporterSettings.maxFileSize": "Max file size for rolling", - "otoroshi.models.SecComHeaders.claimRequestName": "Header name where the info token will be", - "otoroshi.models.SecComHeaders.stateRequestName": "Header name where the validation token will be", - "otoroshi.models.SecComHeaders.stateResponseName": "Header name where the validation token respondewill be", - "otoroshi.models.ServiceDescriptor.accessValidator": "Service access validatiors", - "otoroshi.models.ServiceDescriptor.additionalHeaders": "Specify headers that will be added to each client request. Useful to add authentication", - "otoroshi.models.ServiceDescriptor.additionalHeadersOut": "Specify headers that will be added to each client response", - "otoroshi.models.ServiceDescriptor.allowHttp10": "Allow HTTP/1.0 requests", - "otoroshi.models.ServiceDescriptor.api": "Api exposition settings", - "otoroshi.models.ServiceDescriptor.apiKeyConstraints": "Routing and extraction constraints for the apikeyh", - "otoroshi.models.ServiceDescriptor.authConfigRef": "A reference to a global auth module config", - "otoroshi.models.ServiceDescriptor.buildMode": "Display a construction page when a user try to use the service", - "otoroshi.models.ServiceDescriptor.canary": "Canary settings", - "otoroshi.models.ServiceDescriptor.chaosConfig": "Chaos engineering settings", - "otoroshi.models.ServiceDescriptor.clientConfig": "Http client settings", - "otoroshi.models.ServiceDescriptor.clientValidatorRef": "A reference to validation authority", - "otoroshi.models.ServiceDescriptor.cors": "CORS settings", - "otoroshi.models.ServiceDescriptor.description": "Entity description", - "otoroshi.models.ServiceDescriptor.detectApiKeySooner": "Detect if an apikey is present but do not fail if not", - "otoroshi.models.ServiceDescriptor.domain": "The domain on which the service is available.", - "otoroshi.models.ServiceDescriptor.enabled": "Activate or deactivate your service. Once disabled, users will get an error page saying the service does not exist", - "otoroshi.models.ServiceDescriptor.enforceSecureCommunication": "When enabled, Otoroshi will try to exchange headers with backend service to ensure no one else can use the service from outside", - "otoroshi.models.ServiceDescriptor.env": "The line on which the service is available. Based on that value, the name of the line will be appended to the subdomain. For line prod, nothing will be appended. For example, if the subdomain is 'foo' and line is 'preprod', then the exposed service will be available at 'foo.preprod.mydomain'", - "otoroshi.models.ServiceDescriptor.forceHttps": "Will force redirection to https:// if not present", - "otoroshi.models.ServiceDescriptor.groups": "Each service descriptor is attached to groups. A group can have one or more services. Each API key is linked to a group and allow access to every service in the group", - "otoroshi.models.ServiceDescriptor.gzip": "GZIP settings", - "otoroshi.models.ServiceDescriptor.handleLegacyDomain": "Use 'domain', 'subdomain', 'env' and 'matchingRoot' for routing in addition to hosts, or just use hosts.", - "otoroshi.models.ServiceDescriptor.headersVerification": "Specify headers that will be verified after routing.", - "otoroshi.models.ServiceDescriptor.healthCheck": "Healthcheck settings", - "otoroshi.models.ServiceDescriptor.hosts": "Possible hosts for the service", - "otoroshi.models.ServiceDescriptor.id": "A unique random string to identify your service", - "otoroshi.models.ServiceDescriptor.ipFiltering": "Ip filtering settings", - "otoroshi.models.ServiceDescriptor.issueCert": "Flag to automatically issue a cert for this service", - "otoroshi.models.ServiceDescriptor.issueCertCA": "CA for cert issuance", - "otoroshi.models.ServiceDescriptor.jwtVerifier": "JWT verifiers settings", - "otoroshi.models.ServiceDescriptor.letsEncrypt": "Flag to automatically issue a let's encrypt (ACME) cert for this service", - "otoroshi.models.ServiceDescriptor.localHost": "The host used localy, mainly localhost:xxxx", - "otoroshi.models.ServiceDescriptor.localScheme": "The scheme used localy, mainly http", - "otoroshi.models.ServiceDescriptor.location": "Entity location", - "otoroshi.models.ServiceDescriptor.logAnalyticsOnServer": "Log analytics event on the server", - "otoroshi.models.ServiceDescriptor.maintenanceMode": "Display a maintainance page when a user try to use the service", - "otoroshi.models.ServiceDescriptor.matchingHeaders": "Specify headers that MUST be present on client request to route it. Useful to implement versioning", - "otoroshi.models.ServiceDescriptor.matchingRoot": "The root path on which the service is available", - "otoroshi.models.ServiceDescriptor.metadata": "Just a bunch of random properties", - "otoroshi.models.ServiceDescriptor.missingOnlyHeadersIn": "Add header on client request if they are not present", - "otoroshi.models.ServiceDescriptor.missingOnlyHeadersOut": "Add header on client response if they are not present", - "otoroshi.models.ServiceDescriptor.name": "The name of your service. Only for debug and human readability purposes", - "otoroshi.models.ServiceDescriptor.overrideHost": "Host header will be overriden with Host of the target", - "otoroshi.models.ServiceDescriptor.paths": "Matching paths on request", - "otoroshi.models.ServiceDescriptor.plugins": "Plugins enabled for this service. will replace separate plugins fields in a near future", - "otoroshi.models.ServiceDescriptor.preRouting": "Pre routing plugin settings", - "otoroshi.models.ServiceDescriptor.privateApp": "When enabled, user will be allowed to use the service (UI) only if they are registered users of the private apps domain", - "otoroshi.models.ServiceDescriptor.privatePatterns": "If you define a public pattern that is a little bit too much, you can make some of public URL private again", - "otoroshi.models.ServiceDescriptor.publicPatterns": "By default, every services are private only and you'll need an API key to access it. However, if you want to expose a public UI, you can define one or more public patterns (regex) to allow access to anybody. For example if you want to allow anybody on any URL, just use '/.*'", - "otoroshi.models.ServiceDescriptor.readOnly": "Service only accepts GET, HEAD and OPTIONS requests", - "otoroshi.models.ServiceDescriptor.redirectToLocal": "If you work locally with Otoroshi, you may want to use that feature to redirect one particuliar service to a local host. For example, you can relocate https://foo.preprod.bar.com to http://localhost:8080 to make some tests", - "otoroshi.models.ServiceDescriptor.redirection": "Redirection settings", - "otoroshi.models.ServiceDescriptor.removeHeadersIn": "Remove headers on client request", - "otoroshi.models.ServiceDescriptor.removeHeadersOut": "Remove headers on client response", - "otoroshi.models.ServiceDescriptor.restrictions": "Restriction settings", - "otoroshi.models.ServiceDescriptor.root": "Otoroshi will append this root to any target choosen. If the specified root is '/api/foo', then a request to https://yyyyyyy/bar will actually hit https://xxxxxxxxx/api/foo/bar", - "otoroshi.models.ServiceDescriptor.secComAlgoChallengeBackToOto": "Algorithm to verify challenge token coming from the backend", - "otoroshi.models.ServiceDescriptor.secComAlgoChallengeOtoToBack": "Algorithm to sign challenge token to the backend", - "otoroshi.models.ServiceDescriptor.secComAlgoInfoToken": "Algorithm to verify/sign challenge token coming from/to the backend", - "otoroshi.models.ServiceDescriptor.secComExcludedPatterns": "URI patterns excluded from secured communications", - "otoroshi.models.ServiceDescriptor.secComHeaders": "Header names for sec. com. protocol", - "otoroshi.models.ServiceDescriptor.secComInfoTokenVersion": "Version of the info token", - "otoroshi.models.ServiceDescriptor.secComSettings": "Sec. com. settings", - "otoroshi.models.ServiceDescriptor.secComTtl": "TTL for the info token", - "otoroshi.models.ServiceDescriptor.secComUseSameAlgo": "Use the same algo for info token, challenge token signing, challenge token verification", - "otoroshi.models.ServiceDescriptor.secComVersion": "Version of the sec. com.", - "otoroshi.models.ServiceDescriptor.securityExcludedPatterns": "Exclude some paths", - "otoroshi.models.ServiceDescriptor.sendInfoToken": "Should otoroshi send info token", - "otoroshi.models.ServiceDescriptor.sendOtoroshiHeadersBack": "When enabled, Otoroshi will send headers to consumer like request id, client latency, overhead, etc ...", - "otoroshi.models.ServiceDescriptor.sendStateChallenge": "Should otoroshi send challenge token", - "otoroshi.models.ServiceDescriptor.strictlyPrivate": "When strictly private, private app session will not pass apikey filters", - "otoroshi.models.ServiceDescriptor.stripPath": "Strip matching path in the forwarded request path", - "otoroshi.models.ServiceDescriptor.subdomain": "The subdomain on which the service is available", - "otoroshi.models.ServiceDescriptor.tags": "Entity tags", - "otoroshi.models.ServiceDescriptor.targets": "The list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures", - "otoroshi.models.ServiceDescriptor.targetsLoadBalancing": "Loadbalancing strategy", - "otoroshi.models.ServiceDescriptor.tcpUdpTunneling": "Enabled TCP/UDP tunneling through websocket connection", - "otoroshi.models.ServiceDescriptor.transformerConfig": "Transformer plugins configuration", - "otoroshi.models.ServiceDescriptor.transformerRefs": "Enabled transformer plugins", - "otoroshi.models.ServiceDescriptor.useAkkaHttpClient": "Use akka http client for this service", - "otoroshi.models.ServiceDescriptor.useNewWSClient": "Use akka http client for this service on websocket calls", - "otoroshi.models.ServiceDescriptor.userFacing": "The fact that this service will be seen by users and cannot be impacted by the Snow Monkey", - "otoroshi.models.ServiceDescriptor.xForwardedHeaders": "Send X-Forwarded-* headers", "otoroshi.models.ServiceGroup.description": "Entity description", "otoroshi.models.ServiceGroup.id": "A unique random string to identify your service", "otoroshi.models.ServiceGroup.location": "Entity location", @@ -1843,15 +1621,6 @@ "otoroshi.models.SnowMonkeyConfig.stopTime": "Stop time of Snow Monkey each day", "otoroshi.models.SnowMonkeyConfig.targetGroups": "Groups impacted by Snow Monkey. If empty, all groups will be impacted", "otoroshi.models.SnowMonkeyConfig.timesPerDay": "Number of time per day each service will be outage", - "otoroshi.models.Target.host": "The host on which the HTTP call will be forwarded. Can be a domain name, or an IP address. Can also have a port", - "otoroshi.models.Target.ipAddress": "Target ip address. Usefull to make manual DNS resolution without breaking SNI", - "otoroshi.models.Target.metadata": "Metadata for this target", - "otoroshi.models.Target.mtlsConfig": "TLS settings to contact this target", - "otoroshi.models.Target.predicate": "Predicate to choose this target", - "otoroshi.models.Target.protocol": "Protocol for the target", - "otoroshi.models.Target.scheme": "The protocol used for communication. Can be http or https", - "otoroshi.models.Target.tags": "Tags for this target", - "otoroshi.models.Target.weight": "The weight of the target when choosing", "otoroshi.models.Team.description": "Entity description", "otoroshi.models.Team.id": "Entity id", "otoroshi.models.Team.metadata": "Entity metadata", @@ -1899,10 +1668,7 @@ "otoroshi.models.Webhook.headers": "Headers to authorize the call or whatever", "otoroshi.models.Webhook.mtlsConfig": "TLS config when calling webhook", "otoroshi.models.Webhook.url": "The URL where events are posted", - "otoroshi.models.WeightedBestResponseTime.ratio": "Weight ratio", - "otoroshi.models.ZoneMatch.zone": "Zone name", "otoroshi.next.models.NgBackend.client": "Client config. of the backend", - "otoroshi.next.models.NgBackend.healthCheck": "Healthcheck config og the backend", "otoroshi.next.models.NgBackend.loadBalancing": "Loadbalancing config og the backend", "otoroshi.next.models.NgBackend.rewrite": "Does the backend performs a full url rewrite ?", "otoroshi.next.models.NgBackend.root": "The root path of the backend or the full rewrite path", @@ -2031,8 +1797,6 @@ "otoroshi.next.plugins.ApikeyCalls.updateQuotas": "???", "otoroshi.next.plugins.ApikeyCalls.validate": "Enabled quotas validation", "otoroshi.next.plugins.ApikeyCalls.wipeBackendRequest": "Removes the apikeys from the request to not forward it to the backend", - "otoroshi.next.plugins.AuthModule.module": "Id of the auth. module", - "otoroshi.next.plugins.AuthModule.passWithApikey": "let the request pass if an apikey is present", "otoroshi.next.plugins.AuthorisationException.message": "???", "otoroshi.next.plugins.CanaryMode.root": "The root path for target", "otoroshi.next.plugins.CanaryMode.targets": "The canary targets", @@ -2521,20 +2285,12 @@ "otoroshi.script.AccessContext.index": "The current plugin index", "otoroshi.script.AccessContext.snowflake": "The current request snowflake", "otoroshi.script.AccessContext.user": "The current user", - "otoroshi.script.AccessValidatorRef.config": "Access validator plugins configuration", - "otoroshi.script.AccessValidatorRef.enabled": "Access validator plugins enabled", - "otoroshi.script.AccessValidatorRef.excludedPatterns": "Excluded paths", - "otoroshi.script.AccessValidatorRef.refs": "Enabled plugins", "otoroshi.script.PreRoutingContext.attrs": "The current request attributes", "otoroshi.script.PreRoutingContext.config": "The current plugin config.", "otoroshi.script.PreRoutingContext.descriptor": "The current service descriptor", "otoroshi.script.PreRoutingContext.globalConfig": "The current global config", "otoroshi.script.PreRoutingContext.index": "The current plugin index", "otoroshi.script.PreRoutingContext.snowflake": "The current request snowflake", - "otoroshi.script.PreRoutingRef.config": "pre-routing plugins configuration", - "otoroshi.script.PreRoutingRef.enabled": "pre-routing plugins enabled", - "otoroshi.script.PreRoutingRef.excludedPatterns": "Excluded paths", - "otoroshi.script.PreRoutingRef.refs": "Enabled plugins", "otoroshi.script.Script.code": "The code of the script", "otoroshi.script.Script.desc": "The description of the script", "otoroshi.script.Script.id": "The id of the script", @@ -2632,15 +2388,6 @@ "otoroshi.tcp.TcpTarget.tls": "Use tls", "otoroshi.utils.JsonPathValidator.path": "The path to find the validated value", "otoroshi.utils.JsonPathValidator.value": "The expected value", - "otoroshi.utils.gzip.GzipConfig.blackList": "blocklisted content types", - "otoroshi.utils.gzip.GzipConfig.bufferSize": "Buffer size in bytes", - "otoroshi.utils.gzip.GzipConfig.chunkedThreshold": "Chunk size", - "otoroshi.utils.gzip.GzipConfig.compressionLevel": "Compression level (0 - 9)", - "otoroshi.utils.gzip.GzipConfig.enabled": "Gzip enabled", - "otoroshi.utils.gzip.GzipConfig.excludedPatterns": "Excluded paths", - "otoroshi.utils.gzip.GzipConfig.whiteList": "allow listed content types", - "otoroshi.utils.http.CacheConnectionSettings.enabled": "???", - "otoroshi.utils.http.CacheConnectionSettings.queueSize": "???", "otoroshi.utils.http.MtlsConfig.certs": "???", "otoroshi.utils.http.MtlsConfig.loose": "???", "otoroshi.utils.http.MtlsConfig.mtls": "???", diff --git a/otoroshi/conf/schemas/openapi.json b/otoroshi/conf/schemas/openapi.json index a2e252bc82..832b9332dd 100644 --- a/otoroshi/conf/schemas/openapi.json +++ b/otoroshi/conf/schemas/openapi.json @@ -3,7 +3,7 @@ "info" : { "title" : "Otoroshi Admin API", "description" : "Admin API of the Otoroshi reverse proxy", - "version" : "1.5.0-dev", + "version" : "1.5.12", "contact" : { "name" : "Otoroshi Team", "email" : "oss@maif.fr" @@ -17232,60 +17232,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.CorsSettings" : { - "description" : "Settings for CORS support", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Whether or not cors is enabled", - "type" : "boolean" - }, - "allowCredentials" : { - "description" : "Allow to pass credentials", - "type" : "boolean" - }, - "maxAge" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "number" - } ], - "description" : "Cors max age" - }, - "allowMethods" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "The cors allowed methods" - }, - "allowHeaders" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "The cors allowed headers" - }, - "excludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "The cors excluded patterns" - }, - "exposeHeaders" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "The cors exposed header" - }, - "allowOrigin" : { - "description" : "The cors allowed origin", - "type" : "string" - } - } - }, "otoroshi.plugins.apikeys.BiscuitConf" : { "description" : "Configuration for the biscuit plugin", "type" : "object", @@ -17611,21 +17557,6 @@ "$ref" : "#/components/schemas/otoroshi.models.WebAuthnOtoroshiAdmin" } }, - "otoroshi.models.RegionMatch" : { - "description" : "Match a target if in the same region", - "type" : "object", - "properties" : { - "type" : { - "description" : "the kind of predicate", - "type" : "string", - "enum" : [ "AlwaysMatch", "NetworkLocationMatch", "GeolocationMatch" ] - }, - "region" : { - "description" : "Region name", - "type" : "string" - } - } - }, "otoroshi.next.plugins.Cors" : { "description" : "Plugin to use cors", "type" : "object", @@ -17887,35 +17818,6 @@ } } }, - "otoroshi.models.RefJwtVerifier" : { - "description" : "Reference to a jwt verifier", - "type" : "object", - "properties" : { - "excludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Verifier excluded paths" - }, - "ids" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Verifiers ids" - }, - "type" : { - "description" : "the kind of verifier", - "type" : "string", - "enum" : [ "global", "local", "ref" ] - }, - "enabled" : { - "description" : "Verifier enabled", - "type" : "boolean" - } - } - }, "otoroshi.next.plugins.ViolationsException" : { "description" : "???", "type" : "object", @@ -17941,20 +17843,7 @@ "otoroshi.next.plugins.AuthModule" : { "description" : "Plugin to use auth. modules", "type" : "object", - "properties" : { - "module" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Id of the auth. module" - }, - "pass_with_apikey" : { - "description" : "let the request pass if an apikey is present", - "type" : "boolean" - } - } + "properties" : { } }, "otoroshi.plugins.clientcert.HasClientCertValidator" : { "description" : "Plugin that validates client certificates", @@ -17967,17 +17856,6 @@ "$ref" : "#/components/schemas/otoroshi.script.Script" } }, - "otoroshi.models.WeightedBestResponseTime" : { - "description" : "Loadbalancing policy that route to best response time targets with a weight", - "type" : "object", - "properties" : { - "ratio" : { - "format" : "double", - "description" : "Weight ratio", - "type" : "number" - } - } - }, "otoroshi.next.plugins.AdditionalHeadersIn" : { "description" : "Plugin that add headers on a request", "type" : "object", @@ -18061,67 +17939,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.CustomTimeouts" : { - "description" : "Settings for custom timeouts for a specific path", - "type" : "object", - "properties" : { - "path" : { - "description" : "path on which this configuration works", - "type" : "string" - }, - "callAndStreamTimeout" : { - "format" : "int64", - "description" : "Specify how long each call should last at most in milliseconds (hard timeout, connection will be closed after that duration)", - "type" : "integer" - }, - "callTimeout" : { - "format" : "int64", - "description" : "Specify how long each call should last at most in milliseconds (soft timeout as it's enforced by the circuit breaker)", - "type" : "integer" - }, - "idleTimeout" : { - "format" : "int64", - "description" : "Timeout on idle connection", - "type" : "integer" - }, - "globalTimeout" : { - "format" : "int64", - "description" : "Specify how long the global call (with retries) should last at most in milliseconds", - "type" : "integer" - }, - "connectionTimeout" : { - "format" : "int64", - "description" : "Timeout at connection", - "type" : "integer" - } - } - }, - "otoroshi.models.BasicAuthConstraints" : { - "description" : "Settings to extract apikey from a basic auth header like", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Constraint enabled", - "type" : "boolean" - }, - "headerName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name to get client_id:client_secret base64 encoded" - }, - "queryName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Query param name to get client_id:client_secret base64 encoded" - } - } - }, "otoroshi.models.Transform" : { "description" : "jwt token transformation policy settings", "type" : "object", @@ -18358,21 +18175,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.InfraProviderMatch" : { - "description" : "Match a target if in the same infrastructure", - "type" : "object", - "properties" : { - "provider" : { - "description" : "provider name", - "type" : "string" - }, - "type" : { - "description" : "the kind of predicate", - "type" : "string", - "enum" : [ "AlwaysMatch", "NetworkLocationMatch", "GeolocationMatch" ] - } - } - }, "otoroshi.models.Exporter" : { "oneOf" : [ { "$ref" : "#/components/schemas/otoroshi.events.KafkaConfig" @@ -19983,37 +19785,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.NetworkLocationMatch" : { - "description" : "Match a target if in the same network location", - "type" : "object", - "properties" : { - "rack" : { - "description" : "Rack name", - "type" : "string" - }, - "provider" : { - "description" : "Provider name", - "type" : "string" - }, - "dataCenter" : { - "description" : "Datacenter name", - "type" : "string" - }, - "zone" : { - "description" : "Zone name", - "type" : "string" - }, - "type" : { - "description" : "the kind of predicate", - "type" : "string", - "enum" : [ "AlwaysMatch", "NetworkLocationMatch", "GeolocationMatch" ] - }, - "region" : { - "description" : "Region name", - "type" : "string" - } - } - }, "otoroshi.script.PluginType" : { "type" : "string", "enum" : [ "app", "transformer", "validator", "preroute", "sink", "listener", "job", "exporter" ], @@ -20330,36 +20101,6 @@ "type" : "string", "description" : "the id of a service prefixed by 'service_'" }, - "otoroshi.models.SecComHeaders" : { - "description" : "Header names for the otoroshi exchange protocol", - "type" : "object", - "properties" : { - "claimRequestName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name where the info token will be" - }, - "stateRequestName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name where the validation token will be" - }, - "stateResponseName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name where the validation token respondewill be" - } - } - }, "otoroshi.auth.SAMLCredentials" : { "description" : "Used to sign, encrypt assertions and sign SAML documents", "type" : "object", @@ -20549,463 +20290,48 @@ } } }, - "otoroshi.models.ServiceDescriptor" : { - "description" : "The otoroshi model for a service (handles routing)", + "otoroshi.next.plugins.UdpTunnel" : { + "description" : "Plugin to have udp tunnels over websockets", + "type" : "object", + "properties" : { } + }, + "otoroshi.plugins.izanami.IzanamiCanaryConfig" : { + "description" : "Configuration for IzanamiCanary", "type" : "object", "properties" : { - "buildMode" : { - "description" : "Display a construction page when a user try to use the service", - "type" : "boolean" - }, - "hosts" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Possible hosts for the service" - }, - "privateApp" : { - "description" : "When enabled, user will be allowed to use the service (UI) only if they are registered users of the private apps domain", - "type" : "boolean" - }, - "localScheme" : { - "description" : "The scheme used localy, mainly http", + "izanamiClientId" : { + "description" : "Izanami client id", "type" : "string" }, - "authConfigRef" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "A reference to a global auth module config" - }, - "issueCertCA" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "CA for cert issuance" - }, - "root" : { - "description" : "Otoroshi will append this root to any target choosen. If the specified root is '/api/foo', then a request to https://yyyyyyy/bar will actually hit https://xxxxxxxxx/api/foo/bar", + "configId" : { + "description" : "Id of the target izanami configuration", "type" : "string" }, - "name" : { - "description" : "The name of your service. Only for debug and human readability purposes", + "experimentId" : { + "description" : "Id of the target izanami experiment", "type" : "string" }, - "additionalHeaders" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Specify headers that will be added to each client request. Useful to add authentication" + "mtls" : { + "description" : "Izanami server tls config", + "$ref" : "#/components/schemas/otoroshi.utils.http.MtlsConfig" }, - "domain" : { - "description" : "The domain on which the service is available.", + "izanamiUrl" : { + "description" : "Izanami server url", "type" : "string" }, - "clientConfig" : { - "description" : "Http client settings", - "$ref" : "#/components/schemas/otoroshi.models.ClientConfig" + "timeout" : { + "description" : "Timeout when talking to the izanami server", + "type" : "number" + }, + "izanamiClientSecret" : { + "description" : "Izanami client secret", + "type" : "string" }, - "matchingRoot" : { + "routeConfig" : { "oneOf" : [ { "$ref" : "#/components/schemas/Null" }, { - "type" : "string" - } ], - "description" : "The root path on which the service is available" - }, - "forceHttps" : { - "description" : "Will force redirection to https:// if not present", - "type" : "boolean" - }, - "localHost" : { - "description" : "The host used localy, mainly localhost:xxxx", - "type" : "string" - }, - "sendOtoroshiHeadersBack" : { - "description" : "When enabled, Otoroshi will send headers to consumer like request id, client latency, overhead, etc ...", - "type" : "boolean" - }, - "healthCheck" : { - "description" : "Healthcheck settings", - "$ref" : "#/components/schemas/otoroshi.models.HealthCheck" - }, - "strictlyPrivate" : { - "description" : "When strictly private, private app session will not pass apikey filters", - "type" : "boolean" - }, - "detectApiKeySooner" : { - "description" : "Detect if an apikey is present but do not fail if not", - "type" : "boolean" - }, - "allowHttp10" : { - "description" : "Allow HTTP/1.0 requests", - "type" : "boolean" - }, - "subdomain" : { - "description" : "The subdomain on which the service is available", - "type" : "string" - }, - "paths" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Matching paths on request" - }, - "stripPath" : { - "description" : "Strip matching path in the forwarded request path", - "type" : "boolean" - }, - "secComAlgoChallengeOtoToBack" : { - "description" : "Algorithm to sign challenge token to the backend", - "$ref" : "#/components/schemas/otoroshi.models.AlgoSettings" - }, - "apiKeyConstraints" : { - "description" : "Routing and extraction constraints for the apikeyh", - "$ref" : "#/components/schemas/otoroshi.models.ApiKeyConstraints" - }, - "env" : { - "description" : "The line on which the service is available. Based on that value, the name of the line will be appended to the subdomain. For line prod, nothing will be appended. For example, if the subdomain is 'foo' and line is 'preprod', then the exposed service will be available at 'foo.preprod.mydomain'", - "type" : "string" - }, - "xForwardedHeaders" : { - "description" : "Send X-Forwarded-* headers", - "type" : "boolean" - }, - "transformerRefs" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Enabled transformer plugins" - }, - "enabled" : { - "description" : "Activate or deactivate your service. Once disabled, users will get an error page saying the service does not exist", - "type" : "boolean" - }, - "gzip" : { - "description" : "GZIP settings", - "$ref" : "#/components/schemas/otoroshi.utils.gzip.GzipConfig" - }, - "sendInfoToken" : { - "description" : "Should otoroshi send info token", - "type" : "boolean" - }, - "tcpUdpTunneling" : { - "description" : "Enabled TCP/UDP tunneling through websocket connection", - "type" : "boolean" - }, - "removeHeadersOut" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Remove headers on client response" - }, - "useAkkaHttpClient" : { - "description" : "Use akka http client for this service", - "type" : "boolean" - }, - "maintenanceMode" : { - "description" : "Display a maintainance page when a user try to use the service", - "type" : "boolean" - }, - "id" : { - "description" : "A unique random string to identify your service", - "type" : "string" - }, - "removeHeadersIn" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Remove headers on client request" - }, - "logAnalyticsOnServer" : { - "description" : "Log analytics event on the server", - "type" : "boolean" - }, - "secComAlgoInfoToken" : { - "description" : "Algorithm to verify/sign challenge token coming from/to the backend", - "$ref" : "#/components/schemas/otoroshi.models.AlgoSettings" - }, - "userFacing" : { - "description" : "The fact that this service will be seen by users and cannot be impacted by the Snow Monkey", - "type" : "boolean" - }, - "transformerConfig" : { - "description" : "Transformer plugins configuration", - "type" : "object" - }, - "clientValidatorRef" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "A reference to validation authority" - }, - "securityExcludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Exclude some paths" - }, - "ipFiltering" : { - "description" : "Ip filtering settings", - "$ref" : "#/components/schemas/otoroshi.models.IpFiltering" - }, - "targets" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/otoroshi.models.Target" - }, - "description" : "The list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures" - }, - "redirection" : { - "description" : "Redirection settings", - "$ref" : "#/components/schemas/otoroshi.models.RedirectionSettings" - }, - "tags" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Entity tags" - }, - "restrictions" : { - "description" : "Restriction settings", - "$ref" : "#/components/schemas/otoroshi.models.Restrictions" - }, - "overrideHost" : { - "description" : "Host header will be overriden with Host of the target", - "type" : "boolean" - }, - "accessValidator" : { - "description" : "Service access validatiors", - "$ref" : "#/components/schemas/otoroshi.script.AccessValidatorRef" - }, - "sendStateChallenge" : { - "description" : "Should otoroshi send challenge token", - "type" : "boolean" - }, - "chaosConfig" : { - "description" : "Chaos engineering settings", - "$ref" : "#/components/schemas/otoroshi.models.ChaosConfig" - }, - "secComInfoTokenVersion" : { - "description" : "Version of the info token", - "$ref" : "#/components/schemas/otoroshi.models.SecComInfoTokenVersion" - }, - "additionalHeadersOut" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Specify headers that will be added to each client response" - }, - "secComHeaders" : { - "description" : "Header names for sec. com. protocol", - "$ref" : "#/components/schemas/otoroshi.models.SecComHeaders" - }, - "matchingHeaders" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Specify headers that MUST be present on client request to route it. Useful to implement versioning" - }, - "secComAlgoChallengeBackToOto" : { - "description" : "Algorithm to verify challenge token coming from the backend", - "$ref" : "#/components/schemas/otoroshi.models.AlgoSettings" - }, - "secComUseSameAlgo" : { - "description" : "Use the same algo for info token, challenge token signing, challenge token verification", - "type" : "boolean" - }, - "useNewWSClient" : { - "description" : "Use akka http client for this service on websocket calls", - "type" : "boolean" - }, - "secComExcludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "URI patterns excluded from secured communications" - }, - "redirectToLocal" : { - "description" : "If you work locally with Otoroshi, you may want to use that feature to redirect one particuliar service to a local host. For example, you can relocate https://foo.preprod.bar.com to http://localhost:8080 to make some tests", - "type" : "boolean" - }, - "enforceSecureCommunication" : { - "description" : "When enabled, Otoroshi will try to exchange headers with backend service to ensure no one else can use the service from outside", - "type" : "boolean" - }, - "missingOnlyHeadersOut" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Add header on client response if they are not present" - }, - "secComSettings" : { - "description" : "Sec. com. settings", - "$ref" : "#/components/schemas/otoroshi.models.AlgoSettings" - }, - "handleLegacyDomain" : { - "description" : "Use 'domain', 'subdomain', 'env' and 'matchingRoot' for routing in addition to hosts, or just use hosts.", - "type" : "boolean" - }, - "canary" : { - "description" : "Canary settings", - "$ref" : "#/components/schemas/otoroshi.models.Canary" - }, - "_loc" : { - "description" : "Entity location", - "$ref" : "#/components/schemas/otoroshi.models.EntityLocation" - }, - "plugins" : { - "description" : "Plugins enabled for this service. will replace separate plugins fields in a near future", - "$ref" : "#/components/schemas/otoroshi.script.plugins.Plugins" - }, - "secComTtl" : { - "description" : "TTL for the info token", - "type" : "number" - }, - "description" : { - "description" : "Entity description", - "type" : "string" - }, - "secComVersion" : { - "description" : "Version of the sec. com.", - "$ref" : "#/components/schemas/otoroshi.models.SecComVersion" - }, - "preRouting" : { - "description" : "Pre routing plugin settings", - "$ref" : "#/components/schemas/otoroshi.script.PreRoutingRef" - }, - "groups" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Each service descriptor is attached to groups. A group can have one or more services. Each API key is linked to a group and allow access to every service in the group" - }, - "readOnly" : { - "description" : "Service only accepts GET, HEAD and OPTIONS requests", - "type" : "boolean" - }, - "privatePatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "If you define a public pattern that is a little bit too much, you can make some of public URL private again" - }, - "targetsLoadBalancing" : { - "description" : "Loadbalancing strategy", - "$ref" : "#/components/schemas/otoroshi.models.LoadBalancing" - }, - "cors" : { - "description" : "CORS settings", - "$ref" : "#/components/schemas/otoroshi.models.CorsSettings" - }, - "metadata" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Just a bunch of random properties" - }, - "publicPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "By default, every services are private only and you'll need an API key to access it. However, if you want to expose a public UI, you can define one or more public patterns (regex) to allow access to anybody. For example if you want to allow anybody on any URL, just use '/.*'" - }, - "api" : { - "description" : "Api exposition settings", - "$ref" : "#/components/schemas/otoroshi.models.ApiDescriptor" - }, - "missingOnlyHeadersIn" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Add header on client request if they are not present" - }, - "issueCert" : { - "description" : "Flag to automatically issue a cert for this service", - "type" : "boolean" - }, - "headersVerification" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Specify headers that will be verified after routing." - }, - "jwtVerifier" : { - "description" : "JWT verifiers settings", - "$ref" : "#/components/schemas/otoroshi.models.JwtVerifier" - }, - "letsEncrypt" : { - "description" : "Flag to automatically issue a let's encrypt (ACME) cert for this service", - "type" : "boolean" - } - } - }, - "otoroshi.next.plugins.UdpTunnel" : { - "description" : "Plugin to have udp tunnels over websockets", - "type" : "object", - "properties" : { } - }, - "otoroshi.plugins.izanami.IzanamiCanaryConfig" : { - "description" : "Configuration for IzanamiCanary", - "type" : "object", - "properties" : { - "izanamiClientId" : { - "description" : "Izanami client id", - "type" : "string" - }, - "configId" : { - "description" : "Id of the target izanami configuration", - "type" : "string" - }, - "experimentId" : { - "description" : "Id of the target izanami experiment", - "type" : "string" - }, - "mtls" : { - "description" : "Izanami server tls config", - "$ref" : "#/components/schemas/otoroshi.utils.http.MtlsConfig" - }, - "izanamiUrl" : { - "description" : "Izanami server url", - "type" : "string" - }, - "timeout" : { - "description" : "Timeout when talking to the izanami server", - "type" : "number" - }, - "izanamiClientSecret" : { - "description" : "Izanami client secret", - "type" : "string" - }, - "routeConfig" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "object" + "type" : "object" } ], "description" : "The actual routing config" } @@ -21456,32 +20782,6 @@ } } }, - "otoroshi.models.Canary" : { - "description" : "Settings for canary routing", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Use canary mode for this service", - "type" : "boolean" - }, - "traffic" : { - "format" : "double", - "description" : "Ratio of traffic that will be sent to canary targets.", - "type" : "number" - }, - "targets" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/otoroshi.models.Target" - }, - "description" : "The list of target that Otoroshi will proxy and expose through the subdomain defined before. Otoroshi will do round-robin load balancing between all those targets with circuit breaker mecanism to avoid cascading failures" - }, - "root" : { - "description" : "Otoroshi will append this root to any target choosen. If the specified root is '/api/foo', then a request to https://yyyyyyy/bar will actually hit https://xxxxxxxxx/api/foo/bar", - "type" : "string" - } - } - }, "otoroshi.next.models.NgRouteDomainAndPathWrapper" : { "description" : "Internal api", "type" : "object", @@ -22127,47 +21427,21 @@ "responses" : { "type" : "array", "items" : { - "$ref" : "#/components/schemas/otoroshi.next.plugins.MockResponse" - }, - "description" : "Possible responses" - }, - "pass_through" : { - "description" : "Pass the call if no mocked response found", - "type" : "boolean" - }, - "form_data" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "$ref" : "#/components/schemas/otoroshi.next.plugins.MockFormData" - } ], - "description" : "???" - } - } - }, - "otoroshi.models.ClientIdAuthConstraints" : { - "description" : "Settings to extract apikey (using client_id only) from a header or query param", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Constraint enabled", - "type" : "boolean" + "$ref" : "#/components/schemas/otoroshi.next.plugins.MockResponse" + }, + "description" : "Possible responses" }, - "headerName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name to find client_id" + "pass_through" : { + "description" : "Pass the call if no mocked response found", + "type" : "boolean" }, - "queryName" : { + "form_data" : { "oneOf" : [ { "$ref" : "#/components/schemas/Null" }, { - "type" : "string" + "$ref" : "#/components/schemas/otoroshi.next.plugins.MockFormData" } ], - "description" : "Query param name to find client_id" + "description" : "???" } } }, @@ -22212,85 +21486,6 @@ } } }, - "otoroshi.models.ClientConfig" : { - "description" : "Settings for the http client when http request is forwarded", - "type" : "object", - "properties" : { - "connectionTimeout" : { - "format" : "int64", - "description" : "Timeout at connection", - "type" : "integer" - }, - "useCircuitBreaker" : { - "description" : "Use a circuit breaker to avoid cascading failure when calling chains of services. Highly recommended !", - "type" : "boolean" - }, - "retryInitialDelay" : { - "format" : "int64", - "description" : "Specify the delay between two retries. Each retry, the delay is multiplied by the backoff factor", - "type" : "integer" - }, - "cacheConnectionSettings" : { - "description" : "Cached connection settings", - "$ref" : "#/components/schemas/otoroshi.utils.http.CacheConnectionSettings" - }, - "proxy" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "$ref" : "#/components/schemas/play.api.libs.ws.WSProxyServer" - } ], - "description" : "Web proxy settings for http client" - }, - "callTimeout" : { - "format" : "int64", - "description" : "Specify how long each call should last at most in milliseconds (soft timeout as it's enforced by the circuit breaker)", - "type" : "integer" - }, - "callAndStreamTimeout" : { - "format" : "int64", - "description" : "Specify how long each call should last at most in milliseconds (hard timeout, connection will be closed after that duration)", - "type" : "integer" - }, - "globalTimeout" : { - "format" : "int64", - "description" : "Specify how long the global call (with retries) should last at most in milliseconds", - "type" : "integer" - }, - "maxErrors" : { - "format" : "int32", - "description" : "Specify how many errors can pass before opening the circuit breaker", - "type" : "integer" - }, - "retries" : { - "format" : "int32", - "description" : "Specify how many times the client will try to fetch the result of the request after an error before giving up.", - "type" : "integer" - }, - "backoffFactor" : { - "format" : "int64", - "description" : "Specify the factor to multiply the delay for each retry", - "type" : "integer" - }, - "customTimeouts" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/otoroshi.models.CustomTimeouts" - }, - "description" : "Custom timeouts per path" - }, - "idleTimeout" : { - "format" : "int64", - "description" : "Timeout on idle connection", - "type" : "integer" - }, - "sampleInterval" : { - "format" : "int64", - "description" : "Specify the sliding window time for the circuit breaker in milliseconds, after this time, error count will be reseted", - "type" : "integer" - } - } - }, "otoroshi.next.plugins.NgBadResponse" : { "description" : "Configuration for SnowMonkeyChaos", "type" : "object", @@ -22322,32 +21517,6 @@ "type" : "object", "description" : "Service live stats" }, - "otoroshi.models.ApiKeyConstraints" : { - "description" : "Settings used to extract apikeys from http requests and routing traffic", - "type" : "object", - "properties" : { - "customHeadersAuth" : { - "description" : "Settings to extract apikey from custom headers", - "$ref" : "#/components/schemas/otoroshi.models.CustomHeadersAuthConstraints" - }, - "routing" : { - "description" : "Routing settings for this apikey", - "$ref" : "#/components/schemas/otoroshi.models.ApiKeyRouteMatcher" - }, - "clientIdAuth" : { - "description" : "Settings to extract client_id only apikey", - "$ref" : "#/components/schemas/otoroshi.models.ClientIdAuthConstraints" - }, - "jwtAuth" : { - "description" : "Settings to extract apikey from jwt token", - "$ref" : "#/components/schemas/otoroshi.models.JwtAuthConstraints" - }, - "basicAuth" : { - "description" : "Settings to extract basic auth style apikey", - "$ref" : "#/components/schemas/otoroshi.models.BasicAuthConstraints" - } - } - }, "otoroshi.plugins.metrics.ServiceMetrics" : { "description" : "Plugin to collect service metrics", "type" : "object", @@ -23236,27 +22405,6 @@ } } }, - "otoroshi.models.GeoPositionRadius" : { - "description" : "Geolocation radius", - "type" : "object", - "properties" : { - "latitude" : { - "format" : "double", - "description" : "Latitude of the position", - "type" : "number" - }, - "longitude" : { - "format" : "double", - "description" : "Longitude of the position", - "type" : "number" - }, - "radius" : { - "format" : "double", - "description" : "Radius of the circle in meters", - "type" : "number" - } - } - }, "otoroshi.models.InCookie" : { "description" : "JWT token location (cookie)", "type" : "object", @@ -23387,90 +22535,6 @@ "type" : "object", "description" : "" }, - "otoroshi.models.LocalJwtVerifier" : { - "description" : "Local jwt verifier (deprecated)", - "type" : "object", - "properties" : { - "excludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Verifier excluded paths" - }, - "algoSettings" : { - "description" : "Algo settings", - "$ref" : "#/components/schemas/otoroshi.models.AlgoSettings" - }, - "source" : { - "description" : "Token source", - "$ref" : "#/components/schemas/otoroshi.models.JwtTokenLocation" - }, - "type" : { - "description" : "the kind of verifier", - "type" : "string", - "enum" : [ "global", "local", "ref" ] - }, - "strict" : { - "description" : "Strict token verification", - "type" : "boolean" - }, - "strategy" : { - "description" : "Token strategy", - "$ref" : "#/components/schemas/otoroshi.models.VerifierStrategy" - }, - "enabled" : { - "description" : "Verifier enabled", - "type" : "boolean" - } - } - }, - "otoroshi.utils.gzip.GzipConfig" : { - "description" : "Settings for gzip support", - "type" : "object", - "properties" : { - "compressionLevel" : { - "format" : "int32", - "description" : "Compression level (0 - 9)", - "type" : "integer" - }, - "blackList" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "blocklisted content types" - }, - "chunkedThreshold" : { - "format" : "int32", - "description" : "Chunk size", - "type" : "integer" - }, - "excludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Excluded paths" - }, - "bufferSize" : { - "format" : "int32", - "description" : "Buffer size in bytes", - "type" : "integer" - }, - "whiteList" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "allow listed content types" - }, - "enabled" : { - "description" : "Gzip enabled", - "type" : "boolean" - } - } - }, "otoroshi.plugins.useragent.UserAgentExtractor" : { "description" : "Plugin that extract user-agent related infos", "type" : "object", @@ -23734,61 +22798,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.JwtAuthConstraints" : { - "description" : "Settings to extract apikey from a jwt token", - "type" : "object", - "properties" : { - "keyPairSigned" : { - "description" : "The jwt token is signed by a keypair from a cert found from its id in apikey meta. 'jwt-sign-keypair'", - "type" : "boolean" - }, - "cookieName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Cookie name to extract jwt token" - }, - "queryName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Query param name to extract jwt token" - }, - "headerName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name to extract jwt token" - }, - "secretSigned" : { - "description" : "Jwt token signed with the client_secret", - "type" : "boolean" - }, - "maxJwtLifespanSecs" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "integer", - "format" : "int64" - } ], - "description" : "Check if token does not have a long lifespan" - }, - "enabled" : { - "description" : "Constraint enabled", - "type" : "boolean" - }, - "includeRequestAttributes" : { - "description" : "Jwt token should include verb and path", - "type" : "boolean" - } - } - }, "otoroshi.plugins.apikeys.ClientCredentialFlowExtractor" : { "description" : "Internal api", "type" : "object", @@ -23847,34 +22856,6 @@ "type" : "string", "description" : "the id of a group prefixed by 'group_'" }, - "otoroshi.script.AccessValidatorRef" : { - "description" : "References to access validation plugins", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Access validator plugins enabled", - "type" : "boolean" - }, - "excludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Excluded paths" - }, - "refs" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Enabled plugins" - }, - "config" : { - "description" : "Access validator plugins configuration", - "type" : "object" - } - } - }, "otoroshi.ssl.pki.models.GenCertResponse" : { "description" : "Response for a certificate generation operation", "type" : "object", @@ -24104,20 +23085,6 @@ "$ref" : "#/components/schemas/otoroshi.events.AlertEvent" } }, - "otoroshi.models.HealthCheck" : { - "description" : "Healthcheck settings for a service", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Whether or not healthcheck is enabled on the current service descriptor", - "type" : "boolean" - }, - "url" : { - "description" : "The URL to check", - "type" : "string" - } - } - }, "otoroshi.plugins.core.apikeys.ClientIdApikeyExtractor" : { "description" : "Internal api", "type" : "object", @@ -24282,21 +23249,6 @@ } } }, - "otoroshi.models.ZoneMatch" : { - "description" : "Match a target if in the same zone", - "type" : "object", - "properties" : { - "zone" : { - "description" : "Zone name", - "type" : "string" - }, - "type" : { - "description" : "the kind of predicate", - "type" : "string", - "enum" : [ "AlwaysMatch", "NetworkLocationMatch", "GeolocationMatch" ] - } - } - }, "otoroshi.next.models.NgPlugins" : { "description" : "A set of NgPluginInstance", "type" : "object", @@ -24414,22 +23366,8 @@ "type" : "object", "description" : "Is certificate valid", "properties" : { - "valid" : { - "type" : "boolean" - } - } - }, - "otoroshi.models.RestrictionPath" : { - "description" : "Represent an http request on which restrictions will apply", - "type" : "object", - "properties" : { - "method" : { - "description" : "Method of the http request", - "type" : "string" - }, - "path" : { - "description" : "Path of the http request", - "type" : "string" + "valid" : { + "type" : "boolean" } } }, @@ -24533,24 +23471,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.GeolocationMatch" : { - "description" : "Match a target if in the same geo location radius", - "type" : "object", - "properties" : { - "positions" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/otoroshi.models.GeoPositionRadius" - }, - "description" : "Possible positions" - }, - "type" : { - "description" : "the kind of predicate", - "type" : "string", - "enum" : [ "AlwaysMatch", "NetworkLocationMatch", "GeolocationMatch" ] - } - } - }, "BulkResponseBody" : { "type" : "array", "items" : { @@ -24648,60 +23568,6 @@ } } }, - "otoroshi.models.Target" : { - "description" : "A target model for a service (destination for forwarded requests)", - "type" : "object", - "properties" : { - "tags" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Tags for this target" - }, - "host" : { - "description" : "The host on which the HTTP call will be forwarded. Can be a domain name, or an IP address. Can also have a port", - "type" : "string" - }, - "weight" : { - "format" : "int32", - "description" : "The weight of the target when choosing", - "type" : "integer" - }, - "metadata" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Metadata for this target" - }, - "protocol" : { - "description" : "Protocol for the target", - "type" : "string", - "enum" : [ "HTTP/1.0", "HTTP/1.1", "HTTP/2.0" ] - }, - "predicate" : { - "description" : "Predicate to choose this target", - "$ref" : "#/components/schemas/otoroshi.models.TargetPredicate" - }, - "ipAddress" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Target ip address. Usefull to make manual DNS resolution without breaking SNI" - }, - "mtlsConfig" : { - "description" : "TLS settings to contact this target", - "$ref" : "#/components/schemas/otoroshi.utils.http.MtlsConfig" - }, - "scheme" : { - "description" : "The protocol used for communication. Can be http or https", - "type" : "string" - } - } - }, "otoroshi.next.plugins.Redirection" : { "description" : "Plugin to perform redirections", "type" : "object", @@ -24849,21 +23715,6 @@ } } }, - "otoroshi.utils.http.CacheConnectionSettings" : { - "description" : "???", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "???", - "type" : "boolean" - }, - "queueSize" : { - "format" : "int32", - "description" : "???", - "type" : "integer" - } - } - }, "otoroshi.plugins.jobs.kubernetes.KubernetesAdmissionWebhookSidecarInjector" : { "description" : "Internal api", "type" : "object", @@ -24911,25 +23762,6 @@ } } }, - "otoroshi.models.RedirectionSettings" : { - "description" : "Settings for routing redirection", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Whether or not redirection is enabled", - "type" : "boolean" - }, - "code" : { - "format" : "int32", - "description" : "The http redirect code", - "type" : "integer" - }, - "to" : { - "description" : "The location for redirection", - "type" : "string" - } - } - }, "otoroshi.next.plugins.NgOtoroshiChallengeKeys" : { "description" : "Configuration for OtoroshiChallenge", "type" : "object", @@ -25206,21 +24038,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.RackMatch" : { - "description" : "Match a target if in the same rack", - "type" : "object", - "properties" : { - "rack" : { - "description" : "Rack name", - "type" : "string" - }, - "type" : { - "description" : "the kind of predicate", - "type" : "string", - "enum" : [ "AlwaysMatch", "NetworkLocationMatch", "GeolocationMatch" ] - } - } - }, "otoroshi.plugins.oidc.ThirdPartyApiKeyConfig" : { "description" : "Internal api", "type" : "object", @@ -25645,24 +24462,6 @@ "type" : "object", "description" : "Alert trail event" }, - "otoroshi.models.ApiDescriptor" : { - "description" : "Represent if a service exposes an API with an optional url to an openapi descriptor", - "type" : "object", - "properties" : { - "exposeApi" : { - "description" : "Is this an API", - "type" : "boolean" - }, - "openApiDescriptorUrl" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "openapi descriptor url" - } - } - }, "otoroshi.auth.SessionCookieValues" : { "description" : "The configuration for session cookie", "type" : "object", @@ -25814,75 +24613,6 @@ } } }, - "otoroshi.models.ApiKeyRouteMatcher" : { - "description" : "Routing settings based on apikeys metadata and tags", - "type" : "object", - "properties" : { - "oneTagIn" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "outing if one tag presents in apikey" - }, - "noneMetaKeysIn" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Routing if none meta keys presents in apikey" - }, - "oneMetaIn" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Routing if one meta presents in apikey" - }, - "oneMetaKeyIn" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Routing if one meta key presents in apikey" - }, - "allMetaKeysIn" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Routing if all meta keys presents in apikey" - }, - "noneTagIn" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Routing if none tags presents in apikey" - }, - "allTagsIn" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Routing if all tags presents in apikey" - }, - "allMetaIn" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Routing if all meta presents in apikey" - }, - "noneMetaIn" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - }, - "description" : "Routing if none meta presents in apikey" - } - } - }, "otoroshi.next.plugins.SOAPAction" : { "description" : "Plugin to call SOAP service", "type" : "object", @@ -26298,14 +25028,6 @@ "description" : "The root path of the backend or the full rewrite path", "type" : "string" }, - "health_check" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "$ref" : "#/components/schemas/otoroshi.models.HealthCheck" - } ], - "description" : "Healthcheck config og the backend" - }, "client" : { "description" : "Client config. of the backend", "$ref" : "#/components/schemas/otoroshi.next.models.NgClientConfig" @@ -26386,34 +25108,6 @@ "$ref" : "#/components/schemas/PluginDescription" } }, - "otoroshi.script.PreRoutingRef" : { - "description" : "References to pre-routing plugins", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "pre-routing plugins enabled", - "type" : "boolean" - }, - "excludedPatterns" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Excluded paths" - }, - "refs" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Enabled plugins" - }, - "config" : { - "description" : "pre-routing plugins configuration", - "type" : "object" - } - } - }, "otoroshi.plugins.biscuit.BiscuitConfig" : { "description" : "Internal api", "type" : "object", @@ -26778,32 +25472,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.CustomHeadersAuthConstraints" : { - "description" : "Settings to extract apikey from a custom headers", - "type" : "object", - "properties" : { - "enabled" : { - "description" : "Constraint enabled", - "type" : "boolean" - }, - "clientIdHeaderName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name to find client_id" - }, - "clientSecretHeaderName" : { - "oneOf" : [ { - "$ref" : "#/components/schemas/Null" - }, { - "type" : "string" - } ], - "description" : "Header name to find client_secret" - } - } - }, "otoroshi.models.TlsSettings" : { "description" : "Global TLS settings. The default domain that will be picked if no certificate matches the current request", "type" : "object", @@ -27101,41 +25769,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.Restrictions" : { - "description" : "Http requests restrictions for a service or an apikey", - "type" : "object", - "properties" : { - "forbidden" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/otoroshi.models.RestrictionPath" - }, - "description" : "Forbidden paths (return 403)" - }, - "allowed" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/otoroshi.models.RestrictionPath" - }, - "description" : "Allowed paths" - }, - "notFound" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/otoroshi.models.RestrictionPath" - }, - "description" : "Not found paths (return 404)" - }, - "allowLast" : { - "description" : "Evalute allowed paths after everything else", - "type" : "boolean" - }, - "enabled" : { - "description" : "Restrictions enabled", - "type" : "boolean" - } - } - }, "otoroshi.next.plugins.NgLargeRequestFaultConfig" : { "description" : "Configuration for SnowMonkeyChaos", "type" : "object", @@ -27380,26 +26013,6 @@ } } }, - "otoroshi.models.IpFiltering" : { - "description" : "Settings for ip address filtering for a service or globally", - "type" : "object", - "properties" : { - "whitelist" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Whitelisted IP addresses" - }, - "blacklist" : { - "type" : "array", - "items" : { - "type" : "string" - }, - "description" : "Blacklisted IP addresses" - } - } - }, "otoroshi.plugins.quotas.ServiceQuotas" : { "description" : "Internal api", "type" : "object", @@ -28308,21 +26921,6 @@ "type" : "object", "properties" : { } }, - "otoroshi.models.DataCenterMatch" : { - "description" : "Match a target if in the same datacenter", - "type" : "object", - "properties" : { - "type" : { - "description" : "the kind of predicate", - "type" : "string", - "enum" : [ "AlwaysMatch", "NetworkLocationMatch", "GeolocationMatch" ] - }, - "dc" : { - "description" : "DC name", - "type" : "string" - } - } - }, "otoroshi.models.JwtTokenLocation" : { "oneOf" : [ { "$ref" : "#/components/schemas/otoroshi.models.InCookie" diff --git a/otoroshi/javascript/src/pages/MetricsPage.js b/otoroshi/javascript/src/pages/MetricsPage.js index dcc55e461b..68aae1f576 100644 --- a/otoroshi/javascript/src/pages/MetricsPage.js +++ b/otoroshi/javascript/src/pages/MetricsPage.js @@ -2,19 +2,18 @@ import _ from 'lodash'; import React, { Component } from 'react'; export class MetricsPage extends Component { + state = { metrics: [] }; - state = { metrics: [] } - - possibleTypes = [ 'counters', 'gauges', 'histograms', 'timers' ] + possibleTypes = ['counters', 'gauges', 'histograms', 'timers']; componentDidMount() { this.fetchMetrics(); - this.interval = setInterval(() => this.fetchMetrics(), 5000) + this.interval = setInterval(() => this.fetchMetrics(), 5000); } componentWillUnmount() { if (this.interval) { - clearInterval(this.interval) + clearInterval(this.interval); } } @@ -23,29 +22,31 @@ export class MetricsPage extends Component { method: 'GET', credentials: 'include', headers: { - Accepts: 'application/json' - } - }).then(r => r.json()).then(metrics => { - this.setState({ metrics }) + Accepts: 'application/json', + }, }) - } + .then((r) => r.json()) + .then((metrics) => { + this.setState({ metrics }); + }); + }; clean = (v) => { if (v) { if (_.isNumber(v)) { if (String(v).indexOf('.') > -1) { const x = v.toFixed(5); - const parts = x.toString().split("."); - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, " "); - return parts.join("."); + const parts = x.toString().split('.'); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ' '); + return parts.join('.'); } else { - const parts = v.toString().split("."); - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, " "); - return parts.join("."); + const parts = v.toString().split('.'); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ' '); + return parts.join('.'); } } else if (_.isString(v)) { if (v.length > 20) { - return {v.substring(0, 19)}... + return {v.substring(0, 19)}...; } else { return v; } @@ -53,20 +54,51 @@ export class MetricsPage extends Component { return v; } } else { - return v + return v; } - } + }; render() { const clean = this.clean; - const w_units = this.state.metrics.find(m => m.rate_units) || { rate_units: 'calls/second', duration_units: 'milliseconds' }; - const metrics = _.uniqBy(_.sortBy(this.state.metrics, metric => metric.name).filter(m => m.type !== 'metrics'), m => m.name) - .filter(m => this.state.search ? m.name.indexOf(this.state.search) > -1 : true) + const w_units = this.state.metrics.find((m) => m.rate_units) || { + rate_units: 'calls/second', + duration_units: 'milliseconds', + }; + const metrics = _.uniqBy( + _.sortBy(this.state.metrics, (metric) => metric.name).filter((m) => m.type !== 'metrics'), + (m) => m.name + ).filter((m) => (this.state.search ? m.name.indexOf(this.state.search) > -1 : true)); return ( -
-

{metrics.length} metrics - {w_units.rate_units} - {w_units.duration_units}

- this.setState({ search: e.target.value })} placeholder="search metric name" /> -
+
+

+ {metrics.length} metrics - {w_units.rate_units} - {w_units.duration_units} +

+ this.setState({ search: e.target.value })} + placeholder="search metric name" + /> +
@@ -85,23 +117,87 @@ export class MetricsPage extends Component { - {this.possibleTypes.map(type => { - return metrics.filter(m => m.type === type).map(metric => ( - - - {/**/} - - - - - - - - - - - - )) + {this.possibleTypes.map((type) => { + return metrics + .filter((m) => m.type === type) + .map((metric) => ( + + + {/**/} + + + + + + + + + + + + )); })}
{metric.name}{metric.type}{clean(metric.value) || ''}{clean(metric.count) || ''}{clean(metric.min) || ''}{clean(metric.mean) || ''}{clean(metric.max) || ''}{clean(metric.stddev) || ''}{clean(metric.mean_rate) || ''}{clean(metric.m1_rate) || ''}{clean(metric.m5_rate) || ''}{clean(metric.m15_rate) || ''}
{metric.name}{metric.type} + + {clean(metric.value) || ''} + + + + {clean(metric.count) || ''} + + + + {clean(metric.min) || ''} + + + + {clean(metric.mean) || ''} + + + + {clean(metric.max) || ''} + + + + {clean(metric.stddev) || ''} + + + + {clean(metric.mean_rate) || ''} + + + + {clean(metric.m1_rate) || ''} + + + + {clean(metric.m5_rate) || ''} + + + + {clean(metric.m15_rate) || ''} + +
@@ -109,4 +205,4 @@ export class MetricsPage extends Component {
); } -} \ No newline at end of file +} diff --git a/otoroshi/javascript/src/pages/RouteDesigner/Designer.js b/otoroshi/javascript/src/pages/RouteDesigner/Designer.js index 72365954b6..8a75d9c685 100644 --- a/otoroshi/javascript/src/pages/RouteDesigner/Designer.js +++ b/otoroshi/javascript/src/pages/RouteDesigner/Designer.js @@ -29,7 +29,7 @@ import { snakeCase, camelCase, isEqual, over } from 'lodash'; import { HTTP_COLORS } from './RouteComposition'; import { getPluginsPatterns } from './patterns'; -import { TryIt } from './TryIt' +import { TryIt } from './TryIt'; const HeaderNode = ({ selectedNode, text, icon }) => ( @@ -188,25 +188,30 @@ const FormContainer = ({ }) => { const isOnFrontendBackend = selectedNode && ['Frontend', 'Backend'].includes(selectedNode.id); - return
- - {serviceMode && isOnFrontendBackend && } - {selectedNode && (!serviceMode || (serviceMode && !isOnFrontendBackend)) && ( - - showPreview({ - ...preview, - enabled: false, - }) - } - /> - )} - {alertModal.show && } -
-} + return ( +
+ + {serviceMode && isOnFrontendBackend && } + {selectedNode && (!serviceMode || (serviceMode && !isOnFrontendBackend)) && ( + + showPreview({ + ...preview, + enabled: false, + }) + } + /> + )} + {alertModal.show && } +
+ ); +}; const Modal = ({ question, onOk, onCancel }) => (
@@ -222,43 +227,44 @@ const Modal = ({ question, onOk, onCancel }) => (
); -export default forwardRef(({ value, setSaveButton, setTestingButton, setMenu, history, ...props }, ref) => { - const { routeId } = useParams(); - const location = useLocation(); +export default forwardRef( + ({ value, setSaveButton, setTestingButton, setMenu, history, ...props }, ref) => { + const { routeId } = useParams(); + const location = useLocation(); - const viewPlugins = new URLSearchParams(location.search).get('view_plugins'); - const subTab = new URLSearchParams(location.search).get('sub_tab') + const viewPlugins = new URLSearchParams(location.search).get('view_plugins'); + const subTab = new URLSearchParams(location.search).get('sub_tab'); - const childRef = useRef() + const childRef = useRef(); - useImperativeHandle(ref, () => ({ - onTestingButtonClick() { - childRef.current.toggleTryIt() - } - })) + useImperativeHandle(ref, () => ({ + onTestingButtonClick() { + childRef.current.toggleTryIt(); + }, + })); - useEffect(() => { - if (location?.state?.showTryIt) - childRef.current.toggleTryIt() - }, [location.state]) + useEffect(() => { + if (location?.state?.showTryIt) childRef.current.toggleTryIt(); + }, [location.state]); - return ( - - ); -}) + return ( + + ); + } +); const FrontendNode = ({ frontend, selectedNode, setSelectedNode, removeNode }) => (
@@ -285,26 +291,31 @@ const FrontendNode = ({ frontend, selectedNode, setSelectedNode, removeNode }) = ); const Container = ({ children, onClick }) => { - const [propagate, setPropagate] = useState() - - return
{ - setPropagate(!document.getElementById('form-container')?.contains(e.target) && - ![...document.getElementsByClassName("delete-node-button")].find(d => d.contains(e.target))) - // && - // ![...document.getElementsByClassName("fa-chevron")].find(d => d.contains(e.target)) - // ) - }} - onMouseUp={(e) => { - e.stopPropagation(); - if (propagate) - onClick(e) + const [propagate, setPropagate] = useState(); - setPropagate(false) - }}> - {children} -
+ return ( +
{ + setPropagate( + !document.getElementById('form-container')?.contains(e.target) && + ![...document.getElementsByClassName('delete-node-button')].find((d) => + d.contains(e.target) + ) + ); + // && + // ![...document.getElementsByClassName("fa-chevron")].find(d => d.contains(e.target)) + // ) + }} + onMouseUp={(e) => { + e.stopPropagation(); + if (propagate) onClick(e); + + setPropagate(false); + }}> + {children} +
+ ); }; const BackendNode = ({ selectedNode, backend, ...props }) => ( @@ -459,7 +470,7 @@ class Designer extends React.Component { TransformResponse: true, }, advancedDesignerView: null, - showTryIt: false + showTryIt: false, }; componentDidMount() { @@ -469,8 +480,8 @@ class Designer extends React.Component { } toggleTryIt = () => { - this.setState({ showTryIt: true }) - } + this.setState({ showTryIt: true }); + }; injectSaveButton = () => { this.props.setSaveButton( @@ -508,10 +519,16 @@ class Designer extends React.Component { ); - injectDefaultMenu = () => + injectDefaultMenu = () => ( + + ); injectNavbarMenu = () => { if (this.props.viewPlugins && this.props.viewPlugins !== -1) @@ -529,7 +546,7 @@ class Designer extends React.Component { hiddenSteps: hiddenSteps[route.id], }); } - } catch (_) { } + } catch (_) {} } }; @@ -545,7 +562,7 @@ class Designer extends React.Component { [this.state.route.id]: newHiddenSteps, }) ); - } catch (_) { } + } catch (_) {} } else { localStorage.setItem( 'hidden_steps', @@ -572,11 +589,11 @@ class Designer extends React.Component { let route = this.props.viewPlugins !== null && this.props.viewPlugins !== -1 ? { - ...r, - overridePlugins: true, - plugins: [], - ...r.routes[~~this.props.viewPlugins], - } + ...r, + overridePlugins: true, + plugins: [], + ...r.routes[~~this.props.viewPlugins], + } : r; if (route.error) { @@ -864,14 +881,14 @@ class Designer extends React.Component { }; clearPlugins = () => { - window.newConfirm('Are you sure you want to delete all current plugins ?').then(ok => { + window.newConfirm('Are you sure you want to delete all current plugins ?').then((ok) => { if (ok) { const newRoute = this.state.route; newRoute.plugins = []; this.setState({ route: newRoute, nodes: [] }); this.updateRoute({ ...newRoute }); } - }) + }); }; deleteRoute = () => { @@ -1099,8 +1116,8 @@ class Designer extends React.Component { plugin_index: Object.fromEntries( Object.entries( plugin.plugin_index || - this.state.nodes.find((n) => n.nodeId === plugin.nodeId)?.plugin_index || - {} + this.state.nodes.find((n) => n.nodeId === plugin.nodeId)?.plugin_index || + {} ).map(([key, v]) => [snakeCase(key), v]) ), })), @@ -1284,7 +1301,7 @@ class Designer extends React.Component { searched, backend, advancedDesignerView, - showTryIt + showTryIt, } = this.state; const { serviceMode } = this.props; @@ -1292,17 +1309,17 @@ class Designer extends React.Component { const backendCallNodes = route && route.plugins ? route.plugins - .map((p) => { - const id = p.plugin; - const pluginDef = plugins.filter((pl) => pl.id === id)[0]; - if (pluginDef) { - if (pluginDef.plugin_steps.indexOf('CallBackend') > -1) { - return { ...p, ...pluginDef }; + .map((p) => { + const id = p.plugin; + const pluginDef = plugins.filter((pl) => pl.id === id)[0]; + if (pluginDef) { + if (pluginDef.plugin_steps.indexOf('CallBackend') > -1) { + return { ...p, ...pluginDef }; + } } - } - return null; - }) - .filter((p) => !!p) + return null; + }) + .filter((p) => !!p) : []; const patterns = getPluginsPatterns(plugins, this.setNodes, this.addNodes, this.clearPlugins); @@ -1310,7 +1327,7 @@ class Designer extends React.Component { // TODO - better error display if (!loading && this.state.notFound) return

Route not found

; - const FullForm = showTryIt ? TryIt : advancedDesignerView + const FullForm = showTryIt ? TryIt : advancedDesignerView; return ( @@ -1320,24 +1337,33 @@ class Designer extends React.Component { selectedNode: undefined, }); }}> - {FullForm && { - this.setState({ route }) - }} - hide={e => { - e.stopPropagation() - this.setState({ - selectedNode: backendCallNodes.find(node => node.id.includes(FullForm.name !== 'GraphQLForm' ? - (FullForm.name === 'TryIt' ? this.state.selectedNode : - "otoroshi.next.plugins.MockResponses") : - "otoroshi.next.plugins.GraphQLBackend")), - advancedDesignerView: false - }) + {FullForm && ( + { + this.setState({ route }); + }} + hide={(e) => { + e.stopPropagation(); + this.setState({ + selectedNode: backendCallNodes.find((node) => + node.id.includes( + FullForm.name !== 'GraphQLForm' + ? FullForm.name === 'TryIt' + ? this.state.selectedNode + : 'otoroshi.next.plugins.MockResponses' + : 'otoroshi.next.plugins.GraphQLBackend' + ) + ), + advancedDesignerView: false, + }); - this.setState({ - showTryIt: false - }) - }} />} + this.setState({ + showTryIt: false, + }); + }} + /> + )} - this.setState({ advancedDesignerView: pluginName })} + showAdvancedDesignerView={(pluginName) => + this.setState({ advancedDesignerView: pluginName }) + } serviceMode={serviceMode} clearPlugins={this.clearPlugins} deleteRoute={this.deleteRoute} @@ -1633,13 +1660,13 @@ const UnselectedNode = ({ hideText, route, clearPlugins, deleteRoute }) => { const allMethods = frontend.methods && frontend.methods.length > 0 ? frontend.methods.map((m, i) => ( - - {m} - - )) + + {m} + + )) : [ALL]; return ( <> @@ -1754,8 +1781,8 @@ const UnselectedNode = ({ hideText, route, clearPlugins, deleteRoute }) => { const start = target.tls ? 'https://' : 'http://'; const mtls = target.tls_config && - target.tls_config.enabled && - [...target.tls_config.certs, ...target.tls_config.trusted_certs].length > 0 ? ( + target.tls_config.enabled && + [...target.tls_config.certs, ...target.tls_config.trusted_certs].length > 0 ? ( mTLS @@ -1783,7 +1810,9 @@ const UnselectedNode = ({ hideText, route, clearPlugins, deleteRoute }) => {
- +
@@ -1816,8 +1845,9 @@ const EditViewHeader = ({ icon, name, id, onCloseForm }) => (
{name || id}
@@ -1947,11 +1977,11 @@ class EditView extends React.Component { isFrontendOrBackend ? undefined : 'status', isPluginWithConfiguration ? { - label: isFrontendOrBackend ? null : 'Plugin', - flow: ['plugin'], - collapsed: false, - collapsable: false, - } + label: isFrontendOrBackend ? null : 'Plugin', + flow: ['plugin'], + collapsed: false, + collapsable: false, + } : undefined, ].filter((f) => f); @@ -1979,10 +2009,10 @@ class EditView extends React.Component { }; } - const overridePlugin = PLUGINS[id] + const overridePlugin = PLUGINS[id]; if (overridePlugin) { - formSchema.plugin = overridePlugin(formSchema.plugin, this.props.showAdvancedDesignerView) + formSchema.plugin = overridePlugin(formSchema.plugin, this.props.showAdvancedDesignerView); } let value = route[selectedNode.field]; // matching Frontend and Backend case diff --git a/otoroshi/javascript/src/pages/RouteDesigner/Graph.js b/otoroshi/javascript/src/pages/RouteDesigner/Graph.js index 432c855e68..99a2d0e0c0 100644 --- a/otoroshi/javascript/src/pages/RouteDesigner/Graph.js +++ b/otoroshi/javascript/src/pages/RouteDesigner/Graph.js @@ -118,7 +118,7 @@ export const PLUGINS = { }, form_data: { ...plugin.schema.form_data, - visible: false + visible: false, }, ...plugin.schema, }, @@ -164,7 +164,12 @@ export const DEFAULT_FLOW = { const hostname = getFieldValue('hostname') || ''; const isSecured = getFieldValue('tls'); - onChange('custom_target', `${isSecured ? 'https' : 'http'}://${hostname}${port}${getFieldValue('custom_target')?.endsWith(' ') ? ' ' : ''}`); + onChange( + 'custom_target', + `${isSecured ? 'https' : 'http'}://${hostname}${port}${ + getFieldValue('custom_target')?.endsWith(' ') ? ' ' : '' + }` + ); }, schema: { custom_target: { @@ -172,13 +177,17 @@ export const DEFAULT_FLOW = { type: 'string', disabled: true, render: ({ value, onChange }) => { - const open = value.endsWith(" ") - return
onChange(open ? value.slice(0, -1) : `${value} `)}> - - - {value} -
- } + const open = value.endsWith(' '); + return ( +
onChange(open ? value.slice(0, -1) : `${value} `)}> + + + {value} +
+ ); + }, }, ...Object.fromEntries( Object.entries(generatedSchema.targets.schema).map(([key, value]) => [ @@ -188,7 +197,7 @@ export const DEFAULT_FLOW = { visible: { ref: parentNode, test: (v, idx) => { - return !!v.targets[idx]?.custom_target.endsWith(" "); + return !!v.targets[idx]?.custom_target.endsWith(' '); }, }, }, @@ -198,7 +207,7 @@ export const DEFAULT_FLOW = { ...generatedSchema.targets.schema.hostname, visible: { ref: parentNode, - test: (v, idx) => !!v.targets[idx]?.custom_target.endsWith(" "), + test: (v, idx) => !!v.targets[idx]?.custom_target.endsWith(' '), }, constraints: [ { diff --git a/otoroshi/javascript/src/pages/RouteDesigner/GraphQLForm.js b/otoroshi/javascript/src/pages/RouteDesigner/GraphQLForm.js index 3674e453c1..0f0b641da4 100644 --- a/otoroshi/javascript/src/pages/RouteDesigner/GraphQLForm.js +++ b/otoroshi/javascript/src/pages/RouteDesigner/GraphQLForm.js @@ -1,298 +1,363 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react'; import { graphqlSchemaToJson, jsonToGraphqlSchema } from '../../services/BackOfficeServices'; -import { CodeInput, Form } from '@maif/react-forms' +import { CodeInput, Form } from '@maif/react-forms'; import { isEqual } from 'lodash'; -import { FeedbackButton } from './FeedbackButton' +import { FeedbackButton } from './FeedbackButton'; export default class GraphQLForm extends React.Component { state = { schemaView: false, - tmpSchema: this.props.route?.plugins - .find(p => p.plugin === "cp:otoroshi.next.plugins.GraphQLBackend")?.config.schema - } + tmpSchema: this.props.route?.plugins.find( + (p) => p.plugin === 'cp:otoroshi.next.plugins.GraphQLBackend' + )?.config.schema, + }; render() { - const { route, hide, saveRoute } = this.props + const { route, hide, saveRoute } = this.props; - if (!route) - return null + if (!route) return null; return (
-
{ +
{ if (s) { - const plugin = route.plugins.find(p => p.plugin === "cp:otoroshi.next.plugins.GraphQLBackend")?.config - this.setState({ schemaView: s, tmpSchema: plugin.schema }) - } else - this.setState({ schemaView: s }) - }} /> - {this.state.schemaView ? <> - { - this.props.saveRoute({ - ...route, - plugins: route.plugins.map(p => { - if (p.plugin === "cp:otoroshi.next.plugins.GraphQLBackend") - return { - ...p, - config: { - ...p.config, - schema: e - } - } - return p - }) - }) - this.setState({ tmpSchema: e }) - }} - /> - - : - } + const plugin = route.plugins.find( + (p) => p.plugin === 'cp:otoroshi.next.plugins.GraphQLBackend' + )?.config; + this.setState({ schemaView: s, tmpSchema: plugin.schema }); + } else this.setState({ schemaView: s }); + }} + /> + {this.state.schemaView ? ( + <> + { + this.props.saveRoute({ + ...route, + plugins: route.plugins.map((p) => { + if (p.plugin === 'cp:otoroshi.next.plugins.GraphQLBackend') + return { + ...p, + config: { + ...p.config, + schema: e, + }, + }; + return p; + }), + }); + this.setState({ tmpSchema: e }); + }} + /> + + ) : ( + + )}
- ) + ); } } const CreationButton = ({ confirm, text, placeholder, className }) => { - const [onCreationField, setCreationField] = useState(false) - const [fieldname, setFieldname] = useState("") + const [onCreationField, setCreationField] = useState(false); + const [fieldname, setFieldname] = useState(''); if (onCreationField) - return
e.stopPropagation()}> - setFieldname(e.target.value)} - placeholder={placeholder} - className="form-control flex" /> - - -
+ return ( +
e.stopPropagation()}> + setFieldname(e.target.value)} + placeholder={placeholder} + className="form-control flex" + /> + + +
+ ); - return -} + return ( + + ); +}; class SideView extends React.Component { state = { types: [], selectedField: undefined, - error: undefined - } + error: undefined, + }; componentDidMount() { - const plugin = this.props.route.plugins.find(p => p.plugin === "cp:otoroshi.next.plugins.GraphQLBackend")?.config - graphqlSchemaToJson(plugin.schema) - .then(res => { - if (res.error) - this.setState({ error: res.error }) - else - this.setState({ - error: undefined, - types: res.types.map(type => ({ - ...type, - fields: (type.fields || []).map(field => ({ - ...field, - directives: (field.directives || []).map(directive => ({ - ...directive, - arguments: (directive.arguments || []).reduce((acc, argument) => ({ + const plugin = this.props.route.plugins.find( + (p) => p.plugin === 'cp:otoroshi.next.plugins.GraphQLBackend' + )?.config; + graphqlSchemaToJson(plugin.schema).then((res) => { + if (res.error) this.setState({ error: res.error }); + else + this.setState({ + error: undefined, + types: res.types.map((type) => ({ + ...type, + fields: (type.fields || []).map((field) => ({ + ...field, + directives: (field.directives || []).map((directive) => ({ + ...directive, + arguments: (directive.arguments || []).reduce( + (acc, argument) => ({ ...acc, - [argument.name]: this.transformValue(argument.value) - }), {}) - })) - })) - })) - }); - }) + [argument.name]: this.transformValue(argument.value), + }), + {} + ), + })), + })), + })), + }); + }); } - transformValue = v => { + transformValue = (v) => { try { - if (Array.isArray(v)) - return v + if (Array.isArray(v)) return v; return JSON.parse(v); } catch (_) { return v; } - } + }; onSelectField = (typeIdx, fieldIdx) => { - const field = this.state.types[typeIdx].fields[fieldIdx] - this.setState({ selectedField: undefined }, () => this.setState({ - selectedField: { - field, - fieldIdx, - typeIdx - } - })) - } + const field = this.state.types[typeIdx].fields[fieldIdx]; + this.setState({ selectedField: undefined }, () => + this.setState({ + selectedField: { + field, + fieldIdx, + typeIdx, + }, + }) + ); + }; - transformTypes = types => { - return types.map(type => ({ + transformTypes = (types) => { + return types.map((type) => ({ ...type, - fields: (type.fields || []).map(field => ({ + fields: (type.fields || []).map((field) => ({ ...field, - directives: (field.directives || []).map(directive => ({ + directives: (field.directives || []).map((directive) => ({ ...directive, arguments: Object.entries(directive.arguments || []).map(([k, v]) => ({ - [k]: v - })) - })) - })) - })) - } + [k]: v, + })), + })), + })), + })); + }; savePlugin = () => { - const plugin = this.props.route.plugins.find(p => p.plugin === "cp:otoroshi.next.plugins.GraphQLBackend")?.config - return jsonToGraphqlSchema(plugin.schema, this.transformTypes(this.state.types)) - .then(res => { - if (res.error) - this.setState({ - error: res.error - }) - else { - this.setState({ - error: undefined - }) - this.props.saveRoute({ - ...this.props.route, - plugins: this.props.route.plugins.map(p => { - if (p.plugin === "cp:otoroshi.next.plugins.GraphQLBackend") - return { - ...p, - config: { - ...p.config, - schema: res.schema - } - } - return p - }) - }) - } - }) - } + const plugin = this.props.route.plugins.find( + (p) => p.plugin === 'cp:otoroshi.next.plugins.GraphQLBackend' + )?.config; + return jsonToGraphqlSchema(plugin.schema, this.transformTypes(this.state.types)).then((res) => { + if (res.error) + this.setState({ + error: res.error, + }); + else { + this.setState({ + error: undefined, + }); + this.props.saveRoute({ + ...this.props.route, + plugins: this.props.route.plugins.map((p) => { + if (p.plugin === 'cp:otoroshi.next.plugins.GraphQLBackend') + return { + ...p, + config: { + ...p.config, + schema: res.schema, + }, + }; + return p; + }), + }); + } + }); + }; removeField = (e, typeIdx, fieldIdx) => { e.stopPropagation(); - this.setState({ - types: this.state.types.map((type, i) => ({ - ...type, - fields: i === typeIdx ? type.fields.filter((_, j) => j !== fieldIdx) : type.fields - })) - }, this.savePlugin) - } + this.setState( + { + types: this.state.types.map((type, i) => ({ + ...type, + fields: i === typeIdx ? type.fields.filter((_, j) => j !== fieldIdx) : type.fields, + })), + }, + this.savePlugin + ); + }; removeType = (e, i) => { e.stopPropagation(); - this.setState( - { types: this.state.types.filter((_, j) => j !== i) }, - this.savePlugin - ) - } + this.setState({ types: this.state.types.filter((_, j) => j !== i) }, this.savePlugin); + }; - createField = (fieldname, i) => this.setState({ - types: this.state.types.map((type, t) => { - if (t === i) - return ({ - ...type, - fields: [...type.fields, { - name: fieldname, - fieldType: { - type: "String", - isList: false - }, - arguments: [], - directives: [] - }] - }) - return type - }) - }, () => { - this.onSelectField(i, this.state.types[i].fields.length - 1) - this.savePlugin() - }) + createField = (fieldname, i) => + this.setState( + { + types: this.state.types.map((type, t) => { + if (t === i) + return { + ...type, + fields: [ + ...type.fields, + { + name: fieldname, + fieldType: { + type: 'String', + isList: false, + }, + arguments: [], + directives: [], + }, + ], + }; + return type; + }), + }, + () => { + this.onSelectField(i, this.state.types[i].fields.length - 1); + this.savePlugin(); + } + ); render() { // const { route, saveRoute } = this.props; const { types, selectedField, error } = this.state; - return <> -
-
- this.setState({ - types: [...this.state.types, { - name: newFieldname, - directives: [], - fields: [{ - name: "foo", - fieldType: { - type: "String", - isList: false + return ( + <> +
+
+ + this.setState( + { + types: [ + ...this.state.types, + { + name: newFieldname, + directives: [], + fields: [ + { + name: 'foo', + fieldType: { + type: 'String', + isList: false, + }, + arguments: [], + directives: [], + }, + ], + }, + ], }, - arguments: [], - directives: [] - }] - }] - }, () => { - this.onSelectField(this.state.types.length - 1, 0) - this.savePlugin() - })} /> - {error && {error}} - {types.map((type, i) => selectedField ? selectedField.typeIdx === i && selectedField.fieldIdx === fieldIdx : undefined} - isSelectedType={selectedField ? selectedField.typeIdx === i : undefined} - removeType={e => this.removeType(e, i)} - onSelectField={fieldIdx => this.onSelectField(i, fieldIdx)} - removeField={(e, fieldIdx) => this.removeField(e, i, fieldIdx)} - createField={fieldname => this.createField(fieldname, i)} />)} -
-
- {selectedField && t.name !== "Query" && t.name !== types[selectedField.typeIdx].name).map(t => t.name)} - onChange={newField => this.setState({ - types: types.map((type, i) => { - if (i === selectedField.typeIdx) - return ({ - ...type, - fields: type.fields.map((field, j) => { - if (j === selectedField.fieldIdx) - return newField - return field - }) - }) - return type - }) - }, this.savePlugin)} - />} + () => { + this.onSelectField(this.state.types.length - 1, 0); + this.savePlugin(); + } + ) + } + /> + {error && ( + + {error} + + )} + {types.map((type, i) => ( + + selectedField + ? selectedField.typeIdx === i && selectedField.fieldIdx === fieldIdx + : undefined + } + isSelectedType={selectedField ? selectedField.typeIdx === i : undefined} + removeType={(e) => this.removeType(e, i)} + onSelectField={(fieldIdx) => this.onSelectField(i, fieldIdx)} + removeField={(e, fieldIdx) => this.removeField(e, i, fieldIdx)} + createField={(fieldname) => this.createField(fieldname, i)} + /> + ))} +
+
+ {selectedField && ( + t.name !== 'Query' && t.name !== types[selectedField.typeIdx].name) + .map((t) => t.name)} + onChange={(newField) => + this.setState( + { + types: types.map((type, i) => { + if (i === selectedField.typeIdx) + return { + ...type, + fields: type.fields.map((field, j) => { + if (j === selectedField.fieldIdx) return newField; + return field; + }), + }; + return type; + }), + }, + this.savePlugin + ) + } + /> + )} +
-
- + + ); } } @@ -314,17 +379,17 @@ class FieldForm extends React.Component { format: 'select', type: 'string', createOption: true, - options: ['Int', 'String', 'Boolean', 'Float', ...this.props.types] + options: ['Int', 'String', 'Boolean', 'Float', ...this.props.types], }, required: { type: 'bool', - label: 'Is required ?' + label: 'Is required ?', }, isList: { type: 'bool', - label: 'Is a list of ?' - } - } + label: 'Is a list of ?', + }, + }, }, arguments: { type: 'object', @@ -335,7 +400,7 @@ class FieldForm extends React.Component { schema: { name: { type: 'string', - label: 'Argument name' + label: 'Argument name', }, valueType: { label: 'Argument value', @@ -347,19 +412,19 @@ class FieldForm extends React.Component { label: null, format: 'select', type: 'string', - options: ['Int', 'String', 'Boolean', 'Float'] + options: ['Int', 'String', 'Boolean', 'Float'], }, required: { type: 'bool', - label: 'Is a required argument ?' + label: 'Is a required argument ?', }, isList: { type: 'bool', - label: 'Is a list of ?' - } - } - } - } + label: 'Is a list of ?', + }, + }, + }, + }, }, directives: { type: 'object', @@ -368,15 +433,23 @@ class FieldForm extends React.Component { format: 'form', flow: ['name', 'arguments'], schema: { - 'name': { + name: { type: 'string', label: 'Type', format: 'select', options: [ - 'rest', 'graphql', 'json', 'soap', 'permission', 'allpermissions', 'onePermissionsOf', 'authorize', 'otoroshi (soon)' - ] + 'rest', + 'graphql', + 'json', + 'soap', + 'permission', + 'allpermissions', + 'onePermissionsOf', + 'authorize', + 'otoroshi (soon)', + ], }, - 'arguments': { + arguments: { type: 'object', label: 'Arguments', format: 'form', @@ -388,93 +461,101 @@ class FieldForm extends React.Component { schema: { url: { type: 'string', - label: 'URL' + label: 'URL', }, method: { type: 'string', label: 'HTTP Method', format: 'select', defaultValue: 'GET', - options: ['GET', 'POST'] + options: ['GET', 'POST'], }, headers: { type: 'object', - label: 'Header' + label: 'Header', }, timeout: { type: 'number', label: 'Timeout', - defaultValue: 5000 + defaultValue: 5000, }, paginate: { type: 'bool', label: 'Enable pagination', help: 'Automatically add limit and offset argument', - defaultValue: false - } + defaultValue: false, + }, }, - flow: ['url', 'method', 'headers', 'timeout', 'paginate'] + flow: ['url', 'method', 'headers', 'timeout', 'paginate'], }, { condition: 'graphql', schema: { url: { type: 'string', - label: 'URL' + label: 'URL', }, query: { type: 'string', format: 'code', - label: 'GraphQL Query' + label: 'GraphQL Query', }, headers: { type: 'object', - label: 'Headers' + label: 'Headers', }, timeout: { type: 'number', defaultValue: 5000, - label: 'Timeout' + label: 'Timeout', }, method: { type: 'string', format: 'select', defaultValue: 'GET', options: ['GET', 'POST'], - label: 'HTTP Method' + label: 'HTTP Method', }, response_path_arg: { type: 'string', - label: 'JSON Response path' + label: 'JSON Response path', }, response_filter_arg: { type: 'string', - label: 'JSON response filter path' - } + label: 'JSON response filter path', + }, }, - flow: ['url', 'query', 'headers', 'timeout', 'method', 'response_path_arg', 'response_filter_arg'] + flow: [ + 'url', + 'query', + 'headers', + 'timeout', + 'method', + 'response_path_arg', + 'response_filter_arg', + ], }, { condition: 'json', schema: { path: { type: 'string', - label: 'JSON path' - } - } + label: 'JSON path', + }, + }, }, { condition: 'permission', schema: { value: { type: 'string', - label: 'Value' + label: 'Value', }, unauthorized_value: { type: 'string', - label: 'Unauthorized message' - } - } + label: 'Unauthorized message', + }, + }, }, { condition: 'allpermissions', @@ -482,13 +563,13 @@ class FieldForm extends React.Component { values: { type: 'string', array: true, - label: 'Values' + label: 'Values', }, unauthorized_value: { type: 'string', - label: 'Unauthorized message' - } - } + label: 'Unauthorized message', + }, + }, }, { condition: 'onePermissionsOf', @@ -496,190 +577,221 @@ class FieldForm extends React.Component { values: { type: 'string', array: true, - label: 'Values' + label: 'Values', }, unauthorized_value: { type: 'string', - label: 'Unauthorized message' - } - } + label: 'Unauthorized message', + }, + }, }, { condition: 'authorize', schema: { path: { type: 'string', - label: 'JSON Path' + label: 'JSON Path', }, value: { type: 'string', - label: 'Value' + label: 'Value', }, unauthorized_value: { type: 'string', - label: 'Unauthorized message' - } - } + label: 'Unauthorized message', + }, + }, }, { condition: 'soap', schema: { url: { - type: "string", - label: 'URL' + type: 'string', + label: 'URL', }, envelope: { type: 'string', - label: 'Envelope' + label: 'Envelope', }, action: { type: 'string', - label: 'Action' + label: 'Action', }, preserve_query: { type: 'bool', defaultValue: true, - label: 'Preserve query' + label: 'Preserve query', }, charset: { type: 'string', - label: 'Charset' + label: 'Charset', }, jq_request_filter: { type: 'string', - label: 'JQ Request filter' + label: 'JQ Request filter', }, jq_response_filter: { type: 'string', - label: 'JQ Response filter' - } - } - } - ] - } - } - } - } + label: 'JQ Response filter', + }, + }, + }, + ], + }, + }, + }, + }, }, - formState: null - } + formState: null, + }; componentDidMount() { this.setState({ - formState: this.props.field - }) + formState: this.props.field, + }); } componentDidUpdate(prevProps) { if (!isEqual(prevProps.field, this.props.field)) this.setState({ - formState: this.props.field - }) + formState: this.props.field, + }); } render() { - if (!this.state.formState) - return null; + if (!this.state.formState) return null; const { field, onChange } = this.props; - return
-
{ }} - options={{ autosubmit: true }} - onSubmit={data => { - onChange(data) - }} - footer={() => null} - /> -
+ return ( +
+ {}} + options={{ autosubmit: true }} + onSubmit={(data) => { + onChange(data); + }} + footer={() => null} + /> +
+ ); } } -const Type = ({ name, kind, fields, onSelectField, createField, isSelected, removeField, removeType, isSelectedType }) => { +const Type = ({ + name, + kind, + fields, + onSelectField, + createField, + isSelected, + removeField, + removeType, + isSelectedType, +}) => { const [open, setOpen] = useState(false); useEffect(() => { - if (!open && isSelectedType) - setOpen(true) - }, [isSelectedType]) + if (!open && isSelectedType) setOpen(true); + }, [isSelectedType]); const selectField = (e, i) => { - e.stopPropagation() - onSelectField(i) - } + e.stopPropagation(); + onSelectField(i); + }; - return
setOpen(!open)} className="mb-1"> -
-
- - {name} - {kind} -
-
- {open && } - {fields ? fields.length : 0} fields + return ( +
setOpen(!open)} className="mb-1"> +
+
+ + {name} + {kind} +
+
+ {open && ( + + )} + {fields ? fields.length : 0} fields +
+ {open && + (fields || []).map((field, i) => ( +
selectField(e, i)}> +
+ {field.name} + + {field.fieldType.type} + + + {field.fieldType.isList ? 'LIST' : '\u00a0\u00a0'} + +
+
+ {isSelected(i) === true && ( + + )} + +
+
+ ))} + {open && ( + + )}
- {open && (fields || []).map((field, i) =>
selectField(e, i)} > -
- {field.name} - {field.fieldType.type} - - {field.fieldType.isList ? 'LIST' : '\u00a0\u00a0'} - + ); +}; + +const Header = ({ hide, schemaView, toggleSchema }) => ( +
+
+
+

GraphQL Schema Editor

+
-
- {isSelected(i) === true && } - +
-
)} - {open && } -
-} - -const Header = ({ hide, schemaView, toggleSchema }) =>
-
-
-

GraphQL Schema Editor

- -
-
- -
-
+); diff --git a/otoroshi/javascript/src/pages/RouteDesigner/Informations.js b/otoroshi/javascript/src/pages/RouteDesigner/Informations.js index a211b0c5c8..e6b83f9729 100644 --- a/otoroshi/javascript/src/pages/RouteDesigner/Informations.js +++ b/otoroshi/javascript/src/pages/RouteDesigner/Informations.js @@ -9,7 +9,7 @@ import { FeedbackButton } from './FeedbackButton'; export const Informations = forwardRef(({ isCreation, value, setValue, setSaveButton }, ref) => { const history = useHistory(); - const location = useLocation() + const location = useLocation(); const [informations, setInformations] = useState({ ...value }); const { capitalize, lowercase, fetchName, link } = useEntityFromURI(); @@ -20,9 +20,9 @@ export const Informations = forwardRef(({ isCreation, value, setValue, setSaveBu useImperativeHandle(ref, () => ({ onTestingButtonClick() { - history.push(`/routes/${value.id}?tab=flow`, { showTryIt: true }) - } - })) + history.push(`/routes/${value.id}?tab=flow`, { showTryIt: true }); + }, + })); useEffect(() => { setSaveButton(saveButton('ms-2')); diff --git a/otoroshi/javascript/src/pages/RouteDesigner/MocksDesigner.js b/otoroshi/javascript/src/pages/RouteDesigner/MocksDesigner.js index fe66e9921d..c70459ff46 100644 --- a/otoroshi/javascript/src/pages/RouteDesigner/MocksDesigner.js +++ b/otoroshi/javascript/src/pages/RouteDesigner/MocksDesigner.js @@ -1,860 +1,1019 @@ -import React, { useState } from 'react' -import faker from 'faker' -import { CodeInput } from '@maif/react-forms' -import { FeedbackButton } from './FeedbackButton' -import { BooleanInput, Help, NumberInput, ObjectInput, SelectInput, SimpleBooleanInput, TextInput } from '../../components/inputs' +import React, { useState } from 'react'; +import faker from 'faker'; +import { CodeInput } from '@maif/react-forms'; +import { FeedbackButton } from './FeedbackButton'; +import { + BooleanInput, + Help, + NumberInput, + ObjectInput, + SelectInput, + SimpleBooleanInput, + TextInput, +} from '../../components/inputs'; const castValue = (value, type) => { - if (type === 'String') - return value - else { - try { - if (type === 'Number') - return parseInt(value) - else - return JSON.parse(value) - } catch (err) { - return value - } - } -} - -const stringify = body => { - if (typeof body === 'object' && body !== null) - try { - return JSON.stringify(body) - } catch (err) { - return body - } - return body -} - -const generateFakerValues = (resources, endpoint) => { - const { resource_list, body, length } = endpoint - - const calculateResource = resource => { - const newItem = r => (r.schema || []).reduce((acc, item) => ({ - ...acc, - ...calculateField(item) - }), JSON.parse((resource.additional_data || "{}"))) - return newItem(resource) + if (type === 'String') return value; + else { + try { + if (type === 'Number') return parseInt(value); + else return JSON.parse(value); + } catch (err) { + return value; } + } +}; - const fakeValue = item => { - try { - return castValue(item.value - .split(".") - .reduce((a, c) => a[c], faker)(), item.field_type) - } catch (err) { - return castValue(item.value, item.field_type) - } +const stringify = (body) => { + if (typeof body === 'object' && body !== null) + try { + return JSON.stringify(body); + } catch (err) { + return body; } + return body; +}; - const calculateField = item => ({ - [item.field_name]: item.field_type === "Child" ? - calculateResource(resources.find(f => f.name === item.value)) : - fakeValue(item) - }) - - if (endpoint.resource) { - const resource = resources.find(f => f.name === endpoint.resource) - if (!resource) - return {} - - const newItem = r => (r.schema || []).reduce((acc, item) => ({ - ...acc, - ...calculateField(item) - }), JSON.parse(resource.additional_data || "{}")) - if (resource_list) - return Array.from({ length: length || 10 }, (_, i) => newItem(resource)) - return newItem(resource) - } else { - if (resource_list) - return Array.from({ length: length || 10 }, (_, i) => body) - return body +const generateFakerValues = (resources, endpoint) => { + const { resource_list, body, length } = endpoint; + + const calculateResource = (resource) => { + const newItem = (r) => + (r.schema || []).reduce( + (acc, item) => ({ + ...acc, + ...calculateField(item), + }), + JSON.parse(resource.additional_data || '{}') + ); + return newItem(resource); + }; + + const fakeValue = (item) => { + try { + return castValue(item.value.split('.').reduce((a, c) => a[c], faker)(), item.field_type); + } catch (err) { + return castValue(item.value, item.field_type); } -} + }; + + const calculateField = (item) => ({ + [item.field_name]: + item.field_type === 'Child' + ? calculateResource(resources.find((f) => f.name === item.value)) + : fakeValue(item), + }); + + if (endpoint.resource) { + const resource = resources.find((f) => f.name === endpoint.resource); + if (!resource) return {}; + + const newItem = (r) => + (r.schema || []).reduce( + (acc, item) => ({ + ...acc, + ...calculateField(item), + }), + JSON.parse(resource.additional_data || '{}') + ); + if (resource_list) return Array.from({ length: length || 10 }, (_, i) => newItem(resource)); + return newItem(resource); + } else { + if (resource_list) return Array.from({ length: length || 10 }, (_, i) => body); + return body; + } +}; -const CharlatanActions = ({ generateData, resetData, showGenerateEndpointForm }) =>
+const CharlatanActions = ({ generateData, resetData, showGenerateEndpointForm }) => ( +
} - text="Generate missing data" + onPress={generateData} + icon={() => } + text="Generate missing data" /> -
- -const CharlatanResourcesList = ({ showResourceForm, resources, removeResource }) =>
-
-

Resources

- +
+); + +const CharlatanResourcesList = ({ showResourceForm, resources, removeResource }) => ( +
+
+

Resources

+
-
- {resources.map((resource, idx) => { - return
showResourceForm(idx)}> - - -
- })} +
+ {resources.map((resource, idx) => { + return ( +
showResourceForm(idx)}> + + +
+ ); + })}
{resources.length === 0 && No resources available} -
- -const CharlatanEndpointsList = ({ showEndpointForm, endpoints, removeEndpoint, showData }) =>
-
-

Endpoints

- +
+); + +const CharlatanEndpointsList = ({ showEndpointForm, endpoints, removeEndpoint, showData }) => ( +
+
+

Endpoints

+
-
- {endpoints - .sort((a, b) => a.path.localeCompare(b.path)) - .map((endpoint, idx) => { - return
showEndpointForm(idx)}> -
-
- - {endpoint.method} - -
- {endpoint.path} - - - {endpoint.status} - -
-
- {!endpoint.body && !endpoint.resource && -
- -
} - - -
+
+ {endpoints + .sort((a, b) => a.path.localeCompare(b.path)) + .map((endpoint, idx) => { + return ( +
showEndpointForm(idx)}> +
+
+ + {endpoint.method} +
- })} - {endpoints.length === 0 && No endpoints available} + + {endpoint.path} + + + {endpoint.status} +
+
+ {!endpoint.body && !endpoint.resource && ( +
+ +
+ )} + + +
+
+ ); + })} + {endpoints.length === 0 && No endpoints available}
-
+
+); export default class MocksDesigner extends React.Component { - state = { - resources: [], - endpoints: [] - } - - static getDerivedStateFromProps(props) { - if (props.route) { - const plugin = props.route - .plugins - .find(p => p.plugin === "cp:otoroshi.next.plugins.MockResponses")?.config - - if (plugin && plugin.form_data) - return { ...plugin.form_data } - } - return { - resources: [], endpoints: [] - } + state = { + resources: [], + endpoints: [], + }; + + static getDerivedStateFromProps(props) { + if (props.route) { + const plugin = props.route.plugins.find( + (p) => p.plugin === 'cp:otoroshi.next.plugins.MockResponses' + )?.config; + + if (plugin && plugin.form_data) return { ...plugin.form_data }; } - - saveRoute = res => Promise.resolve(this.props.saveRoute({ + return { + resources: [], + endpoints: [], + }; + } + + saveRoute = (res) => + Promise.resolve( + this.props.saveRoute({ ...this.props.route, - plugins: this.props.route.plugins.map(p => { - const config = { - ...this.state, - ...(res || {}) - } - if (p.plugin === "cp:otoroshi.next.plugins.MockResponses") - return { - ...p, - config: { - ...p.config, - responses: this.configToResponses(config), - form_data: config - } - } - return p - }) - })) - - configToResponses = config => config.endpoints.map(({ path, method, status, headers, body }) => ({ - path, method, status, headers, body: stringify(body) - })) - - setAndSave = res => this.saveRoute(res) - - showForm = (title, idx, elementName, render) => { - const resource = this.state[elementName].find((_, i) => i === idx) - window - .popup( - title, - (ok, cancel) => render(ok, cancel, resource), - { additionalClass: 'designer-modal-dialog' } - ) - .then(data => { - if (data) { - const { value, idx } = data - this.setAndSave({ - [elementName]: Number.isFinite(idx) ? this.state[elementName].map((r, i) => { - if (i === idx) - return value - return r - }) : - [...this.state[elementName], value] - }) - } - }) - } - - showResourceForm = idx => this.showForm('Create a new resource', idx, "resources", (ok, cancel, resource) => - ) - - showEndpointForm = idx => this.showForm('Create a new endpoint', idx, "endpoints", (ok, cancel, endpoint) => - ) - - showGenerateEndpointForm = () => { - window - .popup( - 'Generate an endpoint', - (ok, cancel) => , - { additionalClass: 'designer-modal-dialog' } - ) - .then(generateEndpoints => { - this.setAndSave({ - endpoints: [ - ...this.state.endpoints, - ...generateEndpoints - ] + plugins: this.props.route.plugins.map((p) => { + const config = { + ...this.state, + ...(res || {}), + }; + if (p.plugin === 'cp:otoroshi.next.plugins.MockResponses') + return { + ...p, + config: { + ...p.config, + responses: this.configToResponses(config), + form_data: config, + }, + }; + return p; + }), + }) + ); + + configToResponses = (config) => + config.endpoints.map(({ path, method, status, headers, body }) => ({ + path, + method, + status, + headers, + body: stringify(body), + })); + + setAndSave = (res) => this.saveRoute(res); + + showForm = (title, idx, elementName, render) => { + const resource = this.state[elementName].find((_, i) => i === idx); + window + .popup(title, (ok, cancel) => render(ok, cancel, resource), { + additionalClass: 'designer-modal-dialog', + }) + .then((data) => { + if (data) { + const { value, idx } = data; + this.setAndSave({ + [elementName]: Number.isFinite(idx) + ? this.state[elementName].map((r, i) => { + if (i === idx) return value; + return r; }) - }) - } - - removeEndpoint = idx => { + : [...this.state[elementName], value], + }); + } + }); + }; + + showResourceForm = (idx) => + this.showForm('Create a new resource', idx, 'resources', (ok, cancel, resource) => ( + + )); + + showEndpointForm = (idx) => + this.showForm('Create a new endpoint', idx, 'endpoints', (ok, cancel, endpoint) => ( + + )); + + showGenerateEndpointForm = () => { + window + .popup( + 'Generate an endpoint', + (ok, cancel) => ( + + ), + { additionalClass: 'designer-modal-dialog' } + ) + .then((generateEndpoints) => { this.setAndSave({ - endpoints: this.state.endpoints.filter((_, j) => j !== idx) - }) - } - - removeResource = idx => { + endpoints: [...this.state.endpoints, ...generateEndpoints], + }); + }); + }; + + removeEndpoint = (idx) => { + this.setAndSave({ + endpoints: this.state.endpoints.filter((_, j) => j !== idx), + }); + }; + + removeResource = (idx) => { + this.setAndSave({ + resources: this.state.resources.filter((_, j) => j !== idx), + }); + }; + + showData = (idx) => { + window + .popup( + 'Edit/replace data for users resource. Data must be an array and a valid JSON.', + (ok, cancel) => ( + + ), + { additionalClass: 'designer-modal-dialog' } + ) + .then((res) => { + if (res) + this.setAndSave({ + endpoints: this.state.endpoints.map((endpoint, i) => { + if (i === res.idx) return { ...endpoint, body: res.body }; + return endpoint; + }), + }); + }); + }; + + generateData = () => + this.setAndSave({ + endpoints: this.state.endpoints.map((endpoint) => ({ + ...endpoint, + body: endpoint.body || generateFakerValues(this.state.resources, endpoint), + })), + }); + + resetData = () => { + window.newConfirm(`Are you sure you reset all data ?`).then((ok) => { + if (ok) { this.setAndSave({ - resources: this.state.resources.filter((_, j) => j !== idx) - }) - } - - showData = idx => { - window.popup( - 'Edit/replace data for users resource. Data must be an array and a valid JSON.', - (ok, cancel) => , - { additionalClass: 'designer-modal-dialog' } - ).then(res => { - if (res) - this.setAndSave({ - endpoints: this.state.endpoints.map((endpoint, i) => { - if (i === res.idx) - return { ...endpoint, body: res.body } - return endpoint - }) - }) - }) - } - - generateData = () => this.setAndSave({ - endpoints: this.state.endpoints.map(endpoint => ({ + endpoints: this.state.endpoints.map((endpoint) => ({ ...endpoint, - body: endpoint.body || generateFakerValues(this.state.resources, endpoint) - })) - }) - - resetData = () => { - window.newConfirm(`Are you sure you reset all data ?`) - .then(ok => { - if (ok) { - this.setAndSave({ - endpoints: this.state.endpoints.map(endpoint => ({ - ...endpoint, - body: null - })) - }) - .then(this.generateData) - } - }) - } - - render() { - const { route, hide } = this.props - const { resources, endpoints } = this.state - - if (!route) - return null - - return ( -
-
- -
- -
- - -
-
-
- ) - } -} - -const Data = ({ idx, body, confirm, cancel }) => { - const [res, setRes] = useState(body) - - return
- - -
- - -
-
-} - -const GenerateEndpoint = ({ resources, confirm, cancel }) => { - const [resource, setResource] = useState("") - const [endpoints, setEndpoints] = useState([]) - - const onResourceChange = (name, resourceName) => { - setEndpoints([ - { method: "GET", path: `/${name}s`, enabled: true, resource: resourceName, resource_list: true, status: 200 }, - { method: "GET", path: `/${name}s/:id`, enabled: true, resource: resourceName, status: 200 }, - { method: "POST", path: `/${name}s`, enabled: true, resource: resourceName, status: 201 }, - { method: "PUT", path: `/${name}s/:id`, enabled: true, resource: resourceName, status: 204 }, - { method: "DELETE", path: `/${name}s/:id`, enabled: true, resource: resourceName, status: 202 }, - ]) - } - - return
-
- - { - setResource(e) - onResourceChange(e, e) - }} - possibleValues={resources.map(r => r.name)} + body: null, + })), + }).then(this.generateData); + } + }); + }; + + render() { + const { route, hide } = this.props; + const { resources, endpoints } = this.state; + + if (!route) return null; + + return ( +
+
+ +
+ +
+ -
-
- - { - setResource(e) - onResourceChange(e) - }} + +
-
- - {endpoints.map((endpoint, i) =>
-
- - {endpoint.method} - -
- setEndpoints(endpoints.map((e, j) => { - if (j === i) - return { ...e, path } - return e - }))} - /> -
- setEndpoints(endpoints.map((e, j) => { - if (j === i) - return { ...e, enabled } - return e - }))} /> -
-
)} -
-
- - -
-
+
+ ); + } } -class NewResource extends React.Component { - state = { - name: this.props.resource?.name || '', - schema: this.props.resource?.schema || [], - objectTemplate: this.props.resource?.additional_data || {} - } +const Data = ({ idx, body, confirm, cancel }) => { + const [res, setRes] = useState(body); - onSchemaFieldChange = (idx, field, value, callback) => { - this.setState({ - schema: this.state.schema.map((s, j) => { - if (idx === j) - return { ...s, [field]: value } - return s - }) - }, callback) - } + return ( +
+ - emptyField = className =>
-
- -
+
+ + +
+ ); +}; - render() { - const { name, schema, objectTemplate } = this.state +const GenerateEndpoint = ({ resources, confirm, cancel }) => { + const [resource, setResource] = useState(''); + const [endpoints, setEndpoints] = useState([]); - return
+ const onResourceChange = (name, resourceName) => { + setEndpoints([ + { + method: 'GET', + path: `/${name}s`, + enabled: true, + resource: resourceName, + resource_list: true, + status: 200, + }, + { method: 'GET', path: `/${name}s/:id`, enabled: true, resource: resourceName, status: 200 }, + { method: 'POST', path: `/${name}s`, enabled: true, resource: resourceName, status: 201 }, + { method: 'PUT', path: `/${name}s/:id`, enabled: true, resource: resourceName, status: 204 }, + { + method: 'DELETE', + path: `/${name}s/:id`, + enabled: true, + resource: resourceName, + status: 202, + }, + ]); + }; + + return ( +
+
+ + { + setResource(e); + onResourceChange(e, e); + }} + possibleValues={resources.map((r) => r.name)} + /> +
+
+ + { + setResource(e); + onResourceChange(e); + }} + /> +
+
+ + {endpoints.map((endpoint, i) => ( +
+
+ + {endpoint.method} + +
this.setState({ name: v })} + flex={true} + value={endpoint.path} + onChange={(path) => + setEndpoints( + endpoints.map((e, j) => { + if (j === i) return { ...e, path }; + return e; + }) + ) + } /> -
- -
-
- - - - -
- {schema.map((s, i) => { - const { field_name, field_type, value } = s - const type = field_type - return
- this.onSchemaFieldChange(i, "field_name", v)} - /> - this.onSchemaFieldChange(i, "field_type", v, () => this.onSchemaFieldChange(i, "value", ''))} - possibleValues={[ - 'String', - 'Number', - 'Boolean', - 'Object', - 'Array', - 'Date', - 'Child' - ]} /> - - {type && type !== "Child" ? this.onSchemaFieldChange(i, "value", v)} - possibleValues={FakerOptions} /> - : this.emptyField('me-1')} - {type === "Child" && this.onSchemaFieldChange(i, "value", v)} - possibleValues={this.props.resources.map(a => a.name)} - />} - {!type ? this.emptyField() : (!["Child", "Faker.js"].includes(type) ? this.onSchemaFieldChange(i, "value", castValue(v, type))} - /> : (type !== 'Child' ? this.emptyField() : null))} -
- })} - -
-
-
- -
- this.setState({ objectTemplate: v })} /> -
+
+ + setEndpoints( + endpoints.map((e, j) => { + if (j === i) return { ...e, enabled }; + return e; + }) + ) + } + />
+
+ ))} +
+
+ + +
+
+ ); +}; -
- - +class NewResource extends React.Component { + state = { + name: this.props.resource?.name || '', + schema: this.props.resource?.schema || [], + objectTemplate: this.props.resource?.additional_data || {}, + }; + + onSchemaFieldChange = (idx, field, value, callback) => { + this.setState( + { + schema: this.state.schema.map((s, j) => { + if (idx === j) return { ...s, [field]: value }; + return s; + }), + }, + callback + ); + }; + + emptyField = (className) => ( +
+
+ +
+
+ ); + + render() { + const { name, schema, objectTemplate } = this.state; + + return ( +
+ this.setState({ name: v })} + /> +
+ +
+
+ + + +
-
- } -} - -class NewEndpoint extends React.Component { - state = this.props.endpoint || { - method: 'GET', - path: '/', - status: 200, - body: null, - resource: '', - resource_list: false, - headers: {}, - length: 10 - } - - render() { - const { method, path, status, body, headers, resource, resource_list, length } = this.state - - return
-
-
- - -
-
+ {schema.map((s, i) => { + const { field_name, field_type, value } = s; + const type = field_type; + return ( +
+ this.onSchemaFieldChange(i, 'field_name', v)} + /> + + this.onSchemaFieldChange(i, 'field_type', v, () => + this.onSchemaFieldChange(i, 'value', '') + ) + } + possibleValues={[ + 'String', + 'Number', + 'Boolean', + 'Object', + 'Array', + 'Date', + 'Child', + ]} + /> + + {type && type !== 'Child' ? ( this.setState({ method: v })} + flex={true} + value={value} + className="me-1" + onChange={(v) => this.onSchemaFieldChange(i, 'value', v)} + possibleValues={FakerOptions} /> -
- this.setState({ path: v })} - placeholder="Request path" - /> -
-
-
-
- -
+ ) : ( + this.emptyField('me-1') + )} + {type === 'Child' && ( this.setState({ resource: v })} - possibleValues={this.props.resources.map(r => r.name)} + flex={true} + value={value} + onChange={(v) => this.onSchemaFieldChange(i, 'value', v)} + possibleValues={this.props.resources.map((a) => a.name)} + /> + )} + {!type ? ( + this.emptyField() + ) : !['Child', 'Faker.js'].includes(type) ? ( + this.onSchemaFieldChange(i, 'value', castValue(v, type))} /> + ) : type !== 'Child' ? ( + this.emptyField() + ) : null}
-
- this.setState({ resource_list: v })} + ); + })} + +
+
+
+ +
+ this.setState({ objectTemplate: v })} /> - {resource_list && this.setState({ length: e })} />} - this.setState({ status: v })} - placeholder="200, 201, 400, 500..." +
+
+ +
+ + +
+
+ ); + } +} + +class NewEndpoint extends React.Component { + state = this.props.endpoint || { + method: 'GET', + path: '/', + status: 200, + body: null, + resource: '', + resource_list: false, + headers: {}, + length: 10, + }; + + render() { + const { method, path, status, body, headers, resource, resource_list, length } = this.state; + + return ( +
+
+
+ + +
+
+ this.setState({ method: v })} /> - this.setState({ headers: v })} /> -
- -
- this.setState({ body: v })} /> -
-
-
- - +
+ this.setState({ path: v })} + placeholder="Request path" + />
+
- } +
+ +
+ this.setState({ resource: v })} + possibleValues={this.props.resources.map((r) => r.name)} + /> +
+
+ this.setState({ resource_list: v })} + /> + {resource_list && ( + this.setState({ length: e })} + /> + )} + this.setState({ status: v })} + placeholder="200, 201, 400, 500..." + /> + this.setState({ headers: v })} + /> +
+ +
+ this.setState({ body: v })} /> +
+
+
+ + +
+
+ ); + } } -const Header = ({ hide }) =>
-
-
-

Charlatan

- -
+const Header = ({ hide }) => ( +
+
+
+

Charlatan

+ +
-
+
+); export const HTTP_COLORS = { - GET: 'rgb(52, 170, 182)', - POST: 'rgb(117, 189, 93)', - DELETE: 'rgb(238, 106, 86)', - PATCH: '#9b59b6', - HEAD: '#9b59b6', - PUT: 'rgb(230, 195, 0)', - OPTIONS: '#9b59b6', + GET: 'rgb(52, 170, 182)', + POST: 'rgb(117, 189, 93)', + DELETE: 'rgb(238, 106, 86)', + PATCH: '#9b59b6', + HEAD: '#9b59b6', + PUT: 'rgb(230, 195, 0)', + OPTIONS: '#9b59b6', }; const FakerOptions = [ - { value: "address.zipCode", label: "Zip code" }, - { value: "address.zipCodeByState", label: "Zip code by state" }, - { value: "address.city", label: "City" }, - { value: "address.cityPrefix", label: "City prefix" }, - { value: "address.citySuffix", label: "City suffix" }, - { value: "address.cityName", label: "City name" }, - { value: "address.streetName", label: "Street name" }, - { value: "address.streetAddress", label: "Street address" }, - { value: "address.streetSuffix", label: "Street suffix" }, - { value: "address.streetPrefix", label: "Street prefix" }, - { value: "address.secondaryAddress", label: "Secondary address" }, - { value: "address.county", label: "County" }, - { value: "address.country", label: "Country" }, - { value: "address.countryCode", label: "Country code" }, - { value: "address.state", label: "State" }, - { value: "address.stateAbbr", label: "State abbreviated" }, - { value: "address.latitude", label: "Latitude" }, - { value: "address.longitude", label: "Longitude" }, - { value: "address.direction", label: "Direction" }, - { value: "address.cardinalDirection", label: "Cardinal direction" }, - { value: "address.ordinalDirection", label: "Ordinal direction" }, - { value: "address.nearbyGPSCoordinate", label: "Nearby GPS coordinate" }, - { value: "address.timeZone", label: "Time zone" }, - { value: "animal.dog", label: "Dog" }, - { value: "animal.cat", label: "Cat" }, - { value: "animal.snake", label: "Snake" }, - { value: "animal.bear", label: "Bear" }, - { value: "animal.lion", label: "Lion" }, - { value: "animal.cetacean", label: "Cetacean" }, - { value: "animal.horse", label: "Horse" }, - { value: "animal.bird", label: "Bird" }, - { value: "animal.cow", label: "Cow" }, - { value: "animal.fish", label: "Fish" }, - { value: "animal.crocodilia", label: "Crocodilia" }, - { value: "animal.insect", label: "Insect" }, - { value: "animal.rabbit", label: "Rabbit" }, - { value: "animal.type", label: "Type" }, - { value: "commerce.color", label: "Color" }, - { value: "commerce.department", label: "Department" }, - { value: "commerce.productName", label: "Product name" }, - { value: "commerce.price", label: "Price" }, - { value: "commerce.productAdjective", label: "Product adjective" }, - { value: "commerce.productMaterial", label: "Product material" }, - { value: "commerce.product", label: "Product" }, - { value: "commerce.productDescription", label: "Product description" }, - { value: "company.suffixes", label: "Suffixes" }, - { value: "company.companyName", label: "Company name" }, - { value: "company.companySuffix", label: "Company suffix" }, - { value: "company.catchPhrase", label: "Catch phrase" }, - { value: "company.bs", label: "BS" }, - { value: "company.catchPhraseAdjective", label: "Catch phrase adjective" }, - { value: "company.catchPhraseDescriptor", label: "Catch phrase descriptor" }, - { value: "company.catchPhraseNoun", label: "Catch phrase noun" }, - { value: "company.bsAdjective", label: "BS adjective" }, - { value: "company.bsBuzz", label: "BS buzz" }, - { value: "company.bsNoun", label: "BS noun" }, - { value: "database.column", label: "Column" }, - { value: "database.type", label: "Type" }, - { value: "database.collation", label: "Collation" }, - { value: "database.engine", label: "Engine" }, - { value: "date.past", label: "Past" }, - { value: "date.future", label: "Future" }, - { value: "date.recent", label: "Recent" }, - { value: "date.soon", label: "Soon" }, - { value: "date.month", label: "Month" }, - { value: "date.weekday", label: "Weekday" }, - { value: "finance.account", label: "Account" }, - { value: "finance.accountName", label: "Account name" }, - { value: "finance.routingNumber", label: "Routing number" }, - { value: "finance.mask", label: "Mask" }, - { value: "finance.amount", label: "Amount" }, - { value: "finance.transactionType", label: "Transaction type" }, - { value: "finance.currencyCode", label: "Currency code" }, - { value: "finance.currencyName", label: "Currency name" }, - { value: "finance.currencySymbol", label: "Currency symbol" }, - { value: "finance.bitcoinAddress", label: "Bitcoin address" }, - { value: "finance.litecoinAddress", label: "Litecoin address" }, - { value: "finance.creditCardNumber", label: "Credit card number" }, - { value: "finance.creditCardCVV", label: "Credit card CVV" }, - { value: "finance.ethereumAddress", label: "Ethereum address" }, - { value: "finance.iban", label: "IBAN" }, - { value: "finance.bic", label: "BIC" }, - { value: "finance.transactionDescription", label: "Transaction description" }, - { value: "git.branch", label: "Branch" }, - { value: "git.commitEntry", label: "Commit entry" }, - { value: "git.commitMessage", label: "Commit message" }, - { value: "git.commitSha", label: "Commit sha" }, - { value: "git.shortSha", label: "Short sha" }, - { value: "hacker.abbreviation", label: "Abbreviation" }, - { value: "hacker.adjective", label: "Adjective" }, - { value: "hacker.noun", label: "Noun" }, - { value: "hacker.verb", label: "Verb" }, - { value: "hacker.ingverb", label: "Ingverb" }, - { value: "hacker.phrase", label: "Phrase" }, - { value: "image.image", label: "Image" }, - { value: "image.avatar", label: "Avatar" }, - { value: "image.imageUrl", label: "Image url" }, - { value: "image.abstract", label: "Abstract" }, - { value: "image.animals", label: "Animals" }, - { value: "image.business", label: "Business" }, - { value: "image.cats", label: "Cats" }, - { value: "image.city", label: "City" }, - { value: "image.food", label: "Food" }, - { value: "image.nightlife", label: "Nightlife" }, - { value: "image.fashion", label: "Fashion" }, - { value: "image.people", label: "People" }, - { value: "image.nature", label: "Nature" }, - { value: "image.sports", label: "Sports" }, - { value: "image.technics", label: "Technics" }, - { value: "image.transport", label: "Transport" }, - { value: "image.dataUri", label: "Data uri" }, - { value: "internet.avatar", label: "Avatar" }, - { value: "internet.email", label: "Email" }, - { value: "internet.exampleEmail", label: "Example email" }, - { value: "internet.userName", label: "User name" }, - { value: "internet.protocol", label: "Protocol" }, - { value: "internet.httpMethod", label: "HTTP method" }, - { value: "internet.url", label: "URL" }, - { value: "internet.domainName", label: "Domain name" }, - { value: "internet.domainSuffix", label: "Domain suffix" }, - { value: "internet.domainWord", label: "Domain word" }, - { value: "internet.ip", label: "IP" }, - { value: "internet.ipv6", label: "IPV6" }, - { value: "internet.port", label: "Port" }, - { value: "internet.userAgent", label: "User agent" }, - { value: "internet.color", label: "Color" }, - { value: "internet.mac", label: "Mac" }, - { value: "internet.password", label: "Password" }, - { value: "lorem.word", label: "Word" }, - { value: "lorem.words", label: "Words" }, - { value: "lorem.sentence", label: "Sentence" }, - { value: "lorem.slug", label: "Slug" }, - { value: "lorem.sentences", label: "Sentences" }, - { value: "lorem.paragraph", label: "Paragraph" }, - { value: "lorem.paragraphs", label: "Paragraphs" }, - { value: "lorem.text", label: "Text" }, - { value: "lorem.lines", label: "Lines" }, - { value: "mersenne.rand", label: "Rand" }, - { value: "music.genre", label: "Genre" }, - { value: "name.firstName", label: "First name" }, - { value: "name.lastName", label: "Last name" }, - { value: "name.middleName", label: "Middle name" }, - { value: "name.findName", label: "Find name" }, - { value: "name.jobTitle", label: "Job title" }, - { value: "name.gender", label: "Gender" }, - { value: "name.prefix", label: "Prefix" }, - { value: "name.suffix", label: "Suffix" }, - { value: "name.jobTitle", label: "Title" }, - { value: "name.jobDescriptor", label: "Job descriptor" }, - { value: "name.jobArea", label: "Job area" }, - { value: "name.jobType", label: "Job type" }, - { value: "phone.phoneNumber", label: "Phone number" }, - { value: "phone.phoneNumberFormat", label: "Phone number format" }, - { value: "phone.phoneFormats", label: "Phone formats" }, - { value: "system.fileName", label: "File name" }, - { value: "system.commonFileName", label: "Common file name" }, - { value: "system.mimeType", label: "Mime type" }, - { value: "system.commonFileType", label: "Common file type" }, - { value: "system.commonFileExt", label: "Common file ext" }, - { value: "system.fileType", label: "File type" }, - { value: "system.fileExt", label: "File ext" }, - { value: "system.directoryPath", label: "Directory path" }, - { value: "system.filePath", label: "File path" }, - { value: "system.semver", label: "Semver" }, - { value: "time.recent", label: "Recent" }, - { value: "vehicle.vehicle", label: "Vehicle" }, - { value: "vehicle.manufacturer", label: "Manufacturer" }, - { value: "vehicle.model", label: "Model" }, - { value: "vehicle.type", label: "Type" }, - { value: "vehicle.fuel", label: "Fuel" }, - { value: "vehicle.vin", label: "Vin" }, - { value: "vehicle.color", label: "Color" }, - { value: "vehicle.vrm", label: "Vrm" }, - { value: "vehicle.bicycle", label: "Bicycle" }, -] \ No newline at end of file + { value: 'address.zipCode', label: 'Zip code' }, + { value: 'address.zipCodeByState', label: 'Zip code by state' }, + { value: 'address.city', label: 'City' }, + { value: 'address.cityPrefix', label: 'City prefix' }, + { value: 'address.citySuffix', label: 'City suffix' }, + { value: 'address.cityName', label: 'City name' }, + { value: 'address.streetName', label: 'Street name' }, + { value: 'address.streetAddress', label: 'Street address' }, + { value: 'address.streetSuffix', label: 'Street suffix' }, + { value: 'address.streetPrefix', label: 'Street prefix' }, + { value: 'address.secondaryAddress', label: 'Secondary address' }, + { value: 'address.county', label: 'County' }, + { value: 'address.country', label: 'Country' }, + { value: 'address.countryCode', label: 'Country code' }, + { value: 'address.state', label: 'State' }, + { value: 'address.stateAbbr', label: 'State abbreviated' }, + { value: 'address.latitude', label: 'Latitude' }, + { value: 'address.longitude', label: 'Longitude' }, + { value: 'address.direction', label: 'Direction' }, + { value: 'address.cardinalDirection', label: 'Cardinal direction' }, + { value: 'address.ordinalDirection', label: 'Ordinal direction' }, + { value: 'address.nearbyGPSCoordinate', label: 'Nearby GPS coordinate' }, + { value: 'address.timeZone', label: 'Time zone' }, + { value: 'animal.dog', label: 'Dog' }, + { value: 'animal.cat', label: 'Cat' }, + { value: 'animal.snake', label: 'Snake' }, + { value: 'animal.bear', label: 'Bear' }, + { value: 'animal.lion', label: 'Lion' }, + { value: 'animal.cetacean', label: 'Cetacean' }, + { value: 'animal.horse', label: 'Horse' }, + { value: 'animal.bird', label: 'Bird' }, + { value: 'animal.cow', label: 'Cow' }, + { value: 'animal.fish', label: 'Fish' }, + { value: 'animal.crocodilia', label: 'Crocodilia' }, + { value: 'animal.insect', label: 'Insect' }, + { value: 'animal.rabbit', label: 'Rabbit' }, + { value: 'animal.type', label: 'Type' }, + { value: 'commerce.color', label: 'Color' }, + { value: 'commerce.department', label: 'Department' }, + { value: 'commerce.productName', label: 'Product name' }, + { value: 'commerce.price', label: 'Price' }, + { value: 'commerce.productAdjective', label: 'Product adjective' }, + { value: 'commerce.productMaterial', label: 'Product material' }, + { value: 'commerce.product', label: 'Product' }, + { value: 'commerce.productDescription', label: 'Product description' }, + { value: 'company.suffixes', label: 'Suffixes' }, + { value: 'company.companyName', label: 'Company name' }, + { value: 'company.companySuffix', label: 'Company suffix' }, + { value: 'company.catchPhrase', label: 'Catch phrase' }, + { value: 'company.bs', label: 'BS' }, + { value: 'company.catchPhraseAdjective', label: 'Catch phrase adjective' }, + { value: 'company.catchPhraseDescriptor', label: 'Catch phrase descriptor' }, + { value: 'company.catchPhraseNoun', label: 'Catch phrase noun' }, + { value: 'company.bsAdjective', label: 'BS adjective' }, + { value: 'company.bsBuzz', label: 'BS buzz' }, + { value: 'company.bsNoun', label: 'BS noun' }, + { value: 'database.column', label: 'Column' }, + { value: 'database.type', label: 'Type' }, + { value: 'database.collation', label: 'Collation' }, + { value: 'database.engine', label: 'Engine' }, + { value: 'date.past', label: 'Past' }, + { value: 'date.future', label: 'Future' }, + { value: 'date.recent', label: 'Recent' }, + { value: 'date.soon', label: 'Soon' }, + { value: 'date.month', label: 'Month' }, + { value: 'date.weekday', label: 'Weekday' }, + { value: 'finance.account', label: 'Account' }, + { value: 'finance.accountName', label: 'Account name' }, + { value: 'finance.routingNumber', label: 'Routing number' }, + { value: 'finance.mask', label: 'Mask' }, + { value: 'finance.amount', label: 'Amount' }, + { value: 'finance.transactionType', label: 'Transaction type' }, + { value: 'finance.currencyCode', label: 'Currency code' }, + { value: 'finance.currencyName', label: 'Currency name' }, + { value: 'finance.currencySymbol', label: 'Currency symbol' }, + { value: 'finance.bitcoinAddress', label: 'Bitcoin address' }, + { value: 'finance.litecoinAddress', label: 'Litecoin address' }, + { value: 'finance.creditCardNumber', label: 'Credit card number' }, + { value: 'finance.creditCardCVV', label: 'Credit card CVV' }, + { value: 'finance.ethereumAddress', label: 'Ethereum address' }, + { value: 'finance.iban', label: 'IBAN' }, + { value: 'finance.bic', label: 'BIC' }, + { value: 'finance.transactionDescription', label: 'Transaction description' }, + { value: 'git.branch', label: 'Branch' }, + { value: 'git.commitEntry', label: 'Commit entry' }, + { value: 'git.commitMessage', label: 'Commit message' }, + { value: 'git.commitSha', label: 'Commit sha' }, + { value: 'git.shortSha', label: 'Short sha' }, + { value: 'hacker.abbreviation', label: 'Abbreviation' }, + { value: 'hacker.adjective', label: 'Adjective' }, + { value: 'hacker.noun', label: 'Noun' }, + { value: 'hacker.verb', label: 'Verb' }, + { value: 'hacker.ingverb', label: 'Ingverb' }, + { value: 'hacker.phrase', label: 'Phrase' }, + { value: 'image.image', label: 'Image' }, + { value: 'image.avatar', label: 'Avatar' }, + { value: 'image.imageUrl', label: 'Image url' }, + { value: 'image.abstract', label: 'Abstract' }, + { value: 'image.animals', label: 'Animals' }, + { value: 'image.business', label: 'Business' }, + { value: 'image.cats', label: 'Cats' }, + { value: 'image.city', label: 'City' }, + { value: 'image.food', label: 'Food' }, + { value: 'image.nightlife', label: 'Nightlife' }, + { value: 'image.fashion', label: 'Fashion' }, + { value: 'image.people', label: 'People' }, + { value: 'image.nature', label: 'Nature' }, + { value: 'image.sports', label: 'Sports' }, + { value: 'image.technics', label: 'Technics' }, + { value: 'image.transport', label: 'Transport' }, + { value: 'image.dataUri', label: 'Data uri' }, + { value: 'internet.avatar', label: 'Avatar' }, + { value: 'internet.email', label: 'Email' }, + { value: 'internet.exampleEmail', label: 'Example email' }, + { value: 'internet.userName', label: 'User name' }, + { value: 'internet.protocol', label: 'Protocol' }, + { value: 'internet.httpMethod', label: 'HTTP method' }, + { value: 'internet.url', label: 'URL' }, + { value: 'internet.domainName', label: 'Domain name' }, + { value: 'internet.domainSuffix', label: 'Domain suffix' }, + { value: 'internet.domainWord', label: 'Domain word' }, + { value: 'internet.ip', label: 'IP' }, + { value: 'internet.ipv6', label: 'IPV6' }, + { value: 'internet.port', label: 'Port' }, + { value: 'internet.userAgent', label: 'User agent' }, + { value: 'internet.color', label: 'Color' }, + { value: 'internet.mac', label: 'Mac' }, + { value: 'internet.password', label: 'Password' }, + { value: 'lorem.word', label: 'Word' }, + { value: 'lorem.words', label: 'Words' }, + { value: 'lorem.sentence', label: 'Sentence' }, + { value: 'lorem.slug', label: 'Slug' }, + { value: 'lorem.sentences', label: 'Sentences' }, + { value: 'lorem.paragraph', label: 'Paragraph' }, + { value: 'lorem.paragraphs', label: 'Paragraphs' }, + { value: 'lorem.text', label: 'Text' }, + { value: 'lorem.lines', label: 'Lines' }, + { value: 'mersenne.rand', label: 'Rand' }, + { value: 'music.genre', label: 'Genre' }, + { value: 'name.firstName', label: 'First name' }, + { value: 'name.lastName', label: 'Last name' }, + { value: 'name.middleName', label: 'Middle name' }, + { value: 'name.findName', label: 'Find name' }, + { value: 'name.jobTitle', label: 'Job title' }, + { value: 'name.gender', label: 'Gender' }, + { value: 'name.prefix', label: 'Prefix' }, + { value: 'name.suffix', label: 'Suffix' }, + { value: 'name.jobTitle', label: 'Title' }, + { value: 'name.jobDescriptor', label: 'Job descriptor' }, + { value: 'name.jobArea', label: 'Job area' }, + { value: 'name.jobType', label: 'Job type' }, + { value: 'phone.phoneNumber', label: 'Phone number' }, + { value: 'phone.phoneNumberFormat', label: 'Phone number format' }, + { value: 'phone.phoneFormats', label: 'Phone formats' }, + { value: 'system.fileName', label: 'File name' }, + { value: 'system.commonFileName', label: 'Common file name' }, + { value: 'system.mimeType', label: 'Mime type' }, + { value: 'system.commonFileType', label: 'Common file type' }, + { value: 'system.commonFileExt', label: 'Common file ext' }, + { value: 'system.fileType', label: 'File type' }, + { value: 'system.fileExt', label: 'File ext' }, + { value: 'system.directoryPath', label: 'Directory path' }, + { value: 'system.filePath', label: 'File path' }, + { value: 'system.semver', label: 'Semver' }, + { value: 'time.recent', label: 'Recent' }, + { value: 'vehicle.vehicle', label: 'Vehicle' }, + { value: 'vehicle.manufacturer', label: 'Manufacturer' }, + { value: 'vehicle.model', label: 'Model' }, + { value: 'vehicle.type', label: 'Type' }, + { value: 'vehicle.fuel', label: 'Fuel' }, + { value: 'vehicle.vin', label: 'Vin' }, + { value: 'vehicle.color', label: 'Color' }, + { value: 'vehicle.vrm', label: 'Vrm' }, + { value: 'vehicle.bicycle', label: 'Bicycle' }, +]; diff --git a/otoroshi/javascript/src/pages/RouteDesigner/RouteComposition.js b/otoroshi/javascript/src/pages/RouteDesigner/RouteComposition.js index 4c37d632e2..91f00fbcfe 100644 --- a/otoroshi/javascript/src/pages/RouteDesigner/RouteComposition.js +++ b/otoroshi/javascript/src/pages/RouteDesigner/RouteComposition.js @@ -22,13 +22,13 @@ const Methods = ({ frontend }) => { const hasMethods = frontend.methods && frontend.methods.length > 0; const methods = hasMethods ? frontend.methods.map((m, i) => ( - - {m} - - )) + + {m} + + )) : [ALL]; return (
@@ -274,19 +274,19 @@ const RouteForm = React.memo( value={ usingJsonView ? { - [dirtyField]: value, - } + [dirtyField]: value, + } : value } schema={ usingJsonView ? { - [dirtyField]: { - type: 'json', - format: 'code', - label: null, - }, - } + [dirtyField]: { + type: 'json', + format: 'code', + label: null, + }, + } : schema } flow={usingJsonView ? [dirtyField] : flow} @@ -372,7 +372,7 @@ export default ({ service, setSaveButton, setService, viewPlugins, ref }) => { const [routes, setRoutes] = useState([]); const [templates, setTemplates] = useState({}); const [shouldUpdateRoutes, setUpdatesRoutes] = useState(false); - const history = useHistory() + const history = useHistory(); useEffect(() => { nextClient.template(nextClient.ENTITIES.SERVICES).then(setTemplates); @@ -384,9 +384,9 @@ export default ({ service, setSaveButton, setService, viewPlugins, ref }) => { useImperativeHandle(ref, () => ({ onTestingButtonClick() { - history.push(`/routes/${service.id}?tab=flow`, { showTryIt: true }) - } - })) + history.push(`/routes/${service.id}?tab=flow`, { showTryIt: true }); + }, + })); useEffect(() => { setSaveButton( diff --git a/otoroshi/javascript/src/pages/RouteDesigner/TryIt.js b/otoroshi/javascript/src/pages/RouteDesigner/TryIt.js index d866460891..5f69373093 100644 --- a/otoroshi/javascript/src/pages/RouteDesigner/TryIt.js +++ b/otoroshi/javascript/src/pages/RouteDesigner/TryIt.js @@ -1,12 +1,17 @@ -import React, { useEffect, useState } from 'react' -import { CodeInput, SelectInput } from '@maif/react-forms' -import { BooleanInput } from '../../components/inputs' -import { tryIt, fetchAllApikeys, findAllCertificates, routeEntries } from '../../services/BackOfficeServices' -import { firstLetterUppercase } from '../../util' -import { useLocation } from 'react-router-dom' - -import { Provider } from 'react-redux' -import { Playground, store } from 'graphql-playground-react' +import React, { useEffect, useState } from 'react'; +import { CodeInput, SelectInput } from '@maif/react-forms'; +import { BooleanInput } from '../../components/inputs'; +import { + tryIt, + fetchAllApikeys, + findAllCertificates, + routeEntries, +} from '../../services/BackOfficeServices'; +import { firstLetterUppercase } from '../../util'; +import { useLocation } from 'react-router-dom'; + +import { Provider } from 'react-redux'; +import { Playground, store } from 'graphql-playground-react'; const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']; @@ -16,7 +21,6 @@ const roundNsTo = (ns) => Number.parseFloat(round(ns) / 1000000).toFixed(3); const round = (num) => Math.round((num + Number.EPSILON) * 100000) / 100000; export const TryIt = ({ route, hide }) => { - const [selectedTab, setSelectedTab] = useState('Headers'); const [selectedResponseTab, setSelectedResponseTab] = useState('Body'); const [headersStatus, setHeadersStatus] = useState('down'); @@ -27,7 +31,7 @@ export const TryIt = ({ route, hide }) => { const [sort, setSort] = useState('flow'); const [flow, setFlow] = useState('all'); - const [lastQuery, setLastQuery] = useState() + const [lastQuery, setLastQuery] = useState(); const [request, setRequest] = useState({ path: '/', @@ -53,7 +57,7 @@ export const TryIt = ({ route, hide }) => { const [responseBody, setResponseBody] = useState(); const [loading, setLoading] = useState(false); - const [playgroundUrl, setPlaygroundUrl] = useState() + const [playgroundUrl, setPlaygroundUrl] = useState(); useEffect(() => { if (route && route.id) { @@ -61,69 +65,61 @@ export const TryIt = ({ route, hide }) => { ...request, route_id: route.id, }); - hidePlaygroundStuff(route) + hidePlaygroundStuff(route); - routeEntries(route.id) - .then(data => setPlaygroundUrl(data.entries[0])) + routeEntries(route.id).then((data) => setPlaygroundUrl(data.entries[0])); } }, [route]); useEffect(() => { - loadLastQuery() - }, []) + loadLastQuery(); + }, []); useEffect(() => { - if (lastQuery) - localStorage.removeItem('graphql-playground') - }, [lastQuery]) + if (lastQuery) localStorage.removeItem('graphql-playground'); + }, [lastQuery]); const loadLastQuery = () => { try { - const storedData = JSON.parse(localStorage.getItem("graphql-playground")) - const query = Object.entries(Object.entries(storedData.workspaces)[1][1].history)[0][1].query - setLastQuery(query) - localStorage.setItem("otoroshi-graphql-last-query", query) + const storedData = JSON.parse(localStorage.getItem('graphql-playground')); + const query = Object.entries(Object.entries(storedData.workspaces)[1][1].history)[0][1].query; + setLastQuery(query); + localStorage.setItem('otoroshi-graphql-last-query', query); } catch (_) { - const query = localStorage.getItem("otoroshi-graphql-last-query") - setLastQuery(query || "{}") + const query = localStorage.getItem('otoroshi-graphql-last-query'); + setLastQuery(query || '{}'); } - } - - const hidePlaygroundStuff = route => { - if (!route) - setTimeout(() => hidePlaygroundStuff(route), 250) - else if (route.plugins.find(f => f.plugin.includes('GraphQLBackend'))) { - const input = document.querySelector(".playground input") - if (!input) - setTimeout(() => hidePlaygroundStuff(route), 100) + }; + + const hidePlaygroundStuff = (route) => { + if (!route) setTimeout(() => hidePlaygroundStuff(route), 250); + else if (route.plugins.find((f) => f.plugin.includes('GraphQLBackend'))) { + const input = document.querySelector('.playground input'); + if (!input) setTimeout(() => hidePlaygroundStuff(route), 100); else { - [ - ".playground > div", - ".graphiql-wrapper > div > div > div " - ].forEach(path => { - const element = document.querySelector(path) - if (element.textContent) - element.style.display = 'none' + ['.playground > div', '.graphiql-wrapper > div > div > div '].forEach((path) => { + const element = document.querySelector(path); + if (element.textContent) element.style.display = 'none'; }); - const prettifyButton = [...document.querySelectorAll(".graphiql-wrapper > div > div > div > button")] - .find(f => f.textContent === "Prettify") + const prettifyButton = [ + ...document.querySelectorAll('.graphiql-wrapper > div > div > div > button'), + ].find((f) => f.textContent === 'Prettify'); if (prettifyButton) { - prettifyButton.className = "btn btn-sm btn-success tryit-prettify-button"; + prettifyButton.className = 'btn btn-sm btn-success tryit-prettify-button'; - document.querySelector(".CodeMirror") - .appendChild(prettifyButton); + document.querySelector('.CodeMirror').appendChild(prettifyButton); } [...document.querySelectorAll('.playground svg')] - .filter(svg => svg.textContent === 'Settings') - .forEach(svg => svg.style.display = 'none') + .filter((svg) => svg.textContent === 'Settings') + .forEach((svg) => (svg.style.display = 'none')); - input.style.display = 'none' + input.style.display = 'none'; } } - } + }; useEffect(() => { fetchAllApikeys().then(setApikeys); @@ -139,7 +135,8 @@ export const TryIt = ({ route, hide }) => { { ...request, headers: Object.values(request.headers) - .filter(d => d.key.length > 0).reduce((a, c) => ({ ...a, [c.key]: c.value }), {}) + .filter((d) => d.key.length > 0) + .reduce((a, c) => ({ ...a, [c.key]: c.value }), {}), }, pathname.includes('route-compositions') ? 'service' : 'route' ) @@ -159,7 +156,7 @@ export const TryIt = ({ route, hide }) => { setResponseBody(atob(res.body_base_64).replace(/\n/g, '').trimStart()); } }); - } + }; const bytesToSize = (bytes) => { const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; @@ -195,15 +192,15 @@ export const TryIt = ({ route, hide }) => { ), ...(format === 'basic' ? { - 'authorization-header': { - key: apikeyHeader || request.apikeyHeader, - value: `Basic ${btoa(`${clientId}:${clientSecret}`)}`, - }, - } + 'authorization-header': { + key: apikeyHeader || request.apikeyHeader, + value: `Basic ${btoa(`${clientId}:${clientSecret}`)}`, + }, + } : { - 'Otoroshi-Client-Id': { key: 'Otoroshi-Client-Id', value: clientId }, - 'Otoroshi-Client-Secret': { key: 'Otoroshi-Client-Secret', value: clientSecret }, - }), + 'Otoroshi-Client-Id': { key: 'Otoroshi-Client-Id', value: clientId }, + 'Otoroshi-Client-Secret': { key: 'Otoroshi-Client-Secret', value: clientSecret }, + }), }; }; @@ -211,9 +208,9 @@ export const TryIt = ({ route, hide }) => { return (
-
-
-
+
+
+

Testing

- {route && route.plugins.find(f => f.plugin.includes('GraphQLBackend')) && - route.plugins.find(f => f.plugin.includes('GraphQLBackend')).enabled && playgroundUrl && lastQuery ? + {route && + route.plugins.find((f) => f.plugin.includes('GraphQLBackend')) && + route.plugins.find((f) => f.plugin.includes('GraphQLBackend')).enabled && + playgroundUrl && + lastQuery ? (
- : -
{
{[ { label: 'Authorization', value: 'Authorization' }, - { label: 'Headers', value: `Headers (${Object.keys(request.headers || {}).length})` }, + { + label: 'Headers', + value: `Headers (${Object.keys(request.headers || {}).length})`, + }, { label: 'Body', value: 'Body' }, ].map(({ label, value }) => (