[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Configuration used\n2. Steps to run the software\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Logs**\nIf applicable, any logs and debugging information\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/report-vulnerability.md",
    "content": "---\nname: Report vulnerability\nabout: Report a vulnerability\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\nFor **private vulnerabilities** write to support@devops.faith instead\n\n**Vulnerabilty description**\nExplain the vulnerability in detail and how to reproduce when possible\n\n**Reference**\nE.g: https://nvd.nist.gov/vuln/detail/CVE-2019-6486\n\n**Additional information**\nImpact, Known Affected Software Configurations, etc.\n"
  },
  {
    "path": ".github/label-commenter-config.yml",
    "content": "comment:\n  footer:  |\n    ---\n    > This is an automated comment. Responding to the bot or mentioning it won't have any effect\n\nlabels:\n  - name: invalid\n    labeled:\n      issue:\n        body: |\n          Hi, thanks for bringing this issue to our attention.\n\n          Unfortunately, this issue has been marked invalid and will be closed.\n          The most common reasons for marking an issue or pull request as invalid is because:\n\n          - It's vague or not clearly actionable\n          - It contains insufficient details to reproduce\n          - It's plugin review or custom code review\n          - It does not use the issue template\n          - It's unrelated to the project (e.g., related to one of its libraries)\n          - It does not follow the technical philosophy of the project\n          - Violates our [Code of Conduct](https://lfprojects.org/policies/code-of-conduct/)\n          - It's about KrakenD functionalities (which uses and mantain Lura, but is not part of the Linux Foundation)\n\n          You can still make an edit or leave additional comments that lead to reopening this issue.\n        action: close\n      pr:\n        body: |\n          Hi @{{ pull_request.user.login }}, thanks for having spent the time to code and send an improvement to Lura.\n\n          Unfortunately, this pull request has been marked as invalid and will be closed without merging.\n          The most common reasons for marking an issue or pull request as invalid is because:\n\n          - Contains insufficient details, it's unclear for the reviewer, or it's impossible to move forward without a lot of interaction\n          - It's unrelated to the project (e.g., related to one of its libraries)\n          - It does not follow the philosophy of the project\n          - Violates our [Code of Conduct](https://lfprojects.org/policies/code-of-conduct/)\n\n          You can still make an edit or leave additional comments that lead to reopening this issue.\n        action: close\n  - name: wontfix\n    labeled:\n      issue:\n        body: |\n          Hi, thank you for bringing this issue to our attention.\n\n          Many factors influence our product roadmaps and determine the features, fixes, and suggestions we implement.\n          When deciding what to prioritize and work on, we combine your feedback and suggestions with insights from our development team, product analytics, research findings, and more.\n\n          This information, combined with our product vision, determines what we implement and its priority order. Unfortunately, we don't foresee this issue progressing any further in the short-medium term, and we are closing it.\n\n          While this issue is now closed, we continue monitoring requests for our future roadmap, **including this one**.\n\n          If you have additional information you would like to provide, please share.\n        action: close\n      pr:\n        body: |\n          Hi @{{ pull_request.user.login }}, thanks for having spent the time to code and send an improvement to Lura.\n\n          When deciding what to accept and include in our product, we are cautious about what we add, and the time our team needs to spend to have it done exemplary, considering all edge cases. As a result, we rarely add features, make changes that a tiny number of users need, or are out-of-scope of the project. For example, we might choose safety over having a specific additional feature that adds complexity we don't see crystal clear. Sometimes \"less is more\" because we can focus better on crucial functionality.\n\n          Although it's never nice to reject someone's work, after evaluating your code, we think it's better not to merge it or continue putting effort into it on both sides. If this PR is to solve what you considered a bug, our understanding of the functionality does not need to match your thinking. So while this pull request is now closed, **this is not a definitive decision**. You are free to provide additional information that would help us change our minds.\n\n          Lura is indirectly used in millions of servers every year, and the slightest change has an impact. We are doing it for all community users' benefit and to keep the code's simplicity, philosophy, and maintainability for the long run.\n        action: close\n  - name: duplicate\n    labeled:\n      issue:\n        body: An issue like this already exists, please follow it in the other thread\n        action: close\n  - name: good first issue\n    labeled:\n      issue:\n        body: This issue is easy for contributing. Everyone can work on this.\n  - name: spam\n    labeled:\n      issue:\n        body: |\n          This issue has been **LOCKED** because of spam!\n\n          Please do not spam messages and/or issues on the issue tracker. You may get blocked from this repository for doing so.\n        action: close\n        locking: lock\n        lock_reason: spam\n      pr:\n        body: |\n          This pull-request has been **LOCKED** because of spam!\n\n          Please do not spam messages and/or pull-requests on this project. You may get blocked from this repository for doing so.\n        action: close\n        locking: lock\n        lock_reason: spam"
  },
  {
    "path": ".github/workflows/go.yml",
    "content": "name: Go\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v3\n\n    - name: Set up Go\n      uses: actions/setup-go@v3\n      with:\n        go-version: \"1.25\"\n\n    - name: Build\n      run: go build -v ./...\n\n    - name: Test\n      run: go test -cover -race ./...\n\n    - name: Integration Test\n      run: go test -tags integration ./test"
  },
  {
    "path": ".github/workflows/labels.yml",
    "content": "name: Label commenter\non:\n  issues:\n    types: [labeled, unlabeled]\n  pull_request_target:\n    types: [labeled, unlabeled]\njobs:\n  stale:\n    uses: luraproject/.github/.github/workflows/label-commenter.yml@main\n"
  },
  {
    "path": ".github/workflows/lock-threads.yml",
    "content": "name: 'Lock Threads'\n\non:\n  schedule:\n    - cron: '0 0 * * *'\n  workflow_dispatch:\n\npermissions:\n  issues: write\n  pull-requests: write\n\nconcurrency:\n  group: lock\n\njobs:\n  action:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: dessant/lock-threads@v3\n        with:\n          pr-inactive-days: '90'\n          issue-inactive-days: '90'\n          add-issue-labels: 'locked'\n          issue-comment: >\n            This issue was marked as resolved a long time ago and now has been\n            automatically locked as there has not been any recent activity after it.\n            You can still open a new issue and reference this link.\n          pr-comment: >\n            This pull request was marked as resolved a long time ago and now has been\n            automatically locked as there has not been any recent activity after it.\n            You can still open a new issue and reference this link."
  },
  {
    "path": ".gitignore",
    "content": "vendor\nserver.rsa.crt\nserver.rsa.key\n*.pem\n*.json\n*.toml\n*.dot\n*.out\n*.so\nbench_res\n.cover\n.idea\n\n*DS_Store\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@krakend.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nThank you for your interest in contributing to Lura, there are several ways\nyou can contribute and make this project more awesome, please see below:\n\n## Reporting an Issue\n\nIf you believe you have found an issue with the code please do not hesitate to file an issue in [Github](https://github.com/luraproject/lura/issues). When\nfiling the issue please describe the problem with the maximum level of detail\nand the steps to reproduce the problem, including information about your\nenvironment.\n\nYou can also open an issue requesting for help or doing a question and it's\nalso a good way of contributing since other users might be in a similar\nposition.\n\nPlease note we have a code of conduct, please follow it in all your interactions with the project.\n\n## Code Contributions\n\nWhen contributing to this repository, it is generally a good idea to discuss\nthe change with the owners before investing a lot of time coding. The process\ncould be:\n\n1. Open an issue explaining the improvment or fix you want to add\n2. [Fork the project](https://github.com/luraproject/lura/fork)\n3. Code it in your fork\n4. Submit a [pull request](https://help.github.com/articles/creating-a-pull-request) referencing the issue\n\n\nYour work will then be reviewed as soon as possible (suggestions about some\nchanges, improvements or alternatives may be given).\n\n**Don't forget to add tests**, make sure that they all pass!\n\n# Help with Git\n\nOnce the repository is forked, you should track the upstream (original) one\nusing the following command:\n\n    git remote add upstream https://github.com/luraproject/lura.git\n\nThen you should create your own branch:\n\n    git checkout -b <prefix>/<micro-title>-<issue-number>\n\nOnce your changes are done (`git commit -am '<descriptive-message>'`), get the\nupstream changes:\n\n    git checkout master\n    git pull --rebase origin master\n    git pull --rebase upstream master\n    git checkout <your-branch>\n    git rebase master\n\nFinally, publish your changes:\n\n    git push -f origin <your-branch>\n\nYou should be now ready to make a pull request.\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright © 2021 Lura Project a Series of LF Projects, LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License."
  },
  {
    "path": "Makefile",
    "content": ".PHONY: all test build benchmark\n\nOS := $(shell uname | tr '[:upper:]' '[:lower:]')\nGIT_COMMIT := $(shell git rev-parse --short=7 HEAD)\n\nall: test build\n\ngenerate:\n\tgo generate ./...\n\tgo build -buildmode=plugin -o ./transport/http/client/plugin/tests/lura-client-example.so ./transport/http/client/plugin/tests\n\tgo build -buildmode=plugin -o ./transport/http/server/plugin/tests/lura-server-example.so ./transport/http/server/plugin/tests\n\tgo build -buildmode=plugin -o ./proxy/plugin/tests/lura-request-modifier-example.so ./proxy/plugin/tests/logger\n\tgo build -buildmode=plugin -o ./proxy/plugin/tests/lura-error-example.so ./proxy/plugin/tests/error\n\ntest: generate\n\tgo test -cover -race ./...\n\tgo test -tags integration ./test/...\n\tgo test -tags integration ./transport/...\n\tgo test -tags integration ./proxy/...\n\nbenchmark:\n\t@mkdir -p bench_res\n\t@touch bench_res/${GIT_COMMIT}.out\n\t@go test -run none -bench . -benchmem ./... >> bench_res/${GIT_COMMIT}.out\n\nbuild:\n\tgo build ./...\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"https://luraproject.org/images/lura-logo-header.svg\" width=\"300\" />\n\n# The Lura Project framework\n\n[![Go Report Card](https://goreportcard.com/badge/github.com/luraproject/lura/v2)](https://goreportcard.com/report/github.com/luraproject/lura/v2)\n[![GoDoc](https://godoc.org/github.com/luraproject/lura/v2?status.svg)](https://godoc.org/github.com/luraproject/lura/v2)\n![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3151/badge)\n[![Slack Widget](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=red)](https://gophers.slack.com/messages/lura)\n[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fluraproject%2Flura.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fluraproject%2Flura%2Fv2?ref=badge_shield&issueType=license)\n\n\nAn open framework to assemble ultra performance API Gateways with middlewares; formerly known as _KrakenD framework_, and core service of the [KrakenD API Gateway](http://www.krakend.io).\n\n## Motivation\n\nConsumers of REST API content (specially in microservices) often query backend services that weren't coded for the UI implementation. This is of course a good practice, but the UI consumers need to do implementations that suffer a lot of complexity and burden with the sizes of their microservices responses.\n\nLura is an **API Gateway** builder and proxy generator that sits between the client and all the source servers, adding a new layer that removes all the complexity to the clients, providing them only the information that the UI needs. Lura acts as an **aggregator** of many sources into single endpoints and allows you to group, wrap, transform and shrink responses. Additionally it supports a myriad of middlewares and plugins that allow you to extend the functionality, such as adding Oauth authorization or security layers.\n\nLura not only supports HTTP(S), but because it is a set of generic libraries you can build all type of API Gateways and proxies, including for instance, an RPC gateway.\n\n### Practical Example\n\nA mobile developer needs to construct a single front page that requires data from 4 different calls to their backend services, e.g:\n\n    1) api.store.server/products\n    2) api.store.server/marketing-promos\n    3) api.users.server/users/{id_user}\n    4) api.users.server/shopping-cart/{id_user}\n\nThe screen is very simple, and the mobile client _only_ needs to retrieve data from 4 different sources, wait for the round trip and then hand pick only a few fields from the response.\n\nWhat if the mobile could call a single endpoint?\n\n    1) lura.server/frontpage/{id_user}\n\nThat's something Lura can do for you. And this is how it would look like:\n\n![Gateway](https://luraproject.org/images/docs/lura-gateway.png)\n\nLura would merge all the data and return only the fields you need (the difference in size in the graph).\n\nVisit the [Lura Project website](https://luraproject.org) for more information.\n\n## What's in this repository?\n\nThe source code for the [Lura project](https://luraproject.org) framework. It is designed to work with your own middleware and extend the functionality by using small, independent, reusable components following the Unix philosophy.\n\nUse this repository if you want to **build from source your API Gateway** or if you want to **reuse the components in another application**.\n\nIf you need a fully functional API Gateway you can [download the KrakenD binary for your architecture](http://www.krakend.io/download) or [build it yourself](https://github.com/krakend/krakend-ce).\n\n\n## Library Usage\nThe Lura project is presented as a **Go library** that you can include in your own Go application to build a powerful proxy or API gateway. For a complete example, check the [KrakenD CE repository](https://github.com/krakend/krakend-ce).\n\nOf course, you will need [Go installed](https://golang.org/doc/install) in your system to compile the code.\n\nA ready to use example:\n\n```go\n    package main\n\n    import (\n        \"flag\"\n        \"log\"\n        \"os\"\n\n        \"github.com/luraproject/lura/config\"\n        \"github.com/luraproject/lura/logging\"\n        \"github.com/luraproject/lura/proxy\"\n        \"github.com/luraproject/lura/router/gin\"\n    )\n\n    func main() {\n        port := flag.Int(\"p\", 0, \"Port of the service\")\n        logLevel := flag.String(\"l\", \"ERROR\", \"Logging level\")\n        debug := flag.Bool(\"d\", false, \"Enable the debug\")\n        configFile := flag.String(\"c\", \"/etc/lura/configuration.json\", \"Path to the configuration filename\")\n        flag.Parse()\n\n        parser := config.NewParser()\n        serviceConfig, err := parser.Parse(*configFile)\n        if err != nil {\n            log.Fatal(\"ERROR:\", err.Error())\n        }\n        serviceConfig.Debug = serviceConfig.Debug || *debug\n        if *port != 0 {\n            serviceConfig.Port = *port\n        }\n\n        logger, _ := logging.NewLogger(*logLevel, os.Stdout, \"[LURA]\")\n\n        routerFactory := gin.DefaultFactory(proxy.DefaultFactory(logger), logger)\n\n        routerFactory.New().Run(serviceConfig)\n    }\n```\n\nVisit the [framework overview](/docs/OVERVIEW.md) for more details about the components of the Lura project.\n\n## Configuration file\n\n[Lura config file](/docs/CONFIG.md)\n\n## Benchmarks\n\nCheck out the [benchmark results](/docs/BENCHMARKS.md) of several Lura components\n\n## Contributing\nWe are always happy to receive contributions. If you have questions, suggestions, bugs please open an issue.\nIf you want to submit the code, create the issue and send us a pull request for review.\n\nRead [CONTRIBUTING.md](/CONTRIBUTING.md) for more information.\n\n\n## Want more?\n- Follow us on Twitter: [@luraproject](https://twitter.com/luraproject)\n- Visit our [Slack channel](https://gophers.slack.com/messages/lura)\n- **Read the [documentation](/docs/OVERVIEW.md)**\n\nEnjoy Lura!\n\n\n## License\n[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fluraproject%2Flura.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fluraproject%2Flura?ref=badge_large)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\nLura only fixes the latest version of the software, and does not patch prior versions.\n\n## Reporting a Vulnerability\n\nPlease email security@krakend.io with your discovery. As soon as we read and understand your finding we will provide an answer with next steps and possible timelines.\n\nWe want to thank you in advance for the time you have spent to follow this issue, as it helps all open source users. We develop our software in the open with the help of a global community of developers and contributors with whom we share a common understanding and trust in the free exchange of knowledge.\n\nThe Lura Project DOES NOT provide cash awards for discovered vulnerabilities at this time.\n\nThank you\n\n\n"
  },
  {
    "path": "async/asyncagent.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\n */\npackage async\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\n\t\"github.com/luraproject/lura/v2/backoff\"\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// Options contains the configuration to pass to the async agent factory\ntype Options struct {\n\t// Agent keeps the configuration for the async agent\n\tAgent *config.AsyncAgent\n\t// Endpoint encapsulates the configuration for the associated pipe\n\tEndpoint *config.EndpointConfig\n\t// Proxy is the pipe associated with the async agent\n\tProxy proxy.Proxy\n\t// AgentPing is the channel for the agent to send ping messages\n\tAgentPing chan<- string\n\t// G is the error group responsible for managing the agents and the router itself\n\tG *errgroup.Group\n\t// ShouldContinue is a function signaling when to stop the connection retries\n\tShouldContinue func(int) bool\n\t// BackoffF is a function encapsulating the backoff strategy\n\tBackoffF backoff.TimeToWaitBeforeRetry\n\tLogger   logging.Logger\n}\n\n// Factory is a function able to start an async agent\ntype Factory func(context.Context, Options) bool\n\n// AgentStarter groups a set of factories to be used\ntype AgentStarter []Factory\n\n// Start executes all the factories for each async agent configuration\nfunc (a AgentStarter) Start(\n\tctx context.Context,\n\tagents []*config.AsyncAgent,\n\tlogger logging.Logger,\n\tagentPing chan<- string,\n\tpf proxy.Factory,\n) func() error {\n\tif len(a) == 0 {\n\t\treturn func() error { return ErrNoAgents }\n\t}\n\n\tg, ctx := errgroup.WithContext(ctx)\n\n\tfor i, agent := range agents {\n\t\ti, agent := i, agent\n\t\tif agent.Name == \"\" {\n\t\t\tagent.Name = fmt.Sprintf(\"AsyncAgent-%02d\", i)\n\t\t}\n\n\t\tlogger.Debug(fmt.Sprintf(\"[SERVICE: AsyncAgent][%s] Starting the async agent\", agent.Name))\n\n\t\tfor i := range agent.Backend {\n\t\t\tagent.Backend[i].Timeout = agent.Consumer.Timeout\n\t\t}\n\n\t\tendpoint := &config.EndpointConfig{\n\t\t\tEndpoint:    agent.Name,\n\t\t\tTimeout:     agent.Consumer.Timeout,\n\t\t\tBackend:     agent.Backend,\n\t\t\tExtraConfig: agent.ExtraConfig,\n\t\t}\n\t\tp, err := pf.New(endpoint)\n\t\tif err != nil {\n\t\t\tlogger.Error(fmt.Sprintf(\"[SERVICE: AsyncAgent][%s] building the proxy pipe:\", agent.Name), err)\n\t\t\tcontinue\n\t\t}\n\n\t\tif agent.Connection.MaxRetries <= 0 {\n\t\t\tagent.Connection.MaxRetries = math.MaxInt\n\t\t}\n\n\t\topts := Options{\n\t\t\tAgent:          agent,\n\t\t\tEndpoint:       endpoint,\n\t\t\tProxy:          p,\n\t\t\tAgentPing:      agentPing,\n\t\t\tG:              g,\n\t\t\tShouldContinue: func(i int) bool { return i <= agent.Connection.MaxRetries },\n\t\t\tBackoffF:       backoff.GetByName(agent.Connection.BackoffStrategy),\n\t\t\tLogger:         logger,\n\t\t}\n\n\t\tfor _, f := range a {\n\t\t\tif f(ctx, opts) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t}\n\n\treturn g.Wait\n}\n\nvar ErrNoAgents = errors.New(\"no agent factories defined\")\n"
  },
  {
    "path": "async/asyncagent_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage async\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n)\n\nfunc TestAgentStarter_Start_last(t *testing.T) {\n\tvar firstAgentCalled, secondAgentCalled bool\n\tfirstAgent := func(_ context.Context, opts Options) bool {\n\t\t// TODO: check opts\n\t\tfirstAgentCalled = true\n\t\treturn false\n\t}\n\tsecondAgent := func(_ context.Context, opts Options) bool {\n\t\t// TODO: check opts\n\t\tsecondAgentCalled = true\n\t\treturn true\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tch := make(chan string)\n\tas := AgentStarter([]Factory{firstAgent, secondAgent})\n\tagents := []*config.AsyncAgent{\n\t\t{},\n\t}\n\twait := as.Start(ctx, agents, logging.NoOp, (chan<- string)(ch), noopProxyFactory)\n\n\tif err := wait(); err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif !firstAgentCalled {\n\t\tt.Error(\"first agent not called\")\n\t}\n\n\tif !secondAgentCalled {\n\t\tt.Error(\"second agent not called\")\n\t}\n}\n\nfunc TestAgentStarter_Start_first(t *testing.T) {\n\tvar firstAgentCalled, secondAgentCalled bool\n\tfirstAgent := func(_ context.Context, opts Options) bool {\n\t\tfirstAgentCalled = true\n\t\treturn true\n\t}\n\tsecondAgent := func(_ context.Context, opts Options) bool {\n\t\tsecondAgentCalled = true\n\t\treturn false\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tch := make(chan string)\n\tas := AgentStarter([]Factory{firstAgent, secondAgent})\n\tagents := []*config.AsyncAgent{\n\t\t{},\n\t}\n\twait := as.Start(ctx, agents, logging.NoOp, (chan<- string)(ch), noopProxyFactory)\n\n\tif err := wait(); err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif !firstAgentCalled {\n\t\tt.Error(\"first agent not called\")\n\t}\n\n\tif secondAgentCalled {\n\t\tt.Error(\"second agent called\")\n\t}\n}\n\nvar noopProxyFactory = proxy.FactoryFunc(func(*config.EndpointConfig) (proxy.Proxy, error) {\n\treturn proxy.NoopProxy, nil\n})\n"
  },
  {
    "path": "backoff/backoff.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\nPackage backoff contains some basic implementations and a selector by strategy name\n*/\npackage backoff\n\nimport (\n\t\"math/rand\"\n\t\"strings\"\n\t\"time\"\n)\n\n// GetByName returns the WaitBeforeRetry function implementing the strategy\nfunc GetByName(strategy string) TimeToWaitBeforeRetry {\n\tswitch strings.ToLower(strategy) {\n\tcase \"linear\":\n\t\treturn LinearBackoff\n\tcase \"linear-jitter\":\n\t\treturn LinearJitterBackoff\n\tcase \"exponential\":\n\t\treturn ExponentialBackoff\n\tcase \"exponential-jitter\":\n\t\treturn ExponentialJitterBackoff\n\t}\n\treturn DefaultBackoff\n}\n\n// TimeToWaitBeforeRetry returns the duration to wait before retrying for the\n// given time\ntype TimeToWaitBeforeRetry func(int) time.Duration\n\n// DefaultBackoffDuration is the duration returned by the DefaultBackoff\nvar DefaultBackoffDuration = time.Second\n\n// DefaultBackoff always returns DefaultBackoffDuration\nfunc DefaultBackoff(_ int) time.Duration {\n\treturn DefaultBackoffDuration\n}\n\n// ExponentialBackoff returns ever increasing backoffs by a power of 2\nfunc ExponentialBackoff(i int) time.Duration {\n\treturn time.Duration(1<<uint(i)) * time.Second\n}\n\n// ExponentialJitterBackoff returns ever increasing backoffs by a power of 2\n// with +/- 0-33% to prevent sychronized requests.\nfunc ExponentialJitterBackoff(i int) time.Duration {\n\treturn jitter(int(1 << uint(i)))\n}\n\n// LinearBackoff returns increasing durations, each a second longer than the last\nfunc LinearBackoff(i int) time.Duration {\n\treturn time.Duration(i) * time.Second\n}\n\n// LinearJitterBackoff returns increasing durations, each a second longer than the last\n// with +/- 0-33% to prevent sychronized requests.\nfunc LinearJitterBackoff(i int) time.Duration {\n\treturn jitter(i)\n}\n\nvar random *rand.Rand\n\nfunc init() {\n\trandom = rand.New(rand.NewSource(time.Now().UnixNano()))\n}\n\n// jitter keeps the +/- 0-33% logic in one place\nfunc jitter(i int) time.Duration {\n\tms := i * 1000\n\tmaxJitter := ms/3 + 1\n\tms += random.Intn(2*maxJitter) - maxJitter\n\tif ms <= 0 {\n\t\tms = 1\n\t}\n\n\treturn time.Duration(ms) * time.Millisecond\n}\n"
  },
  {
    "path": "backoff/backoff_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage backoff\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestExponentialBackoff(t *testing.T) {\n\tnext := 1\n\tfor i := 0; i < 10; i++ {\n\t\tif v := int(ExponentialBackoff(i) / time.Second); v != next {\n\t\t\tt.Errorf(\"have: %d, want: %d\", v, next)\n\t\t}\n\t\tnext *= 2\n\t}\n}\n\nfunc TestLinearBackoff(t *testing.T) {\n\tfor i := 0; i < 10; i++ {\n\t\tif v := int(LinearBackoff(i) / time.Second); v != i {\n\t\t\tt.Errorf(\"have: %d, want: %d\", v, i)\n\t\t}\n\t}\n}\n\nfunc TestDefaultBackoff(t *testing.T) {\n\tfor i := 0; i < 10; i++ {\n\t\tif v := int(DefaultBackoff(i) / time.Second); v != 1 {\n\t\t\tt.Errorf(\"have: %d, want: %d\", v, 1)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "config/config.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\nPackage config defines the config structs and some config parser interfaces and implementations\n*/\npackage config\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/encoding\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n)\n\nconst (\n\t// BracketsRouterPatternBuilder uses brackets as route params delimiter\n\tBracketsRouterPatternBuilder = iota\n\t// ColonRouterPatternBuilder use a colon as route param delimiter\n\tColonRouterPatternBuilder\n\t// DefaultMaxIdleConnsPerHost is the default value for the MaxIdleConnsPerHost param\n\tDefaultMaxIdleConnsPerHost = 250\n\t// DefaultTimeout is the default value to use for the ServiceConfig.Timeout param\n\tDefaultTimeout = 2 * time.Second\n\n\t// ConfigVersion is the current version of the config struct\n\tConfigVersion = 3\n)\n\n// RoutingPattern to use during route conversion. By default, use the colon router pattern\nvar RoutingPattern = ColonRouterPatternBuilder\n\n// ServiceConfig defines the lura service\ntype ServiceConfig struct {\n\t// name of the service\n\tName string `mapstructure:\"name\"`\n\t// set of endpoint definitions\n\tEndpoints []*EndpointConfig `mapstructure:\"endpoints\"`\n\t// set of async agent definitions\n\tAsyncAgents []*AsyncAgent `mapstructure:\"async_agent\"`\n\t// defafult timeout\n\tTimeout time.Duration `mapstructure:\"timeout\"`\n\t// default TTL for GET\n\tCacheTTL time.Duration `mapstructure:\"cache_ttl\"`\n\t// default set of hosts\n\tHost []string `mapstructure:\"host\"`\n\t// port to bind the lura service\n\tPort int `mapstructure:\"port\"`\n\t// address to listen\n\tAddress string `mapstructure:\"listen_ip\"`\n\t// version code of the configuration\n\tVersion int `mapstructure:\"version\"`\n\t// OutputEncoding defines the default encoding strategy to use for the endpoint responses\n\tOutputEncoding string `mapstructure:\"output_encoding\"`\n\t// Extra configuration for customized behaviour\n\tExtraConfig ExtraConfig `mapstructure:\"extra_config\"`\n\n\t// ReadTimeout is the maximum duration for reading the entire\n\t// request, including the body.\n\t//\n\t// Because ReadTimeout does not let Handlers make per-request\n\t// decisions on each request body's acceptable deadline or\n\t// upload rate, most users will prefer to use\n\t// ReadHeaderTimeout. It is valid to use them both.\n\tReadTimeout time.Duration `mapstructure:\"read_timeout\"`\n\t// WriteTimeout is the maximum duration before timing out\n\t// writes of the response. It is reset whenever a new\n\t// request's header is read. Like ReadTimeout, it does not\n\t// let Handlers make decisions on a per-request basis.\n\tWriteTimeout time.Duration `mapstructure:\"write_timeout\"`\n\t// IdleTimeout is the maximum amount of time to wait for the\n\t// next request when keep-alives are enabled. If IdleTimeout\n\t// is zero, the value of ReadTimeout is used. If both are\n\t// zero, ReadHeaderTimeout is used.\n\tIdleTimeout time.Duration `mapstructure:\"idle_timeout\"`\n\t// ReadHeaderTimeout is the amount of time allowed to read\n\t// request headers. The connection's read deadline is reset\n\t// after reading the headers and the Handler can decide what\n\t// is considered too slow for the body.\n\tReadHeaderTimeout time.Duration `mapstructure:\"read_header_timeout\"`\n\t// MaxHeaderBytes controls the maximum number of bytes the\n\t// server will read parsing the request header's keys and\n\t// values, including the request line. It does not limit the\n\t// size of the request body.\n\t// If zero, DefaultMaxHeaderBytes (1MB) is used.\n\tMaxHeaderBytes int `mapstructure:\"max_header_bytes\"`\n\n\t// DisableKeepAlives, if true, prevents re-use of TCP connections\n\t// between different HTTP requests.\n\tDisableKeepAlives bool `mapstructure:\"disable_keep_alives\"`\n\t// DisableCompression, if true, prevents the Transport from\n\t// requesting compression with an \"Accept-Encoding: gzip\"\n\t// request header when the Request contains no existing\n\t// Accept-Encoding value. If the Transport requests gzip on\n\t// its own and gets a gzipped response, it's transparently\n\t// decoded in the Response.Body. However, if the user\n\t// explicitly requested gzip it is not automatically\n\t// uncompressed.\n\tDisableCompression bool `mapstructure:\"disable_compression\"`\n\t// MaxIdleConns controls the maximum number of idle (keep-alive)\n\t// connections across all hosts. Zero means no limit.\n\tMaxIdleConns int `mapstructure:\"max_idle_connections\"`\n\t// MaxIdleConnsPerHost, if non-zero, controls the maximum idle\n\t// (keep-alive) connections to keep per-host. If zero,\n\t// DefaultMaxIdleConnsPerHost is used.\n\tMaxIdleConnsPerHost int `mapstructure:\"max_idle_connections_per_host\"`\n\t// IdleConnTimeout is the maximum amount of time an idle\n\t// (keep-alive) connection will remain idle before closing\n\t// itself.\n\t// Zero means no limit.\n\tIdleConnTimeout time.Duration `mapstructure:\"idle_connection_timeout\"`\n\t// ResponseHeaderTimeout, if non-zero, specifies the amount of\n\t// time to wait for a server's response headers after fully\n\t// writing the request (including its body, if any). This\n\t// time does not include the time to read the response body.\n\tResponseHeaderTimeout time.Duration `mapstructure:\"response_header_timeout\"`\n\t// ExpectContinueTimeout, if non-zero, specifies the amount of\n\t// time to wait for a server's first response headers after fully\n\t// writing the request headers if the request has an\n\t// \"Expect: 100-continue\" header. Zero means no timeout and\n\t// causes the body to be sent immediately, without\n\t// waiting for the server to approve.\n\t// This time does not include the time to send the request header.\n\tExpectContinueTimeout time.Duration `mapstructure:\"expect_continue_timeout\"`\n\t// DialerTimeout is the maximum amount of time a dial will wait for\n\t// a connect to complete. If Deadline is also set, it may fail\n\t// earlier.\n\t//\n\t// The default is no timeout.\n\t//\n\t// When using TCP and dialing a host name with multiple IP\n\t// addresses, the timeout may be divided between them.\n\t//\n\t// With or without a timeout, the operating system may impose\n\t// its own earlier timeout. For instance, TCP timeouts are\n\t// often around 3 minutes.\n\tDialerTimeout time.Duration `mapstructure:\"dialer_timeout\"`\n\t// DialerFallbackDelay specifies the length of time to wait before\n\t// spawning a fallback connection, when DualStack is enabled.\n\t// If zero, a default delay of 300ms is used.\n\tDialerFallbackDelay time.Duration `mapstructure:\"dialer_fallback_delay\"`\n\t// DialerKeepAlive specifies the keep-alive period for an active\n\t// network connection.\n\t// If zero, keep-alives are not enabled. Network protocols\n\t// that do not support keep-alives ignore this field.\n\tDialerKeepAlive time.Duration `mapstructure:\"dialer_keep_alive\"`\n\n\t// DisableStrictREST flags if the REST enforcement is disabled\n\tDisableStrictREST bool `mapstructure:\"disable_rest\"`\n\n\t// Plugin defines the configuration for the plugin loader\n\tPlugin *Plugin `mapstructure:\"plugin\"`\n\n\t// TLS defines the configuration params for enabling TLS (HTTPS & HTTP/2) at\n\t// the router layer\n\tTLS *TLS `mapstructure:\"tls\"`\n\n\t// UseH2C enables h2c support.\n\tUseH2C bool `mapstructure:\"use_h2c\"`\n\n\t// run lura in debug mode\n\tDebug     bool `mapstructure:\"debug_endpoint\"`\n\tEcho      bool `mapstructure:\"echo_endpoint\"`\n\turiParser SafeURIParser\n\n\t// SequentialStart flags if the agents should be started sequentially\n\t// before starting the router\n\tSequentialStart bool `mapstructure:\"sequential_start\"`\n\n\t// AllowInsecureConnections sets the http client tls configuration to allow\n\t// insecure connections to the backends for development (enables InsecureSkipVerify)\n\tAllowInsecureConnections bool `mapstructure:\"allow_insecure_connections\"`\n\n\t// ClientTLS is used to configure the http default transport\n\t// with TLS parameters\n\tClientTLS *ClientTLS `mapstructure:\"client_tls\"`\n\n\t// DNSCacheTTL is the duration of the cached data for the DNS lookups\n\tDNSCacheTTL time.Duration `mapstructure:\"dns_cache_ttl\"`\n\n\t// MaxShutdownDuration is the maximum duration to wait for the graceful shutdown\n\t// of the service. If 0, it will wait indefinitely until all the requests are served\n\t// or the process is killed.\n\tMaxShutdownDuration time.Duration `mapstructure:\"max_shutdown_wait_time\"`\n}\n\n// AsyncAgent defines the configuration of a single subscriber/consumer to be initialized\n// and maintained by the lura service\ntype AsyncAgent struct {\n\tName       string     `mapstructure:\"name\"`\n\tConnection Connection `mapstructure:\"connection\"`\n\tConsumer   Consumer   `mapstructure:\"consumer\"`\n\t// the encoding format\n\tEncoding string `mapstructure:\"encoding\"`\n\t// set of definitions of the backends to be linked to this endpoint\n\tBackend []*Backend `mapstructure:\"backend\"`\n\n\t// Endpoint Extra configuration for customized behaviour\n\tExtraConfig ExtraConfig `mapstructure:\"extra_config\"`\n}\n\ntype Consumer struct {\n\t// timeout of the pipe defined by this subscriber\n\tTimeout time.Duration `mapstructure:\"timeout\"`\n\tWorkers int           `mapstructure:\"workers\"`\n\tTopic   string        `mapstructure:\"topic\"`\n\tMaxRate float64       `mapstructure:\"max_rate\"`\n}\n\ntype Connection struct {\n\tMaxRetries      int           `mapstructure:\"max_retries\"`\n\tBackoffStrategy string        `mapstructure:\"backoff_strategy\"`\n\tHealthInterval  time.Duration `mapstructure:\"health_interval\"`\n}\n\n// EndpointConfig defines the configuration of a single endpoint to be exposed\n// by the lura service\ntype EndpointConfig struct {\n\t// url pattern to be registered and exposed to the world\n\tEndpoint string `mapstructure:\"endpoint\"`\n\t// HTTP method of the endpoint (GET, POST, PUT, etc)\n\tMethod string `mapstructure:\"method\"`\n\t// set of definitions of the backends to be linked to this endpoint\n\tBackend []*Backend `mapstructure:\"backend\"`\n\t// number of concurrent calls this endpoint must send to the backends\n\tConcurrentCalls int `mapstructure:\"concurrent_calls\"`\n\t// timeout of this endpoint\n\tTimeout time.Duration `mapstructure:\"timeout\"`\n\t// duration of the cache header\n\tCacheTTL time.Duration `mapstructure:\"cache_ttl\"`\n\t// list of query string params to be extracted from the URI\n\tQueryString []string `mapstructure:\"input_query_strings\"`\n\t// Endpoint Extra configuration for customized behaviour\n\tExtraConfig ExtraConfig `mapstructure:\"extra_config\"`\n\t// HeadersToPass defines the list of headers to pass to the backends\n\tHeadersToPass []string `mapstructure:\"input_headers\"`\n\t// OutputEncoding defines the encoding strategy to use for the endpoint responses\n\tOutputEncoding string `mapstructure:\"output_encoding\"`\n}\n\n// Backend defines how lura should connect to the backend service (the API resource to consume)\n// and how it should process the received response\ntype Backend struct {\n\t// Group defines the name of the property the response should be moved to. If empty, the response is\n\t// not changed\n\tGroup string `mapstructure:\"group\"`\n\t// Method defines the HTTP method of the request to send to the backend\n\tMethod string `mapstructure:\"method\"`\n\t// Host is a set of hosts of the API\n\tHost []string `mapstructure:\"host\"`\n\t// HostSanitizationDisabled can be set to false if the hostname should be sanitized\n\tHostSanitizationDisabled bool `mapstructure:\"disable_host_sanitize\"`\n\t// URLPattern is the URL pattern to use to locate the resource to be consumed\n\tURLPattern string `mapstructure:\"url_pattern\"`\n\t// AllowList is a set of response fields to allow. If empty, the filter id not used\n\tAllowList []string `mapstructure:\"allow\"`\n\t// DenyList is a set of response fields to remove. If empty, the filter id not used\n\tDenyList []string `mapstructure:\"deny\"`\n\t// map of response fields to be renamed and their new names\n\tMapping map[string]string `mapstructure:\"mapping\"`\n\t// the encoding format\n\tEncoding string `mapstructure:\"encoding\"`\n\t// the response to process is a collection\n\tIsCollection bool `mapstructure:\"is_collection\"`\n\t// name of the field to extract to the root. If empty, the formater will do nothing\n\tTarget string `mapstructure:\"target\"`\n\t// name of the service discovery driver to use\n\tSD string `mapstructure:\"sd\"`\n\t// scheme to use for servers fetched from\n\tSDScheme string `mapstructure:\"sd_scheme\"`\n\n\t// list of keys to be replaced in the URLPattern\n\tURLKeys []string\n\t// number of concurrent calls this endpoint must send to the API\n\tConcurrentCalls int\n\t// timeout of this backend\n\tTimeout time.Duration\n\t// decoder to use in order to parse the received response from the API\n\tDecoder encoding.Decoder `json:\"-\"`\n\t// Backend Extra configuration for customized behaviours\n\tExtraConfig ExtraConfig `mapstructure:\"extra_config\"`\n\t// HeadersToPass defines the list of headers to pass to this backend\n\tHeadersToPass []string `mapstructure:\"input_headers\"`\n\t// QueryStringsToPass has the list of query string params to be sent to the backend\n\tQueryStringsToPass []string `mapstructure:\"input_query_strings\"`\n\n\t// ParentEndpoint is to be filled by the parent endpoint with its pattern enpoint\n\t// so logs and other instrumentation can output better info (thus, it is not loaded\n\t// with `mapstructure` or `json` tags).\n\tParentEndpoint string `json:\"-\" mapstructure:\"-\"`\n\t// ParentEndpointMethod is to be filled by the parent endpoint with its enpoint method\n\t// so logs and other instrumentation can output better info (thus, it is not loaded\n\t// with `mapstructure` or `json` tags).\n\tParentEndpointMethod string `json:\"-\" mapstructure:\"-\"`\n}\n\n// Plugin contains the config required by the plugin module\ntype Plugin struct {\n\tFolder  string `mapstructure:\"folder\"`\n\tPattern string `mapstructure:\"pattern\"`\n}\n\n// TLSKeyPair contains a pair of public and private keys\ntype TLSKeyPair struct {\n\tPublicKey  string `mapstructure:\"public_key\"`\n\tPrivateKey string `mapstructure:\"private_key\"`\n}\n\n// TLS defines the configuration params for enabling TLS (HTTPS & HTTP/2) at the router layer\ntype TLS struct {\n\tIsDisabled               bool         `mapstructure:\"disabled\"`\n\tPublicKey                string       `mapstructure:\"public_key\"`\n\tPrivateKey               string       `mapstructure:\"private_key\"`\n\tCaCerts                  []string     `mapstructure:\"ca_certs\"`\n\tMinVersion               string       `mapstructure:\"min_version\"`\n\tMaxVersion               string       `mapstructure:\"max_version\"`\n\tCurvePreferences         []uint16     `mapstructure:\"curve_preferences\"`\n\tPreferServerCipherSuites bool         `mapstructure:\"prefer_server_cipher_suites\"`\n\tCipherSuites             []uint16     `mapstructure:\"cipher_suites\"`\n\tEnableMTLS               bool         `mapstructure:\"enable_mtls\"`\n\tDisableSystemCaPool      bool         `mapstructure:\"disable_system_ca_pool\"`\n\tKeys                     []TLSKeyPair `mapstructure:\"keys\"`\n}\n\n// ClientTLS defines the configuration params for an HTTP Client\ntype ClientTLS struct {\n\tAllowInsecureConnections bool            `mapstructure:\"allow_insecure_connections\"`\n\tCaCerts                  []string        `mapstructure:\"ca_certs\"`\n\tDisableSystemCaPool      bool            `mapstructure:\"disable_system_ca_pool\"`\n\tMinVersion               string          `mapstructure:\"min_version\"`\n\tMaxVersion               string          `mapstructure:\"max_version\"`\n\tCurvePreferences         []uint16        `mapstructure:\"curve_preferences\"`\n\tCipherSuites             []uint16        `mapstructure:\"cipher_suites\"`\n\tClientCerts              []ClientTLSCert `mapstructure:\"client_certs\"`\n}\n\n// ClientTLSCert holds a certificate with its private key to be\n// used for mTLS against the backend services\ntype ClientTLSCert struct {\n\tCertificate string `mapstructure:\"certificate\"`\n\tPrivateKey  string `mapstructure:\"private_key\"`\n}\n\n// ExtraConfig is a type to store extra configurations for customized behaviours\ntype ExtraConfig map[string]interface{}\n\nfunc (e *ExtraConfig) sanitize() {\n\tfor module, extra := range *e {\n\t\tif extra, ok := extra.(map[interface{}]interface{}); ok {\n\t\t\tsanitized := map[string]interface{}{}\n\t\t\tfor k, v := range extra {\n\t\t\t\tsanitized[fmt.Sprintf(\"%v\", k)] = v\n\t\t\t}\n\t\t\t(*e)[module] = sanitized\n\t\t}\n\t}\n}\n\nfunc (e *ExtraConfig) Normalize() {\n\tfor module := range *e {\n\t\tif alias, ok := ExtraConfigAlias[module]; ok {\n\t\t\t(*e)[alias] = (*e)[module]\n\t\t\tdelete(*e, module)\n\t\t}\n\t}\n}\n\n// ExtraConfigAlias is the set of alias to accept as namespace\nvar ExtraConfigAlias = map[string]string{}\n\nvar (\n\tsimpleURLKeysPattern    = regexp.MustCompile(`\\{([\\w\\-\\.:/]+)\\}`)\n\tsequentialParamsPattern = regexp.MustCompile(`^(resp[\\d]+_.+)?(JWT\\.([\\w\\-\\.:/]+))?$`)\n\tinvalidPattern          = `^[^/]|\\*.|/__(debug|echo|health)(/.*)?$`\n\terrInvalidHost          = errors.New(\"invalid host\")\n\terrInvalidNoOpEncoding  = errors.New(\"can not use NoOp encoding with more than one backends connected to the same endpoint\")\n\tdefaultPort             = 8080\n)\n\n// Hash returns the sha 256 hash of the configuration in a standard base64 encoded string. It ignores the\n// name in order to reduce the noise.\nfunc (s *ServiceConfig) Hash() (string, error) {\n\tvar name string\n\tname, s.Name = s.Name, \"\"\n\tdefer func() { s.Name = name }()\n\n\tb, err := json.Marshal(s)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tsum := sha256.Sum256(b)\n\treturn base64.StdEncoding.EncodeToString(sum[:]), nil\n}\n\n// Init initializes the configuration struct and its defined endpoints and backends.\n// Init also sanitizes the values, applies the default ones whenever necessary and\n// normalizes all the things.\nfunc (s *ServiceConfig) Init() error {\n\ts.uriParser = NewSafeURIParser()\n\n\tif s.Version != ConfigVersion {\n\t\treturn &UnsupportedVersionError{\n\t\t\tHave: s.Version,\n\t\t\tWant: ConfigVersion,\n\t\t}\n\t}\n\n\tif err := s.initGlobalParams(); err != nil {\n\t\treturn err\n\t}\n\n\tif err := s.initAsyncAgents(); err != nil {\n\t\treturn err\n\t}\n\n\treturn s.initEndpoints()\n}\n\nfunc (s *ServiceConfig) Normalize() {\n\ts.ExtraConfig.Normalize()\n\tfor _, e := range s.Endpoints {\n\t\te.ExtraConfig.Normalize()\n\t\tfor _, b := range e.Backend {\n\t\t\tb.ExtraConfig.Normalize()\n\t\t}\n\t}\n\tfor _, a := range s.AsyncAgents {\n\t\ta.ExtraConfig.Normalize()\n\t\tfor _, b := range a.Backend {\n\t\t\tb.ExtraConfig.Normalize()\n\t\t}\n\t}\n}\n\nfunc (s *ServiceConfig) initGlobalParams() error {\n\tif s.Port == 0 {\n\t\ts.Port = defaultPort\n\t}\n\n\tif s.Address != \"\" {\n\t\tif !validateAddress(s.Address) {\n\t\t\treturn fmt.Errorf(\"invalid ip address %s\", s.Address)\n\t\t}\n\t}\n\n\tif s.MaxIdleConnsPerHost == 0 {\n\t\ts.MaxIdleConnsPerHost = DefaultMaxIdleConnsPerHost\n\t}\n\tif s.Timeout == 0 {\n\t\ts.Timeout = DefaultTimeout\n\t}\n\n\tvar err error\n\ts.Host, err = s.uriParser.SafeCleanHosts(s.Host)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.ExtraConfig.sanitize()\n\treturn nil\n}\n\nfunc (s *ServiceConfig) initAsyncAgents() error {\n\tfor i, e := range s.AsyncAgents {\n\t\ts.initAsyncAgentDefaults(i)\n\n\t\te.ExtraConfig.sanitize()\n\n\t\tfor _, b := range e.Backend {\n\t\t\tif len(b.Host) == 0 {\n\t\t\t\tb.Host = s.Host\n\t\t\t} else if !b.HostSanitizationDisabled {\n\t\t\t\tvar err error\n\t\t\t\tb.Host, err = s.uriParser.SafeCleanHosts(b.Host)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tif b.Method == \"\" {\n\t\t\t\tb.Method = http.MethodGet\n\t\t\t}\n\t\t\tb.Timeout = e.Consumer.Timeout\n\t\t\tb.Decoder = encoding.GetRegister().Get(strings.ToLower(b.Encoding))(b.IsCollection)\n\n\t\t\tb.ExtraConfig.sanitize()\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *ServiceConfig) initEndpoints() error {\n\tfor i, e := range s.Endpoints {\n\t\te.Endpoint = s.uriParser.CleanPath(e.Endpoint)\n\n\t\tif err := e.validate(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor i := range e.HeadersToPass {\n\t\t\te.HeadersToPass[i] = textproto.CanonicalMIMEHeaderKey(e.HeadersToPass[i])\n\t\t}\n\n\t\tinputParams := s.extractPlaceHoldersFromURLTemplate(e.Endpoint, s.paramExtractionPattern())\n\t\tinputSet := map[string]interface{}{}\n\t\tfor ip := range inputParams {\n\t\t\tinputSet[inputParams[ip]] = nil\n\t\t}\n\n\t\te.Endpoint = s.uriParser.GetEndpointPath(e.Endpoint, inputParams)\n\n\t\ts.initEndpointDefaults(i)\n\n\t\tif e.OutputEncoding == encoding.NOOP && len(e.Backend) > 1 {\n\t\t\treturn errInvalidNoOpEncoding\n\t\t}\n\n\t\te.ExtraConfig.sanitize()\n\n\t\tfor j, b := range e.Backend {\n\t\t\t// we \"tell\" the backend which is his parent endpoint\n\t\t\tb.ParentEndpoint = e.Endpoint\n\t\t\tb.ParentEndpointMethod = e.Method\n\t\t\tif err := s.initBackendDefaults(i, j); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := s.initBackendURLMappings(i, j, inputSet); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tb.ExtraConfig.sanitize()\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *ServiceConfig) paramExtractionPattern() *regexp.Regexp {\n\tif s.DisableStrictREST {\n\t\treturn simpleURLKeysPattern\n\t}\n\treturn endpointURLKeysPattern\n}\n\nfunc (*ServiceConfig) extractPlaceHoldersFromURLTemplate(subject string, pattern *regexp.Regexp) []string {\n\tmatches := pattern.FindAllStringSubmatch(subject, -1)\n\tkeys := make([]string, len(matches))\n\tfor k, v := range matches {\n\t\tkeys[k] = v[1]\n\t}\n\treturn keys\n}\n\nfunc (s *ServiceConfig) initEndpointDefaults(e int) {\n\tendpoint := s.Endpoints[e]\n\tif endpoint.Method == \"\" {\n\t\tendpoint.Method = \"GET\"\n\t}\n\tif s.CacheTTL != 0 && endpoint.CacheTTL == 0 {\n\t\tendpoint.CacheTTL = s.CacheTTL\n\t}\n\tif s.Timeout != 0 && endpoint.Timeout == 0 {\n\t\tendpoint.Timeout = s.Timeout\n\t}\n\tif endpoint.ConcurrentCalls == 0 {\n\t\tendpoint.ConcurrentCalls = 1\n\t}\n\tif endpoint.OutputEncoding == \"\" {\n\t\tif s.OutputEncoding != \"\" {\n\t\t\tendpoint.OutputEncoding = s.OutputEncoding\n\t\t} else {\n\t\t\tendpoint.OutputEncoding = encoding.JSON\n\t\t}\n\t}\n}\n\nfunc (s *ServiceConfig) initAsyncAgentDefaults(e int) {\n\tagent := s.AsyncAgents[e]\n\tif s.Timeout != 0 && agent.Consumer.Timeout == 0 {\n\t\tagent.Consumer.Timeout = s.Timeout\n\t}\n\tif agent.Consumer.Workers < 1 {\n\t\tagent.Consumer.Workers = 1\n\t}\n\tif agent.Connection.HealthInterval < time.Second {\n\t\tagent.Connection.HealthInterval = time.Second\n\t}\n}\n\nfunc (s *ServiceConfig) initBackendDefaults(e, b int) error {\n\tendpoint := s.Endpoints[e]\n\tbackend := endpoint.Backend[b]\n\tif len(backend.Host) == 0 {\n\t\tbackend.Host = s.Host\n\t} else if !backend.HostSanitizationDisabled {\n\t\tvar err error\n\t\tbackend.Host, err = s.uriParser.SafeCleanHosts(backend.Host)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif backend.Method == \"\" {\n\t\tbackend.Method = endpoint.Method\n\t}\n\tif endpoint.OutputEncoding == encoding.NOOP {\n\t\tbackend.Encoding = encoding.NOOP\n\t}\n\tbackend.Timeout = endpoint.Timeout\n\tbackend.ConcurrentCalls = endpoint.ConcurrentCalls\n\tbackend.Decoder = encoding.GetRegister().Get(strings.ToLower(backend.Encoding))(backend.IsCollection)\n\n\tfor i := range backend.HeadersToPass {\n\t\tbackend.HeadersToPass[i] = textproto.CanonicalMIMEHeaderKey(backend.HeadersToPass[i])\n\t}\n\tif backend.SDScheme == \"\" {\n\t\tbackend.SDScheme = \"http\"\n\t}\n\treturn nil\n}\n\nfunc (s *ServiceConfig) initBackendURLMappings(e, b int, inputParams map[string]interface{}) error {\n\tbackend := s.Endpoints[e].Backend[b]\n\n\tbackend.URLPattern = s.uriParser.CleanPath(backend.URLPattern)\n\n\toutputParams, outputSetSize := uniqueOutput(s.extractPlaceHoldersFromURLTemplate(backend.URLPattern, simpleURLKeysPattern))\n\n\tip := fromSetToSortedSlice(inputParams)\n\n\tif outputSetSize > len(ip) {\n\t\treturn &WrongNumberOfParamsError{\n\t\t\tEndpoint:     s.Endpoints[e].Endpoint,\n\t\t\tMethod:       s.Endpoints[e].Method,\n\t\t\tBackend:      b,\n\t\t\tInputParams:  ip,\n\t\t\tOutputParams: outputParams,\n\t\t}\n\t}\n\n\ttitle := cases.Title(language.Und)\n\tbackend.URLKeys = []string{}\n\tfor _, output := range outputParams {\n\t\tif !sequentialParamsPattern.MatchString(output) {\n\t\t\tif _, ok := inputParams[output]; !ok {\n\t\t\t\treturn &UndefinedOutputParamError{\n\t\t\t\t\tParam:        output,\n\t\t\t\t\tEndpoint:     s.Endpoints[e].Endpoint,\n\t\t\t\t\tMethod:       s.Endpoints[e].Method,\n\t\t\t\t\tBackend:      b,\n\t\t\t\t\tInputParams:  ip,\n\t\t\t\t\tOutputParams: outputParams,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tkey := title.String(output[:1]) + output[1:]\n\t\tbackend.URLPattern = strings.ReplaceAll(backend.URLPattern, \"{\"+output+\"}\", \"{{.\"+key+\"}}\")\n\t\tbackend.URLKeys = append(backend.URLKeys, key)\n\t}\n\treturn nil\n}\n\nfunc fromSetToSortedSlice(set map[string]interface{}) []string {\n\tres := make([]string, 0, len(set))\n\tfor element := range set {\n\t\tres = append(res, element)\n\t}\n\tsort.Strings(res)\n\treturn res\n}\n\nfunc uniqueOutput(output []string) ([]string, int) {\n\tsort.Strings(output)\n\tj := 0\n\toutputSetSize := 0\n\tfor i := 1; i < len(output); i++ {\n\t\tif output[j] == output[i] {\n\t\t\tcontinue\n\t\t}\n\t\tif !sequentialParamsPattern.MatchString(output[j]) {\n\t\t\toutputSetSize++\n\t\t}\n\t\tj++\n\t\toutput[j] = output[i]\n\t}\n\tif j == len(output) {\n\t\treturn output, outputSetSize\n\t}\n\treturn output[:j+1], outputSetSize\n}\n\nfunc (e *EndpointConfig) validate() error {\n\tmatched, err := regexp.MatchString(invalidPattern, e.Endpoint)\n\tif err != nil {\n\t\treturn &EndpointMatchError{\n\t\t\tErr:    err,\n\t\t\tPath:   e.Endpoint,\n\t\t\tMethod: e.Method,\n\t\t}\n\t}\n\tif matched {\n\t\treturn &EndpointPathError{Path: e.Endpoint, Method: e.Method}\n\t}\n\n\tif len(e.Backend) == 0 {\n\t\treturn &NoBackendsError{Path: e.Endpoint, Method: e.Method}\n\t}\n\treturn nil\n}\n\n// EndpointMatchError is the error returned by the configuration init process when the endpoint pattern\n// check fails\ntype EndpointMatchError struct {\n\tPath   string\n\tMethod string\n\tErr    error\n}\n\n// Error returns a string representation of the EndpointMatchError\nfunc (e *EndpointMatchError) Error() string {\n\treturn fmt.Sprintf(\"ignoring the '%s %s' endpoint due to a parsing error: %s\", e.Method, e.Path, e.Err.Error())\n}\n\n// NoBackendsError is the error returned by the configuration init process when an endpoint\n// is connected to 0 backends\ntype NoBackendsError struct {\n\tPath   string\n\tMethod string\n}\n\n// Error returns a string representation of the NoBackendsError\nfunc (n *NoBackendsError) Error() string {\n\treturn \"ignoring the '\" + n.Method + \" \" + n.Path + \"' endpoint, since it has 0 backends defined!\"\n}\n\n// UnsupportedVersionError is the error returned by the configuration init process when the configuration\n// version is not supported\ntype UnsupportedVersionError struct {\n\tHave int\n\tWant int\n}\n\n// Error returns a string representation of the UnsupportedVersionError\nfunc (u *UnsupportedVersionError) Error() string {\n\treturn fmt.Sprintf(\"unsupported version: %d (want: %d)\", u.Have, u.Want)\n}\n\n// EndpointPathError is the error returned by the configuration init process when an endpoint\n// is using a forbidden path\ntype EndpointPathError struct {\n\tPath   string\n\tMethod string\n}\n\n// Error returns a string representation of the EndpointPathError\nfunc (e *EndpointPathError) Error() string {\n\treturn \"ignoring the '\" + e.Method + \" \" + e.Path + \"' endpoint, since it is invalid!!!\"\n}\n\n// UndefinedOutputParamError is the error returned by the configuration init process when an output\n// param is not present in the input param set\ntype UndefinedOutputParamError struct {\n\tEndpoint     string\n\tMethod       string\n\tBackend      int\n\tInputParams  []string\n\tOutputParams []string\n\tParam        string\n}\n\n// Error returns a string representation of the UndefinedOutputParamError\nfunc (u *UndefinedOutputParamError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"undefined output param '%s'! endpoint: %s %s, backend: %d. input: %v, output: %v\",\n\t\tu.Param,\n\t\tu.Method,\n\t\tu.Endpoint,\n\t\tu.Backend,\n\t\tu.InputParams,\n\t\tu.OutputParams,\n\t)\n}\n\n// WrongNumberOfParamsError is the error returned by the configuration init process when the number of output\n// params is greatter than the number of input params\ntype WrongNumberOfParamsError struct {\n\tEndpoint     string\n\tMethod       string\n\tBackend      int\n\tInputParams  []string\n\tOutputParams []string\n}\n\n// Error returns a string representation of the WrongNumberOfParamsError\nfunc (w *WrongNumberOfParamsError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"input and output params do not match. endpoint: %s %s, backend: %d. input: %v, output: %v\",\n\t\tw.Method,\n\t\tw.Endpoint,\n\t\tw.Backend,\n\t\tw.InputParams,\n\t\tw.OutputParams,\n\t)\n}\n\nfunc SetSequentialParamsPattern(pattern string) error {\n\tre, err := regexp.Compile(pattern)\n\tif err != nil {\n\t\treturn err\n\t}\n\tsequentialParamsPattern = re\n\treturn nil\n}\n\n// SetInvalidPattern sets the invalidPattern variable to the provided value.\nfunc SetInvalidPattern(pattern string) {\n\tinvalidPattern = pattern\n}\n\nfunc validateAddress(address string) bool {\n\tip := net.ParseIP(address)\n\treturn ip != nil\n}\n"
  },
  {
    "path": "config/config_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage config\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestConfig_rejectInvalidVersion(t *testing.T) {\n\tsubject := ServiceConfig{}\n\terr := subject.Init()\n\tif err == nil || strings.Index(err.Error(), \"unsupported version: 0 (want: 3)\") != 0 {\n\t\tt.Error(\"Error expected. Got\", err.Error())\n\t}\n}\n\nfunc TestConfig_rejectInvalidEndpoints(t *testing.T) {\n\tsamples := []string{\n\t\t\"/__debug\",\n\t\t\"/__debug/\",\n\t\t\"/__debug/foo\",\n\t\t\"/__debug/foo/bar\",\n\t}\n\n\tfor _, e := range samples {\n\t\tsubject := ServiceConfig{Version: ConfigVersion, Endpoints: []*EndpointConfig{{Endpoint: e, Method: \"GET\"}}}\n\t\terr := subject.Init()\n\t\tif err == nil || err.Error() != fmt.Sprintf(\"ignoring the 'GET %s' endpoint, since it is invalid!!!\", e) {\n\t\t\tt.Errorf(\"Unexpected error processing '%s': %v\", e, err)\n\t\t}\n\t}\n}\n\nfunc TestConfig_initBackendURLMappings_ok(t *testing.T) {\n\tsamples := []string{\n\t\t\"supu/{tupu}\",\n\t\t\"/supu/{tupu1}\",\n\t\t\"/supu.local/\",\n\t\t\"supu/{tupu_56}/{supu-5t6}?a={foo}&b={foo}\",\n\t\t\"supu/{tupu_56}{supu-5t6}?a={foo}&b={foo}\",\n\t\t\"supu/tupu{supu-5t6}?a={foo}&b={foo}\",\n\t\t\"{resp0_x}/{tupu1}/{tupu_56}{supu-5t6}?a={tupu}&b={foo}\",\n\t\t\"{resp0_x}/{tupu1}/{JWT.foo}\",\n\t\t\"{resp0_x}/{tupu1}/{JWT.http://example.com/foo_bar}\",\n\t}\n\n\texpected := []string{\n\t\t\"/supu/{{.Tupu}}\",\n\t\t\"/supu/{{.Tupu1}}\",\n\t\t\"/supu.local/\",\n\t\t\"/supu/{{.Tupu_56}}/{{.Supu-5t6}}?a={{.Foo}}&b={{.Foo}}\",\n\t\t\"/supu/{{.Tupu_56}}{{.Supu-5t6}}?a={{.Foo}}&b={{.Foo}}\",\n\t\t\"/supu/tupu{{.Supu-5t6}}?a={{.Foo}}&b={{.Foo}}\",\n\t\t\"/{{.Resp0_x}}/{{.Tupu1}}/{{.Tupu_56}}{{.Supu-5t6}}?a={{.Tupu}}&b={{.Foo}}\",\n\t\t\"/{{.Resp0_x}}/{{.Tupu1}}/{{.JWT.foo}}\",\n\t\t\"/{{.Resp0_x}}/{{.Tupu1}}/{{.JWT.http://example.com/foo_bar}}\",\n\t}\n\n\tbackend := Backend{}\n\tendpoint := EndpointConfig{Backend: []*Backend{&backend}}\n\tsubject := ServiceConfig{Endpoints: []*EndpointConfig{&endpoint}, uriParser: NewSafeURIParser()}\n\n\tinputSet := map[string]interface{}{\n\t\t\"tupu\":     nil,\n\t\t\"tupu1\":    nil,\n\t\t\"tupu_56\":  nil,\n\t\t\"supu-5t6\": nil,\n\t\t\"foo\":      nil,\n\t}\n\n\tfor i := range samples {\n\t\tbackend.URLPattern = samples[i]\n\t\tif err := subject.initBackendURLMappings(0, 0, inputSet); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif backend.URLPattern != expected[i] {\n\t\t\tt.Errorf(\"want: %s, have: %s\\n\", expected[i], backend.URLPattern)\n\t\t}\n\t}\n}\n\nfunc TestConfig_initBackendURLMappings_tooManyOutput(t *testing.T) {\n\tbackend := Backend{URLPattern: \"supu/{tupu_56}/{supu-5t6}?a={foo}&b={foo}\"}\n\tendpoint := EndpointConfig{\n\t\tMethod:   \"GET\",\n\t\tEndpoint: \"/some/{tupu}\",\n\t\tBackend:  []*Backend{&backend},\n\t}\n\tsubject := ServiceConfig{Endpoints: []*EndpointConfig{&endpoint}, uriParser: NewSafeURIParser()}\n\n\tinputSet := map[string]interface{}{\n\t\t\"tupu\": nil,\n\t}\n\n\texpectedErrMsg := \"input and output params do not match. endpoint: GET /some/{tupu}, backend: 0. input: [tupu], output: [foo supu-5t6 tupu_56]\"\n\n\terr := subject.initBackendURLMappings(0, 0, inputSet)\n\tif err == nil || err.Error() != expectedErrMsg {\n\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t}\n}\n\nfunc TestConfig_initBackendURLMappings_undefinedOutput(t *testing.T) {\n\tbackend := Backend{URLPattern: \"supu/{tupu_56}/{supu-5t6}?a={foo}&b={foo}\"}\n\tendpoint := EndpointConfig{Endpoint: \"/\", Method: \"GET\", Backend: []*Backend{&backend}}\n\tsubject := ServiceConfig{Endpoints: []*EndpointConfig{&endpoint}, uriParser: NewSafeURIParser()}\n\n\tinputSet := map[string]interface{}{\n\t\t\"tupu\": nil,\n\t\t\"supu\": nil,\n\t\t\"foo\":  nil,\n\t}\n\n\texpectedErrMsg := \"undefined output param 'supu-5t6'! endpoint: GET /, backend: 0. input: [foo supu tupu], output: [foo supu-5t6 tupu_56]\"\n\terr := subject.initBackendURLMappings(0, 0, inputSet)\n\tif err == nil || err.Error() != expectedErrMsg {\n\t\tt.Errorf(\"error expected. have: %v\", err)\n\t}\n}\n\nfunc TestConfig_init(t *testing.T) {\n\tsupuBackend := Backend{\n\t\tURLPattern: \"/__debug/supu\",\n\t}\n\tsupuEndpoint := EndpointConfig{\n\t\tEndpoint:       \"/supu\",\n\t\tMethod:         \"post\",\n\t\tTimeout:        1500 * time.Millisecond,\n\t\tCacheTTL:       6 * time.Hour,\n\t\tBackend:        []*Backend{&supuBackend},\n\t\tOutputEncoding: \"some_render\",\n\t}\n\n\tgithubBackend := Backend{\n\t\tURLPattern: \"/\",\n\t\tHost:       []string{\"https://api.github.com\"},\n\t\tAllowList:  []string{\"authorizations_url\", \"code_search_url\"},\n\t}\n\tgithubEndpoint := EndpointConfig{\n\t\tEndpoint: \"/github\",\n\t\tTimeout:  1500 * time.Millisecond,\n\t\tCacheTTL: 6 * time.Hour,\n\t\tBackend:  []*Backend{&githubBackend},\n\t}\n\n\tuserBackend := Backend{\n\t\tURLPattern: \"/users/{user}\",\n\t\tHost:       []string{\"https://jsonplaceholder.typicode.com\"},\n\t\tMapping:    map[string]string{\"email\": \"personal_email\"},\n\t}\n\trssBackend := Backend{\n\t\tURLPattern: \"/users/{user}\",\n\t\tHost:       []string{\"https://jsonplaceholder.typicode.com\"},\n\t\tEncoding:   \"rss\",\n\t}\n\tpostBackend := Backend{\n\t\tURLPattern: \"/posts/{user}\",\n\t\tHost:       []string{\"https://jsonplaceholder.typicode.com\"},\n\t\tGroup:      \"posts\",\n\t\tEncoding:   \"xml\",\n\t}\n\tuserEndpoint := EndpointConfig{\n\t\tEndpoint: \"/users/{user}\",\n\t\tBackend:  []*Backend{&userBackend, &rssBackend, &postBackend},\n\t}\n\n\tsubject := ServiceConfig{\n\t\tVersion:   ConfigVersion,\n\t\tTimeout:   5 * time.Second,\n\t\tCacheTTL:  30 * time.Minute,\n\t\tHost:      []string{\"http://127.0.0.1:8080\"},\n\t\tEndpoints: []*EndpointConfig{&supuEndpoint, &githubEndpoint, &userEndpoint},\n\t}\n\n\tif err := subject.Init(); err != nil {\n\t\tt.Error(\"Error at the configuration init:\", err.Error())\n\t}\n\n\tif len(supuBackend.Host) != 1 || supuBackend.Host[0] != subject.Host[0] {\n\t\tt.Error(\"Default hosts not applied to the supu backend\", supuBackend.Host)\n\t}\n\n\tfor level, method := range map[string]string{\n\t\t\"userBackend\":  userBackend.Method,\n\t\t\"postBackend\":  postBackend.Method,\n\t\t\"userEndpoint\": userEndpoint.Method,\n\t} {\n\t\tif method != \"GET\" {\n\t\t\tt.Errorf(\"Default method not applied at %s. Get: %s\", level, method)\n\t\t}\n\t}\n\n\tif supuBackend.Method != \"post\" {\n\t\tt.Error(\"unexpected supuBackend\")\n\t}\n\n\tif userBackend.Timeout != subject.Timeout {\n\t\tt.Error(\"default timeout not applied to the userBackend\")\n\t}\n\n\tif userEndpoint.CacheTTL != subject.CacheTTL {\n\t\tt.Error(\"default CacheTTL not applied to the userEndpoint\")\n\t}\n\n\thash, err := subject.Hash()\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t}\n\n\tif hash != \"/X+fgDf29kmtPpCUh9DeJBOwewpExy3IGEjeqA9zExA=\" {\n\t\tt.Errorf(\"unexpected hash: %s\", hash)\n\t}\n}\n\nfunc TestConfig_initKONoBackends(t *testing.T) {\n\tsubject := ServiceConfig{\n\t\tVersion: ConfigVersion,\n\t\tHost:    []string{\"http://127.0.0.1:8080\"},\n\t\tEndpoints: []*EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/supu\",\n\t\t\t\tMethod:   \"POST\",\n\t\t\t\tBackend:  []*Backend{},\n\t\t\t},\n\t\t},\n\t}\n\n\tif err := subject.Init(); err == nil ||\n\t\terr.Error() != \"ignoring the 'POST /supu' endpoint, since it has 0 backends defined!\" {\n\t\tt.Error(\"Unexpected error at the configuration init!\", err)\n\t}\n}\n\nfunc TestConfig_initKOMultipleBackendsForNoopEncoder(t *testing.T) {\n\tsubject := ServiceConfig{\n\t\tVersion: ConfigVersion,\n\t\tHost:    []string{\"http://127.0.0.1:8080\"},\n\t\tEndpoints: []*EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint:       \"/supu\",\n\t\t\t\tMethod:         \"post\",\n\t\t\t\tOutputEncoding: \"no-op\",\n\t\t\t\tBackend: []*Backend{\n\t\t\t\t\t{\n\t\t\t\t\t\tEncoding: \"no-op\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tEncoding: \"no-op\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tif err := subject.Init(); err != errInvalidNoOpEncoding {\n\t\tt.Error(\"Expecting an error at the configuration init!\", err)\n\t}\n}\n\nfunc TestConfig_initKOInvalidHost(t *testing.T) {\n\tsubject := ServiceConfig{\n\t\tVersion: ConfigVersion,\n\t\tHost:    []string{\"http://127.0.0.1:8080http://127.0.0.1:8080\"},\n\t\tEndpoints: []*EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/supu\",\n\t\t\t\tMethod:   \"post\",\n\t\t\t\tBackend:  []*Backend{},\n\t\t\t},\n\t\t},\n\t}\n\n\terr := subject.Init()\n\tif err == nil {\n\t\tt.Errorf(\"expected to fail with invalid host\")\n\t\treturn\n\t}\n\n\tif !errors.Is(err, errInvalidHost) {\n\t\tt.Errorf(\"expected 'errInvalidHost' got: %s\", err.Error())\n\t\treturn\n\t}\n}\n\nfunc TestConfig_initKOInvalidDebugPattern(t *testing.T) {\n\tdp := invalidPattern\n\n\tinvalidPattern = \"a(b\"\n\tsubject := ServiceConfig{\n\t\tVersion: ConfigVersion,\n\t\tHost:    []string{\"http://127.0.0.1:8080\"},\n\t\tEndpoints: []*EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/__debug/supu\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tBackend:  []*Backend{},\n\t\t\t},\n\t\t},\n\t}\n\n\tif err := subject.Init(); err == nil ||\n\t\terr.Error() != \"ignoring the 'GET /__debug/supu' endpoint due to a parsing error: error parsing regexp: missing closing ): `a(b`\" {\n\t\tt.Error(\"Expecting an error at the configuration init!\", err)\n\t}\n\n\tinvalidPattern = dp\n}\n\nfunc TestConfig_initKOValidSetinvalidPattern(t *testing.T) {\n\tdp := invalidPattern\n\n\tinvalidPattern = `^[^/]|/__(debug|echo|health)(/.*)?$`\n\tsubject := ServiceConfig{\n\t\tVersion: ConfigVersion,\n\t\tHost:    []string{\"http://127.0.0.1:8080\"},\n\t\tEndpoints: []*EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/*\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tBackend: []*Backend{\n\t\t\t\t\t{\n\t\t\t\t\t\tURLPattern: \"/\",\n\t\t\t\t\t\tHost:       []string{\"https://api.github.com\"},\n\t\t\t\t\t\tAllowList:  []string{\"authorizations_url\", \"code_search_url\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tif err := subject.Init(); err != nil {\n\t\tt.Error(err)\n\t}\n\n\tinvalidPattern = dp\n}\n"
  },
  {
    "path": "config/parser.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage config\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n)\n\n// Parser reads a configuration file, parses it and returns the content as an init ServiceConfig struct\ntype Parser interface {\n\tParse(configFile string) (ServiceConfig, error)\n}\n\n// ParserFunc type is an adapter to allow the use of ordinary functions as subscribers.\n// If f is a function with the appropriate signature, ParserFunc(f) is a Parser that calls f.\ntype ParserFunc func(string) (ServiceConfig, error)\n\n// Parse implements the Parser interface\nfunc (f ParserFunc) Parse(configFile string) (ServiceConfig, error) { return f(configFile) }\n\n// NewParser creates a new parser using the json library\nfunc NewParser() Parser {\n\treturn NewParserWithFileReader(os.ReadFile)\n}\n\n// NewParserWithFileReader returns a Parser with the injected FileReaderFunc function\nfunc NewParserWithFileReader(f FileReaderFunc) Parser {\n\treturn parser{fileReader: f}\n}\n\ntype parser struct {\n\tfileReader FileReaderFunc\n}\n\n// Parser implements the Parse interface\nfunc (p parser) Parse(configFile string) (ServiceConfig, error) {\n\tvar result ServiceConfig\n\tvar cfg parseableServiceConfig\n\tdata, err := p.fileReader(configFile)\n\tif err != nil {\n\t\treturn result, CheckErr(err, configFile)\n\t}\n\tif err = json.Unmarshal(data, &cfg); err != nil {\n\t\treturn result, CheckErr(err, configFile)\n\t}\n\tresult = cfg.normalize()\n\n\tif err = result.Init(); err != nil {\n\t\treturn result, CheckErr(err, configFile)\n\t}\n\n\treturn result, nil\n}\n\n// CheckErr returns a proper documented error\nfunc CheckErr(err error, configFile string) error {\n\tswitch e := err.(type) {\n\tcase *json.SyntaxError:\n\t\treturn NewParseError(err, configFile, int(e.Offset))\n\tcase *json.UnmarshalTypeError:\n\t\treturn NewParseError(err, configFile, int(e.Offset))\n\tcase *os.PathError:\n\t\treturn fmt.Errorf(\n\t\t\t\"'%s' (%s): %s\",\n\t\t\tconfigFile,\n\t\t\te.Op,\n\t\t\te.Err.Error(),\n\t\t)\n\tdefault:\n\t\treturn fmt.Errorf(\"'%s': %v\", configFile, err)\n\t}\n}\n\n// NewParseError returns a new ParseError\nfunc NewParseError(err error, configFile string, offset int) *ParseError {\n\tb, _ := os.ReadFile(configFile)\n\trow, col := getErrorRowCol(b, offset)\n\treturn &ParseError{\n\t\tConfigFile: configFile,\n\t\tErr:        err,\n\t\tOffset:     offset,\n\t\tRow:        row,\n\t\tCol:        col,\n\t}\n}\n\nfunc getErrorRowCol(source []byte, offset int) (row, col int) {\n\tif len(source) < offset {\n\t\toffset = len(source) - 1\n\t}\n\tfor i := 0; i < offset; i++ {\n\t\tv := source[i]\n\t\tif v == '\\r' {\n\t\t\tcontinue\n\t\t}\n\t\tif v == '\\n' {\n\t\t\tcol = 0\n\t\t\trow++\n\t\t\tcontinue\n\t\t}\n\t\tcol++\n\t}\n\treturn\n}\n\n// ParseError is an error containing details regarding the row and column where\n// an parse error occurred\ntype ParseError struct {\n\tConfigFile string\n\tOffset     int\n\tRow        int\n\tCol        int\n\tErr        error\n}\n\n// Error returns the error message for the ParseError\nfunc (p *ParseError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"'%s': %v, offset: %v, row: %v, col: %v\",\n\t\tp.ConfigFile,\n\t\tp.Err.Error(),\n\t\tp.Offset,\n\t\tp.Row,\n\t\tp.Col,\n\t)\n}\n\n// FileReaderFunc is a function used to read the content of a config file\ntype FileReaderFunc func(string) ([]byte, error)\n\ntype parseableServiceConfig struct {\n\tName                  string                     `json:\"name\"`\n\tEndpoints             []*parseableEndpointConfig `json:\"endpoints\"`\n\tAsyncAgents           []*parseableAsyncAgent     `json:\"async_agent\"`\n\tTimeout               string                     `json:\"timeout\"`\n\tCacheTTL              string                     `json:\"cache_ttl\"`\n\tHost                  []string                   `json:\"host\"`\n\tPort                  int                        `json:\"port\"`\n\tAddress               string                     `json:\"listen_ip\"`\n\tVersion               int                        `json:\"version\"`\n\tExtraConfig           *ExtraConfig               `json:\"extra_config,omitempty\"`\n\tReadTimeout           string                     `json:\"read_timeout\"`\n\tWriteTimeout          string                     `json:\"write_timeout\"`\n\tIdleTimeout           string                     `json:\"idle_timeout\"`\n\tReadHeaderTimeout     string                     `json:\"read_header_timeout\"`\n\tMaxHeaderBytes        int                        `json:\"max_header_bytes\"`\n\tDisableKeepAlives     bool                       `json:\"disable_keep_alives\"`\n\tDisableCompression    bool                       `json:\"disable_compression\"`\n\tDisableStrictREST     bool                       `json:\"disable_rest\"`\n\tMaxIdleConns          int                        `json:\"max_idle_connections\"`\n\tMaxIdleConnsPerHost   int                        `json:\"max_idle_connections_per_host\"`\n\tIdleConnTimeout       string                     `json:\"idle_connection_timeout\"`\n\tResponseHeaderTimeout string                     `json:\"response_header_timeout\"`\n\tExpectContinueTimeout string                     `json:\"expect_continue_timeout\"`\n\tOutputEncoding        string                     `json:\"output_encoding\"`\n\tDialerTimeout         string                     `json:\"dialer_timeout\"`\n\tDialerFallbackDelay   string                     `json:\"dialer_fallback_delay\"`\n\tDialerKeepAlive       string                     `json:\"dialer_keep_alive\"`\n\tDebug                 bool                       `json:\"debug_endpoint\"`\n\tEcho                  bool                       `json:\"echo_endpoint\"`\n\tPlugin                *Plugin                    `json:\"plugin,omitempty\"`\n\tTLS                   *parseableTLS              `json:\"tls,omitempty\"`\n\tClientTLS             *parseableClientTLS        `json:\"client_tls,omitempty\"`\n\tUseH2C                bool                       `json:\"use_h2c,omitempty\"`\n\tDNSCacheTTL           string                     `json:\"dns_cache_ttl\"`\n\tMaxShutdownDuration   string                     `json:\"max_shutdown_wait_time\"`\n}\n\nfunc (p *parseableServiceConfig) normalize() ServiceConfig {\n\tcfg := ServiceConfig{\n\t\tName:                  p.Name,\n\t\tTimeout:               parseDuration(p.Timeout),\n\t\tCacheTTL:              parseDuration(p.CacheTTL),\n\t\tHost:                  p.Host,\n\t\tPort:                  p.Port,\n\t\tAddress:               p.Address,\n\t\tVersion:               p.Version,\n\t\tDebug:                 p.Debug,\n\t\tEcho:                  p.Echo,\n\t\tReadTimeout:           parseDuration(p.ReadTimeout),\n\t\tWriteTimeout:          parseDuration(p.WriteTimeout),\n\t\tIdleTimeout:           parseDuration(p.IdleTimeout),\n\t\tReadHeaderTimeout:     parseDuration(p.ReadHeaderTimeout),\n\t\tMaxHeaderBytes:        p.MaxHeaderBytes,\n\t\tDisableKeepAlives:     p.DisableKeepAlives,\n\t\tDisableCompression:    p.DisableCompression,\n\t\tDisableStrictREST:     p.DisableStrictREST,\n\t\tMaxIdleConns:          p.MaxIdleConns,\n\t\tMaxIdleConnsPerHost:   p.MaxIdleConnsPerHost,\n\t\tIdleConnTimeout:       parseDuration(p.IdleConnTimeout),\n\t\tResponseHeaderTimeout: parseDuration(p.ResponseHeaderTimeout),\n\t\tExpectContinueTimeout: parseDuration(p.ExpectContinueTimeout),\n\t\tDialerTimeout:         parseDuration(p.DialerTimeout),\n\t\tDialerFallbackDelay:   parseDuration(p.DialerFallbackDelay),\n\t\tDialerKeepAlive:       parseDuration(p.DialerKeepAlive),\n\t\tOutputEncoding:        p.OutputEncoding,\n\t\tPlugin:                p.Plugin,\n\t\tUseH2C:                p.UseH2C,\n\t\tDNSCacheTTL:           parseDuration(p.DNSCacheTTL),\n\t\tMaxShutdownDuration:   parseDuration(p.MaxShutdownDuration),\n\t}\n\tif p.TLS != nil {\n\t\tcfg.TLS = &TLS{\n\t\t\tIsDisabled:               p.TLS.IsDisabled,\n\t\t\tPublicKey:                p.TLS.PublicKey,\n\t\t\tPrivateKey:               p.TLS.PrivateKey,\n\t\t\tCaCerts:                  p.TLS.CaCerts,\n\t\t\tMinVersion:               p.TLS.MinVersion,\n\t\t\tMaxVersion:               p.TLS.MaxVersion,\n\t\t\tCurvePreferences:         p.TLS.CurvePreferences,\n\t\t\tPreferServerCipherSuites: p.TLS.PreferServerCipherSuites,\n\t\t\tCipherSuites:             p.TLS.CipherSuites,\n\t\t\tEnableMTLS:               p.TLS.EnableMTLS,\n\t\t\tDisableSystemCaPool:      p.TLS.DisableSystemCaPool,\n\t\t}\n\t\tfor _, k := range p.TLS.Keys {\n\t\t\tcfg.TLS.Keys = append(cfg.TLS.Keys, TLSKeyPair(k))\n\t\t}\n\t}\n\tif p.ClientTLS != nil {\n\t\tcfg.ClientTLS = &ClientTLS{\n\t\t\tAllowInsecureConnections: p.ClientTLS.AllowInsecureConnections,\n\t\t\tCaCerts:                  p.ClientTLS.CaCerts,\n\t\t\tDisableSystemCaPool:      p.ClientTLS.DisableSystemCaPool,\n\t\t\tMinVersion:               p.ClientTLS.MinVersion,\n\t\t\tMaxVersion:               p.ClientTLS.MaxVersion,\n\t\t\tCurvePreferences:         p.ClientTLS.CurvePreferences,\n\t\t\tCipherSuites:             p.ClientTLS.CipherSuites,\n\t\t\tClientCerts:              make([]ClientTLSCert, 0, len(p.ClientTLS.ClientCerts)),\n\t\t}\n\t\tfor _, cc := range p.ClientTLS.ClientCerts {\n\t\t\tcfg.ClientTLS.ClientCerts = append(cfg.ClientTLS.ClientCerts, ClientTLSCert(cc))\n\t\t}\n\t}\n\tif p.ExtraConfig != nil {\n\t\tcfg.ExtraConfig = *p.ExtraConfig\n\t}\n\tendpoints := make([]*EndpointConfig, 0, len(p.Endpoints))\n\tfor _, e := range p.Endpoints {\n\t\tendpoints = append(endpoints, e.normalize())\n\t}\n\tcfg.Endpoints = endpoints\n\tagents := make([]*AsyncAgent, 0, len(p.AsyncAgents))\n\tfor _, a := range p.AsyncAgents {\n\t\tagents = append(agents, a.normalize())\n\t}\n\tcfg.AsyncAgents = agents\n\treturn cfg\n}\n\ntype parseableTLSKeyPair struct {\n\tPublicKey  string `json:\"public_key\"`\n\tPrivateKey string `json:\"private_key\"`\n}\n\ntype parseableTLS struct {\n\tIsDisabled               bool                  `json:\"disabled\"`\n\tPublicKey                string                `json:\"public_key\"`\n\tPrivateKey               string                `json:\"private_key\"`\n\tCaCerts                  []string              `json:\"ca_certs\"`\n\tMinVersion               string                `json:\"min_version\"`\n\tMaxVersion               string                `json:\"max_version\"`\n\tCurvePreferences         []uint16              `json:\"curve_preferences\"`\n\tPreferServerCipherSuites bool                  `json:\"prefer_server_cipher_suites\"`\n\tCipherSuites             []uint16              `json:\"cipher_suites\"`\n\tEnableMTLS               bool                  `json:\"enable_mtls\"`\n\tDisableSystemCaPool      bool                  `json:\"disable_system_ca_pool\"`\n\tKeys                     []parseableTLSKeyPair `json:\"keys\"`\n}\n\ntype parseableClientTLS struct {\n\tAllowInsecureConnections bool                     `json:\"allow_insecure_connections\"`\n\tCaCerts                  []string                 `json:\"ca_certs\"`\n\tDisableSystemCaPool      bool                     `json:\"disable_system_ca_pool\"`\n\tMinVersion               string                   `json:\"min_version\"`\n\tMaxVersion               string                   `json:\"max_version\"`\n\tCurvePreferences         []uint16                 `json:\"curve_preferences\"`\n\tCipherSuites             []uint16                 `json:\"cipher_suites\"`\n\tClientCerts              []parseableClientTLSCert `json:\"client_certs\"`\n}\n\ntype parseableClientTLSCert struct {\n\tCertificate string `json:\"certificate\"`\n\tPrivateKey  string `json:\"private_key\"`\n}\n\ntype parseableEndpointConfig struct {\n\tEndpoint        string              `json:\"endpoint\"`\n\tMethod          string              `json:\"method\"`\n\tBackend         []*parseableBackend `json:\"backend\"`\n\tConcurrentCalls int                 `json:\"concurrent_calls\"`\n\tTimeout         string              `json:\"timeout\"`\n\tCacheTTL        string              `json:\"cache_ttl\"`\n\tQueryString     []string            `json:\"input_query_strings\"`\n\tExtraConfig     *ExtraConfig        `json:\"extra_config,omitempty\"`\n\tHeadersToPass   []string            `json:\"input_headers\"`\n\tOutputEncoding  string              `json:\"output_encoding\"`\n}\n\nfunc (p *parseableEndpointConfig) normalize() *EndpointConfig {\n\te := EndpointConfig{\n\t\tEndpoint:        p.Endpoint,\n\t\tMethod:          p.Method,\n\t\tConcurrentCalls: p.ConcurrentCalls,\n\t\tTimeout:         parseDuration(p.Timeout),\n\t\tCacheTTL:        parseDuration(p.CacheTTL),\n\t\tQueryString:     p.QueryString,\n\t\tHeadersToPass:   p.HeadersToPass,\n\t\tOutputEncoding:  p.OutputEncoding,\n\t}\n\tif p.ExtraConfig != nil {\n\t\te.ExtraConfig = *p.ExtraConfig\n\t}\n\tbackends := make([]*Backend, 0, len(p.Backend))\n\tfor _, b := range p.Backend {\n\t\tbackends = append(backends, b.normalize())\n\t}\n\te.Backend = backends\n\treturn &e\n}\n\ntype parseableAsyncAgent struct {\n\tName       string `json:\"name\"`\n\tConnection struct {\n\t\tMaxRetries      int    `json:\"max_retries\"`\n\t\tBackoffStrategy string `json:\"backoff_strategy\"`\n\t\tHealthInterval  string `json:\"health_interval\"`\n\t} `json:\"connection\"`\n\tConsumer struct {\n\t\tTimeout string  `json:\"timeout\"`\n\t\tWorkers int     `json:\"workers\"`\n\t\tTopic   string  `json:\"topic\"`\n\t\tMaxRate float64 `json:\"max_rate\"`\n\t} `json:\"consumer\"`\n\tEncoding    string              `json:\"encoding\"`\n\tBackend     []*parseableBackend `json:\"backend\"`\n\tExtraConfig ExtraConfig         `json:\"extra_config\"`\n}\n\nfunc (p *parseableAsyncAgent) normalize() *AsyncAgent {\n\te := AsyncAgent{\n\t\tName:     p.Name,\n\t\tEncoding: p.Encoding,\n\t\tConnection: Connection{\n\t\t\tMaxRetries:      p.Connection.MaxRetries,\n\t\t\tBackoffStrategy: p.Connection.BackoffStrategy,\n\t\t\tHealthInterval:  parseDuration(p.Connection.HealthInterval),\n\t\t},\n\t\tConsumer: Consumer{\n\t\t\tTimeout: parseDuration(p.Consumer.Timeout),\n\t\t\tWorkers: p.Consumer.Workers,\n\t\t\tTopic:   p.Consumer.Topic,\n\t\t\tMaxRate: p.Consumer.MaxRate,\n\t\t},\n\t}\n\tif p.ExtraConfig != nil {\n\t\te.ExtraConfig = p.ExtraConfig\n\t}\n\tbackends := make([]*Backend, 0, len(p.Backend))\n\tfor _, b := range p.Backend {\n\t\tbackends = append(backends, b.normalize())\n\t}\n\te.Backend = backends\n\treturn &e\n}\n\ntype parseableBackend struct {\n\tGroup                    string            `json:\"group\"`\n\tMethod                   string            `json:\"method\"`\n\tHost                     []string          `json:\"host\"`\n\tHostSanitizationDisabled bool              `json:\"disable_host_sanitize\"`\n\tURLPattern               string            `json:\"url_pattern\"`\n\tAllowList                []string          `json:\"allow\"`\n\tDenyList                 []string          `json:\"deny\"`\n\tMapping                  map[string]string `json:\"mapping\"`\n\tEncoding                 string            `json:\"encoding\"`\n\tIsCollection             bool              `json:\"is_collection\"`\n\tTarget                   string            `json:\"target\"`\n\tExtraConfig              *ExtraConfig      `json:\"extra_config,omitempty\"`\n\tSD                       string            `json:\"sd\"`\n\tHeadersToPass            []string          `json:\"input_headers\"`\n\tSDScheme                 string            `json:\"sd_scheme\"`\n\tQueryStringsToPass       []string          `json:\"input_query_strings\"`\n}\n\nfunc (p *parseableBackend) normalize() *Backend {\n\tb := Backend{\n\t\tGroup:                    p.Group,\n\t\tMethod:                   p.Method,\n\t\tHost:                     p.Host,\n\t\tHostSanitizationDisabled: p.HostSanitizationDisabled,\n\t\tURLPattern:               p.URLPattern,\n\t\tMapping:                  p.Mapping,\n\t\tEncoding:                 p.Encoding,\n\t\tIsCollection:             p.IsCollection,\n\t\tTarget:                   p.Target,\n\t\tSD:                       p.SD,\n\t\tSDScheme:                 p.SDScheme,\n\t\tAllowList:                p.AllowList,\n\t\tDenyList:                 p.DenyList,\n\t\tHeadersToPass:            p.HeadersToPass,\n\t\tQueryStringsToPass:       p.QueryStringsToPass,\n\t}\n\tif b.SDScheme == \"\" {\n\t\tb.SDScheme = \"http\"\n\t}\n\tif p.ExtraConfig != nil {\n\t\tb.ExtraConfig = *p.ExtraConfig\n\t}\n\treturn &b\n}\n\nfunc parseDuration(v string) time.Duration {\n\td, err := time.ParseDuration(v)\n\tif err != nil {\n\t\treturn 0\n\t}\n\treturn d\n}\n"
  },
  {
    "path": "config/parser_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage config\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestNewParser_ok(t *testing.T) {\n\tconfigPath := \"/tmp/ok.json\"\n\tconfigContent := []byte(`{\n    \"version\": 3,\n    \"name\": \"My lovely gateway\",\n    \"port\": 8080,\n    \"cache_ttl\": \"3600s\",\n    \"timeout\": \"3s\",\n    \"max_header_bytes\": 10000,\n    \"tls\": {\n\t\t\"public_key\":  \"cert.pem\",\n\t\t\"private_key\": \"key.pem\"\n\t},\n\t\"async_agent\": [\n\t\t{\n\t\t\t\"name\": \"agent\",\n\t\t\t\"connection\": {\n\t\t\t\t\"max_retries\": 2\n\t\t\t},\n\t\t\t\"consumer\": {\n\t\t\t\t\"topic\": \"foo.*\"\n\t\t\t},\n            \"backend\": [\n                {\n                    \"host\": [\n                        \"https://api.github.com\"\n                    ],\n                    \"url_pattern\": \"/\",\n                    \"extra_config\" : {\"user\":\"test\",\"hits\":6,\"parents\":[\"gomez\",\"morticia\"]}\n                }\n            ]\n\t\t}\n\t],\n    \"endpoints\": [\n        {\n            \"endpoint\": \"/github\",\n            \"method\": \"GET\",\n            \"extra_config\" : {\"user\":\"test\",\"hits\":6,\"parents\":[\"gomez\",\"morticia\"]},\n            \"backend\": [\n                {\n                    \"host\": [\n                        \"https://api.github.com\"\n                    ],\n                    \"url_pattern\": \"/\",\n                    \"allow\": [\n                        \"authorizations_url\",\n                        \"code_search_url\"\n                    ],\n                    \"extra_config\" : {\"user\":\"test\",\"hits\":6,\"parents\":[\"gomez\",\"morticia\"]}\n                }\n            ]\n        },\n        {\n            \"endpoint\": \"/supu\",\n            \"method\": \"GET\",\n            \"concurrent_calls\": 3,\n            \"backend\": [\n                {\n                    \"host\": [\n                        \"http://127.0.0.1:8080\"\n                    ],\n                    \"url_pattern\": \"/__debug/supu\"\n                }\n            ]\n        },\n        {\n            \"endpoint\": \"/combination/{id}\",\n            \"method\": \"GET\",\n            \"backend\": [\n                {\n                    \"group\": \"first_post\",\n                    \"host\": [\n                        \"https://jsonplaceholder.typicode.com\"\n                    ],\n                    \"url_pattern\": \"/posts/{id}\",\n                    \"deny\": [\n                        \"userId\"\n                    ]\n                },\n                {\n                    \"host\": [\n                        \"https://jsonplaceholder.typicode.com\"\n                    ],\n                    \"url_pattern\": \"/users/{id}\",\n                    \"mapping\": {\n                        \"email\": \"personal_email\"\n                    }\n                }\n            ]\n        }\n    ],\n    \"extra_config\" : {\"user\":\"test\",\"hits\":6,\"parents\":[\"gomez\",\"morticia\"]}\n}`)\n\tif err := os.WriteFile(configPath, configContent, 0644); err != nil {\n\t\tt.FailNow()\n\t}\n\n\tserviceConfig, err := NewParser().Parse(configPath)\n\n\tif err != nil {\n\t\tt.Error(\"Unexpected error. Got\", err.Error())\n\t}\n\tif serviceConfig.MaxHeaderBytes != 10000 {\n\t\tt.Errorf(\"unexpected max_header_bytes value. have %d, want 10000\", serviceConfig.MaxHeaderBytes)\n\t}\n\ttestExtraConfig(serviceConfig.ExtraConfig, t)\n\n\tif endpoints := len(serviceConfig.Endpoints); endpoints != 3 {\n\t\tt.Errorf(\"Unexpected number of endpoints: %d\", endpoints)\n\t\treturn\n\t}\n\n\tendpoint := serviceConfig.Endpoints[0]\n\tendpointExtraConfiguration := endpoint.ExtraConfig\n\n\tif endpointExtraConfiguration != nil {\n\t\ttestExtraConfig(endpointExtraConfiguration, t)\n\t} else {\n\t\tt.Error(\"Extra config is not present in EndpointConfig\")\n\t}\n\n\tif serviceConfig.TLS == nil {\n\t\tt.Error(\"TLS config not present\")\n\t} else {\n\t\tif serviceConfig.TLS.PublicKey != \"cert.pem\" {\n\t\t\tt.Error(\"Unexpected TLS Public key\")\n\t\t}\n\t\tif serviceConfig.TLS.PrivateKey != \"key.pem\" {\n\t\t\tt.Error(\"Unexpected TLS Private key\")\n\t\t}\n\t}\n\n\tbackend := endpoint.Backend[0]\n\tbackendExtraConfiguration := backend.ExtraConfig\n\tif backendExtraConfiguration != nil {\n\t\ttestExtraConfig(backendExtraConfiguration, t)\n\t} else {\n\t\tt.Error(\"Extra config is not present in BackendConfig\")\n\t}\n\n\tif err := os.Remove(configPath); err != nil {\n\t\tt.FailNow()\n\t}\n\n\tif l := len(serviceConfig.AsyncAgents); l != 1 {\n\t\tt.Errorf(\"Unexpected number of agents. Have %d, want 1\", l)\n\t}\n}\n\nfunc TestNewParser_errorMessages(t *testing.T) {\n\tfor _, configContent := range []struct {\n\t\tname    string\n\t\tpath    string\n\t\tcontent []byte\n\t\texpErr  string\n\t}{\n\t\t{\n\t\t\tname:    \"case0\",\n\t\t\tpath:    \"/tmp/ok.json\",\n\t\t\tcontent: []byte(`{`),\n\t\t\texpErr:  \"'/tmp/ok.json': unexpected end of JSON input, offset: 1, row: 0, col: 1\",\n\t\t},\n\t\t{\n\t\t\tname:    \"case1\",\n\t\t\tpath:    \"/tmp/ok.json\",\n\t\t\tcontent: []byte(`>`),\n\t\t\texpErr:  \"'/tmp/ok.json': invalid character '>' looking for beginning of value, offset: 1, row: 0, col: 1\",\n\t\t},\n\t\t{\n\t\t\tname:    \"case2\",\n\t\t\tpath:    \"/tmp/ok.json\",\n\t\t\tcontent: []byte(`\"`),\n\t\t\texpErr:  \"'/tmp/ok.json': unexpected end of JSON input, offset: 1, row: 0, col: 1\",\n\t\t},\n\t\t{\n\t\t\tname:    \"case3\",\n\t\t\tpath:    \"/tmp/ok.json\",\n\t\t\tcontent: []byte(``),\n\t\t\texpErr:  \"'/tmp/ok.json': unexpected end of JSON input, offset: 0, row: 0, col: 0\",\n\t\t},\n\t\t{\n\t\t\tname:    \"case4\",\n\t\t\tpath:    \"/tmp/ok.json\",\n\t\t\tcontent: []byte(`[{}]`),\n\t\t\texpErr:  \"'/tmp/ok.json': json: cannot unmarshal array into Go value of type config.parseableServiceConfig, offset: 1, row: 0, col: 1\",\n\t\t},\n\t\t{\n\t\t\tname:    \"case5\",\n\t\t\tpath:    \"/tmp/ok.json\",\n\t\t\tcontent: []byte(`42`),\n\t\t\texpErr:  \"'/tmp/ok.json': json: cannot unmarshal number into Go value of type config.parseableServiceConfig, offset: 2, row: 0, col: 2\",\n\t\t},\n\t\t{\n\t\t\tname:    \"case6\",\n\t\t\tpath:    \"/tmp/ok.json\",\n\t\t\tcontent: []byte(\"\\r\\n42\"),\n\t\t\texpErr:  \"'/tmp/ok.json': json: cannot unmarshal number into Go value of type config.parseableServiceConfig, offset: 4, row: 1, col: 2\",\n\t\t},\n\t\t{\n\t\t\tname: \"case7\",\n\t\t\tpath: \"/tmp/ok.json\",\n\t\t\tcontent: []byte(`{\n\t\"version\": 3,\n\t\"name\": \"My lovely gateway\",\n\t\"port\": 8080,\n\t\"cache_ttl\": 3600\n\t\"timeout\": \"3s\",\n\t\"endpoints\": []\n}`),\n\t\t\texpErr: \"'/tmp/ok.json': invalid character '\\\"' after object key:value pair, offset: 83, row: 5, col: 2\",\n\t\t},\n\t} {\n\t\tt.Run(configContent.name, func(t *testing.T) {\n\t\t\tif err := os.WriteFile(configContent.path, configContent.content, 0644); err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t_, err := NewParser().Parse(configContent.path)\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"%s: Expecting error\", configContent.name)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif errMsg := err.Error(); errMsg != configContent.expErr {\n\t\t\t\tt.Errorf(\"%s: Unexpected error. Got '%s' want '%s'\", configContent.name, errMsg, configContent.expErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err := os.Remove(configContent.path); err != nil {\n\t\t\t\tt.Errorf(\"%s: %s\", err.Error(), configContent.name)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc testExtraConfig(extraConfig map[string]interface{}, t *testing.T) {\n\tuserVar := extraConfig[\"user\"]\n\tif userVar != \"test\" {\n\t\tt.Error(\"User in extra config is not test\")\n\t}\n\tparents, ok := extraConfig[\"parents\"].([]interface{})\n\tif !ok || parents[0] != \"gomez\" {\n\t\tt.Error(\"Parent 0 of user us not gomez\")\n\t}\n\tif !ok || parents[1] != \"morticia\" {\n\t\tt.Error(\"Parent 1 of user us not morticia\")\n\t}\n}\n\nfunc TestNewParser_unknownFile(t *testing.T) {\n\t_, err := NewParser().Parse(\"/nowhere/in/the/fs.json\")\n\tif err == nil || err.Error() != \"'/nowhere/in/the/fs.json' (open): no such file or directory\" {\n\t\tt.Errorf(\"error expected. got '%v'\", err)\n\t}\n}\n\nfunc TestNewParser_readingError(t *testing.T) {\n\twrongConfigPath := \"/tmp/reading.json\"\n\twrongConfigContent := []byte(\"{hello\\ngo\\n\")\n\tif err := os.WriteFile(wrongConfigPath, wrongConfigContent, 0644); err != nil {\n\t\tt.FailNow()\n\t}\n\n\texpected := \"'/tmp/reading.json': invalid character 'h' looking for beginning of object key string, offset: 2, row: 0, col: 2\"\n\t_, err := NewParser().Parse(wrongConfigPath)\n\tif err == nil || err.Error() != expected {\n\t\tt.Error(\"Error expected. Got\", err)\n\t}\n\tif err = os.Remove(wrongConfigPath); err != nil {\n\t\tt.FailNow()\n\t}\n}\n\nfunc TestNewParser_initError(t *testing.T) {\n\twrongConfigPath := \"/tmp/unmarshall.json\"\n\twrongConfigContent := []byte(\"{\\\"a\\\":42}\")\n\tif err := os.WriteFile(wrongConfigPath, wrongConfigContent, 0644); err != nil {\n\t\tt.FailNow()\n\t}\n\n\t_, err := NewParser().Parse(wrongConfigPath)\n\tif err == nil || err.Error() != \"'/tmp/unmarshall.json': unsupported version: 0 (want: 3)\" {\n\t\tt.Error(\"Error expected. Got\", err)\n\t}\n\tif err = os.Remove(wrongConfigPath); err != nil {\n\t\tt.FailNow()\n\t}\n}\n\nfunc TestParserFunc(t *testing.T) {\n\texpected := ServiceConfig{Version: 42}\n\tresult, err := ParserFunc(func(_ string) (ServiceConfig, error) { return expected, nil })(\"path/to/the/config/file\")\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t}\n\tif result.Version != expected.Version {\n\t\tt.Error(\"unexpected parsed config:\", result)\n\t}\n}\n"
  },
  {
    "path": "config/uri.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage config\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nvar (\n\tendpointURLKeysPattern = regexp.MustCompile(`/\\{([a-zA-Z\\-_0-9]+)\\}`)\n\thostPattern            = regexp.MustCompile(`(https?://)?([a-zA-Z0-9\\._\\-]+)(:[0-9]{2,6})?/?`)\n)\n\n// URIParser defines the interface for all the URI manipulation required by KrakenD\ntype URIParser interface {\n\tCleanHosts([]string) []string\n\tCleanHost(string) string\n\tCleanPath(string) string\n\tGetEndpointPath(string, []string) string\n}\n\n// Like URIParser but with safe versions of the clean host functionality that\n// does not panic but returns an error.\ntype SafeURIParser interface {\n\tSafeCleanHosts([]string) ([]string, error)\n\tSafeCleanHost(string) (string, error)\n\tCleanPath(string) string\n\tGetEndpointPath(string, []string) string\n}\n\n// NewURIParser creates a new URIParser using the package variable RoutingPattern\nfunc NewURIParser() URIParser {\n\treturn URI(RoutingPattern)\n}\n\n// NewSafeURIParser creates a safe URI parser that does not panic when cleaning hosts\nfunc NewSafeURIParser() URI {\n\treturn URI(RoutingPattern)\n}\n\n// URI implements the URIParser interface\ntype URI int\n\n// SafeCleanHosts applies the SafeCleanHost method to every member of the received array of hosts\nfunc (u URI) SafeCleanHosts(hosts []string) ([]string, error) {\n\tcleaned := make([]string, 0, len(hosts))\n\tfor i := range hosts {\n\t\th, err := u.SafeCleanHost(hosts[i])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"host %s not valid: %w\", hosts[i], errInvalidHost)\n\t\t}\n\t\tcleaned = append(cleaned, h)\n\t}\n\treturn cleaned, nil\n}\n\n// CleanHosts applies the CleanHost method to every member of the received array of hosts\n// Panics in case of error.\nfunc (u URI) CleanHosts(hosts []string) []string {\n\tss, e := u.SafeCleanHosts(hosts)\n\tif e != nil {\n\t\tpanic(e)\n\t}\n\treturn ss\n}\n\n// SafeCleanHost sanitizes the received host\nfunc (URI) SafeCleanHost(host string) (string, error) {\n\tmatches := hostPattern.FindAllStringSubmatch(host, -1)\n\tif len(matches) != 1 {\n\t\treturn \"\", errInvalidHost\n\t}\n\tkeys := matches[0][1:]\n\tif keys[0] == \"\" {\n\t\tkeys[0] = \"http://\"\n\t}\n\treturn strings.Join(keys, \"\"), nil\n}\n\n// CleanHost sanitizes the received host.\n// Panics on error.\nfunc (u URI) CleanHost(host string) string {\n\th, err := u.SafeCleanHost(host)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn h\n}\n\n// CleanPath trims all the extra slashes from the received URI path\nfunc (URI) CleanPath(path string) string {\n\treturn \"/\" + strings.TrimPrefix(path, \"/\")\n}\n\n// GetEndpointPath applies the proper replacement in the received path to generate valid route patterns\nfunc (u URI) GetEndpointPath(path string, params []string) string {\n\tresult := path\n\tif u == ColonRouterPatternBuilder {\n\t\tfor p := range params {\n\t\t\tparts := strings.Split(result, \"?\")\n\t\t\tparts[0] = strings.ReplaceAll(parts[0], \"{\"+params[p]+\"}\", \":\"+params[p])\n\t\t\tresult = strings.Join(parts, \"?\")\n\t\t}\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "config/uri_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage config\n\nimport \"testing\"\n\nfunc TestURIParser_cleanHosts(t *testing.T) {\n\tsamples := []string{\n\t\t\"supu\",\n\t\t\"127.0.0.1\",\n\t\t\"https://supu.local/\",\n\t\t\"http://127.0.0.1\",\n\t\t\"supu_42.local:8080/\",\n\t\t\"http://127.0.0.1:8080\",\n\t}\n\n\texpected := []string{\n\t\t\"http://supu\",\n\t\t\"http://127.0.0.1\",\n\t\t\"https://supu.local\",\n\t\t\"http://127.0.0.1\",\n\t\t\"http://supu_42.local:8080\",\n\t\t\"http://127.0.0.1:8080\",\n\t}\n\n\tresult := NewURIParser().CleanHosts(samples)\n\tfor i := range result {\n\t\tif expected[i] != result[i] {\n\t\t\tt.Errorf(\"want: %s, have: %s\\n\", expected[i], result[i])\n\t\t}\n\t}\n}\n\nfunc TestURIParser_cleanPath(t *testing.T) {\n\tsamples := []string{\n\t\t\"supu/{tupu}\",\n\t\t\"supu/{tupu}{supu}\",\n\t\t\"/supu/{tupu}\",\n\t\t\"/supu.local/\",\n\t\t\"supu_supu.txt\",\n\t\t\"supu_42.local?a=8080\",\n\t\t\"supu/supu/supu?a=1&b=2\",\n\t\t\"debug/supu/supu?a=1&b=2\",\n\t}\n\n\texpected := []string{\n\t\t\"/supu/{tupu}\",\n\t\t\"/supu/{tupu}{supu}\",\n\t\t\"/supu/{tupu}\",\n\t\t\"/supu.local/\",\n\t\t\"/supu_supu.txt\",\n\t\t\"/supu_42.local?a=8080\",\n\t\t\"/supu/supu/supu?a=1&b=2\",\n\t\t\"/debug/supu/supu?a=1&b=2\",\n\t}\n\n\tsubject := URI(BracketsRouterPatternBuilder)\n\n\tfor i := range samples {\n\t\tif have := subject.CleanPath(samples[i]); expected[i] != have {\n\t\t\tt.Errorf(\"want: %s, have: %s\\n\", expected[i], have)\n\t\t}\n\t}\n}\n\nfunc TestURIParser_getEndpointPath(t *testing.T) {\n\tsamples := []string{\n\t\t\"supu/{tupu}\",\n\t\t\"/supu/{tupu}{supu}\",\n\t\t\"/supu/{tupu}\",\n\t\t\"/supu.local/\",\n\t\t\"supu/{tupu}/{supu}?a={s}&b=2\",\n\t}\n\n\texpected := []string{\n\t\t\"supu/:tupu\",\n\t\t\"/supu/:tupu{supu}\",\n\t\t\"/supu/:tupu\",\n\t\t\"/supu.local/\",\n\t\t\"supu/:tupu/:supu?a={s}&b=2\",\n\t}\n\n\tsc := ServiceConfig{}\n\tsubject := NewURIParser()\n\n\tfor i := range samples {\n\t\tparams := sc.extractPlaceHoldersFromURLTemplate(samples[i], sc.paramExtractionPattern())\n\t\tif have := subject.GetEndpointPath(samples[i], params); expected[i] != have {\n\t\t\tt.Errorf(\"want: %s, have: %s\\n\", expected[i], have)\n\t\t}\n\t}\n}\nfunc TestURIParser_getEndpointPath_notStrictREST(t *testing.T) {\n\tsamples := []string{\n\t\t\"supu/{tupu}\",\n\t\t\"/supu/{tupu}{supu}\",\n\t\t\"/supu/{tupu}\",\n\t\t\"/supu.local/\",\n\t\t\"supu/{tupu}/{supu}?a={s}&b=2\",\n\t}\n\n\texpected := []string{\n\t\t\"supu/:tupu\",\n\t\t\"/supu/:tupu:supu\",\n\t\t\"/supu/:tupu\",\n\t\t\"/supu.local/\",\n\t\t\"supu/:tupu/:supu?a={s}&b=2\",\n\t}\n\n\tsc := ServiceConfig{DisableStrictREST: true}\n\tsubject := NewURIParser()\n\n\tfor i := range samples {\n\t\tparams := sc.extractPlaceHoldersFromURLTemplate(samples[i], sc.paramExtractionPattern())\n\t\tif have := subject.GetEndpointPath(samples[i], params); expected[i] != have {\n\t\t\tt.Errorf(\"want: %s, have: %s\\n\", expected[i], have)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "core/version.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\nPackage core contains some basic constants and variables\n*/\npackage core\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n\t\"strings\"\n)\n\n// KrakendHeaderName is the name of the custom KrakenD header\nconst KrakendHeaderName = \"X-KRAKEND\"\n\n// KrakendVersion is the version of the build\nvar KrakendVersion = \"undefined\"\n\n// GoVersion is the version of the go compiler used at build time\nvar GoVersion = strings.TrimPrefix(runtime.Version(), \"go\")\n\n// GlibcVersion is the version of the glibc used by CGO at build time\nvar GlibcVersion = \"undefined\"\n\n// KrakendHeaderValue is the value of the custom KrakenD header\nvar KrakendHeaderValue = fmt.Sprintf(\"Version %s\", KrakendVersion)\n\n// KrakendUserAgent is the value of the user agent header sent to the backends\nvar KrakendUserAgent = fmt.Sprintf(\"KrakenD Version %s\", KrakendVersion)\n"
  },
  {
    "path": "docs/BENCHMARKS.md",
    "content": "Benchmarks\n---\n\nHere you'll find some benchmarks of the different components of the Lura framework in several scenarios.\n\n# Proxy components\n\n## Proxy middleware stack\n\n    BenchmarkProxyStack_single-8                   500000          9106 ns/op        1848 B/op         35 allocs/op\n    BenchmarkProxyStack_multi/with_1_backends-8    500000          9183 ns/op        1848 B/op         35 allocs/op\n    BenchmarkProxyStack_multi/with_2_backends-8    300000         16130 ns/op        3520 B/op         73 allocs/op\n    BenchmarkProxyStack_multi/with_3_backends-8    200000         20780 ns/op        5097 B/op        105 allocs/op\n    BenchmarkProxyStack_multi/with_4_backends-8    200000         22420 ns/op        6641 B/op        137 allocs/op\n    BenchmarkProxyStack_multi/with_5_backends-8    200000         23966 ns/op        8218 B/op        169 allocs/op\n\n## Proxy middlewares\n\n    BenchmarkNewLoadBalancedMiddleware-8                10000000           435 ns/op         328 B/op          6 allocs/op\n    BenchmarkNewConcurrentMiddleware_singleNext-8         500000          9351 ns/op        1072 B/op         18 allocs/op\n    BenchmarkNewRequestBuilderMiddleware-8              30000000           115 ns/op         160 B/op          2 allocs/op\n    BenchmarkNewMergeDataMiddleware/with_2_parts-8       1000000          6746 ns/op        1360 B/op         20 allocs/op\n    BenchmarkNewMergeDataMiddleware/with_3_parts-8        500000         10179 ns/op        1488 B/op         22 allocs/op\n    BenchmarkNewMergeDataMiddleware/with_4_parts-8        500000         10299 ns/op        1584 B/op         24 allocs/op\n\n# Response manipulation\n\n## Response property whitelisting\n\n    BenchmarkEntityFormatter_whitelistingFilter/with_0_elements_with_0_extra_fields-8           50000000            80.6 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_1_elements_with_0_extra_fields-8           10000000           441 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_2_elements_with_0_extra_fields-8           10000000           474 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_3_elements_with_0_extra_fields-8           10000000           516 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_4_elements_with_0_extra_fields-8           10000000           519 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_0_elements_with_5_extra_fields-8           50000000            84.3 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_1_elements_with_5_extra_fields-8           10000000           565 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_2_elements_with_5_extra_fields-8           10000000           601 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_3_elements_with_5_extra_fields-8           10000000           638 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_4_elements_with_5_extra_fields-8           10000000           627 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_0_elements_with_10_extra_fields-8          50000000            80.7 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_1_elements_with_10_extra_fields-8          10000000           703 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_2_elements_with_10_extra_fields-8           5000000           746 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_3_elements_with_10_extra_fields-8           5000000           779 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_4_elements_with_10_extra_fields-8           5000000           785 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_0_elements_with_15_extra_fields-8          50000000            81.4 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_1_elements_with_15_extra_fields-8           5000000           845 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_2_elements_with_15_extra_fields-8           5000000           886 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_3_elements_with_15_extra_fields-8           5000000           919 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_4_elements_with_15_extra_fields-8           5000000           929 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_0_elements_with_20_extra_fields-8          50000000            80.9 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_1_elements_with_20_extra_fields-8           5000000           988 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_2_elements_with_20_extra_fields-8           5000000           984 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_3_elements_with_20_extra_fields-8           5000000           998 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_4_elements_with_20_extra_fields-8           5000000          1014 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_0_elements_with_25_extra_fields-8          50000000            78.1 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_1_elements_with_25_extra_fields-8           5000000          1149 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_2_elements_with_25_extra_fields-8           3000000          1279 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_3_elements_with_25_extra_fields-8           3000000          1348 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_whitelistingFilter/with_4_elements_with_25_extra_fields-8           3000000          1349 ns/op         384 B/op          3 allocs/op\n\n## Response property blacklisting\n\n    BenchmarkEntityFormatter_blacklistingFilter/with_0_elements_with_0_extra_fields-8           50000000            82.4 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_1_elements_with_0_extra_fields-8           30000000           174 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_2_elements_with_0_extra_fields-8           20000000           205 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_3_elements_with_0_extra_fields-8           100000000           63.5 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_4_elements_with_0_extra_fields-8           100000000           62.9 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_0_elements_with_5_extra_fields-8           50000000            80.5 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_1_elements_with_5_extra_fields-8           30000000           175 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_2_elements_with_5_extra_fields-8           20000000           207 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_3_elements_with_5_extra_fields-8           20000000           255 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_4_elements_with_5_extra_fields-8           20000000           299 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_0_elements_with_10_extra_fields-8          50000000            82.9 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_1_elements_with_10_extra_fields-8          30000000           162 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_2_elements_with_10_extra_fields-8          20000000           193 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_3_elements_with_10_extra_fields-8          20000000           229 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_4_elements_with_10_extra_fields-8          20000000           272 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_0_elements_with_15_extra_fields-8          50000000            76.7 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_1_elements_with_15_extra_fields-8          30000000           161 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_2_elements_with_15_extra_fields-8          20000000           195 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_3_elements_with_15_extra_fields-8          20000000           243 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_4_elements_with_15_extra_fields-8          20000000           292 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_0_elements_with_20_extra_fields-8          50000000            81.4 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_1_elements_with_20_extra_fields-8          30000000           161 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_2_elements_with_20_extra_fields-8          20000000           197 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_3_elements_with_20_extra_fields-8          20000000           239 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_4_elements_with_20_extra_fields-8          20000000           289 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_0_elements_with_25_extra_fields-8          50000000            80.9 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_1_elements_with_25_extra_fields-8          30000000           176 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_2_elements_with_25_extra_fields-8          20000000           200 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_3_elements_with_25_extra_fields-8          20000000           250 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_blacklistingFilter/with_4_elements_with_25_extra_fields-8          20000000           312 ns/op          48 B/op          1 allocs/op\n\n## Response property grouping\n\n    BenchmarkEntityFormatter_grouping/with_0_elements-8             20000000           277 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_grouping/with_5_elements-8             20000000           299 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_grouping/with_10_elements-8            20000000           300 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_grouping/with_15_elements-8            20000000           298 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_grouping/with_20_elements-8            20000000           298 ns/op         384 B/op          3 allocs/op\n    BenchmarkEntityFormatter_grouping/with_25_elements-8            20000000           298 ns/op         384 B/op          3 allocs/op\n\n## Response property mapping\n\n    BenchmarkEntityFormatter_mapping/with_0_elements_with_0_extra_fields-8          100000000           61.1 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_1_elements_with_0_extra_fields-8          100000000           63.5 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_2_elements_with_0_extra_fields-8          100000000           61.8 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_3_elements_with_0_extra_fields-8          100000000           63.9 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_4_elements_with_0_extra_fields-8          100000000           63.7 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_5_elements_with_0_extra_fields-8          100000000           64.0 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_0_elements_with_5_extra_fields-8          50000000            81.4 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_1_elements_with_5_extra_fields-8          20000000           177 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_2_elements_with_5_extra_fields-8          20000000           204 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_3_elements_with_5_extra_fields-8          20000000           233 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_4_elements_with_5_extra_fields-8          20000000           266 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_5_elements_with_5_extra_fields-8          20000000           295 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_0_elements_with_10_extra_fields-8         50000000            77.4 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_1_elements_with_10_extra_fields-8         30000000           163 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_2_elements_with_10_extra_fields-8         20000000           198 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_3_elements_with_10_extra_fields-8         20000000           237 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_4_elements_with_10_extra_fields-8         20000000           298 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_5_elements_with_10_extra_fields-8         20000000           331 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_0_elements_with_15_extra_fields-8         50000000            79.5 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_1_elements_with_15_extra_fields-8         30000000           171 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_2_elements_with_15_extra_fields-8         20000000           212 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_3_elements_with_15_extra_fields-8         20000000           265 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_4_elements_with_15_extra_fields-8         20000000           295 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_5_elements_with_15_extra_fields-8         20000000           340 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_0_elements_with_20_extra_fields-8         50000000            77.5 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_1_elements_with_20_extra_fields-8         30000000           163 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_2_elements_with_20_extra_fields-8         20000000           199 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_3_elements_with_20_extra_fields-8         20000000           237 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_4_elements_with_20_extra_fields-8         20000000           287 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_5_elements_with_20_extra_fields-8         20000000           320 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_0_elements_with_25_extra_fields-8         50000000            83.2 ns/op        48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_1_elements_with_25_extra_fields-8         30000000           181 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_2_elements_with_25_extra_fields-8         20000000           222 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_3_elements_with_25_extra_fields-8         20000000           275 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_4_elements_with_25_extra_fields-8         20000000           292 ns/op          48 B/op          1 allocs/op\n    BenchmarkEntityFormatter_mapping/with_5_elements_with_25_extra_fields-8         20000000           339 ns/op          48 B/op          1 allocs/op\n\n# Request generator\n\n    BenchmarkRequestGeneratePath//a-8                                         10000000           460 ns/op          96 B/op         10 allocs/op\n    BenchmarkRequestGeneratePath//a/{{.Supu}}-8                               10000000           522 ns/op         106 B/op         10 allocs/op\n    BenchmarkRequestGeneratePath//a?b={{.Tupu}}-8                             10000000           567 ns/op         136 B/op         10 allocs/op\n    BenchmarkRequestGeneratePath//a/{{.Supu}}/foo/{{.Foo}}-8                  10000000           615 ns/op         182 B/op         10 allocs/op\n    BenchmarkRequestGeneratePath//a/{{.Supu}}/foo/{{.Foo}}/b?c={{.Tupu}}-8    10000000           655 ns/op         236 B/op         10 allocs/op\n\n# Router Handlers\n\n## Gin\n\n    BenchmarkEndpointHandler_ko-8                1000000          5440 ns/op        3026 B/op         31 allocs/op\n    BenchmarkEndpointHandler_ok-8                1000000          6456 ns/op        3393 B/op         36 allocs/op\n    BenchmarkEndpointHandler_ko_Parallel-8       5000000          1534 ns/op        3028 B/op         31 allocs/op\n    BenchmarkEndpointHandler_ok_Parallel-8       5000000          1846 ns/op        3393 B/op         36 allocs/op\n\n## Mux\n\n    BenchmarkEndpointHandler_ko-8                5000000          1815 ns/op        1088 B/op         13 allocs/op\n    BenchmarkEndpointHandler_ok-8                5000000          1693 ns/op        1088 B/op         13 allocs/op\n    BenchmarkEndpointHandler_ko_Parallel-8      20000000           558 ns/op        1088 B/op         13 allocs/op\n    BenchmarkEndpointHandler_ok_Parallel-8      20000000           597 ns/op        1088 B/op         13 allocs/op\n"
  },
  {
    "path": "docs/CONFIG.md",
    "content": "# Configuration file\n\nThe configuration file needs to be a `json` file. The viper parser supports other formats but they haven't been as tested as the recommended one.\n\n## Json example\n\n\n    {\n\t\"version\": 3,\n\t\"name\": \"My lovely gateway\",\n\t\"port\": 8080,\n\t\"timeout\": \"10s\",\n\t\"cache_ttl\": \"3600s\",\n\t\"host\": [\n\t\t\"http://127.0.0.1:8080\",\n\t\t\"http://127.0.0.2:8000\",\n\t\t\"http://127.0.0.3:9000\",\n\t\t\"http://127.0.0.4\"\n\t],\n\t\"endpoints\": [{\n\t\t\t\"endpoint\": \"/users/{user}\",\n\t\t\t\"method\": \"GET\",\n\t\t\t\"backend\": [{\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"http://127.0.0.3:9000\",\n\t\t\t\t\t\t\"http://127.0.0.4\"\n\t\t\t\t\t],\n\t\t\t\t\t\"url_pattern\": \"/registered/{user}\",\n\t\t\t\t\t\"allow\": [\n\t\t\t\t\t\t\"some\",\n\t\t\t\t\t\t\"what\"\n\t\t\t\t\t],\n\t\t\t\t\t\"mapping\": {\n\t\t\t\t\t\t\"email\": \"personal_email\"\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"http://127.0.0.1:8080\"\n\t\t\t\t\t],\n\t\t\t\t\t\"url_pattern\": \"/users/{user}/permissions\",\n\t\t\t\t\t\"deny\": [\n\t\t\t\t\t\t\"spam2\",\n\t\t\t\t\t\t\"notwanted2\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"concurrent_calls\": 2,\n\t\t\t\"timeout\": \"1000s\",\n\t\t\t\"cache_ttl\": 3600,\n\t\t\t\"input_query_strings\": [\n\t\t\t\t\"page\",\n\t\t\t\t\"limit\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"endpoint\": \"/foo/bar\",\n\t\t\t\"method\": \"POST\",\n\t\t\t\"backend\": [{\n\t\t\t\t\"host\": [\n\t\t\t\t\t\"https://127.0.0.1:8081\"\n\t\t\t\t],\n\t\t\t\t\"url_pattern\": \"/__debug/tupu\"\n\t\t\t}],\n\t\t\t\"concurrent_calls\": 1,\n\t\t\t\"timeout\": \"1000s\",\n\t\t\t\"cache_ttl\": 3600\n\t\t},\n\t\t{\n\t\t\t\"endpoint\": \"/github\",\n\t\t\t\"method\": \"GET\",\n\t\t\t\"backend\": [{\n\t\t\t\t\"host\": [\n\t\t\t\t\t\"https://api.github.com\"\n\t\t\t\t],\n\t\t\t\t\"url_pattern\": \"/\",\n\t\t\t\t\"allow\": [\n\t\t\t\t\t\"authorizations_url\",\n\t\t\t\t\t\"code_search_url\"\n\t\t\t\t]\n\t\t\t}],\n\t\t\t\"concurrent_calls\": 2,\n\t\t\t\"timeout\": \"1000s\",\n\t\t\t\"cache_ttl\": 3600\n\t\t},\n\t\t{\n\t\t\t\"endpoint\": \"/combination/{id}/{supu}\",\n\t\t\t\"method\": \"GET\",\n\t\t\t\"backend\": [{\n\t\t\t\t\t\"group\": \"first_post\",\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"https://jsonplaceholder.typicode.com\"\n\t\t\t\t\t],\n\t\t\t\t\t\"url_pattern\": \"/posts/{id}?supu={supu}\",\n\t\t\t\t\t\"deny\": [\n\t\t\t\t\t\t\"userId\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\"https://jsonplaceholder.typicode.com\"\n\t\t\t\t\t],\n\t\t\t\t\t\"url_pattern\": \"/users/{id}\",\n\t\t\t\t\t\"mapping\": {\n\t\t\t\t\t\t\"email\": \"personal_email\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"concurrent_calls\": 3,\n\t\t\t\"timeout\": \"1000s\",\n\t\t\t\"input_query_strings\": [\n\t\t\t\t\"page\",\n\t\t\t\t\"limit\"\n\t\t\t]\n\t\t}\n\t]}\n"
  },
  {
    "path": "docs/OVERVIEW.md",
    "content": "# Overview\n\n## The Lura rules\n\n* [Reactive is key](http://www.reactivemanifesto.org/)\n* Reactive is key (yes, it is very very important)\n* Failing fast is better than succeeding slow (say it one more time!)\n* The simpler, the better\n* Everything is plugglable\n* Each request must be processed in its own request-scoped context\n\n## The big picture\n\nThe Lura framework is composed of a set of packages designed as building blocks for creating pipes and processors between an exposed endpoint and one or several API resources served by your backends.\n\nThe most important packages are:\n\n1. the `config` package defines the service.\n2. the `router` package sets up the endpoints exposed to the clients.\n3. the `proxy` package adds the required middlewares and components for further processing of the requests to send and the received responses sent by the backends, and also to manage the connections against those backends. \n\nThe rest of the packages of the framework contain some helpers and adapters for complementary tasks, like encoding, logging or service discovery.\n\n## The `config` package\n\nThe `config` package contains the structs required for the service description.\n\nThe `ServiceConfig` struct defines the entire service. It should be initialized before using it in order to be sure that all parameters have been normalized and default values have been applied.\n\nThe `config` package also defines an interface for a file config parser and a parser based on the [viper](https://github.com/spf13/viper) library.\n\n## The `router` package\n\nThe `router` package contains an interface and several implementations for the Lura router layer using the `mux` router from the `net/http` and the `httprouter` wrapped in the `gin` framework.\n\nThe router layer is responsible for setting up the HTTP(S) services, binding the endpoints defined at the `ServiceConfig` struct and transforming the http request into proxy requests before delegating the task to the inner layer (proxy). Once the internal proxy layer returns a proxy response, the router layer converts it into a proper HTTP response and sends it to the user.\n\nThis layer can be easily extended in order to use any HTTP router, framework or middleware of your choice. Adding transport layer adapters for other protocols (Thrift, gRPC, AMQP, NATS, etc) is in the roadmap. As always, PRs are welcome!\n\n## The `proxy` package\n\nThe `proxy` package is where the most part of the Lura components and features are placed. It defines two important interfaces, designed to be stacked:\n\n* *Proxy* is a function that converts a given context and request into a response.\n* *Middleware* is a function that accepts one or more proxies and returns a single proxy wrapping them.\n\nThis layer transforms the request received from the outter layer (router) into a single or several requests to your backend services, processes the responses and returns a single response.\n\nMiddlewares generates custom proxies that are chained depending on the workflow defined in the configuration until each possible branch ends in a transport-related proxy. Every one of these generated proxies is able to transform the input or even clone it several times and pass it or them to the next element in the chain. Finally, they can also modify the received response or responses adding all kinds of features to the generated pipe.\n\nThe Lura framework provides a default implementation of the proxy stack factory.\n\n### Middlewares available\n\n* The `balancing` middleware uses some type of strategy for selecting a backend host to query.\n* The `concurrent` middleware improves the QoS by sending several concurrent requests to the next step of the chain and returning the first succesful response using a timeout for canceling the generated workload.\n* The `logging` middleware logs the received request and response and also the duration of the segment execution.\n* The `merging` middleware is a fork-and-join middleware. It is intended to split the process of the request into several concurrent processes, each one against a different backend, and to merge all the received responses from those created pipes into a single one. It applies a timeout, as the `concurrent` one does.\n* The `http` middleware completes the received proxy request by replacing the parameters extracted from the user request in the defined `URLPattern`.\n\n### Proxies available\n\n* The `http` proxy translates a proxy request into an HTTP one, sends it to the backend API using a `HTTPClientFactory`, decodes the returned HTTP response with a `Decoder`, manipulates the response data with an `EntityFormatter` and returns it to the caller.\n\n### Other components of the `proxy` package\n\nThe `proxy` package also defines the `EntityFormatter`, the block responsible for enabling a powerful and fast response manipulation.\n"
  },
  {
    "path": "docs/README.md",
    "content": "<img src=\"https://luraproject.org/images/lura-logo-header.svg\" width=\"300\" />\n\n# The Lura Project\n\n## How to use it\n\nVisit the [framework overview](/docs/OVERVIEW.md) for details about the components of the Lura project.\n\nA good example about how to use it can be found in the [KrakenD CE](https://github.com/krakend/krakend-ce)\nAPI Gateway project.\n\n## Configuration file\n\n[Lura config file](/docs/CONFIG.md).\n \n## Benchmarks\n\nCheck out the [benchmark results](/docs/BENCHMARKS.md) of several Lura components.\n\n## Contributing\n\nRead the guidelines about [contributing](../CONTRIBUTING.md).\n\n"
  },
  {
    "path": "encoding/encoding.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\nPackage encoding provides basic decoding implementations.\n\nDecode decodes HTTP responses:\n\n\tresp, _ := http.Get(\"http://api.example.com/\")\n\t...\n\tvar data map[string]interface{}\n\terr := JSONDecoder(resp.Body, &data)\n*/\npackage encoding\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n)\n\n// Decoder is a function that reads from the reader and decodes it\n// into an map of interfaces\ntype Decoder func(io.Reader, *map[string]interface{}) error\n\n// DecoderFactory is a function that returns CollectionDecoder or an EntityDecoder\ntype DecoderFactory func(bool) func(io.Reader, *map[string]interface{}) error\n\n// NOOP is the key for the NoOp encoding\nconst NOOP = \"no-op\"\n\n// NoOpDecoder is a decoder that does nothing\nfunc NoOpDecoder(_ io.Reader, _ *map[string]interface{}) error { return nil }\n\nfunc noOpDecoderFactory(_ bool) func(io.Reader, *map[string]interface{}) error { return NoOpDecoder }\n\n// JSON is the key for the json encoding\nconst JSON = \"json\"\n\n// NewJSONDecoder returns the right JSON decoder\nfunc NewJSONDecoder(isCollection bool) func(io.Reader, *map[string]interface{}) error {\n\tif isCollection {\n\t\treturn JSONCollectionDecoder\n\t}\n\treturn JSONDecoder\n}\n\n// JSONDecoder decodes a json message into a map\nfunc JSONDecoder(r io.Reader, v *map[string]interface{}) error {\n\td := json.NewDecoder(r)\n\td.UseNumber()\n\treturn d.Decode(v)\n}\n\n// JSONCollectionDecoder decodes a json collection and returns a map with the array at the 'collection' key\nfunc JSONCollectionDecoder(r io.Reader, v *map[string]interface{}) error {\n\tvar collection []interface{}\n\td := json.NewDecoder(r)\n\td.UseNumber()\n\tif err := d.Decode(&collection); err != nil {\n\t\treturn err\n\t}\n\t*(v) = map[string]interface{}{\"collection\": collection}\n\treturn nil\n}\n\n// SAFE_JSON is the key for the json encoding\nconst SAFE_JSON = \"safejson\"\n\n// NewSafeJSONDecoder returns the universal json decoder\nfunc NewSafeJSONDecoder(_ bool) func(io.Reader, *map[string]interface{}) error {\n\treturn SafeJSONDecoder\n}\n\n// SafeJSONDecoder decodes both json objects and collections\nfunc SafeJSONDecoder(r io.Reader, v *map[string]interface{}) error {\n\td := json.NewDecoder(r)\n\td.UseNumber()\n\tvar t interface{}\n\tif err := d.Decode(&t); err != nil {\n\t\treturn err\n\t}\n\tswitch tt := t.(type) {\n\tcase map[string]interface{}:\n\t\t*v = tt\n\tcase []interface{}:\n\t\t*v = map[string]interface{}{\"collection\": tt}\n\tdefault:\n\t\t*v = map[string]interface{}{\"content\": tt}\n\t}\n\treturn nil\n}\n\n// STRING is the key for the string encoding\nconst STRING = \"string\"\n\n// NewStringDecoder returns a String decoder\nfunc NewStringDecoder(_ bool) func(io.Reader, *map[string]interface{}) error {\n\treturn StringDecoder\n}\n\n// StringDecoder returns a map with the content of the reader under the key 'content'\nfunc StringDecoder(r io.Reader, v *map[string]interface{}) error {\n\tdata, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\t*(v) = map[string]interface{}{\"content\": string(data)}\n\treturn nil\n}\n"
  },
  {
    "path": "encoding/encoding_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage encoding\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/register\"\n)\n\nfunc TestNoOpDecoder(t *testing.T) {\n\tdecoders = initDecoderRegister()\n\tdefer func() { decoders = initDecoderRegister() }()\n\n\td := decoders.Get(NOOP)(false)\n\n\terrorMsg := erroredReader(\"this error should never been sent\")\n\tvar result map[string]interface{}\n\tif err := d(errorMsg, &result); err != nil {\n\t\tt.Error(\"Unexpected error:\", err.Error())\n\t}\n\tif result != nil {\n\t\tt.Error(\"Unexpected value:\", result)\n\t}\n}\n\nfunc TestRegister(t *testing.T) {\n\tdecoders = initDecoderRegister()\n\tdefer func() { decoders = initDecoderRegister() }()\n\n\toriginal := GetRegister()\n\n\tif len(original.data.Clone()) != 4 {\n\t\tt.Error(\"Unexpected number of registered factories:\", len(original.data.Clone()))\n\t}\n\n\tdecoders = &DecoderRegister{data: register.NewUntyped()}\n\tdecoders.Register(\"some\", NewJSONDecoder)\n\n\tif len(decoders.data.Clone()) != 1 {\n\t\tt.Error(\"Unexpected number of registered factories:\", len(decoders.data.Clone()))\n\t}\n}\n\nfunc TestGet(t *testing.T) {\n\tdecoders = initDecoderRegister()\n\tdefer func() { decoders = initDecoderRegister() }()\n\n\tif len(decoders.data.Clone()) != 4 {\n\t\tt.Error(\"Unexpected number of registered factories:\", len(decoders.data.Clone()))\n\t}\n\n\tcheckDecoder(t, JSON)\n\tcheckDecoder(t, \"some\")\n\n\tdecoders = &DecoderRegister{data: register.NewUntyped()}\n\tdecoders.Register(\"some\", NewJSONDecoder)\n\n\tif len(decoders.data.Clone()) != 1 {\n\t\tt.Error(\"Unexpected number of registered factories:\", len(decoders.data.Clone()))\n\t}\n\n\tcheckDecoder(t, JSON)\n\tcheckDecoder(t, \"some\")\n}\n\nfunc TestRegister_complete_ok(t *testing.T) {\n\tdecoders = initDecoderRegister()\n\tdefer func() { decoders = initDecoderRegister() }()\n\n\texpectedMsg := \"a custom message to decode\"\n\texpectedResponse := map[string]interface{}{\"a\": 42}\n\n\tif err := decoders.Register(\"custom\", func(_ bool) func(io.Reader, *map[string]interface{}) error {\n\t\treturn func(r io.Reader, v *map[string]interface{}) error {\n\t\t\td, err := io.ReadAll(r)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif expectedMsg != string(d) {\n\t\t\t\tt.Errorf(\"unexpected msg: %s\", string(d))\n\t\t\t\treturn errors.New(\"unexpected msg to decode\")\n\t\t\t}\n\t\t\t*v = expectedResponse\n\t\t\treturn nil\n\t\t}\n\t}); err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\tdecoder := decoders.Get(\"custom\")(false)\n\tinput := strings.NewReader(expectedMsg)\n\tvar result map[string]interface{}\n\tif err := decoder(input, &result); err != nil {\n\t\tt.Error(\"Unexpected error:\", err.Error())\n\t}\n\tif v, ok := result[\"a\"]; !ok || v.(int) != 42 {\n\t\tt.Error(\"Unexpected value:\", result)\n\t}\n}\n\nfunc TestRegister_complete_ko(t *testing.T) {\n\tdecoders = initDecoderRegister()\n\tdefer func() { decoders = initDecoderRegister() }()\n\n\texpectedMsg := \"a custom message to decode\"\n\texpectedErr := errors.New(\"expect me\")\n\n\tif err := decoders.Register(\"custom\", func(_ bool) func(io.Reader, *map[string]interface{}) error {\n\t\treturn func(r io.Reader, v *map[string]interface{}) error {\n\t\t\td, err := io.ReadAll(r)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif expectedMsg != string(d) {\n\t\t\t\tt.Errorf(\"unexpected msg: %s\", string(d))\n\t\t\t\treturn errors.New(\"unexpected msg to decode\")\n\t\t\t}\n\t\t\t// v = nil\n\t\t\treturn expectedErr\n\t\t}\n\t}); err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\tdecoder := decoders.Get(\"custom\")(false)\n\tinput := strings.NewReader(expectedMsg)\n\tvar result map[string]interface{}\n\tif err := decoder(input, &result); err != expectedErr {\n\t\tt.Error(\"Unexpected error:\", err)\n\t}\n\tif result != nil {\n\t\tt.Error(\"Unexpected value:\", result)\n\t}\n}\n\nfunc checkDecoder(t *testing.T, name string) {\n\td := decoders.Get(name)(false)\n\n\tinput := strings.NewReader(`{\"foo\": \"bar\"}`)\n\tvar result map[string]interface{}\n\tif err := d(input, &result); err != nil {\n\t\tt.Error(\"Unexpected error:\", err.Error())\n\t}\n\tif result[\"foo\"] != \"bar\" {\n\t\tt.Error(\"Unexpected value:\", result[\"foo\"])\n\t}\n}\n\ntype erroredReader string\n\nfunc (e erroredReader) Error() string {\n\treturn string(e)\n}\n\nfunc (e erroredReader) Read(_ []byte) (n int, err error) {\n\treturn 0, e\n}\n"
  },
  {
    "path": "encoding/json_benchmark_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage encoding\n\nimport (\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc BenchmarkDecoder(b *testing.B) {\n\tfor _, dec := range []struct {\n\t\tname    string\n\t\tdecoder func(io.Reader, *map[string]interface{}) error\n\t}{\n\t\t{\n\t\t\tname:    \"json-collection\",\n\t\t\tdecoder: NewJSONDecoder(true),\n\t\t},\n\t\t{\n\t\t\tname:    \"json-map\",\n\t\t\tdecoder: NewJSONDecoder(false),\n\t\t},\n\t\t{\n\t\t\tname:    \"safe-json-collection\",\n\t\t\tdecoder: NewSafeJSONDecoder(true),\n\t\t},\n\t\t{\n\t\t\tname:    \"safe-json-map\",\n\t\t\tdecoder: NewSafeJSONDecoder(true),\n\t\t},\n\t} {\n\t\tfor _, tc := range []struct {\n\t\t\tname  string\n\t\t\tinput string\n\t\t}{\n\t\t\t{\n\t\t\t\tname:  \"collection\",\n\t\t\t\tinput: `[\"a\",\"b\",\"c\"]`,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:  \"map\",\n\t\t\t\tinput: `{\"foo\": \"bar\", \"supu\": false, \"tupu\": 4.20}`,\n\t\t\t},\n\t\t} {\n\t\t\tb.Run(dec.name+\"/\"+tc.name, func(b *testing.B) {\n\t\t\t\tvar result map[string]interface{}\n\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t_ = dec.decoder(strings.NewReader(tc.input), &result)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "encoding/json_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage encoding\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc ExampleNewJSONDecoder_map() {\n\tdecoder := NewJSONDecoder(false)\n\toriginal := strings.NewReader(`{\"foo\": \"bar\", \"supu\": false, \"tupu\": 4.20}`)\n\tvar result map[string]interface{}\n\tif err := decoder(original, &result); err != nil {\n\t\tfmt.Println(\"Unexpected error:\", err.Error())\n\t}\n\tfmt.Printf(\"%+v\\n\", result)\n\n\t// output:\n\t// map[foo:bar supu:false tupu:4.20]\n}\n\nfunc ExampleNewJSONDecoder_collection() {\n\tdecoder := NewJSONDecoder(true)\n\toriginal := strings.NewReader(`[\"foo\", \"bar\", \"supu\"]`)\n\tvar result map[string]interface{}\n\tif err := decoder(original, &result); err != nil {\n\t\tfmt.Println(\"Unexpected error:\", err.Error())\n\t}\n\tfmt.Printf(\"%+v\\n\", result)\n\n\t// output:\n\t// map[collection:[foo bar supu]]\n}\n\nfunc TestNewJSONDecoder_map(t *testing.T) {\n\tdecoder := NewJSONDecoder(false)\n\toriginal := strings.NewReader(`{\"foo\": \"bar\", \"supu\": false, \"tupu\": 4.20}`)\n\tvar result map[string]interface{}\n\tif err := decoder(original, &result); err != nil {\n\t\tt.Error(\"Unexpected error:\", err.Error())\n\t}\n\tif len(result) != 3 {\n\t\tt.Error(\"Unexpected result:\", result)\n\t}\n\tif v, ok := result[\"foo\"]; !ok || v.(string) != \"bar\" {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n\tif v, ok := result[\"supu\"]; !ok || v.(bool) {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n\tif v, ok := result[\"tupu\"]; !ok || v.(json.Number).String() != \"4.20\" {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n}\n\nfunc TestNewJSONDecoder_collection(t *testing.T) {\n\tdecoder := NewJSONDecoder(true)\n\toriginal := strings.NewReader(`[\"foo\", \"bar\", \"supu\"]`)\n\tvar result map[string]interface{}\n\tif err := decoder(original, &result); err != nil {\n\t\tt.Error(\"Unexpected error:\", err.Error())\n\t}\n\tif len(result) != 1 {\n\t\tt.Error(\"Unexpected result:\", result)\n\t}\n\tv, ok := result[\"collection\"]\n\tif !ok {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n\tembedded := v.([]interface{})\n\tif embedded[0].(string) != \"foo\" {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n\tif embedded[1].(string) != \"bar\" {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n\tif embedded[2].(string) != \"supu\" {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n}\n\nfunc TestNewJSONDecoder_ko(t *testing.T) {\n\tdecoder := NewJSONDecoder(true)\n\toriginal := strings.NewReader(`3`)\n\tvar result map[string]interface{}\n\tif err := decoder(original, &result); err == nil {\n\t\tt.Error(\"Expecting error!\")\n\t}\n}\n\nfunc ExampleNewSafeJSONDecoder() {\n\tdecoder := NewSafeJSONDecoder(true)\n\tfor _, body := range []string{\n\t\t`{\"foo\": \"bar\", \"supu\": false, \"tupu\": 4.20}`,\n\t\t`[\"foo\", \"bar\", \"supu\"]`,\n\t} {\n\t\tvar result map[string]interface{}\n\t\tif err := decoder(strings.NewReader(body), &result); err != nil {\n\t\t\tfmt.Println(\"Unexpected error:\", err.Error())\n\t\t}\n\t\tfmt.Printf(\"%+v\\n\", result)\n\t}\n\n\t// output:\n\t// map[foo:bar supu:false tupu:4.20]\n\t// map[collection:[foo bar supu]]\n}\n\nfunc TestNewSafeJSONDecoder_map(t *testing.T) {\n\tdecoder := NewSafeJSONDecoder(false)\n\toriginal := strings.NewReader(`{\"foo\": \"bar\", \"supu\": false, \"tupu\": 4.20}`)\n\tvar result map[string]interface{}\n\tif err := decoder(original, &result); err != nil {\n\t\tt.Error(\"Unexpected error:\", err.Error())\n\t}\n\tif len(result) != 3 {\n\t\tt.Error(\"Unexpected result:\", result)\n\t}\n\tif v, ok := result[\"foo\"]; !ok || v.(string) != \"bar\" {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n\tif v, ok := result[\"supu\"]; !ok || v.(bool) {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n\tif v, ok := result[\"tupu\"]; !ok || v.(json.Number).String() != \"4.20\" {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n}\n\nfunc TestNewSafeJSONDecoder_collection(t *testing.T) {\n\tdecoder := NewSafeJSONDecoder(true)\n\toriginal := strings.NewReader(`[\"foo\", \"bar\", \"supu\"]`)\n\tvar result map[string]interface{}\n\tif err := decoder(original, &result); err != nil {\n\t\tt.Error(\"Unexpected error:\", err.Error())\n\t}\n\tif len(result) != 1 {\n\t\tt.Error(\"Unexpected result:\", result)\n\t}\n\tv, ok := result[\"collection\"]\n\tif !ok {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n\tembedded := v.([]interface{})\n\tif embedded[0].(string) != \"foo\" {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n\tif embedded[1].(string) != \"bar\" {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n\tif embedded[2].(string) != \"supu\" {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n}\n\nfunc TestNewSafeJSONDecoder_other(t *testing.T) {\n\tdecoder := NewSafeJSONDecoder(true)\n\toriginal := strings.NewReader(`3`)\n\tvar result map[string]interface{}\n\tif err := decoder(original, &result); err != nil {\n\t\tt.Error(\"Unexpected error:\", err.Error())\n\t}\n\tif v, ok := result[\"content\"]; !ok || v.(json.Number).String() != \"3\" {\n\t\tt.Error(\"wrong result:\", result)\n\t}\n}\n"
  },
  {
    "path": "encoding/register.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage encoding\n\nimport (\n\t\"io\"\n\n\t\"github.com/luraproject/lura/v2/register\"\n)\n\n// GetRegister returns the package register\nfunc GetRegister() *DecoderRegister {\n\treturn decoders\n}\n\ntype untypedRegister interface {\n\tRegister(name string, v interface{})\n\tGet(name string) (interface{}, bool)\n\tClone() map[string]interface{}\n}\n\n// DecoderRegister is the struct responsible of registering the decoder factories\ntype DecoderRegister struct {\n\tdata untypedRegister\n}\n\n// Register adds a decoder factory to the register\nfunc (r *DecoderRegister) Register(name string, dec func(bool) func(io.Reader, *map[string]interface{}) error) error {\n\tr.data.Register(name, dec)\n\treturn nil\n}\n\n// Get returns a decoder factory from the register by name. If no factory is found, it returns a JSON decoder factory\nfunc (r *DecoderRegister) Get(name string) func(bool) func(io.Reader, *map[string]interface{}) error {\n\tfor _, n := range []string{name, JSON} {\n\t\tif v, ok := r.data.Get(n); ok {\n\t\t\tif dec, ok := v.(func(bool) func(io.Reader, *map[string]interface{}) error); ok {\n\t\t\t\treturn dec\n\t\t\t}\n\t\t}\n\t}\n\treturn NewJSONDecoder\n}\n\nvar (\n\tdecoders        = initDecoderRegister()\n\tdefaultDecoders = map[string]func(bool) func(io.Reader, *map[string]interface{}) error{\n\t\tJSON:      NewJSONDecoder,\n\t\tSAFE_JSON: NewSafeJSONDecoder,\n\t\tSTRING:    NewStringDecoder,\n\t\tNOOP:      noOpDecoderFactory,\n\t}\n)\n\nfunc initDecoderRegister() *DecoderRegister {\n\tr := &DecoderRegister{data: register.NewUntyped()}\n\tfor k, v := range defaultDecoders {\n\t\tr.Register(k, v)\n\t}\n\treturn r\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/luraproject/lura/v2\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/dimfeld/httptreemux/v5 v5.5.0\n\tgithub.com/gin-contrib/sse v1.1.0 // indirect\n\tgithub.com/gin-gonic/gin v1.12.0\n\tgithub.com/go-chi/chi/v5 v5.2.2\n\tgithub.com/gorilla/mux v1.8.1\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/urfave/negroni/v2 v2.0.2\n\tgithub.com/valyala/fastrand v1.1.0\n)\n\nrequire (\n\tgithub.com/krakend/flatmap v1.2.0\n\tgolang.org/x/net v0.51.0\n\tgolang.org/x/sync v0.19.0\n\tgolang.org/x/text v0.34.0\n)\n\nrequire (\n\tgithub.com/bytedance/gopkg v0.1.3 // indirect\n\tgithub.com/bytedance/sonic v1.15.0 // indirect\n\tgithub.com/bytedance/sonic/loader v0.5.0 // indirect\n\tgithub.com/cloudwego/base64x v0.1.6 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.12 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.30.1 // indirect\n\tgithub.com/goccy/go-json v0.10.5 // indirect\n\tgithub.com/goccy/go-yaml v1.19.2 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.3.0 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/quic-go/qpack v0.6.0 // indirect\n\tgithub.com/quic-go/quic-go v0.59.0 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.3.1 // indirect\n\tgo.mongodb.org/mongo-driver/v2 v2.5.0 // indirect\n\tgolang.org/x/arch v0.22.0 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/sys v0.41.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.10 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=\ngithub.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=\ngithub.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=\ngithub.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=\ngithub.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=\ngithub.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=\ngithub.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=\ngithub.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dimfeld/httptreemux/v5 v5.5.0 h1:p8jkiMrCuZ0CmhwYLcbNbl7DDo21fozhKHQ2PccwOFQ=\ngithub.com/dimfeld/httptreemux/v5 v5.5.0/go.mod h1:QeEylH57C0v3VO0tkKraVz9oD3Uu93CKPnTLbsidvSw=\ngithub.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=\ngithub.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=\ngithub.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=\ngithub.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=\ngithub.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=\ngithub.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=\ngithub.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=\ngithub.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=\ngithub.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=\ngithub.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=\ngithub.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=\ngithub.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=\ngithub.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=\ngithub.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=\ngithub.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/krakend/flatmap v1.2.0 h1:4NPncAKH7Ca/t878kbGlc/LPWLa+m4sgBhs8aT2Q1SY=\ngithub.com/krakend/flatmap v1.2.0/go.mod h1:FyCOoggdVlWr31+aQaOFvBxlMgYfCE5yuwInLbW1/jM=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=\ngithub.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=\ngithub.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=\ngithub.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=\ngithub.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=\ngithub.com/urfave/negroni/v2 v2.0.2 h1:27gJcVxYJ2a/ytEoCHoJ7ybvyhymV4cAhGuMxkyCsrU=\ngithub.com/urfave/negroni/v2 v2.0.2/go.mod h1:SjdApKzYrObukpN/NnlejbQiZWIUjfDFzQltScGYigI=\ngithub.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=\ngithub.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=\ngo.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=\ngo.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=\ngo.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=\ngo.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=\ngolang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=\ngolang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=\ngolang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngoogle.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=\ngoogle.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "logging/log.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\n\tPackage logging provides a simple logger interface and implementations\n*/\npackage logging\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n)\n\n// Logger collects logging information at several levels\ntype Logger interface {\n\tDebug(v ...interface{})\n\tInfo(v ...interface{})\n\tWarning(v ...interface{})\n\tError(v ...interface{})\n\tCritical(v ...interface{})\n\tFatal(v ...interface{})\n}\n\nconst (\n\t// LEVEL_DEBUG = 0\n\tLEVEL_DEBUG = iota\n\t// LEVEL_INFO = 1\n\tLEVEL_INFO\n\t// LEVEL_WARNING = 2\n\tLEVEL_WARNING\n\t// LEVEL_ERROR = 3\n\tLEVEL_ERROR\n\t// LEVEL_CRITICAL = 4\n\tLEVEL_CRITICAL\n)\n\nvar (\n\t// ErrInvalidLogLevel is used when an invalid log level has been used.\n\tErrInvalidLogLevel = errors.New(\"invalid log level\")\n\tdefaultLogger      = BasicLogger{Level: LEVEL_CRITICAL, Logger: log.New(os.Stderr, \"\", log.LstdFlags)}\n\tlogLevels          = map[string]int{\n\t\t\"DEBUG\":    LEVEL_DEBUG,\n\t\t\"INFO\":     LEVEL_INFO,\n\t\t\"WARNING\":  LEVEL_WARNING,\n\t\t\"ERROR\":    LEVEL_ERROR,\n\t\t\"CRITICAL\": LEVEL_CRITICAL,\n\t}\n\t// NoOp is the NO-OP logger\n\tNoOp, _ = NewLogger(\"CRITICAL\", io.Discard, \"\")\n)\n\n// NewLogger creates and returns a Logger object\nfunc NewLogger(level string, out io.Writer, prefix string) (BasicLogger, error) {\n\tl, ok := logLevels[strings.ToUpper(level)]\n\tif !ok {\n\t\treturn defaultLogger, ErrInvalidLogLevel\n\t}\n\treturn BasicLogger{Level: l, Prefix: prefix, Logger: log.New(out, \"\", log.LstdFlags)}, nil\n}\n\ntype BasicLogger struct {\n\tLevel  int\n\tPrefix string\n\tLogger *log.Logger\n}\n\n// Debug logs a message using DEBUG as log level.\nfunc (l BasicLogger) Debug(v ...interface{}) {\n\tif l.Level > LEVEL_DEBUG {\n\t\treturn\n\t}\n\tl.prependLog(\"DEBUG:\", v...)\n}\n\n// Info logs a message using INFO as log level.\nfunc (l BasicLogger) Info(v ...interface{}) {\n\tif l.Level > LEVEL_INFO {\n\t\treturn\n\t}\n\tl.prependLog(\"INFO:\", v...)\n}\n\n// Warning logs a message using WARNING as log level.\nfunc (l BasicLogger) Warning(v ...interface{}) {\n\tif l.Level > LEVEL_WARNING {\n\t\treturn\n\t}\n\tl.prependLog(\"WARNING:\", v...)\n}\n\n// Error logs a message using ERROR as log level.\nfunc (l BasicLogger) Error(v ...interface{}) {\n\tif l.Level > LEVEL_ERROR {\n\t\treturn\n\t}\n\tl.prependLog(\"ERROR:\", v...)\n}\n\n// Critical logs a message using CRITICAL as log level.\nfunc (l BasicLogger) Critical(v ...interface{}) {\n\tl.prependLog(\"CRITICAL:\", v...)\n}\n\n// Fatal is equivalent to l.Critical(fmt.Sprint()) followed by a call to os.Exit(1).\nfunc (l BasicLogger) Fatal(v ...interface{}) {\n\tl.prependLog(\"FATAL:\", v...)\n\tos.Exit(1)\n}\n\nfunc (l BasicLogger) prependLog(level string, v ...interface{}) {\n\tmsg := make([]interface{}, len(v)+2)\n\tmsg[0] = l.Prefix\n\tmsg[1] = level\n\tcopy(msg[2:], v)\n\tl.Logger.Println(msg...)\n}\n"
  },
  {
    "path": "logging/log_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage logging\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"testing\"\n)\n\nconst (\n\tdebugMsg    = \"Debug msg\"\n\tinfoMsg     = \"Info msg\"\n\twarningMsg  = \"Warning msg\"\n\terrorMsg    = \"Error msg\"\n\tcriticalMsg = \"Critical msg\"\n\tfatalMsg    = \"Fatal msg\"\n)\n\nfunc TestNewLogger(t *testing.T) {\n\tlevels := []string{\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"}\n\tregexps := []*regexp.Regexp{\n\t\tregexp.MustCompile(debugMsg),\n\t\tregexp.MustCompile(infoMsg),\n\t\tregexp.MustCompile(warningMsg),\n\t\tregexp.MustCompile(errorMsg),\n\t\tregexp.MustCompile(criticalMsg),\n\t}\n\n\tfor i, level := range levels {\n\t\toutput := logSomeStuff(level)\n\t\tfor j := i; j < len(regexps); j++ {\n\t\t\tif !regexps[j].MatchString(output) {\n\t\t\t\tt.Errorf(\"The output doesn't contain the expected msg for the level: %s. [%s]\", level, output)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestNewLogger_unknownLevel(t *testing.T) {\n\t_, err := NewLogger(\"UNKNOWN\", bytes.NewBuffer(make([]byte, 1024)), \"pref\")\n\tif err == nil {\n\t\tt.Error(\"The factory didn't return the expected error\")\n\t\treturn\n\t}\n\tif err != ErrInvalidLogLevel {\n\t\tt.Errorf(\"The factory didn't return the expected error. Got: %s\", err.Error())\n\t}\n}\n\nfunc TestNewLogger_fatal(t *testing.T) {\n\tif os.Getenv(\"BE_CRASHER\") == \"1\" {\n\t\tl, err := NewLogger(\"Critical\", bytes.NewBuffer(make([]byte, 1024)), \"pref\")\n\t\tif err != nil {\n\t\t\tt.Error(\"The factory returned an expected error:\", err.Error())\n\t\t\treturn\n\t\t}\n\t\tl.Fatal(\"crash!!!\")\n\t\treturn\n\t}\n\tcmd := exec.Command(os.Args[0], \"-test.run=TestNewLogger_fatal\")\n\tcmd.Env = append(os.Environ(), \"BE_CRASHER=1\")\n\terr := cmd.Run()\n\tif e, ok := err.(*exec.ExitError); ok && !e.Success() {\n\t\treturn\n\t}\n\tt.Fatalf(\"process ran with err %v, want exit status 1\", err)\n}\n\nfunc logSomeStuff(level string) string {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, _ := NewLogger(level, buff, \"pref\")\n\n\tlogger.Debug(debugMsg)\n\tlogger.Info(infoMsg)\n\tlogger.Warning(warningMsg)\n\tlogger.Error(errorMsg)\n\tlogger.Critical(criticalMsg)\n\n\treturn buff.String()\n}\n"
  },
  {
    "path": "plugin/plugin.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\nPackage plugin provides tools for loading and registering plugins\n*/\npackage plugin\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// Scan returns all the files contained in the received folder with a filename matching the given pattern\nfunc Scan(folder, pattern string) ([]string, error) {\n\tfiles, err := os.ReadDir(folder)\n\tif err != nil {\n\t\treturn []string{}, err\n\t}\n\n\tvar plugins []string\n\n\tfor _, file := range files {\n\t\tif !file.IsDir() && strings.Contains(file.Name(), pattern) {\n\t\t\tplugins = append(plugins, filepath.Join(folder, file.Name()))\n\t\t}\n\t}\n\n\treturn plugins, nil\n}\n"
  },
  {
    "path": "plugin/plugin_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage plugin\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestScan_ok(t *testing.T) {\n\ttmpDir, err := os.MkdirTemp(\".\", \"test\")\n\tif err != nil {\n\t\tt.Error(\"unexpected error:\", err.Error())\n\t\treturn\n\t}\n\tdefer os.RemoveAll(tmpDir)\n\tf, err := os.CreateTemp(tmpDir, \"test.so\")\n\tif err != nil {\n\t\tt.Error(\"unexpected error:\", err.Error())\n\t\treturn\n\t}\n\tf.Close()\n\tdefer os.RemoveAll(tmpDir)\n\n\ttot, err := Scan(tmpDir, \".so\")\n\tif len(tot) != 1 {\n\t\tt.Error(\"unexpected number of plugins found:\", tot)\n\t}\n\tif err != nil {\n\t\tt.Error(\"unexpected error:\", err.Error())\n\t}\n}\n\nfunc TestScan_noFolder(t *testing.T) {\n\texpectedErr := \"open unknown: no such file or directory\"\n\ttot, err := Scan(\"unknown\", \"\")\n\tif len(tot) != 0 {\n\t\tt.Error(\"unexpected number of plugins loaded:\", tot)\n\t}\n\tif err == nil {\n\t\tt.Error(\"expecting error!\")\n\t\treturn\n\t}\n\tif err.Error() != expectedErr {\n\t\tt.Error(\"unexpected error:\", err.Error())\n\t}\n}\n\nfunc TestScan_emptyFolder(t *testing.T) {\n\tname, err := os.MkdirTemp(\".\", \"test\")\n\tif err != nil {\n\t\tt.Error(\"unexpected error:\", err.Error())\n\t\treturn\n\t}\n\ttot, err := Scan(name, \"\")\n\tif len(tot) != 0 {\n\t\tt.Error(\"unexpected number of plugins loaded:\", tot)\n\t}\n\tif err != nil {\n\t\tt.Error(\"unexpected error:\", err.Error())\n\t}\n\tos.RemoveAll(name)\n}\n\nfunc TestScan_noMatches(t *testing.T) {\n\ttmpDir, err := os.MkdirTemp(\".\", \"test\")\n\tif err != nil {\n\t\tt.Error(\"unexpected error:\", err.Error())\n\t\treturn\n\t}\n\tdefer os.RemoveAll(tmpDir)\n\tf, err := os.CreateTemp(tmpDir, \"test\")\n\tif err != nil {\n\t\tt.Error(\"unexpected error:\", err.Error())\n\t\treturn\n\t}\n\tf.Close()\n\tdefer os.RemoveAll(tmpDir)\n\ttot, err := Scan(tmpDir, \".so\")\n\tif len(tot) != 0 {\n\t\tt.Error(\"unexpected number of plugins loaded:\", tot)\n\t}\n\tif err != nil {\n\t\tt.Error(\"unexpected error:\", err.Error())\n\t}\n}\n"
  },
  {
    "path": "proxy/balancing.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"net/url\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/sd\"\n)\n\n// NewLoadBalancedMiddleware creates proxy middleware adding the most perfomant balancer\n// over a default subscriber\nfunc NewLoadBalancedMiddleware(remote *config.Backend) Middleware {\n\treturn NewLoadBalancedMiddlewareWithSubscriber(sd.GetRegister().Get(remote.SD)(remote))\n}\n\n// NewLoadBalancedMiddlewareWithSubscriber creates proxy middleware adding the most perfomant balancer\n// over the received subscriber\nfunc NewLoadBalancedMiddlewareWithSubscriber(subscriber sd.Subscriber) Middleware {\n\treturn newLoadBalancedMiddleware(logging.NoOp, sd.NewBalancer(subscriber))\n}\n\n// NewRoundRobinLoadBalancedMiddleware creates proxy middleware adding a round robin balancer\n// over a default subscriber\nfunc NewRoundRobinLoadBalancedMiddleware(remote *config.Backend) Middleware {\n\treturn NewRoundRobinLoadBalancedMiddlewareWithSubscriber(sd.GetRegister().Get(remote.SD)(remote))\n}\n\n// NewRandomLoadBalancedMiddleware creates proxy middleware adding a random balancer\n// over a default subscriber\nfunc NewRandomLoadBalancedMiddleware(remote *config.Backend) Middleware {\n\treturn NewRandomLoadBalancedMiddlewareWithSubscriber(sd.GetRegister().Get(remote.SD)(remote))\n}\n\n// NewRoundRobinLoadBalancedMiddlewareWithSubscriber creates proxy middleware adding a round robin\n// balancer over the received subscriber\nfunc NewRoundRobinLoadBalancedMiddlewareWithSubscriber(subscriber sd.Subscriber) Middleware {\n\treturn newLoadBalancedMiddleware(logging.NoOp, sd.NewRoundRobinLB(subscriber))\n}\n\n// NewRandomLoadBalancedMiddlewareWithSubscriber creates proxy middleware adding a random\n// balancer over the received subscriber\nfunc NewRandomLoadBalancedMiddlewareWithSubscriber(subscriber sd.Subscriber) Middleware {\n\treturn newLoadBalancedMiddleware(logging.NoOp, sd.NewRandomLB(subscriber))\n}\n\n// NewLoadBalancedMiddlewareWithLogger creates proxy middleware adding the most perfomant balancer\n// over a default subscriber\nfunc NewLoadBalancedMiddlewareWithLogger(l logging.Logger, remote *config.Backend) Middleware {\n\treturn NewLoadBalancedMiddlewareWithSubscriberAndLogger(l, sd.GetRegister().Get(remote.SD)(remote))\n}\n\n// NewLoadBalancedMiddlewareWithSubscriberAndLogger creates proxy middleware adding the most perfomant balancer\n// over the received subscriber\nfunc NewLoadBalancedMiddlewareWithSubscriberAndLogger(l logging.Logger, subscriber sd.Subscriber) Middleware {\n\treturn newLoadBalancedMiddleware(l, sd.NewBalancer(subscriber))\n}\n\n// NewRoundRobinLoadBalancedMiddlewareWithLogger creates proxy middleware adding a round robin balancer\n// over a default subscriber\nfunc NewRoundRobinLoadBalancedMiddlewareWithLogger(l logging.Logger, remote *config.Backend) Middleware {\n\treturn NewRoundRobinLoadBalancedMiddlewareWithSubscriberAndLogger(l, sd.GetRegister().Get(remote.SD)(remote))\n}\n\n// NewRandomLoadBalancedMiddlewareWithLogger creates proxy middleware adding a random balancer\n// over a default subscriber\nfunc NewRandomLoadBalancedMiddlewareWithLogger(l logging.Logger, remote *config.Backend) Middleware {\n\treturn NewRandomLoadBalancedMiddlewareWithSubscriberAndLogger(l, sd.GetRegister().Get(remote.SD)(remote))\n}\n\n// NewRoundRobinLoadBalancedMiddlewareWithSubscriberAndLogger creates proxy middleware adding a round robin\n// balancer over the received subscriber\nfunc NewRoundRobinLoadBalancedMiddlewareWithSubscriberAndLogger(l logging.Logger, subscriber sd.Subscriber) Middleware {\n\treturn newLoadBalancedMiddleware(l, sd.NewRoundRobinLB(subscriber))\n}\n\n// NewRandomLoadBalancedMiddlewareWithSubscriberAndLogger creates proxy middleware adding a random\n// balancer over the received subscriber\nfunc NewRandomLoadBalancedMiddlewareWithSubscriberAndLogger(l logging.Logger, subscriber sd.Subscriber) Middleware {\n\treturn newLoadBalancedMiddleware(l, sd.NewRandomLB(subscriber))\n}\n\nfunc newLoadBalancedMiddleware(l logging.Logger, lb sd.Balancer) Middleware {\n\treturn func(next ...Proxy) Proxy {\n\t\tif len(next) > 1 {\n\t\t\tl.Fatal(\"too many proxies for this proxy middleware: newLoadBalancedMiddleware only accepts 1 proxy, got %d\", len(next))\n\t\t\treturn nil\n\t\t}\n\t\treturn func(ctx context.Context, r *Request) (*Response, error) {\n\t\t\thost, err := lb.Host()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tr.URL, err = url.Parse(host + r.Path)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif len(r.Query) > 0 {\n\t\t\t\tif len(r.URL.RawQuery) > 0 {\n\t\t\t\t\tr.URL.RawQuery += \"&\" + r.Query.Encode()\n\t\t\t\t} else {\n\t\t\t\t\tr.URL.RawQuery += r.Query.Encode()\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn next[0](ctx, r)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "proxy/balancing_benchmark_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nconst veryLargeString = \"abcdefghijklmopqrstuvwxyzabcdefghijklmopqrstuvwxyzabcdefghijklmopqrstuvwxyzabcdefghijklmopqrstuvwxyz\"\n\nfunc BenchmarkNewLoadBalancedMiddleware(b *testing.B) {\n\tfor _, tc := range []int{3, 5, 9, 13, 17, 21, 25, 50, 100} {\n\t\tb.Run(strconv.Itoa(tc), func(b *testing.B) {\n\t\t\tproxy := newLoadBalancedMiddleware(logging.NoOp, dummyBalancer(veryLargeString[:tc]))(dummyProxy(&Response{}))\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\tproxy(context.Background(), &Request{\n\t\t\t\t\tPath: veryLargeString[:tc],\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkNewLoadBalancedMiddleware_parallel3(b *testing.B) {\n\tbenchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:3])\n}\n\nfunc BenchmarkNewLoadBalancedMiddleware_parallel5(b *testing.B) {\n\tbenchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:5])\n}\n\nfunc BenchmarkNewLoadBalancedMiddleware_parallel9(b *testing.B) {\n\tbenchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:9])\n}\n\nfunc BenchmarkNewLoadBalancedMiddleware_parallel13(b *testing.B) {\n\tbenchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:13])\n}\n\nfunc BenchmarkNewLoadBalancedMiddleware_parallel17(b *testing.B) {\n\tbenchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:17])\n}\n\nfunc BenchmarkNewLoadBalancedMiddleware_parallel21(b *testing.B) {\n\tbenchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:21])\n}\n\nfunc BenchmarkNewLoadBalancedMiddleware_parallel25(b *testing.B) {\n\tbenchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:25])\n}\n\nfunc BenchmarkNewLoadBalancedMiddleware_parallel50(b *testing.B) {\n\tbenchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:50])\n}\n\nfunc BenchmarkNewLoadBalancedMiddleware_parallel100(b *testing.B) {\n\tbenchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:100])\n}\n\nfunc benchmarkNewLoadBalancedMiddleware_parallel(b *testing.B, subject string) {\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tproxy := newLoadBalancedMiddleware(logging.NoOp, dummyBalancer(subject))(dummyProxy(&Response{}))\n\t\tfor pb.Next() {\n\t\t\tproxy(context.Background(), &Request{\n\t\t\t\tPath: subject,\n\t\t\t})\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "proxy/balancing_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/sd/dnssrv\"\n)\n\nfunc TestNewLoadBalancedMiddleware_ok(t *testing.T) {\n\twant := \"supu:8080/tupu\"\n\tlb := newLoadBalancedMiddleware(logging.NoOp, dummyBalancer(\"supu:8080\"))\n\tassertion := func(ctx context.Context, request *Request) (*Response, error) {\n\t\tif request.URL.String() != want {\n\t\t\tt.Errorf(\"The middleware did not update the request URL! want [%s], have [%s]\\n\", want, request.URL)\n\t\t}\n\t\treturn nil, nil\n\t}\n\tif _, err := lb(assertion)(context.Background(), &Request{\n\t\tPath: \"/tupu\",\n\t}); err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n}\n\nfunc TestNewLoadBalancedMiddleware_explosiveBalancer(t *testing.T) {\n\texpected := errors.New(\"supu\")\n\tlb := newLoadBalancedMiddleware(logging.NoOp, explosiveBalancer{expected})\n\tif _, err := lb(explosiveProxy(t))(context.Background(), &Request{}); err != expected {\n\t\tt.Errorf(\"The middleware did not propagate the lb error\\n\")\n\t}\n}\n\nfunc TestNewRoundRobinLoadBalancedMiddleware(t *testing.T) {\n\ttestLoadBalancedMw(t, NewRoundRobinLoadBalancedMiddleware(&config.Backend{\n\t\tHost: []string{\"http://127.0.0.1:8080\"},\n\t}))\n}\n\nfunc TestNewRandomLoadBalancedMiddleware(t *testing.T) {\n\ttestLoadBalancedMw(t, NewRandomLoadBalancedMiddleware(&config.Backend{\n\t\tHost: []string{\"http://127.0.0.1:8080\"},\n\t}))\n}\n\nfunc testLoadBalancedMw(t *testing.T, lb Middleware) {\n\tfor _, tc := range []struct {\n\t\tpath     string\n\t\tquery    url.Values\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tpath:     \"/tupu\",\n\t\t\texpected: \"http://127.0.0.1:8080/tupu\",\n\t\t},\n\t\t{\n\t\t\tpath:     \"/tupu?extra=true\",\n\t\t\texpected: \"http://127.0.0.1:8080/tupu?extra=true\",\n\t\t},\n\t\t{\n\t\t\tpath:     \"/tupu?extra=true\",\n\t\t\tquery:    url.Values{\"some\": []string{\"none\"}},\n\t\t\texpected: \"http://127.0.0.1:8080/tupu?extra=true&some=none\",\n\t\t},\n\t\t{\n\t\t\tpath:     \"/tupu\",\n\t\t\tquery:    url.Values{\"some\": []string{\"none\"}},\n\t\t\texpected: \"http://127.0.0.1:8080/tupu?some=none\",\n\t\t},\n\t} {\n\t\tassertion := func(ctx context.Context, request *Request) (*Response, error) {\n\t\t\tif request.URL.String() != tc.expected {\n\t\t\t\tt.Errorf(\"The middleware did not update the request URL! want [%s], have [%s]\\n\", tc.expected, request.URL)\n\t\t\t}\n\t\t\treturn nil, nil\n\t\t}\n\t\tif _, err := lb(assertion)(context.Background(), &Request{\n\t\t\tPath:  tc.path,\n\t\t\tQuery: tc.query,\n\t\t}); err != nil {\n\t\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t\t}\n\t}\n}\n\nfunc TestNewLoadBalancedMiddleware_parsingError(t *testing.T) {\n\tlb := NewRandomLoadBalancedMiddleware(&config.Backend{\n\t\tHost: []string{\"127.0.0.1:8080\"},\n\t})\n\tassertion := func(ctx context.Context, request *Request) (*Response, error) {\n\t\tt.Error(\"The middleware didn't block the request!\")\n\t\treturn nil, nil\n\t}\n\tif _, err := lb(assertion)(context.Background(), &Request{\n\t\tPath: \"/tupu\",\n\t}); err == nil {\n\t\tt.Error(\"The middleware didn't propagate the expected error\")\n\t}\n}\n\nfunc TestNewRoundRobinLoadBalancedMiddleware_DNSSRV(t *testing.T) {\n\tdefaultLookup := dnssrv.DefaultLookup\n\n\tdnssrv.DefaultLookup = func(service, proto, name string) (cname string, addrs []*net.SRV, err error) {\n\t\treturn \"cname\", []*net.SRV{\n\t\t\t{\n\t\t\t\tPort:   8080,\n\t\t\t\tTarget: \"127.0.0.1\",\n\t\t\t\tWeight: 1,\n\t\t\t},\n\t\t}, nil\n\t}\n\ttestLoadBalancedMw(t, NewRoundRobinLoadBalancedMiddlewareWithSubscriber(dnssrv.New(\"some.service.example.tld\")))\n\n\tdnssrv.DefaultLookup = defaultLookup\n}\n\ntype dummyBalancer string\n\nfunc (d dummyBalancer) Host() (string, error) { return string(d), nil }\n\ntype explosiveBalancer struct {\n\tError error\n}\n\nfunc (e explosiveBalancer) Host() (string, error) { return \"\", e.Error }\n"
  },
  {
    "path": "proxy/concurrent.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\n// NewConcurrentMiddlewareWithLogger creates a proxy middleware that enables sending several requests concurrently\nfunc NewConcurrentMiddlewareWithLogger(logger logging.Logger, remote *config.Backend) Middleware {\n\tif remote.ConcurrentCalls == 1 {\n\t\tlogger.Fatal(fmt.Sprintf(\"too few concurrent calls for %s %s -> %s: NewConcurrentMiddleware expects more than 1 concurrent call, got %d\",\n\t\t\tremote.ParentEndpointMethod, remote.ParentEndpoint, remote.URLPattern, remote.ConcurrentCalls))\n\t\treturn nil\n\t}\n\tserviceTimeout := time.Duration(75*remote.Timeout.Nanoseconds()/100) * time.Nanosecond\n\n\treturn func(next ...Proxy) Proxy {\n\t\tif len(next) > 1 {\n\t\t\tlogger.Fatal(fmt.Sprintf(\"too many proxies for this %s %s -> %s proxy middleware: NewConcurrentMiddleware only accepts 1 proxy, got %d\",\n\t\t\t\tremote.ParentEndpointMethod, remote.ParentEndpoint, remote.URLPattern, len(next)))\n\t\t\treturn nil\n\t\t}\n\n\t\treturn func(ctx context.Context, request *Request) (*Response, error) {\n\t\t\tlocalCtx, cancel := context.WithTimeout(ctx, serviceTimeout)\n\n\t\t\tresults := make(chan *Response, remote.ConcurrentCalls)\n\t\t\tfailed := make(chan error, remote.ConcurrentCalls)\n\n\t\t\tfor i := 0; i < remote.ConcurrentCalls; i++ {\n\t\t\t\tif i < remote.ConcurrentCalls-1 {\n\t\t\t\t\tgo processConcurrentCall(localCtx, next[0], CloneRequest(request), results, failed)\n\t\t\t\t} else {\n\t\t\t\t\tgo processConcurrentCall(localCtx, next[0], request, results, failed)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar response *Response\n\t\t\tvar err error\n\n\t\t\tfor i := 0; i < remote.ConcurrentCalls; i++ {\n\t\t\t\tselect {\n\t\t\t\tcase response = <-results:\n\t\t\t\t\tif response != nil && response.IsComplete {\n\t\t\t\t\t\tcancel()\n\t\t\t\t\t\treturn response, nil\n\t\t\t\t\t}\n\t\t\t\tcase err = <-failed:\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t}\n\t\t\t}\n\t\t\tcancel()\n\t\t\treturn response, err\n\t\t}\n\t}\n}\n\n// NewConcurrentMiddlewareWithLogger creates a proxy middleware that enables sending several requests concurrently.\n// Is recommended to use the version with a logger param.\nfunc NewConcurrentMiddleware(remote *config.Backend) Middleware {\n\treturn NewConcurrentMiddlewareWithLogger(logging.NoOp, remote)\n}\n\nvar errNullResult = errors.New(\"invalid response\")\n\nfunc processConcurrentCall(ctx context.Context, next Proxy, request *Request, out chan<- *Response, failed chan<- error) {\n\tlocalCtx, cancel := context.WithCancel(ctx)\n\n\tresult, err := next(localCtx, request)\n\tif err != nil {\n\t\tfailed <- err\n\t\tcancel()\n\t\treturn\n\t}\n\tif result == nil {\n\t\tfailed <- errNullResult\n\t\tcancel()\n\t\treturn\n\t}\n\tselect {\n\tcase out <- result:\n\tcase <-ctx.Done():\n\t\tfailed <- ctx.Err()\n\t}\n\tcancel()\n}\n"
  },
  {
    "path": "proxy/concurrent_benchmark_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n)\n\nfunc BenchmarkNewConcurrentMiddleware_singleNext(b *testing.B) {\n\tbackend := config.Backend{\n\t\tConcurrentCalls: 3,\n\t\tTimeout:         time.Duration(100) * time.Millisecond,\n\t}\n\tproxy := NewConcurrentMiddleware(&backend)(dummyProxy(&Response{}))\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor i := 0; i < b.N; i++ {\n\t\tproxy(context.Background(), &Request{})\n\t}\n}\n"
  },
  {
    "path": "proxy/concurrent_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n)\n\nfunc TestNewConcurrentMiddleware_ok(t *testing.T) {\n\ttimeout := 700\n\ttotalCalls := 3\n\tbackend := config.Backend{\n\t\tConcurrentCalls: totalCalls,\n\t\tTimeout:         time.Duration(timeout) * time.Millisecond,\n\t}\n\texpected := Response{\n\t\tData:       map[string]interface{}{\"supu\": 42, \"tupu\": true, \"foo\": \"bar\"},\n\t\tIsComplete: true,\n\t}\n\tmw := NewConcurrentMiddleware(&backend)\n\tmustEnd := time.After(time.Duration(timeout) * time.Millisecond)\n\tresult, err := mw(dummyProxy(&expected))(context.Background(), &Request{})\n\tif err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t}\n\tif result == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tif !result.IsComplete {\n\t\tt.Errorf(\"The proxy returned an incomplete result: %v\\n\", result)\n\t}\n\tif v, ok := result.Data[\"supu\"]; !ok || v.(int) != 42 {\n\t\tt.Errorf(\"The proxy returned an unexpected result: %v\\n\", result)\n\t}\n\tif v, ok := result.Data[\"tupu\"]; !ok || !v.(bool) {\n\t\tt.Errorf(\"The proxy returned an unexpected result: %v\\n\", result)\n\t}\n\tif v, ok := result.Data[\"foo\"]; !ok || v.(string) != \"bar\" {\n\t\tt.Errorf(\"The proxy returned an unexpected result: %v\\n\", result)\n\t}\n}\n\nfunc TestNewConcurrentMiddleware_okAfterKo(t *testing.T) {\n\ttimeout := 700\n\ttotalCalls := 3\n\tbackend := config.Backend{\n\t\tConcurrentCalls: totalCalls,\n\t\tTimeout:         time.Duration(timeout) * time.Millisecond,\n\t}\n\texpected := Response{\n\t\tData:       map[string]interface{}{\"supu\": 42, \"tupu\": true, \"foo\": \"bar\"},\n\t\tIsComplete: true,\n\t}\n\tmw := NewConcurrentMiddleware(&backend)\n\n\tcalls := uint64(0)\n\tmock := func(_ context.Context, _ *Request) (*Response, error) {\n\t\ttotal := atomic.AddUint64(&calls, 1)\n\t\tif total%2 == 0 {\n\t\t\treturn &expected, nil\n\t\t}\n\t\treturn nil, nil\n\t}\n\tmustEnd := time.After(time.Duration(timeout) * time.Millisecond)\n\tresult, err := mw(mock)(context.Background(), &Request{})\n\n\tif result == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tif err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t}\n\tif !result.IsComplete {\n\t\tt.Errorf(\"The proxy returned an incomplete result: %v\\n\", result)\n\t}\n\tif v, ok := result.Data[\"supu\"]; !ok || v.(int) != 42 {\n\t\tt.Errorf(\"The proxy returned an unexpected result: %v\\n\", result)\n\t}\n\tif v, ok := result.Data[\"tupu\"]; !ok || !v.(bool) {\n\t\tt.Errorf(\"The proxy returned an unexpected result: %v\\n\", result)\n\t}\n\tif v, ok := result.Data[\"foo\"]; !ok || v.(string) != \"bar\" {\n\t\tt.Errorf(\"The proxy returned an unexpected result: %v\\n\", result)\n\t}\n}\n\nfunc TestNewConcurrentMiddleware_timeout(t *testing.T) {\n\ttimeout := 100\n\ttotalCalls := 3\n\tbackend := config.Backend{\n\t\tConcurrentCalls: totalCalls,\n\t\tTimeout:         time.Duration(timeout) * time.Millisecond,\n\t}\n\tmw := NewConcurrentMiddleware(&backend)\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\n\tresponse, err := mw(delayedProxy(t, time.Duration(5*timeout)*time.Millisecond, &Response{}))(context.Background(), &Request{})\n\tif err == nil || err.Error() != \"context deadline exceeded\" {\n\t\tt.Errorf(\"The middleware didn't propagate a timeout error: %s\\n\", err)\n\t}\n\tif response != nil {\n\t\tt.Errorf(\"We weren't expecting a response but we got one: %v\\n\", response)\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response at this point in time!\\n\")\n\t\treturn\n\tdefault:\n\t}\n}\n"
  },
  {
    "path": "proxy/factory.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/sd\"\n)\n\n// Factory creates proxies based on the received endpoint configuration.\n//\n// Both, factories and backend factories, create proxies but factories are designed as a stack makers\n// because they are intended to generate the complete proxy stack for a given frontend endpoint\n// the app would expose and they could wrap several proxies provided by a backend factory\ntype Factory interface {\n\tNew(cfg *config.EndpointConfig) (Proxy, error)\n}\n\n// FactoryFunc type is an adapter to allow the use of ordinary functions as proxy factories.\n// If f is a function with the appropriate signature, FactoryFunc(f) is a Factory that calls f.\ntype FactoryFunc func(*config.EndpointConfig) (Proxy, error)\n\n// New implements the Factory interface\nfunc (f FactoryFunc) New(cfg *config.EndpointConfig) (Proxy, error) { return f(cfg) }\n\n// DefaultFactory returns a default http proxy factory with the injected logger\nfunc DefaultFactory(logger logging.Logger) Factory {\n\treturn NewDefaultFactory(httpProxy, logger)\n}\n\n// DefaultFactoryWithSubscriber returns a default proxy factory with the injected logger and subscriber factory\nfunc DefaultFactoryWithSubscriber(logger logging.Logger, sF sd.SubscriberFactory) Factory {\n\treturn NewDefaultFactoryWithSubscriber(httpProxy, logger, sF)\n}\n\n// NewDefaultFactory returns a default proxy factory with the injected proxy builder and logger\nfunc NewDefaultFactory(backendFactory BackendFactory, logger logging.Logger) Factory {\n\tsf := func(remote *config.Backend) sd.Subscriber {\n\t\treturn sd.GetRegister().Get(remote.SD)(remote)\n\t}\n\treturn NewDefaultFactoryWithSubscriber(backendFactory, logger, sf)\n}\n\n// NewDefaultFactoryWithSubscriber returns a default proxy factory with the injected proxy builder,\n// logger and subscriber factory\nfunc NewDefaultFactoryWithSubscriber(backendFactory BackendFactory, logger logging.Logger, sF sd.SubscriberFactory) Factory {\n\treturn defaultFactory{backendFactory, logger, sF}\n}\n\ntype defaultFactory struct {\n\tbackendFactory    BackendFactory\n\tlogger            logging.Logger\n\tsubscriberFactory sd.SubscriberFactory\n}\n\n// New implements the Factory interface\nfunc (pf defaultFactory) New(cfg *config.EndpointConfig) (p Proxy, err error) {\n\tswitch len(cfg.Backend) {\n\tcase 0:\n\t\terr = ErrNoBackends\n\tcase 1:\n\t\tp, err = pf.newSingle(cfg)\n\tdefault:\n\t\tp, err = pf.newMulti(cfg)\n\t}\n\tif err != nil {\n\t\treturn\n\t}\n\n\tp = NewPluginMiddleware(pf.logger, cfg)(p)\n\tp = NewStaticMiddleware(pf.logger, cfg)(p)\n\treturn\n}\n\nfunc (pf defaultFactory) newMulti(cfg *config.EndpointConfig) (p Proxy, err error) {\n\tbackendProxy := make([]Proxy, len(cfg.Backend))\n\tfor i, backend := range cfg.Backend {\n\t\tbackendProxy[i] = pf.newStack(backend)\n\t}\n\tp = NewMergeDataMiddleware(pf.logger, cfg)(backendProxy...)\n\tp = NewFlatmapMiddleware(pf.logger, cfg)(p)\n\treturn\n}\n\nfunc (pf defaultFactory) newSingle(cfg *config.EndpointConfig) (Proxy, error) {\n\treturn pf.newStack(cfg.Backend[0]), nil\n}\n\nfunc (pf defaultFactory) newStack(backend *config.Backend) (p Proxy) {\n\tp = pf.backendFactory(backend)\n\tp = NewBackendPluginMiddleware(pf.logger, backend)(p)\n\tp = NewGraphQLMiddleware(pf.logger, backend)(p)\n\tp = NewFilterHeadersMiddleware(pf.logger, backend)(p)\n\tp = NewLoadBalancedMiddlewareWithSubscriberAndLogger(pf.logger, pf.subscriberFactory(backend))(p)\n\tif backend.ConcurrentCalls > 1 {\n\t\tp = NewConcurrentMiddlewareWithLogger(pf.logger, backend)(p)\n\t}\n\tp = NewRequestBuilderMiddlewareWithLogger(pf.logger, backend)(p)\n\t// we need to filter the input query strings before the request is constructed\n\t// so the query strings are properly added to the URL:\n\tp = NewFilterQueryStringsMiddleware(pf.logger, backend)(p)\n\treturn\n}\n"
  },
  {
    "path": "proxy/factory_test.go",
    "content": "//go:build integration || !race\n// +build integration !race\n\n// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/sd\"\n)\n\nfunc TestFactoryFunc(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tfactory := FactoryFunc(func(cfg *config.EndpointConfig) (Proxy, error) { return DefaultFactory(logger).New(cfg) })\n\n\tif _, err := factory.New(&config.EndpointConfig{}); err != ErrNoBackends {\n\t\tt.Errorf(\"Expecting ErrNoBackends. Got: %v\\n\", err)\n\t}\n}\n\nfunc TestDefaultFactoryWithSubscriber(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tfactory := DefaultFactoryWithSubscriber(logger, sd.FixedSubscriberFactory)\n\n\tif _, err := factory.New(&config.EndpointConfig{}); err != ErrNoBackends {\n\t\tt.Errorf(\"Expecting ErrNoBackends. Got: %v\\n\", err)\n\t}\n}\n\nfunc TestDefaultFactory_noBackends(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\tfactory := DefaultFactory(logger)\n\n\tif _, err := factory.New(&config.EndpointConfig{}); err != ErrNoBackends {\n\t\tt.Errorf(\"Expecting ErrNoBackends. Got: %v\\n\", err)\n\t}\n}\n\nfunc TestNewDefaultFactory_ok(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\texpectedResponse := Response{\n\t\tIsComplete: true,\n\t\tData:       map[string]interface{}{\"foo\": \"bar\"},\n\t}\n\texpectedMethod := \"SOME\"\n\texpectedHost := \"http://example.com/\"\n\texpectedPath := \"/foo\"\n\texpectedURL := expectedHost + strings.TrimLeft(expectedPath, \"/\")\n\n\tURL, err := url.Parse(expectedHost)\n\tif err != nil {\n\t\tt.Errorf(\"building the sample url: %s\\n\", err.Error())\n\t}\n\n\trequest := Request{\n\t\tMethod: expectedMethod,\n\t\tPath:   expectedPath,\n\t\tURL:    URL,\n\t\tBody:   newDummyReadCloser(\"\"),\n\t}\n\n\tassertion := func(ctx context.Context, request *Request) (*Response, error) {\n\t\tif request.URL.String() != expectedURL {\n\t\t\tt.Errorf(\"The middlewares did not update the request URL! want [%s], have [%s]\\n\", expectedURL, request.URL)\n\t\t}\n\t\treturn &expectedResponse, nil\n\t}\n\tfactory := NewDefaultFactory(func(_ *config.Backend) Proxy { return assertion }, logger)\n\n\tbackend := config.Backend{\n\t\tURLPattern: expectedPath,\n\t\tMethod:     expectedMethod,\n\t}\n\tendpointSingle := config.EndpointConfig{\n\t\tBackend: []*config.Backend{&backend},\n\t}\n\tendpointMulti := config.EndpointConfig{\n\t\tBackend:         []*config.Backend{&backend, &backend},\n\t\tConcurrentCalls: 3,\n\t}\n\tserviceConfig := config.ServiceConfig{\n\t\tVersion:   config.ConfigVersion,\n\t\tEndpoints: []*config.EndpointConfig{&endpointSingle, &endpointMulti},\n\t\tTimeout:   100 * time.Millisecond,\n\t\tHost:      []string{expectedHost},\n\t}\n\tif err := serviceConfig.Init(); err != nil {\n\t\tt.Errorf(\"Error during the config init: %s\\n\", err.Error())\n\t}\n\n\tproxyMulti, err := factory.New(&endpointMulti)\n\tif err != nil {\n\t\tt.Errorf(\"The factory returned an unexpected error: %s\\n\", err.Error())\n\t}\n\n\tresponse, err := proxyMulti(context.Background(), &request)\n\tif err != nil {\n\t\tt.Errorf(\"The proxy middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif !response.IsComplete || len(response.Data) != len(expectedResponse.Data) {\n\t\tt.Errorf(\"The proxy middleware propagated an unexpected error: %v\\n\", response)\n\t}\n\n\tproxySingle, err := factory.New(&endpointSingle)\n\tif err != nil {\n\t\tt.Errorf(\"The factory returned an unexpected error: %s\\n\", err.Error())\n\t}\n\n\tresponse, err = proxySingle(context.Background(), &request)\n\tif err != nil {\n\t\tt.Errorf(\"The proxy middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif !response.IsComplete || len(response.Data) != len(expectedResponse.Data) {\n\t\tt.Errorf(\"The proxy middleware propagated an unexpected error: %v\\n\", response)\n\t}\n}\n"
  },
  {
    "path": "proxy/formatter.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/krakend/flatmap/tree\"\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\n// EntityFormatter formats the response data\ntype EntityFormatter interface {\n\tFormat(Response) Response\n}\n\n// EntityFormatterFunc holds the formatter function\ntype EntityFormatterFunc func(Response) Response\n\n// Format implements the EntityFormatter interface\nfunc (e EntityFormatterFunc) Format(entity Response) Response { return e(entity) }\n\ntype propertyFilter func(*Response)\n\ntype entityFormatter struct {\n\tTarget         string\n\tPrefix         string\n\tPropertyFilter propertyFilter\n\tMapping        map[string]string\n}\n\n// NewEntityFormatter creates an entity formatter with the received backend definition\nfunc NewEntityFormatter(remote *config.Backend) EntityFormatter {\n\tif ef := newFlatmapFormatter(remote.ExtraConfig, remote.Target, remote.Group); ef != nil {\n\t\treturn ef\n\t}\n\n\tvar propertyFilter propertyFilter\n\tif len(remote.AllowList) > 0 {\n\t\tpropertyFilter = newAllowlistingFilter(remote.AllowList)\n\t} else {\n\t\tpropertyFilter = newDenylistingFilter(remote.DenyList)\n\t}\n\tsanitizedMappings := make(map[string]string, len(remote.Mapping))\n\tfor i, m := range remote.Mapping {\n\t\tv := strings.Split(m, \".\")\n\t\tsanitizedMappings[i] = v[0]\n\t}\n\treturn entityFormatter{\n\t\tTarget:         remote.Target,\n\t\tPrefix:         remote.Group,\n\t\tPropertyFilter: propertyFilter,\n\t\tMapping:        sanitizedMappings,\n\t}\n}\n\n// Format implements the EntityFormatter interface\nfunc (e entityFormatter) Format(entity Response) Response {\n\tif e.Target != \"\" {\n\t\textractTarget(e.Target, &entity)\n\t}\n\tif len(entity.Data) > 0 {\n\t\te.PropertyFilter(&entity)\n\t}\n\tif len(entity.Data) > 0 {\n\t\tfor formerKey, newKey := range e.Mapping {\n\t\t\tif v, ok := entity.Data[formerKey]; ok {\n\t\t\t\tentity.Data[newKey] = v\n\t\t\t\tdelete(entity.Data, formerKey)\n\t\t\t}\n\t\t}\n\t}\n\tif e.Prefix != \"\" {\n\t\tentity.Data = map[string]interface{}{e.Prefix: entity.Data}\n\t}\n\treturn entity\n}\n\nfunc extractTarget(target string, entity *Response) {\n\tfor _, part := range strings.Split(target, \".\") {\n\t\tif tmp, ok := entity.Data[part]; ok {\n\t\t\tentity.Data, ok = tmp.(map[string]interface{})\n\t\t\tif !ok {\n\t\t\t\tentity.Data = map[string]interface{}{}\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\tentity.Data = map[string]interface{}{}\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc AllowlistPrune(wlDict, inDict map[string]interface{}) bool {\n\tcanDelete := true\n\tvar deleteSibling bool\n\tfor k, v := range inDict {\n\t\tdeleteSibling = true\n\t\tif subWl, ok := wlDict[k]; ok {\n\t\t\tif subWlDict, okk := subWl.(map[string]interface{}); okk {\n\t\t\t\tif subInDict, isDict := v.(map[string]interface{}); isDict && !AllowlistPrune(subWlDict, subInDict) {\n\t\t\t\t\tdeleteSibling = false\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Allowlist leaf, maintain this branch\n\t\t\t\tdeleteSibling = false\n\t\t\t}\n\t\t}\n\t\tif deleteSibling {\n\t\t\tdelete(inDict, k)\n\t\t} else {\n\t\t\tcanDelete = false\n\t\t}\n\t}\n\treturn canDelete\n}\n\nfunc newAllowlistingFilter(Allowlist []string) propertyFilter {\n\twlDict := make(map[string]interface{})\n\tfor _, k := range Allowlist {\n\t\twlFields := strings.Split(k, \".\")\n\t\td := buildDictPath(wlDict, wlFields[:len(wlFields)-1])\n\t\td[wlFields[len(wlFields)-1]] = true\n\t}\n\n\treturn func(entity *Response) {\n\t\tif AllowlistPrune(wlDict, entity.Data) {\n\t\t\tfor k := range entity.Data {\n\t\t\t\tdelete(entity.Data, k)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc buildDictPath(accumulator map[string]interface{}, fields []string) map[string]interface{} {\n\tvar ok bool\n\tvar c map[string]interface{}\n\tvar fIdx int\n\tfEnd := len(fields)\n\tp := accumulator\n\tfor fIdx = 0; fIdx < fEnd; fIdx++ {\n\t\tif c, ok = p[fields[fIdx]].(map[string]interface{}); !ok {\n\t\t\tbreak\n\t\t}\n\t\tp = c\n\t}\n\tfor ; fIdx < fEnd; fIdx++ {\n\t\tc = make(map[string]interface{})\n\t\tp[fields[fIdx]] = c\n\t\tp = c\n\t}\n\treturn p\n}\n\nfunc buildDenyTree(path []string, tree map[string]interface{}) {\n\tif len(path) == 0 {\n\t\treturn\n\t}\n\tn := path[0]\n\tif len(path) == 1 {\n\t\t// this is the node to be deleted, so, any other child\n\t\t// that is under this node, does not need to be visited:\n\t\t// we \"delete\" any descendant from this node\n\t\ttree[n] = nil\n\t\treturn\n\t}\n\n\tif k, ok := tree[n]; ok {\n\t\tif k == nil {\n\t\t\t// all this child should be deleted, so, no matter\n\t\t\t// if the entry says to delete some extra child..\n\t\t\t// everything will be deleted\n\t\t\treturn\n\t\t}\n\t\tchildTree, ok := k.(map[string]interface{})\n\t\tif !ok {\n\t\t\t// this should never happen if this algorithm is correct\n\t\t\ttree[n] = nil\n\t\t\treturn\n\t\t}\n\t\tbuildDenyTree(path[1:], childTree)\n\t\treturn\n\t}\n\n\t// it the key does not exist, we need to keep building the children,\n\t// and at this point we know that path is at least len = 2, and that\n\t// tree[n] does not exist\n\tchildTree := make(map[string]interface{}, 1)\n\ttree[n] = childTree\n\tbuildDenyTree(path[1:], childTree)\n}\n\nfunc recDelete(ref map[string]interface{}, v interface{}) {\n\tm, ok := v.(map[string]interface{})\n\tif !ok || m == nil {\n\t\treturn\n\t}\n\n\tfor rk, rv := range ref {\n\t\tdv, dok := m[rk]\n\t\tif !dok {\n\t\t\tcontinue\n\t\t}\n\t\tif rv == nil {\n\t\t\tdelete(m, rk)\n\t\t\tcontinue\n\t\t}\n\t\trecDelete(rv.(map[string]interface{}), dv)\n\t}\n}\n\nfunc newDenylistingFilter(blacklist []string) propertyFilter {\n\tbl := make(map[string]interface{}, len(blacklist))\n\tfor _, key := range blacklist {\n\t\tkeys := strings.Split(key, \".\")\n\t\tbuildDenyTree(keys, bl)\n\t}\n\n\treturn func(entity *Response) {\n\t\trecDelete(bl, entity.Data)\n\t}\n}\n\nconst flatmapKey = \"flatmap_filter\"\n\ntype flatmapFormatter struct {\n\tTarget string\n\tPrefix string\n\tOps    []flatmapOp\n}\n\ntype flatmapOp struct {\n\tType string\n\tArgs [][]string\n}\n\n// Format implements the EntityFormatter interface\nfunc (e flatmapFormatter) Format(entity Response) Response {\n\tif e.Target != \"\" {\n\t\textractTarget(e.Target, &entity)\n\t}\n\n\te.processOps(&entity)\n\n\tif e.Prefix != \"\" {\n\t\tentity.Data = map[string]interface{}{e.Prefix: entity.Data}\n\t}\n\treturn entity\n}\n\nfunc (e flatmapFormatter) processOps(entity *Response) {\n\tflatten, err := tree.New(entity.Data)\n\tif err != nil {\n\t\treturn\n\t}\n\tfor _, op := range e.Ops {\n\t\tswitch op.Type {\n\t\tcase \"move\":\n\t\t\tflatten.Move(op.Args[0], op.Args[1])\n\t\tcase \"append\":\n\t\t\tflatten.Append(op.Args[0], op.Args[1])\n\t\tcase \"del\":\n\t\t\tfor _, k := range op.Args {\n\t\t\t\tflatten.Del(k)\n\t\t\t}\n\t\tdefault:\n\t\t}\n\t}\n\n\tentity.Data, _ = flatten.Get([]string{}).(map[string]interface{})\n}\n\nfunc newFlatmapFormatter(cfg config.ExtraConfig, target, group string) *flatmapFormatter {\n\tif v, ok := cfg[Namespace]; ok {\n\t\tif e, ok := v.(map[string]interface{}); ok {\n\t\t\tif vs, ok := e[flatmapKey].([]interface{}); ok {\n\t\t\t\tif len(vs) == 0 {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tvar ops []flatmapOp\n\t\t\t\tfor _, v := range vs {\n\t\t\t\t\tm, ok := v.(map[string]interface{})\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\top := flatmapOp{}\n\t\t\t\t\tif t, ok := m[\"type\"].(string); ok {\n\t\t\t\t\t\top.Type = t\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif args, ok := m[\"args\"].([]interface{}); ok {\n\t\t\t\t\t\top.Args = make([][]string, len(args))\n\t\t\t\t\t\tfor k, arg := range args {\n\t\t\t\t\t\t\tif t, ok := arg.(string); ok {\n\t\t\t\t\t\t\t\top.Args[k] = strings.Split(t, \".\")\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tops = append(ops, op)\n\t\t\t\t}\n\t\t\t\tif len(ops) == 0 {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn &flatmapFormatter{\n\t\t\t\t\tTarget: target,\n\t\t\t\t\tPrefix: group,\n\t\t\t\t\tOps:    ops,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// NewFlatmapMiddleware creates a proxy middleware that enables applying flatmap operations to the proxy response\nfunc NewFlatmapMiddleware(logger logging.Logger, cfg *config.EndpointConfig) Middleware {\n\tformatter := newFlatmapFormatter(cfg.ExtraConfig, \"\", \"\")\n\treturn func(next ...Proxy) Proxy {\n\t\tif len(next) > 1 {\n\t\t\tlogger.Fatal(\"too many proxies for this proxy middleware: NewFlatmapMiddleware only accepts 1 proxy, got %d\", len(next))\n\t\t\treturn nil\n\t\t}\n\n\t\tif formatter == nil {\n\t\t\treturn next[0]\n\t\t}\n\n\t\tlogger.Debug(\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"[ENDPOINT: %s][Flatmap] Adding flatmap manipulator with %d operations\",\n\t\t\t\tcfg.Endpoint,\n\t\t\t\tlen(formatter.Ops),\n\t\t\t),\n\t\t)\n\n\t\treturn func(ctx context.Context, request *Request) (*Response, error) {\n\t\t\tresp, err := next[0](ctx, request)\n\t\t\tif err != nil {\n\t\t\t\treturn resp, err\n\t\t\t}\n\t\t\tr := formatter.Format(*resp)\n\t\t\treturn &r, nil\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "proxy/formatter_benchmark_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n)\n\nfunc BenchmarkEntityFormatter_allowFilter(b *testing.B) {\n\tdata := map[string]interface{}{\n\t\t\"supu\": 42,\n\t\t\"tupu\": false,\n\t\t\"foo\":  \"bar\",\n\t}\n\n\tfor _, extraFields := range []int{0, 5, 10, 15, 20, 25} {\n\t\tsampleData := data\n\t\tfor i := 0; i < extraFields; i++ {\n\t\t\tsampleData[fmt.Sprintf(\"%d\", i)] = i\n\t\t}\n\t\tfor _, testCase := range [][]string{\n\t\t\t{},\n\t\t\t{\"supu\"},\n\t\t\t{\"supu\", \"tupu\"},\n\t\t\t{\"supu\", \"tupu\", \"foo\"},\n\t\t\t{\"supu\", \"tupu\", \"foo\", \"unknown\"},\n\t\t} {\n\t\t\tsample := Response{\n\t\t\t\tData:       sampleData,\n\t\t\t\tIsComplete: true,\n\t\t\t}\n\t\t\tb.Run(fmt.Sprintf(\"with %d elements with %d extra fields\", len(testCase), extraFields), func(b *testing.B) {\n\t\t\t\tf := NewEntityFormatter(&config.Backend{AllowList: testCase})\n\t\t\t\tb.ResetTimer()\n\t\t\t\tb.ReportAllocs()\n\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\tf.Format(sample)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n\n}\n\nfunc benchmarkDeepChilds(depth, extraSiblings int) map[string]interface{} {\n\tdata := make(map[string]interface{}, extraSiblings+1)\n\tfor i := 0; i < extraSiblings; i++ {\n\t\tdata[fmt.Sprintf(\"extra%d\", i)] = \"sibling_value\"\n\t}\n\tif depth > 0 {\n\t\tdata[fmt.Sprintf(\"child%d\", depth)] = benchmarkDeepChilds(depth-1, extraSiblings)\n\t} else {\n\t\tdata[\"child0\"] = 1\n\t}\n\treturn data\n}\n\nfunc benchmarkDeepStructure(numTargets, targetDepth, extraFields, extraSiblings int) (map[string]interface{}, []string) {\n\tdata := make(map[string]interface{}, numTargets+extraFields)\n\ttargetKeys := make([]string, numTargets)\n\tfor i := 0; i < numTargets; i++ {\n\t\tdata[fmt.Sprintf(\"target%d\", i)] = benchmarkDeepChilds(targetDepth-1, extraSiblings)\n\t}\n\tfor j := 0; j < extraFields; j++ {\n\t\tdata[fmt.Sprintf(\"extra%d\", j)] = benchmarkDeepChilds(targetDepth-1, extraSiblings)\n\t}\n\t// create the target list\n\tfor i := 0; i < numTargets; i++ {\n\t\tvar buffer bytes.Buffer\n\t\tbuffer.WriteString(fmt.Sprintf(\"target%d\", i))\n\t\tfor j := targetDepth - 1; j >= 0; j-- {\n\t\t\tbuffer.WriteString(fmt.Sprintf(\".child%d\", j))\n\t\t}\n\t\ttargetKeys[i] = buffer.String()\n\t}\n\treturn data, targetKeys\n}\n\nfunc BenchmarkEntityFormatter_deepAllowFilter(b *testing.B) {\n\tnumTargets := []int{0, 1, 2, 5, 10}\n\tdepths := []int{1, 3, 7}\n\tfor _, nTargets := range numTargets {\n\t\tfor _, depth := range depths {\n\t\t\textraFields := nTargets + depth*2\n\t\t\textraSiblings := nTargets\n\t\t\tdata, allow := benchmarkDeepStructure(nTargets, depth, extraFields, extraSiblings)\n\t\t\tsample := Response{\n\t\t\t\tData:       data,\n\t\t\t\tIsComplete: true,\n\t\t\t}\n\t\t\tf := NewEntityFormatter(&config.Backend{AllowList: allow})\n\t\t\tb.Run(fmt.Sprintf(\"numTargets:%d,depth:%d,extraFields:%d,extraSiblings:%d\", nTargets, depth, extraFields, extraSiblings), func(b *testing.B) {\n\t\t\t\tb.ReportAllocs()\n\t\t\t\tb.ResetTimer()\n\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\tf.Format(sample)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc BenchmarkEntityFormatter_denyFilter(b *testing.B) {\n\tdata := map[string]interface{}{\n\t\t\"supu\": 42,\n\t\t\"tupu\": false,\n\t\t\"foo\":  \"bar\",\n\t}\n\n\tfor _, extraFields := range []int{0, 5, 10, 15, 20, 25} {\n\t\tsampleData := data\n\t\tfor i := 0; i < extraFields; i++ {\n\t\t\tsampleData[fmt.Sprintf(\"%d\", i)] = i\n\t\t}\n\t\tfor _, testCase := range [][]string{\n\t\t\t{},\n\t\t\t{\"supu\"},\n\t\t\t{\"supu\", \"tupu\"},\n\t\t\t{\"supu\", \"tupu\", \"foo\"},\n\t\t\t{\"supu\", \"tupu\", \"foo\", \"unknown\"},\n\t\t} {\n\t\t\tsample := Response{\n\t\t\t\tData:       sampleData,\n\t\t\t\tIsComplete: true,\n\t\t\t}\n\t\t\tb.Run(fmt.Sprintf(\"with %d elements with %d extra fields\", len(testCase), extraFields), func(b *testing.B) {\n\t\t\t\tf := NewEntityFormatter(&config.Backend{DenyList: testCase})\n\t\t\t\tb.ResetTimer()\n\t\t\t\tb.ReportAllocs()\n\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\tf.Format(sample)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc BenchmarkEntityFormatter_grouping(b *testing.B) {\n\tpreffix := \"group1\"\n\tfor _, extraFields := range []int{0, 5, 10, 15, 20, 25} {\n\t\tsampleData := make(map[string]interface{}, extraFields)\n\t\tfor i := 0; i < extraFields; i++ {\n\t\t\tsampleData[fmt.Sprintf(\"%d\", i)] = i\n\t\t}\n\t\tsample := Response{\n\t\t\tData:       sampleData,\n\t\t\tIsComplete: true,\n\t\t}\n\t\tb.Run(fmt.Sprintf(\"with %d elements\", extraFields), func(b *testing.B) {\n\t\t\tf := NewEntityFormatter(&config.Backend{Group: preffix})\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\tf.Format(sample)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkEntityFormatter_mapping(b *testing.B) {\n\tfor _, extraFields := range []int{0, 5, 10, 15, 20, 25} {\n\t\tsampleData := make(map[string]interface{}, extraFields)\n\t\tfor i := 0; i < extraFields; i++ {\n\t\t\tsampleData[fmt.Sprintf(\"%d\", i)] = i\n\t\t}\n\t\tfor _, testCase := range []map[string]string{\n\t\t\t{},\n\t\t\t{\"1\": \"supu\"},\n\t\t\t{\"1\": \"supu\", \"2\": \"tupu\"},\n\t\t\t{\"1\": \"supu\", \"2\": \"tupu\", \"3\": \"foo\"},\n\t\t\t{\"1\": \"supu\", \"2\": \"tupu\", \"3\": \"foo\", \"4\": \"bar\"},\n\t\t\t{\"1\": \"supu\", \"2\": \"tupu\", \"3\": \"foo\", \"4\": \"bar\", \"5\": \"a\"},\n\t\t} {\n\t\t\tsample := Response{\n\t\t\t\tData:       sampleData,\n\t\t\t\tIsComplete: true,\n\t\t\t}\n\t\t\tb.Run(fmt.Sprintf(\"with %d elements with %d extra fields\", len(testCase), extraFields), func(b *testing.B) {\n\t\t\t\tf := NewEntityFormatter(&config.Backend{Mapping: testCase})\n\t\t\t\tb.ResetTimer()\n\t\t\t\tb.ReportAllocs()\n\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\tf.Format(sample)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc BenchmarkEntityFormatter_flatmapAlt(b *testing.B) {\n\tf := NewEntityFormatter(&config.Backend{\n\t\tTarget: \"content\",\n\t\tGroup:  \"group\",\n\t\tExtraConfig: config.ExtraConfig{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\tflatmapKey: []interface{}{\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"del\",\n\t\t\t\t\t\t\"args\": []interface{}{\"c\"},\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"move\",\n\t\t\t\t\t\t\"args\": []interface{}{\"supu\", \"SUPUUUUU\"},\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"move\",\n\t\t\t\t\t\t\"args\": []interface{}{\"a.b\", \"a.BOOOOO\"},\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"del\",\n\t\t\t\t\t\t\"args\": []interface{}{\"collection.*.b\"},\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"del\",\n\t\t\t\t\t\t\"args\": []interface{}{\"collection.*.d\"},\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"del\",\n\t\t\t\t\t\t\"args\": []interface{}{\"collection.*.e\"},\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"move\",\n\t\t\t\t\t\t\"args\": []interface{}{\"collection.*.c\", \"collection.*.x\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\n\tfor _, size := range []int{1, 2, 5, 10, 20, 50, 100, 500} {\n\t\tb.Run(strconv.Itoa(size), func(b *testing.B) {\n\t\t\tsub := map[string]interface{}{\n\t\t\t\t\"b\": true,\n\t\t\t\t\"c\": 42,\n\t\t\t\t\"d\": \"tupu\",\n\t\t\t\t\"e\": []interface{}{1, 2, 3, 4},\n\t\t\t}\n\t\t\tsample := Response{\n\t\t\t\tData: map[string]interface{}{\n\t\t\t\t\t\"content\": map[string]interface{}{\n\t\t\t\t\t\t\"supu\":       42,\n\t\t\t\t\t\t\"tupu\":       false,\n\t\t\t\t\t\t\"foo\":        \"bar\",\n\t\t\t\t\t\t\"a\":          sub,\n\t\t\t\t\t\t\"collection\": []interface{}{sub, sub, sub, sub},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tIsComplete: true,\n\t\t\t}\n\t\t\tvar subCol []interface{}\n\n\t\t\tfor i := 0; i < size; i++ {\n\t\t\t\tsubCol = append(subCol, i)\n\t\t\t}\n\t\t\tsub[\"e\"] = subCol\n\t\t\tvar sampleSubCol []interface{}\n\n\t\t\tfor i := 0; i < size; i++ {\n\t\t\t\tsampleSubCol = append(sampleSubCol, sub)\n\t\t\t}\n\t\t\tsample.Data[\"collection\"] = sampleSubCol\n\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\tf.Format(sample)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkEntityFormatter_flatmap(b *testing.B) {\n\tnumTargets := []int{0, 1, 2, 5, 10}\n\tdepths := []int{1, 3, 7}\n\tfor _, nTargets := range numTargets {\n\t\tfor _, depth := range depths {\n\t\t\textraFields := nTargets + depth*2\n\t\t\textraSiblings := nTargets\n\t\t\tdata, blacklist := benchmarkDeepStructure(nTargets, depth, extraFields, extraSiblings)\n\t\t\tsample := Response{\n\t\t\t\tData:       data,\n\t\t\t\tIsComplete: true,\n\t\t\t}\n\n\t\t\tvar cmds []interface{}\n\n\t\t\tfor _, path := range blacklist {\n\t\t\t\tcmds = append(cmds, map[string]interface{}{\n\t\t\t\t\t\"type\": \"del\",\n\t\t\t\t\t\"args\": []interface{}{path},\n\t\t\t\t})\n\t\t\t}\n\t\t\tf := NewEntityFormatter(&config.Backend{\n\t\t\t\tExtraConfig: config.ExtraConfig{\n\t\t\t\t\tNamespace: map[string]interface{}{\n\t\t\t\t\t\tflatmapKey: cmds,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\t\tb.Run(fmt.Sprintf(\"numTargets:%d,depth:%d,extraFields:%d,extraSiblings:%d\", nTargets, depth, extraFields, extraSiblings), func(b *testing.B) {\n\t\t\t\tb.ReportAllocs()\n\t\t\t\tb.ResetTimer()\n\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\tf.Format(sample)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "proxy/formatter_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nfunc TestEntityFormatterFunc(t *testing.T) {\n\texpected := Response{Data: map[string]interface{}{\"one\": 1}, IsComplete: true}\n\tf := func(_ Response) Response { return expected }\n\tformatter := EntityFormatterFunc(f)\n\tresult := formatter.Format(Response{})\n\tif result.Data[\"one\"].(int) != 1 {\n\t\tt.Error(\"unexpected result:\", result.Data)\n\t}\n\tif !result.IsComplete {\n\t\tt.Error(\"unexpected result:\", result)\n\t}\n}\n\nfunc TestEntityFormatter_newAllowFilter(t *testing.T) {\n\tsample := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"supu\": 42,\n\t\t\t\"tupu\": false,\n\t\t\t\"foo\":  \"bar\",\n\t\t\t\"a\": map[string]interface{}{\n\t\t\t\t\"b\": true,\n\t\t\t\t\"c\": 42,\n\t\t\t\t\"d\": \"tupu\",\n\t\t\t},\n\t\t},\n\t\tIsComplete: true,\n\t}\n\texpected := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"supu\": 42,\n\t\t\t\"a\": map[string]interface{}{\n\t\t\t\t\"b\": true,\n\t\t\t\t\"c\": 42,\n\t\t\t},\n\t\t},\n\t\tIsComplete: true,\n\t}\n\tf := NewEntityFormatter(&config.Backend{AllowList: []string{\"supu\", \"a.b\", \"a.c\", \"foo.unknown\"}})\n\tresult := f.Format(sample)\n\tif v, ok := result.Data[\"supu\"]; !ok || v != expected.Data[\"supu\"] {\n\t\tt.Errorf(\"The formatter returned an unexpected result for the field supu: %v\\n\", result)\n\t}\n\tv, ok := result.Data[\"a\"]\n\tif !ok {\n\t\tt.Errorf(\"The formatter returned an unexpected result for the fields a.b & a.c: %v\\n\", result)\n\t}\n\ttmp := v.(map[string]interface{})\n\tif b, okk := tmp[\"b\"]; !okk || !b.(bool) {\n\t\tt.Errorf(\"The formatter returned an unexpected result for the field a.b: %v\\n\", result)\n\t}\n\tif c, okk := tmp[\"c\"]; !okk || c.(int) != 42 {\n\t\tt.Errorf(\"The formatter returned an unexpected result for the field a.c: %v\\n\", result)\n\t}\n\tif len(tmp) != 2 {\n\t\tt.Errorf(\"The formatter returned an unexpected result size for the field a: %v\\n\", result)\n\t}\n\tif len(result.Data) != 2 || result.IsComplete != expected.IsComplete {\n\t\tt.Errorf(\"The formatter returned an unexpected result size: %v\\n\", result)\n\t}\n}\n\nfunc TestEntityFormatter_newAllowDeepFields(t *testing.T) {\n\tsample := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"id\": 42,\n\t\t\t\"tupu\": map[string]interface{}{\n\t\t\t\t\"muku\": map[string]interface{}{\n\t\t\t\t\t\"supu\": 1,\n\t\t\t\t\t\"muku\": 2,\n\t\t\t\t\t\"gutu\": map[string]interface{}{\n\t\t\t\t\t\t\"kugu\": 42,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"supu\": map[string]interface{}{\n\t\t\t\t\t\"supu\": 3,\n\t\t\t\t\t\"muku\": 4,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tIsComplete: true,\n\t}\n\texpectedSupuChild := 1\n\n\tvar ok bool\n\tf := NewEntityFormatter(&config.Backend{AllowList: []string{\"tupu.muku.supu\", \"tupu.muku.gutu.kugu\"}})\n\tres := f.Format(sample)\n\tvar tupu map[string]interface{}\n\tvar muku map[string]interface{}\n\tvar gutu map[string]interface{}\n\tvar kugu int\n\tvar supuChild int\n\tif tupu, ok = res.Data[\"tupu\"].(map[string]interface{}); !ok {\n\t\tt.Errorf(\"The formatter does not have field tupu\\n\")\n\t}\n\tif muku, ok = tupu[\"muku\"].(map[string]interface{}); !ok {\n\t\tt.Errorf(\"The formatter does not have field tupu.muku\\n\")\n\t}\n\tif supuChild, ok = muku[\"supu\"].(int); !ok || supuChild != expectedSupuChild {\n\t\tt.Errorf(\"The formatter does not have field tupu.muku.supu or wrong value\\n\")\n\t}\n\tif _, ok = tupu[\"supu\"].(map[string]interface{}); ok {\n\t\tt.Errorf(\"The formatter should have removed tupu.supu\\n\")\n\t}\n\tif _, ok = muku[\"muku\"]; ok {\n\t\tt.Errorf(\"The formatter should have removed tupu.muku.muku\\n\")\n\t}\n\tif gutu, ok = muku[\"gutu\"].(map[string]interface{}); !ok {\n\t\tt.Errorf(\"The formatter does not have field tupu.muku.gutu\\n\")\n\t}\n\tif kugu, ok = gutu[\"kugu\"].(int); !ok || kugu != 42 {\n\t\tt.Errorf(\"The formatter does not have field tupu.muku.gutu.kugu\\n\")\n\t}\n}\n\nfunc TestEntityFormatter_newDenyFilter(t *testing.T) {\n\tsample := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"supu\": 42,\n\t\t\t\"tupu\": false,\n\t\t\t\"foo\":  \"bar\",\n\t\t\t\"a\": map[string]interface{}{\n\t\t\t\t\"a\": map[string]interface{}{\n\t\t\t\t\t\"b\": true,\n\t\t\t\t\t\"c\": 42,\n\t\t\t\t\t\"d\": \"tupu\",\n\t\t\t\t\t\"deeper\": map[string]interface{}{\n\t\t\t\t\t\t\"a\": map[string]interface{}{\n\t\t\t\t\t\t\t\"aa\": \"deleteme deeper.a.aa\",\n\t\t\t\t\t\t\t\"bb\": \"do not deleteme deeper.a.bb\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"b\": map[string]interface{}{\n\t\t\t\t\t\t\t\"aa\": \"deleteme deeper.b.aa\",\n\t\t\t\t\t\t\t\"bb\": \"do not deleteme deeper.b.bb\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"b\": true,\n\t\t\t\t\"c\": 42,\n\t\t\t\t\"d\": \"tupu\",\n\t\t\t},\n\t\t},\n\t\tIsComplete: true,\n\t}\n\texpected := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"tupu\": false,\n\t\t\t\"foo\":  \"bar\",\n\t\t\t\"a\": map[string]interface{}{\n\t\t\t\t\"a\": map[string]interface{}{\n\t\t\t\t\t\"c\": 42,\n\t\t\t\t\t\"d\": \"tupu\",\n\t\t\t\t\t\"deeper\": map[string]interface{}{\n\t\t\t\t\t\t\"a\": map[string]interface{}{\n\t\t\t\t\t\t\t\"bb\": \"do not deleteme deeper.a.bb\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"b\": map[string]interface{}{\n\t\t\t\t\t\t\t\"bb\": \"do not deleteme deeper.b.bb\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"d\": \"tupu\",\n\t\t\t},\n\t\t},\n\t\tIsComplete: true,\n\t}\n\tf := NewEntityFormatter(&config.Backend{DenyList: []string{\n\t\t\"supu\",\n\t\t\"a.b\",\n\t\t\"a.c\",\n\t\t\"foo.unknown\",\n\t\t\"a.a.b\",\n\t\t\"a.a.deeper.a.aa\",\n\t\t\"a.a.deeper.b.aa\",\n\t}})\n\tresult := f.Format(sample)\n\tif v, ok := result.Data[\"tupu\"]; !ok || v != expected.Data[\"tupu\"] {\n\t\tt.Errorf(\"The formatter returned an unexpected result for the field tupu: %v\\n\", result)\n\t}\n\tif v, ok := result.Data[\"foo\"]; !ok || v != expected.Data[\"foo\"] {\n\t\tt.Errorf(\"The formatter returned an unexpected result for the field foo: %v\\n\", result)\n\t}\n\tv, ok := result.Data[\"a\"]\n\tif !ok {\n\t\tt.Errorf(\"The formatter returned an unexpected result for the field a.d: %v\\n\", result)\n\t}\n\ttmp := v.(map[string]interface{})\n\tif d, okk := tmp[\"d\"]; !okk || d != \"tupu\" {\n\t\tt.Errorf(\"The formatter returned an unexpected result for the field a.d: %v\\n\", result)\n\t}\n\tif len(tmp) != 2 {\n\t\t// a.a should exist , and a.d should exist\n\t\tt.Errorf(\"The formatter returned an unexpected result size for the field a: %v\\n\", result)\n\t}\n\tif len(result.Data) != 3 || result.IsComplete != expected.IsComplete {\n\t\tt.Errorf(\"The formatter returned an unexpected result size: %v\\n\", result)\n\t}\n\n\tif !reflect.DeepEqual(expected.Data, result.Data) {\n\t\tt.Errorf(\"unexpected response. have: %+v, want: %+v\", result.Data, expected.Data)\n\t}\n}\n\nfunc TestEntityFormatter_grouping(t *testing.T) {\n\tpreffix := \"group1\"\n\tsample := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"supu\": 42,\n\t\t\t\"tupu\": false,\n\t\t\t\"foo\":  \"bar\",\n\t\t},\n\t\tIsComplete: true,\n\t}\n\texpected := Response{\n\t\tData: map[string]interface{}{\n\t\t\tpreffix: map[string]interface{}{\n\t\t\t\t\"supu\": 42,\n\t\t\t\t\"tupu\": false,\n\t\t\t\t\"foo\":  \"bar\",\n\t\t\t},\n\t\t},\n\t\tIsComplete: true,\n\t}\n\tf := NewEntityFormatter(&config.Backend{Group: preffix})\n\tresult := f.Format(sample)\n\tif len(result.Data) != 1 || result.IsComplete != expected.IsComplete {\n\t\tt.Fail()\n\t}\n\tif _, ok := result.Data[preffix]; !ok {\n\t\tt.Fail()\n\t}\n\tgroup := result.Data[preffix].(map[string]interface{})\n\tfor k, expectedValue := range expected.Data[preffix].(map[string]interface{}) {\n\t\tif v, ok := group[k]; !ok || v != expectedValue {\n\t\t\tt.Fail()\n\t\t}\n\t}\n}\n\nfunc TestEntityFormatter_mapping(t *testing.T) {\n\tmapping := map[string]string{\"supu\": \"SUPUUUUU\", \"tupu\": \"TUPUUUUU\", \"a.b\": \"a.BOOOOO\"}\n\n\tsub := map[string]interface{}{\n\t\t\"b\": true,\n\t\t\"c\": 42,\n\t\t\"d\": \"tupu\",\n\t}\n\tsample := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"supu\": 42,\n\t\t\t\"tupu\": false,\n\t\t\t\"foo\":  \"bar\",\n\t\t\t\"a\":    sub,\n\t\t},\n\t\tIsComplete: true,\n\t}\n\texpected := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"SUPUUUUU\": 42,\n\t\t\t\"TUPUUUUU\": false,\n\t\t\t\"foo\":      \"bar\",\n\t\t\t\"a\":        sub,\n\t\t},\n\t\tIsComplete: true,\n\t}\n\tf := NewEntityFormatter(&config.Backend{Mapping: mapping})\n\tresult := f.Format(sample)\n\n\tif len(result.Data) != 4 || result.IsComplete != expected.IsComplete {\n\t\tt.Errorf(\"The formatter returned an unexpected result size: %v\\n\", result.Data)\n\t}\n\tfor k, expectedValue := range expected.Data {\n\t\tif k == \"a\" {\n\t\t\tcontinue\n\t\t}\n\t\tif v, ok := result.Data[k]; !ok || v != expectedValue {\n\t\t\tt.Errorf(\"The formatter returned an unexpected result for the key %s: %v\\n\", k, v)\n\t\t}\n\t}\n\tgroup := result.Data[\"a\"].(map[string]interface{})\n\tfor k, expectedValue := range expected.Data[\"a\"].(map[string]interface{}) {\n\t\tif v, ok := group[k]; !ok || v != expectedValue {\n\t\t\tt.Errorf(\"The formatter returned an unexpected result for the key %s: %v\\n\", k, v)\n\t\t}\n\t}\n\n\tif len(group) != 3 {\n\t\tt.Errorf(\"The formatter returned an unexpected result size for the subentity: %v\\n\", group)\n\t}\n}\n\nfunc TestEntityFormatter_targeting(t *testing.T) {\n\ttarget := \"group1\"\n\tsub := map[string]interface{}{\n\t\t\"b\": true,\n\t\t\"c\": 42,\n\t\t\"d\": \"tupu\",\n\t}\n\tsample := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"supu\": 42,\n\t\t\t\"tupu\": false,\n\t\t\t\"foo\":  \"bar\",\n\t\t\ttarget: sub,\n\t\t},\n\t\tIsComplete: true,\n\t}\n\texpected := Response{\n\t\tData:       sub,\n\t\tIsComplete: true,\n\t}\n\tf := NewEntityFormatter(&config.Backend{Target: target})\n\tresult := f.Format(sample)\n\tif len(result.Data) != 3 || result.IsComplete != expected.IsComplete {\n\t\tt.Errorf(\"The formatter returned an unexpected result size: %v\\n\", result)\n\t}\n\tfor k, expectedValue := range expected.Data {\n\t\tif v, ok := result.Data[k]; !ok || v != expectedValue {\n\t\t\tt.Errorf(\"The formatter returned an unexpected result for the key %s: %v\\n\", k, v)\n\t\t}\n\t}\n}\n\nfunc TestEntityFormatter_targetingNested(t *testing.T) {\n\ttarget := \"group1\"\n\tsub := map[string]interface{}{\n\t\t\"b\": true,\n\t\t\"c\": 42,\n\t\t\"d\": \"tupu\",\n\t}\n\tsample := Response{\n\t\tData: map[string]interface{}{\n\t\t\ttarget: map[string]interface{}{\n\t\t\t\t\"supu\": 42,\n\t\t\t\t\"tupu\": false,\n\t\t\t\t\"foo\":  \"bar\",\n\t\t\t\ttarget: sub,\n\t\t\t},\n\t\t},\n\t\tIsComplete: true,\n\t}\n\texpected := Response{\n\t\tData:       sub,\n\t\tIsComplete: true,\n\t}\n\tf := NewEntityFormatter(&config.Backend{Target: target + \".\" + target})\n\tresult := f.Format(sample)\n\tif len(result.Data) != 3 || result.IsComplete != expected.IsComplete {\n\t\tt.Errorf(\"The formatter returned an unexpected result size: %v\\n\", result)\n\t}\n\tfor k, expectedValue := range expected.Data {\n\t\tif v, ok := result.Data[k]; !ok || v != expectedValue {\n\t\t\tt.Errorf(\"The formatter returned an unexpected result for the key %s: %v\\n\", k, v)\n\t\t}\n\t}\n}\n\nfunc TestEntityFormatter_targetingUnknownFields(t *testing.T) {\n\ttarget := \"group1\"\n\tsample := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"supu\": 42,\n\t\t\t\"tupu\": false,\n\t\t\t\"foo\":  \"bar\",\n\t\t},\n\t\tIsComplete: true,\n\t}\n\tf := NewEntityFormatter(&config.Backend{Target: target})\n\tresult := f.Format(sample)\n\tif len(result.Data) != 0 || result.IsComplete != sample.IsComplete {\n\t\tt.Errorf(\"The formatter returned an unexpected result size: %v\\n\", result)\n\t}\n}\n\nfunc TestEntityFormatter_targetingNonObjects(t *testing.T) {\n\ttarget := \"group1\"\n\tsample := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"supu\": 42,\n\t\t\t\"tupu\": false,\n\t\t\t\"foo\":  \"bar\",\n\t\t\ttarget: false,\n\t\t},\n\t\tIsComplete: true,\n\t}\n\tf := NewEntityFormatter(&config.Backend{Target: target})\n\tresult := f.Format(sample)\n\tif len(result.Data) != 0 || result.IsComplete != sample.IsComplete {\n\t\tt.Errorf(\"The formatter returned an unexpected result size: %v\\n\", result)\n\t}\n}\n\nfunc TestEntityFormatter_altogether(t *testing.T) {\n\tsample := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"supu\": 42,\n\t\t\t\"tupu\": false,\n\t\t\t\"foo\":  \"bar\",\n\t\t\t\"a\": map[string]interface{}{\n\t\t\t\t\"b\": true,\n\t\t\t\t\"c\": 42,\n\t\t\t\t\"d\": \"tupu\",\n\t\t\t},\n\t\t},\n\t\tIsComplete: true,\n\t}\n\texpected := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"group\": map[string]interface{}{\n\t\t\t\t\"D\": \"tupu\",\n\t\t\t},\n\t\t},\n\t\tIsComplete: true,\n\t}\n\tf := NewEntityFormatter(&config.Backend{\n\t\tTarget:    \"a\",\n\t\tAllowList: []string{\"d\"},\n\t\tGroup:     \"group\",\n\t\tMapping:   map[string]string{\"d\": \"D\"},\n\t})\n\tresult := f.Format(sample)\n\tv, ok := result.Data[\"group\"]\n\tif !ok {\n\t\tt.Errorf(\"The formatter returned an unexpected result for the field group.D: %v\\n\", result)\n\t}\n\ttmp := v.(map[string]interface{})\n\tif d, okk := tmp[\"D\"]; !okk || d != \"tupu\" {\n\t\tt.Errorf(\"The formatter returned an unexpected result for the field group.D: %v\\n\", result)\n\t}\n\tif len(tmp) != 1 {\n\t\tt.Errorf(\"The formatter returned an unexpected result size for the field group: %v\\n\", result)\n\t}\n\tif len(result.Data) != 1 || result.IsComplete != expected.IsComplete {\n\t\tt.Errorf(\"The formatter returned an unexpected result size: %v\\n\", result)\n\t}\n}\n\nfunc TestEntityFormatter_flatmap(t *testing.T) {\n\tsub := map[string]interface{}{\n\t\t\"b\": true,\n\t\t\"c\": 42,\n\t\t\"d\": \"tupu\",\n\t\t\"e\": []interface{}{1, 2, 3, 4},\n\t}\n\tsample := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"content\": map[string]interface{}{\n\t\t\t\t\"supu\":       42,\n\t\t\t\t\"tupu\":       false,\n\t\t\t\t\"foo\":        \"bar\",\n\t\t\t\t\"a\":          sub,\n\t\t\t\t\"collection\": []interface{}{sub, sub, sub, sub},\n\t\t\t\t\"y\":          []interface{}{0, 1, 2, 3, 4, 5, 6},\n\t\t\t\t\"z\":          []interface{}{10, 11, 12, 13, 14, 15, 16},\n\t\t\t},\n\t\t},\n\t\tIsComplete: true,\n\t}\n\texpected := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"group\": map[string]interface{}{\n\t\t\t\t\"SUPUUUUU\": 42,\n\t\t\t\t\"tupu\":     false,\n\t\t\t\t\"foo\":      \"bar\",\n\t\t\t\t\"a\": map[string]interface{}{\n\t\t\t\t\t\"BOOOOO\": true,\n\t\t\t\t\t\"c\":      42,\n\t\t\t\t\t\"d\":      \"tupu\",\n\t\t\t\t\t\"e\":      []interface{}{1, 2, 3, 4},\n\t\t\t\t},\n\t\t\t\t\"collection\": []interface{}{\n\t\t\t\t\tmap[string]interface{}{\"x\": 42},\n\t\t\t\t\tmap[string]interface{}{\"x\": 42},\n\t\t\t\t\tmap[string]interface{}{\"x\": 42},\n\t\t\t\t\tmap[string]interface{}{\"x\": 42},\n\t\t\t\t},\n\t\t\t\t\"z\": []interface{}{10, 11, 12, 13, 14, 15, 16, 0, 1, 2, 3, 4, 5, 6},\n\t\t\t},\n\t\t},\n\t\tIsComplete: true,\n\t}\n\tf := NewEntityFormatter(&config.Backend{\n\t\tTarget: \"content\",\n\t\tGroup:  \"group\",\n\t\tExtraConfig: config.ExtraConfig{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\tflatmapKey: []interface{}{\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"del\",\n\t\t\t\t\t\t\"args\": []interface{}{\"c\"},\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"append\",\n\t\t\t\t\t\t\"args\": []interface{}{\"y\", \"z\"},\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"move\",\n\t\t\t\t\t\t\"args\": []interface{}{\"supu\", \"SUPUUUUU\"},\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"move\",\n\t\t\t\t\t\t\"args\": []interface{}{\"a.b\", \"a.BOOOOO\"},\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"del\",\n\t\t\t\t\t\t\"args\": []interface{}{\n\t\t\t\t\t\t\t\"collection.*.b\",\n\t\t\t\t\t\t\t\"collection.*.d\",\n\t\t\t\t\t\t\t\"collection.*.e\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"move\",\n\t\t\t\t\t\t\"args\": []interface{}{\"collection.*.c\", \"collection.*.x\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\tresult := f.Format(sample)\n\n\tif len(result.Data) != len(expected.Data) || result.IsComplete != expected.IsComplete {\n\t\tt.Errorf(\"The formatter returned an unexpected result size: %v\\n\", result.Data)\n\t}\n\n\tif !reflect.DeepEqual(expected.Data, result.Data) {\n\t\tt.Errorf(\"unexpected result: %v\", result.Data)\n\t}\n}\n\nfunc TestNewFlatmapMiddleware(t *testing.T) {\n\tsub := map[string]interface{}{\n\t\t\"b\": true,\n\t\t\"c\": 42,\n\t\t\"d\": \"tupu\",\n\t\t\"e\": []interface{}{1, 2, 3, 4},\n\t}\n\tsample := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"supu\":       42,\n\t\t\t\"tupu\":       false,\n\t\t\t\"foo\":        \"bar\",\n\t\t\t\"a\":          sub,\n\t\t\t\"collection\": []interface{}{sub, sub, sub, sub},\n\t\t\t\"y\":          []interface{}{0, 1, 2, 3, 4, 5, 6},\n\t\t\t\"z\":          []interface{}{10, 11, 12, 13, 14, 15, 16},\n\t\t},\n\t\tIsComplete: true,\n\t}\n\texpected := Response{\n\t\tData: map[string]interface{}{\n\t\t\t\"SUPUUUUU\": 42,\n\t\t\t\"tupu\":     false,\n\t\t\t\"foo\":      \"bar\",\n\t\t\t\"a\": map[string]interface{}{\n\t\t\t\t\"BOOOOO\": true,\n\t\t\t\t\"c\":      42,\n\t\t\t\t\"d\":      \"tupu\",\n\t\t\t\t\"e\":      []interface{}{1, 2, 3, 4},\n\t\t\t},\n\t\t\t\"collection\": []interface{}{\n\t\t\t\tmap[string]interface{}{\"x\": 42},\n\t\t\t\tmap[string]interface{}{\"x\": 42},\n\t\t\t\tmap[string]interface{}{\"x\": 42},\n\t\t\t\tmap[string]interface{}{\"x\": 42},\n\t\t\t},\n\t\t\t\"z\": []interface{}{10, 11, 12, 13, 14, 15, 16, 0, 1, 2, 3, 4, 5, 6},\n\t\t},\n\t\tIsComplete: true,\n\t}\n\tp := NewFlatmapMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.EndpointConfig{\n\t\t\tExtraConfig: config.ExtraConfig{\n\t\t\t\tNamespace: map[string]interface{}{\n\t\t\t\t\tflatmapKey: []interface{}{\n\t\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\t\"type\": \"del\",\n\t\t\t\t\t\t\t\"args\": []interface{}{\"c\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\t\"type\": \"append\",\n\t\t\t\t\t\t\t\"args\": []interface{}{\"y\", \"z\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\t\"type\": \"move\",\n\t\t\t\t\t\t\t\"args\": []interface{}{\"supu\", \"SUPUUUUU\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\t\"type\": \"move\",\n\t\t\t\t\t\t\t\"args\": []interface{}{\"a.b\", \"a.BOOOOO\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\t\"type\": \"del\",\n\t\t\t\t\t\t\t\"args\": []interface{}{\"collection.*.b\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\t\"type\": \"del\",\n\t\t\t\t\t\t\t\"args\": []interface{}{\"collection.*.d\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\t\"type\": \"del\",\n\t\t\t\t\t\t\t\"args\": []interface{}{\"collection.*.e\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\t\"type\": \"move\",\n\t\t\t\t\t\t\t\"args\": []interface{}{\"collection.*.c\", \"collection.*.x\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)(func(_ context.Context, _ *Request) (*Response, error) {\n\t\treturn &sample, nil\n\t})\n\n\tresult, err := p(context.TODO(), nil)\n\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif len(result.Data) != len(expected.Data) {\n\t\tt.Errorf(\"The formatter returned an unexpected result size: %v\\n\", result.Data)\n\t}\n\n\tif result.IsComplete != expected.IsComplete {\n\t\tt.Errorf(\"The formatter returned an unexpected completion flag: %v\\n\", result.IsComplete)\n\t}\n\n\tif !reflect.DeepEqual(expected.Data, result.Data) {\n\t\tt.Errorf(\"unexpected result: %v\", result.Data)\n\t}\n}\n"
  },
  {
    "path": "proxy/graphql.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/transport/http/client/graphql\"\n)\n\n// NewGraphQLMiddleware returns a middleware with or without the GraphQL\n// proxy wrapping the next element (depending on the configuration).\n// It supports both queries and mutations.\n// For queries, it completes the variables object using the request params.\n// For mutations, it overides the defined variables with the request body.\n// The resulting request will have a proper graphql body with the query and the\n// variables\nfunc NewGraphQLMiddleware(logger logging.Logger, remote *config.Backend) Middleware {\n\topt, err := graphql.GetOptions(remote.ExtraConfig)\n\tif err != nil {\n\t\tif err != graphql.ErrNoConfigFound {\n\t\t\tlogger.Warning(\n\t\t\t\tfmt.Sprintf(\"[BACKEND: %s %s -> %s][GraphQL] %s\", remote.ParentEndpoint, remote.ParentEndpoint, remote.URLPattern, err.Error()))\n\t\t}\n\t\treturn emptyMiddlewareFallback(logger)\n\t}\n\n\textractor := graphql.New(*opt)\n\tvar generateBodyFn func(*Request) ([]byte, error)\n\tvar generateQueryFn func(*Request) (url.Values, error)\n\n\tswitch opt.Type {\n\tcase graphql.OperationMutation:\n\t\tgenerateBodyFn = func(req *Request) ([]byte, error) {\n\t\t\tif req.Body == nil {\n\t\t\t\treturn extractor.BodyFromBody(strings.NewReader(\"\"))\n\t\t\t}\n\t\t\tdefer req.Body.Close()\n\t\t\treturn extractor.BodyFromBody(req.Body)\n\t\t}\n\t\tgenerateQueryFn = func(req *Request) (url.Values, error) {\n\t\t\tif req.Body == nil {\n\t\t\t\treturn extractor.QueryFromBody(strings.NewReader(\"\"))\n\t\t\t}\n\t\t\tdefer req.Body.Close()\n\t\t\treturn extractor.QueryFromBody(req.Body)\n\t\t}\n\n\tcase graphql.OperationQuery:\n\t\tgenerateBodyFn = func(req *Request) ([]byte, error) {\n\t\t\treturn extractor.BodyFromParams(req.Params)\n\t\t}\n\t\tgenerateQueryFn = func(req *Request) (url.Values, error) {\n\t\t\treturn extractor.QueryFromParams(req.Params)\n\t\t}\n\n\tdefault:\n\t\treturn emptyMiddlewareFallback(logger)\n\t}\n\n\treturn func(next ...Proxy) Proxy {\n\t\tif len(next) > 1 {\n\t\t\tlogger.Fatal(\"too many proxies for this %s %s -> %s proxy middleware: NewGraphQLMiddleware only accepts 1 proxy, got %d\",\n\t\t\t\tremote.ParentEndpointMethod, remote.ParentEndpoint, remote.URLPattern, len(next))\n\t\t\treturn nil\n\t\t}\n\n\t\tlogger.Debug(\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"[BACKEND: %s %s -> %s][GraphQL] Operation: %s, Method: %s\",\n\t\t\t\tremote.ParentEndpointMethod,\n\t\t\t\tremote.ParentEndpoint,\n\t\t\t\tremote.URLPattern,\n\t\t\t\topt.Type,\n\t\t\t\topt.Method,\n\t\t\t),\n\t\t)\n\n\t\tif opt.Method == graphql.MethodGet {\n\t\t\treturn func(ctx context.Context, req *Request) (*Response, error) {\n\t\t\t\tq, err := generateQueryFn(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\treq.Body = io.NopCloser(bytes.NewReader([]byte{}))\n\t\t\t\treq.Method = string(opt.Method)\n\t\t\t\treq.Headers[\"Content-Length\"] = []string{\"0\"}\n\t\t\t\t// even when there is no content, we just set the content-type\n\t\t\t\t// header to be safe if the server side checks it:\n\t\t\t\treq.Headers[\"Content-Type\"] = []string{\"application/json\"}\n\t\t\t\tif req.Query != nil {\n\t\t\t\t\tfor k, vs := range q {\n\t\t\t\t\t\tfor _, v := range vs {\n\t\t\t\t\t\t\treq.Query.Add(k, v)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\treq.Query = q\n\t\t\t\t}\n\n\t\t\t\treturn next[0](ctx, req)\n\t\t\t}\n\t\t}\n\n\t\treturn func(ctx context.Context, req *Request) (*Response, error) {\n\t\t\tb, err := generateBodyFn(req)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treq.Body = io.NopCloser(bytes.NewReader(b))\n\t\t\treq.Method = string(opt.Method)\n\t\t\treq.Headers[\"Content-Length\"] = []string{strconv.Itoa(len(b))}\n\t\t\treq.Headers[\"Content-Type\"] = []string{\"application/json\"}\n\n\t\t\treturn next[0](ctx, req)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "proxy/graphql_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/transport/http/client/graphql\"\n)\n\nfunc TestNewGraphQLMiddleware_mutation(t *testing.T) {\n\tquery := \"mutation addAuthor($author: [AddAuthorInput!]!) {\\n  addAuthor(input: $author) {\\n    author {\\n      id\\n      name\\n    }\\n  }\\n}\\n\"\n\tmw := NewGraphQLMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.Backend{\n\t\t\tExtraConfig: config.ExtraConfig{\n\t\t\t\tgraphql.Namespace: map[string]interface{}{\n\t\t\t\t\t\"type\":  \"mutation\",\n\t\t\t\t\t\"query\": query,\n\t\t\t\t\t\"variables\": map[string]interface{}{\n\t\t\t\t\t\t\"author\": map[string]interface{}{\n\t\t\t\t\t\t\t\"name\":  \"A.N. Author\",\n\t\t\t\t\t\t\t\"dob\":   \"2000-01-01\",\n\t\t\t\t\t\t\t\"posts\": []interface{}{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)\n\n\texpectedResponse := &Response{\n\t\tData: map[string]interface{}{\"foo\": \"bar\"},\n\t}\n\tprxy := mw(func(ctx context.Context, req *Request) (*Response, error) {\n\t\tb, err := io.ReadAll(req.Body)\n\t\treq.Body.Close()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar request graphql.GraphQLRequest\n\t\tif err := json.Unmarshal(b, &request); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn expectedResponse, nil\n\t})\n\n\tresp, err := prxy(context.Background(), &Request{\n\t\tBody: io.NopCloser(strings.NewReader(`{\n\t\t\t\"name\": \"foo\",\n\t\t\t\"dob\": \"bar\"\n\t\t}`)),\n\t\tParams:  map[string]string{},\n\t\tHeaders: map[string][]string{},\n\t})\n\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\tif !reflect.DeepEqual(resp, expectedResponse) {\n\t\tt.Errorf(\"unexpected response: %v\", resp)\n\t}\n}\n\nfunc TestNewGraphQLMiddleware_query(t *testing.T) {\n\tquery := \"{ q(func: uid(1)) { uid } }\"\n\tmw := NewGraphQLMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.Backend{\n\t\t\tExtraConfig: config.ExtraConfig{\n\t\t\t\tgraphql.Namespace: map[string]interface{}{\n\t\t\t\t\t\"method\": \"get\",\n\t\t\t\t\t\"type\":   \"query\",\n\t\t\t\t\t\"query\":  query,\n\t\t\t\t\t\"variables\": map[string]interface{}{\n\t\t\t\t\t\t\"name\":  \"{foo}\",\n\t\t\t\t\t\t\"dob\":   \"{bar}\",\n\t\t\t\t\t\t\"posts\": []interface{}{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)\n\n\texpectedResponse := &Response{Data: map[string]interface{}{\"foo\": \"bar\"}}\n\n\tprxy := mw(func(ctx context.Context, req *Request) (*Response, error) {\n\t\trequest := graphql.GraphQLRequest{\n\t\t\tQuery:         req.Query.Get(\"query\"),\n\t\t\tOperationName: req.Query.Get(\"operationName\"),\n\t\t\tVariables:     map[string]interface{}{},\n\t\t}\n\t\tjson.Unmarshal([]byte(req.Query.Get(\"variables\")), &request.Variables)\n\n\t\tif request.Query != query {\n\t\t\tt.Errorf(\"unexpected query: %s\", request.Query)\n\t\t}\n\t\tif len(request.Variables) != 3 {\n\t\t\tt.Errorf(\"unexpected variables: %v\", request.Variables)\n\t\t}\n\t\tif v, ok := request.Variables[\"name\"].(string); !ok || v != \"foo\" {\n\t\t\tt.Errorf(\"unexpected var name: %v\", request.Variables[\"name\"])\n\t\t}\n\t\tif v, ok := request.Variables[\"dob\"].(string); !ok || v != \"bar\" {\n\t\t\tt.Errorf(\"unexpected var dob: %v\", request.Variables[\"dob\"])\n\t\t}\n\n\t\treturn expectedResponse, nil\n\t})\n\n\tresp, err := prxy(context.Background(), &Request{\n\t\tParams: map[string]string{\n\t\t\t\"Foo\": \"foo\",\n\t\t\t\"Bar\": \"bar\",\n\t\t},\n\t\tBody:    io.NopCloser(strings.NewReader(\"\")),\n\t\tHeaders: map[string][]string{},\n\t})\n\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\tif !reflect.DeepEqual(resp, expectedResponse) {\n\t\tt.Errorf(\"unexpected response: %v\", resp)\n\t}\n}\n"
  },
  {
    "path": "proxy/headers_filter.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\n// NewFilterHeadersMiddleware returns a middleware with or without a header filtering\n// proxy wrapping the next element (depending on the configuration).\nfunc NewFilterHeadersMiddleware(logger logging.Logger, remote *config.Backend) Middleware {\n\tif len(remote.HeadersToPass) == 0 {\n\t\treturn emptyMiddlewareFallback(logger)\n\t}\n\n\treturn func(next ...Proxy) Proxy {\n\t\tif len(next) > 1 {\n\t\t\tlogger.Fatal(\"too many proxies for this %s %s -> %s proxy middleware: NewFilterHeadersMiddleware only accepts 1 proxy, got %d\", remote.ParentEndpointMethod, remote.ParentEndpoint, remote.URLPattern, len(next))\n\t\t\treturn nil\n\t\t}\n\t\tnextProxy := next[0]\n\t\treturn func(ctx context.Context, request *Request) (*Response, error) {\n\t\t\tif len(request.Headers) == 0 {\n\t\t\t\treturn nextProxy(ctx, request)\n\t\t\t}\n\t\t\tnumHeadersToPass := 0\n\t\t\tfor _, v := range remote.HeadersToPass {\n\t\t\t\tif _, ok := request.Headers[v]; ok {\n\t\t\t\t\tnumHeadersToPass++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif numHeadersToPass == len(request.Headers) {\n\t\t\t\t// all the headers should pass, no need to clone the headers\n\t\t\t\treturn nextProxy(ctx, request)\n\t\t\t}\n\t\t\t// ATTENTION: this is not a clone of headers!\n\t\t\t// this just filters the headers we do not want to send:\n\t\t\t// issues and race conditions could happen the same way as when we\n\t\t\t// do not filter the headers. This is a design decission, and if we\n\t\t\t// want to clone the header values (because of write modifications),\n\t\t\t// that should be done at an upper level (so the approach is the same\n\t\t\t// for non filtered parallel requests).\n\t\t\tnewHeaders := make(map[string][]string, numHeadersToPass)\n\t\t\tfor _, v := range remote.HeadersToPass {\n\t\t\t\tif values, ok := request.Headers[v]; ok {\n\t\t\t\t\tnewHeaders[v] = values\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nextProxy(ctx, &Request{\n\t\t\t\tMethod:  request.Method,\n\t\t\t\tURL:     request.URL,\n\t\t\t\tQuery:   request.Query,\n\t\t\t\tPath:    request.Path,\n\t\t\t\tBody:    request.Body,\n\t\t\t\tParams:  request.Params,\n\t\t\t\tHeaders: newHeaders,\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "proxy/headers_filter_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nfunc TestNewFilterHeadersMiddleware(t *testing.T) {\n\tmw := NewFilterHeadersMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.Backend{\n\t\t\tHeadersToPass: []string{\n\t\t\t\t\"X-This-Shall-Pass\",\n\t\t\t\t\"X-Gandalf-Will-Pass\",\n\t\t\t},\n\t\t},\n\t)\n\n\tvar receivedReq *Request\n\tprxy := mw(func(ctx context.Context, req *Request) (*Response, error) {\n\t\treceivedReq = req\n\t\treturn nil, nil\n\t})\n\n\tsentReq := &Request{\n\t\tBody:   nil,\n\t\tParams: map[string]string{},\n\t\tHeaders: map[string][]string{\n\t\t\t\"X-This-Shall-Pass\":    []string{\"tupu\", \"supu\"},\n\t\t\t\"X-You-Shall-Not-Pass\": []string{\"Balrog\"},\n\t\t\t\"X-Gandalf-Will-Pass\":  []string{\"White\", \"Grey\"},\n\t\t\t\"X-Drop-Tables\":        []string{\"foo\"},\n\t\t},\n\t}\n\n\tprxy(context.Background(), sentReq)\n\n\tif receivedReq == sentReq {\n\t\tt.Errorf(\"request should be different\")\n\t\treturn\n\t}\n\n\tif _, ok := receivedReq.Headers[\"X-This-Shall-Pass\"]; !ok {\n\t\tt.Errorf(\"missing X-This-Shall-Pass\")\n\t\treturn\n\t}\n\n\tif _, ok := receivedReq.Headers[\"X-Gandalf-Will-Pass\"]; !ok {\n\t\tt.Errorf(\"missing X-Gandalf-Will-Pass\")\n\t\treturn\n\t}\n\n\tif _, ok := receivedReq.Headers[\"X-Drop-Tables\"]; ok {\n\t\tt.Errorf(\"should not be there X-Drop-Tables\")\n\t\treturn\n\t}\n\n\tif _, ok := receivedReq.Headers[\"X-You-Shall-Not-Pass\"]; ok {\n\t\tt.Errorf(\"should not be there X-You-Shall-Not-Pass\")\n\t\treturn\n\t}\n\n\t// check that when headers are the expected, no need to copy\n\tsentReq = &Request{\n\t\tBody:   nil,\n\t\tParams: map[string]string{},\n\t\tHeaders: map[string][]string{\n\t\t\t\"X-This-Shall-Pass\": []string{\"tupu\", \"supu\"},\n\t\t},\n\t}\n\n\tprxy(context.Background(), sentReq)\n\n\tif receivedReq != sentReq {\n\t\tt.Errorf(\"request should be the same, no modification of headers expected\")\n\t\treturn\n\t}\n}\n\nfunc TestNewFilterHeadersMiddlewareBlockAll(t *testing.T) {\n\tmw := NewFilterHeadersMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.Backend{\n\t\t\tHeadersToPass: []string{\"\"},\n\t\t},\n\t)\n\n\tvar receivedReq *Request\n\tprxy := mw(func(ctx context.Context, req *Request) (*Response, error) {\n\t\treceivedReq = req\n\t\treturn nil, nil\n\t})\n\n\tsentReq := &Request{\n\t\tBody:   nil,\n\t\tParams: map[string]string{},\n\t\tHeaders: map[string][]string{\n\t\t\t\"X-This-Shall-Pass\":    []string{\"tupu\", \"supu\"},\n\t\t\t\"X-You-Shall-Not-Pass\": []string{\"Balrog\"},\n\t\t},\n\t}\n\n\tprxy(context.Background(), sentReq)\n\n\tif receivedReq == sentReq {\n\t\tt.Errorf(\"request should be different\")\n\t\treturn\n\t}\n\n\tif len(receivedReq.Headers) != 0 {\n\t\tt.Errorf(\"should have blocked all headers\")\n\t\treturn\n\t}\n}\n\nfunc TestNewFilterHeadersMiddlewareAllowAll(t *testing.T) {\n\tmw := NewFilterHeadersMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.Backend{\n\t\t\tHeadersToPass: []string{},\n\t\t},\n\t)\n\n\tvar receivedReq *Request\n\tprxy := mw(func(ctx context.Context, req *Request) (*Response, error) {\n\t\treceivedReq = req\n\t\treturn nil, nil\n\t})\n\n\tsentReq := &Request{\n\t\tBody:   nil,\n\t\tParams: map[string]string{},\n\t\tHeaders: map[string][]string{\n\t\t\t\"X-This-Shall-Pass\":    []string{\"tupu\", \"supu\"},\n\t\t\t\"X-You-Shall-Not-Pass\": []string{\"Balrog\"},\n\t\t},\n\t}\n\n\tprxy(context.Background(), sentReq)\n\n\tif len(receivedReq.Headers) != 2 {\n\t\tt.Errorf(\"should have let pass all headers\")\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "proxy/http.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/encoding\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/transport/http/client\"\n)\n\nvar httpProxy = CustomHTTPProxyFactory(client.NewHTTPClient)\n\n// HTTPProxyFactory returns a BackendFactory. The Proxies it creates will use the received net/http.Client\nfunc HTTPProxyFactory(client *http.Client) BackendFactory {\n\treturn CustomHTTPProxyFactory(func(_ context.Context) *http.Client { return client })\n}\n\n// CustomHTTPProxyFactory returns a BackendFactory. The Proxies it creates will use the received HTTPClientFactory\nfunc CustomHTTPProxyFactory(cf client.HTTPClientFactory) BackendFactory {\n\treturn func(backend *config.Backend) Proxy {\n\t\treturn NewHTTPProxy(backend, cf, backend.Decoder)\n\t}\n}\n\n// NewHTTPProxy creates a http proxy with the injected configuration, HTTPClientFactory and Decoder\nfunc NewHTTPProxy(remote *config.Backend, cf client.HTTPClientFactory, decode encoding.Decoder) Proxy {\n\treturn NewHTTPProxyWithHTTPExecutor(remote, client.DefaultHTTPRequestExecutor(cf), decode)\n}\n\n// NewHTTPProxyWithHTTPExecutor creates a http proxy with the injected configuration, HTTPRequestExecutor and Decoder\nfunc NewHTTPProxyWithHTTPExecutor(remote *config.Backend, re client.HTTPRequestExecutor, dec encoding.Decoder) Proxy {\n\tif remote.Encoding == encoding.NOOP {\n\t\treturn NewHTTPProxyDetailed(remote, re, client.NoOpHTTPStatusHandler, NoOpHTTPResponseParser)\n\t}\n\n\tef := NewEntityFormatter(remote)\n\trp := DefaultHTTPResponseParserFactory(HTTPResponseParserConfig{dec, ef})\n\treturn NewHTTPProxyDetailed(remote, re, client.GetHTTPStatusHandler(remote), rp)\n}\n\nconst (\n\tclientHTTPOptions            string = \"backend/http/client\"\n\tclientHTTPOptionRedirectPost string = \"send_body_on_redirect\"\n)\n\n// redirectPostReaderFactory checks if the clientHTTPOptionRedirectPost is enabled\n// This will read the body and return a bytes.Buffer with the body content, so we\n// delegate to http.NewRequest the population of request.GetBody so a redirect (307\n// and 308) is executed while maintaining the method and the body\n// This is necessary since the request comes from another http.Client and it's not\n// a concrete type that can be copied but just a io.ReaderCloser (*http.body)\nfunc redirectPostReaderFactory(cfg *config.Backend) func(r io.ReadCloser) io.Reader {\n\temptyFactory := func(r io.ReadCloser) io.Reader { return r }\n\tif cfg == nil || cfg.ExtraConfig == nil {\n\t\treturn emptyFactory\n\t}\n\tv, ok := cfg.ExtraConfig[clientHTTPOptions].(map[string]interface{})\n\tif !ok {\n\t\treturn emptyFactory\n\t}\n\tif opt, ok := v[clientHTTPOptionRedirectPost].(bool); !ok || !opt {\n\t\treturn emptyFactory\n\t}\n\treturn func(r io.ReadCloser) io.Reader {\n\t\tif r == http.NoBody || r == nil {\n\t\t\treturn r\n\t\t}\n\t\tbuf := new(bytes.Buffer)\n\t\tbuf.ReadFrom(r)\n\t\tr.Close()\n\t\treturn buf\n\t}\n}\n\n// NewHTTPProxyDetailed creates a http proxy with the injected configuration, HTTPRequestExecutor,\n// Decoder and HTTPResponseParser\nfunc NewHTTPProxyDetailed(cfg *config.Backend, re client.HTTPRequestExecutor, ch client.HTTPStatusHandler, rp HTTPResponseParser) Proxy {\n\tbodyFactory := redirectPostReaderFactory(cfg)\n\treturn func(ctx context.Context, request *Request) (*Response, error) {\n\t\trequestToBackend, err := http.NewRequest(strings.ToTitle(request.Method), request.URL.String(), bodyFactory(request.Body))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trequestToBackend.Header = make(map[string][]string, len(request.Headers))\n\t\tfor k, vs := range request.Headers {\n\t\t\ttmp := make([]string, len(vs))\n\t\t\tcopy(tmp, vs)\n\t\t\trequestToBackend.Header[k] = tmp\n\t\t}\n\t\tif request.Body != nil {\n\t\t\tif v, ok := request.Headers[\"Content-Length\"]; ok && len(v) == 1 && v[0] != \"chunked\" {\n\t\t\t\tif size, err := strconv.Atoi(v[0]); err == nil {\n\t\t\t\t\trequestToBackend.ContentLength = int64(size)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tresp, err := re(ctx, requestToBackend)\n\t\tif requestToBackend.Body != nil {\n\t\t\trequestToBackend.Body.Close()\n\t\t}\n\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresp, err = ch(ctx, resp)\n\t\tif err != nil {\n\t\t\tif t, ok := err.(responseError); ok {\n\t\t\t\treturn &Response{\n\t\t\t\t\tData: map[string]interface{}{\n\t\t\t\t\t\tfmt.Sprintf(\"error_%s\", t.Name()): t,\n\t\t\t\t\t},\n\t\t\t\t\tMetadata: Metadata{StatusCode: t.StatusCode()},\n\t\t\t\t}, nil\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn rp(ctx, resp)\n\t}\n}\n\n// NewRequestBuilderMiddleware creates a proxy middleware that parses the request params received\n// from the outer layer and generates the path to the backend endpoints\nvar NewRequestBuilderMiddleware = func(remote *config.Backend) Middleware {\n\treturn newRequestBuilderMiddleware(logging.NoOp, remote)\n}\n\nfunc NewRequestBuilderMiddlewareWithLogger(logger logging.Logger, remote *config.Backend) Middleware {\n\treturn newRequestBuilderMiddleware(logger, remote)\n}\n\nfunc newRequestBuilderMiddleware(l logging.Logger, remote *config.Backend) Middleware {\n\treturn func(next ...Proxy) Proxy {\n\t\tif len(next) > 1 {\n\t\t\tl.Fatal(\"too many proxies for this %s %s -> %s proxy middleware: newRequestBuilderMiddleware only accepts 1 proxy, got %d\", remote.ParentEndpointMethod, remote.ParentEndpoint, remote.URLPattern, len(next))\n\t\t\treturn nil\n\t\t}\n\t\treturn func(ctx context.Context, r *Request) (*Response, error) {\n\t\t\tr.GeneratePath(remote.URLPattern)\n\t\t\tr.Method = remote.Method\n\t\t\treturn next[0](ctx, r)\n\t\t}\n\t}\n}\n\ntype responseError interface {\n\tError() string\n\tName() string\n\tStatusCode() int\n}\n"
  },
  {
    "path": "proxy/http_benchmark_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n)\n\nfunc BenchmarkNewRequestBuilderMiddleware(b *testing.B) {\n\tbackend := config.Backend{\n\t\tURLPattern: \"/supu\",\n\t\tMethod:     \"GET\",\n\t}\n\tproxy := NewRequestBuilderMiddleware(&backend)(dummyProxy(&Response{}))\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor i := 0; i < b.N; i++ {\n\t\tproxy(context.Background(), &Request{})\n\t}\n}\n"
  },
  {
    "path": "proxy/http_response.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"compress/gzip\"\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/luraproject/lura/v2/encoding\"\n)\n\n// HTTPResponseParser defines how the response is parsed from http.Response to Response object\ntype HTTPResponseParser func(context.Context, *http.Response) (*Response, error)\n\n// DefaultHTTPResponseParserConfig defines a default HTTPResponseParserConfig\nvar DefaultHTTPResponseParserConfig = HTTPResponseParserConfig{\n\tfunc(_ io.Reader, _ *map[string]interface{}) error { return nil },\n\tEntityFormatterFunc(func(r Response) Response { return r }),\n}\n\n// HTTPResponseParserConfig contains the config for a given HttpResponseParser\ntype HTTPResponseParserConfig struct {\n\tDecoder         encoding.Decoder\n\tEntityFormatter EntityFormatter\n}\n\n// HTTPResponseParserFactory creates HTTPResponseParser from a given HTTPResponseParserConfig\ntype HTTPResponseParserFactory func(HTTPResponseParserConfig) HTTPResponseParser\n\n// DefaultHTTPResponseParserFactory is the default implementation of HTTPResponseParserFactory\nfunc DefaultHTTPResponseParserFactory(cfg HTTPResponseParserConfig) HTTPResponseParser {\n\treturn func(_ context.Context, resp *http.Response) (*Response, error) {\n\t\tdefer resp.Body.Close()\n\n\t\tvar reader io.ReadCloser\n\t\tswitch resp.Header.Get(\"Content-Encoding\") {\n\t\tcase \"gzip\":\n\t\t\tgzipReader, err := gzip.NewReader(resp.Body)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treader = gzipReader\n\t\t\tdefer reader.Close()\n\t\tdefault:\n\t\t\treader = resp.Body\n\t\t}\n\n\t\tvar data map[string]interface{}\n\t\tif err := cfg.Decoder(reader, &data); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tnewResponse := Response{Data: data, IsComplete: true}\n\t\tnewResponse = cfg.EntityFormatter.Format(newResponse)\n\t\treturn &newResponse, nil\n\t}\n}\n\n// NoOpHTTPResponseParser is a HTTPResponseParser implementation that just copies the\n// http response body into the proxy response IO\nfunc NoOpHTTPResponseParser(ctx context.Context, resp *http.Response) (*Response, error) {\n\treturn &Response{\n\t\tData:       map[string]interface{}{},\n\t\tIsComplete: true,\n\t\tIo:         NewReadCloserWrapper(ctx, resp.Body),\n\t\tMetadata: Metadata{\n\t\t\tStatusCode: resp.StatusCode,\n\t\t\tHeaders:    resp.Header,\n\t\t},\n\t}, nil\n}\n"
  },
  {
    "path": "proxy/http_response_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"compress/gzip\"\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/encoding\"\n)\n\nfunc TestNopHTTPResponseParser(t *testing.T) {\n\tw := httptest.NewRecorder()\n\thandler := func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"header1\", \"value1\")\n\t\tw.Write([]byte(\"some nice, interesting and long content\"))\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"/url\", http.NoBody)\n\thandler(w, req)\n\tresult, err := NoOpHTTPResponseParser(context.Background(), w.Result())\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\tif !result.IsComplete {\n\t\tt.Error(\"unexpected result\")\n\t}\n\tif len(result.Data) != 0 {\n\t\tt.Error(\"unexpected result\")\n\t}\n\tif result.Metadata.StatusCode != http.StatusOK {\n\t\tt.Error(\"unexpected result\")\n\t}\n\theaders := result.Metadata.Headers\n\tif h, ok := headers[\"Header1\"]; !ok || h[0] != \"value1\" {\n\t\tt.Error(\"unexpected result:\", result.Metadata.Headers)\n\t}\n\tbody, err := io.ReadAll(result.Io)\n\tif err != nil {\n\t\tt.Error(\"unexpected error:\", err.Error())\n\t}\n\tif string(body) != \"some nice, interesting and long content\" {\n\t\tt.Error(\"unexpected result\")\n\t}\n}\n\nfunc TestDefaultHTTPResponseParser_gzipped(t *testing.T) {\n\tw := httptest.NewRecorder()\n\thandler := func(w http.ResponseWriter, _ *http.Request) {\n\t\tgzipWriter, _ := gzip.NewWriterLevel(w, gzip.BestSpeed)\n\t\tdefer gzipWriter.Close()\n\n\t\tw.Header().Set(\"Vary\", \"Accept-Encoding\")\n\t\tw.Header().Set(\"Content-Encoding\", \"gzip\")\n\t\tw.Header().Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\t\tgzipWriter.Write([]byte(`{\"msg\":\"some nice, interesting and long content\"}`))\n\t\tgzipWriter.Flush()\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"/url\", http.NoBody)\n\treq.Header.Add(\"Accept-Encoding\", \"gzip\")\n\thandler(w, req)\n\n\tresult, err := DefaultHTTPResponseParserFactory(HTTPResponseParserConfig{\n\t\tDecoder:         encoding.JSONDecoder,\n\t\tEntityFormatter: DefaultHTTPResponseParserConfig.EntityFormatter,\n\t})(context.Background(), w.Result())\n\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif !result.IsComplete {\n\t\tt.Error(\"unexpected result\")\n\t}\n\tif len(result.Data) != 1 {\n\t\tt.Error(\"unexpected result\")\n\t}\n\tif m, ok := result.Data[\"msg\"]; !ok || m != \"some nice, interesting and long content\" {\n\t\tt.Error(\"unexpected result\")\n\t}\n}\n\nfunc TestDefaultHTTPResponseParser_gzipped_bad_header(t *testing.T) {\n\tw := httptest.NewRecorder()\n\thandler := func(w http.ResponseWriter, _ *http.Request) {\n\t\tgzipWriter, _ := gzip.NewWriterLevel(w, gzip.BestSpeed)\n\t\tdefer gzipWriter.Close()\n\n\t\tw.Header().Set(\"Vary\", \"Accept-Encoding\")\n\t\tw.Header().Set(\"Content-Encoding\", \"gzip\")\n\t\tw.Header().Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\t\tgzipWriter.Write([]byte(`{\"msg\":\"some nice, interesting and long content\"}`))\n\t\tgzipWriter.Flush()\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"/url\", http.NoBody)\n\t// explicitly disable gzip encoding\n\treq.Header.Add(\"Accept-Encoding\", \"identity;q=0\")\n\thandler(w, req)\n\n\tresult, err := DefaultHTTPResponseParserFactory(HTTPResponseParserConfig{\n\t\tDecoder:         encoding.JSONDecoder,\n\t\tEntityFormatter: DefaultHTTPResponseParserConfig.EntityFormatter,\n\t})(context.Background(), w.Result())\n\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif !result.IsComplete {\n\t\tt.Error(\"unexpected result\")\n\t}\n\tif len(result.Data) != 1 {\n\t\tt.Error(\"unexpected result\")\n\t}\n\tif m, ok := result.Data[\"msg\"]; !ok || m != \"some nice, interesting and long content\" {\n\t\tt.Error(\"unexpected result\")\n\t}\n}\n\nfunc TestDefaultHTTPResponseParser_plain(t *testing.T) {\n\tw := httptest.NewRecorder()\n\thandler := func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\t\tw.Write([]byte(`{\"msg\":\"some nice, interesting and long content\"}`))\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"/url\", http.NoBody)\n\thandler(w, req)\n\n\tresult, err := DefaultHTTPResponseParserFactory(HTTPResponseParserConfig{\n\t\tDecoder:         encoding.JSONDecoder,\n\t\tEntityFormatter: DefaultHTTPResponseParserConfig.EntityFormatter,\n\t})(context.Background(), w.Result())\n\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif !result.IsComplete {\n\t\tt.Error(\"unexpected result\")\n\t}\n\tif len(result.Data) != 1 {\n\t\tt.Error(\"unexpected result\")\n\t}\n\tif m, ok := result.Data[\"msg\"]; !ok || m != \"some nice, interesting and long content\" {\n\t\tt.Error(\"unexpected result\")\n\t}\n}\n"
  },
  {
    "path": "proxy/http_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/encoding\"\n\t\"github.com/luraproject/lura/v2/transport/http/client\"\n)\n\nfunc TestNewHTTPProxy_ok(t *testing.T) {\n\texpectedMethod := \"GET\"\n\tbackendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.ContentLength != 11 {\n\t\t\tt.Errorf(\"unexpected request size. Want: 11. Have: %d\", r.ContentLength)\n\t\t}\n\t\tif h := r.Header.Get(\"Content-Length\"); h != \"11\" {\n\t\t\tt.Errorf(\"unexpected content-length header. Want: 11. Have: %s\", h)\n\t\t}\n\t\tif r.Method != expectedMethod {\n\t\t\tt.Errorf(\"Wrong request method. Want: %s. Have: %s\", expectedMethod, r.Method)\n\t\t}\n\t\tif h := r.Header.Get(\"X-First\"); h != \"first\" {\n\t\t\tt.Errorf(\"unexpected first header: %s\", h)\n\t\t}\n\t\tif h := r.Header.Get(\"X-Second\"); h != \"second\" {\n\t\t\tt.Errorf(\"unexpected second header: %s\", h)\n\t\t}\n\t\tr.Header.Del(\"X-Second\")\n\t\tfmt.Fprintf(w, \"{\\\"supu\\\":42, \\\"tupu\\\":true, \\\"foo\\\": \\\"bar\\\"}\")\n\t}))\n\tdefer backendServer.Close()\n\n\trpURL, _ := url.Parse(backendServer.URL)\n\tbackend := config.Backend{\n\t\tDecoder: encoding.JSONDecoder,\n\t}\n\trequest := Request{\n\t\tMethod: expectedMethod,\n\t\tPath:   \"/\",\n\t\tURL:    rpURL,\n\t\tBody:   newDummyReadCloser(`{\"abc\": 42}`),\n\t\tHeaders: map[string][]string{\n\t\t\t\"X-First\":        {\"first\"},\n\t\t\t\"X-Second\":       {\"second\"},\n\t\t\t\"Content-Length\": {\"11\"},\n\t\t},\n\t}\n\tmustEnd := time.After(time.Duration(150) * time.Millisecond)\n\n\tresult, err := HTTPProxyFactory(http.DefaultClient)(&backend)(context.Background(), &request)\n\tif err != nil {\n\t\tt.Errorf(\"The proxy returned an unexpected error: %s\\n\", err.Error())\n\t\treturn\n\t}\n\tif result == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"Error: expected response\")\n\t\treturn\n\tdefault:\n\t}\n\n\ttmp, ok := result.Data[\"supu\"]\n\tif !ok {\n\t\tt.Errorf(\"The proxy returned an unexpected result: %v\\n\", result)\n\t}\n\tsupuValue, err := tmp.(json.Number).Int64()\n\tif err != nil || supuValue != 42 {\n\t\tt.Errorf(\"The proxy returned an unexpected result: %v\\n\", supuValue)\n\t}\n\tif v, ok := result.Data[\"tupu\"]; !ok || !v.(bool) {\n\t\tt.Errorf(\"The proxy returned an unexpected result: %v\\n\", result)\n\t}\n\tif v, ok := result.Data[\"foo\"]; !ok || v.(string) != \"bar\" {\n\t\tt.Errorf(\"The proxy returned an unexpected result: %v\\n\", result)\n\t}\n\tif v, ok := request.Headers[\"X-Second\"]; !ok || len(v) != 1 {\n\t\tt.Errorf(\"the proxy request headers were changed: %v\", request.Headers)\n\t}\n}\n\nfunc TestNewHTTPProxy_cancel(t *testing.T) {\n\texpectedMethod := \"GET\"\n\tbackendServer := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {\n\t\ttime.Sleep(time.Duration(300) * time.Millisecond)\n\t}))\n\tdefer backendServer.Close()\n\n\trpURL, _ := url.Parse(backendServer.URL)\n\tbackend := config.Backend{\n\t\tDecoder: encoding.JSONDecoder,\n\t}\n\trequest := Request{\n\t\tMethod: expectedMethod,\n\t\tPath:   \"/\",\n\t\tURL:    rpURL,\n\t\tBody:   newDummyReadCloser(\"\"),\n\t}\n\tmustEnd := time.After(time.Duration(150) * time.Millisecond)\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Millisecond)\n\tdefer cancel()\n\tresponse, err := httpProxy(&backend)(ctx, &request)\n\tif err == nil || err.Error() != \"context deadline exceeded\" {\n\t\tt.Errorf(\"The proxy didn't propagate a timeout error: %s\\n\", err)\n\t}\n\tif response != nil {\n\t\tt.Errorf(\"We weren't expecting a response but we got one: %v\\n\", response)\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response at this point in time!\\n\")\n\t\treturn\n\tdefault:\n\t}\n}\n\nfunc TestNewHTTPProxy_badResponseBody(t *testing.T) {\n\texpectedMethod := \"GET\"\n\tbackendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tfmt.Fprintf(w, \"supu\")\n\t}))\n\tdefer backendServer.Close()\n\n\trpURL, _ := url.Parse(backendServer.URL)\n\tbackend := config.Backend{\n\t\tDecoder: encoding.JSONDecoder,\n\t}\n\trequest := Request{\n\t\tMethod: expectedMethod,\n\t\tPath:   \"/\",\n\t\tURL:    rpURL,\n\t\tBody:   newDummyReadCloser(\"\"),\n\t}\n\tmustEnd := time.After(time.Duration(150) * time.Millisecond)\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Millisecond)\n\tdefer cancel()\n\tresponse, err := httpProxy(&backend)(ctx, &request)\n\tif err == nil || err.Error() != \"invalid character 's' looking for beginning of value\" {\n\t\tt.Errorf(\"The proxy didn't propagate the backend error: %s\\n\", err)\n\t}\n\tif response != nil {\n\t\tt.Errorf(\"We weren't expecting a response but we got one: %v\\n\", response)\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"Error: expected response\")\n\tdefault:\n\t}\n}\n\nfunc TestNewHTTPProxy_badStatusCode(t *testing.T) {\n\texpectedMethod := \"GET\"\n\tbackendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\thttp.Error(w, \"booom\", 500)\n\t}))\n\tdefer backendServer.Close()\n\n\trpURL, _ := url.Parse(backendServer.URL)\n\tbackend := config.Backend{\n\t\tDecoder: encoding.JSONDecoder,\n\t}\n\trequest := Request{\n\t\tMethod: expectedMethod,\n\t\tPath:   \"/\",\n\t\tURL:    rpURL,\n\t\tBody:   newDummyReadCloser(\"\"),\n\t}\n\tmustEnd := time.After(time.Duration(150) * time.Millisecond)\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Millisecond)\n\tdefer cancel()\n\tresponse, err := httpProxy(&backend)(ctx, &request)\n\tif err == nil || !strings.HasPrefix(err.Error(), \"invalid status code\") {\n\t\tt.Errorf(\"The proxy didn't propagate the backend error: %s\\n\", err)\n\t}\n\tif response != nil {\n\t\tt.Errorf(\"We weren't expecting a response but we got one: %v\\n\", response)\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"Error: expected response\")\n\tdefault:\n\t}\n}\n\nfunc TestNewHTTPProxy_badStatusCode_detailed(t *testing.T) {\n\texpectedMethod := \"GET\"\n\tbackendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\thttp.Error(w, \"booom\", 500)\n\t}))\n\tdefer backendServer.Close()\n\n\trpURL, _ := url.Parse(backendServer.URL)\n\tbackend := config.Backend{\n\t\tDecoder: encoding.JSONDecoder,\n\t\tExtraConfig: config.ExtraConfig{\n\t\t\tclient.Namespace: map[string]interface{}{\n\t\t\t\t\"return_error_details\": \"some\",\n\t\t\t},\n\t\t},\n\t}\n\trequest := Request{\n\t\tMethod: expectedMethod,\n\t\tPath:   \"/\",\n\t\tURL:    rpURL,\n\t\tBody:   newDummyReadCloser(\"\"),\n\t}\n\tmustEnd := time.After(time.Duration(150) * time.Millisecond)\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Millisecond)\n\tdefer cancel()\n\tresponse, err := httpProxy(&backend)(ctx, &request)\n\tif err != nil {\n\t\tt.Errorf(\"The proxy propagated the backend error: %s\", err.Error())\n\t}\n\tif response == nil {\n\t\tt.Error(\"We were expecting a response but we got none\")\n\t\treturn\n\t}\n\tif response.Metadata.StatusCode != 500 {\n\t\tt.Errorf(\"unexpected error code: %d\", response.Metadata.StatusCode)\n\t}\n\tb, _ := json.Marshal(response.Data)\n\tif string(b) != `{\"error_some\":{\"http_status_code\":500,\"http_body\":\"booom\\n\",\"http_body_encoding\":\"text/plain; charset=utf-8\"}}` {\n\t\tt.Errorf(\"unexpected response content: %s\", string(b))\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"Error: expected response\")\n\tdefault:\n\t}\n}\n\nfunc TestNewHTTPProxy_decodingError(t *testing.T) {\n\texpectedMethod := \"GET\"\n\tbackendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tfmt.Fprintf(w, `{\"supu\": 42}`)\n\t}))\n\tdefer backendServer.Close()\n\n\trpURL, _ := url.Parse(backendServer.URL)\n\tbackend := config.Backend{\n\t\tDecoder: func(_ io.Reader, _ *map[string]interface{}) error {\n\t\t\treturn errors.New(\"booom\")\n\t\t},\n\t}\n\trequest := Request{\n\t\tMethod: expectedMethod,\n\t\tPath:   \"/\",\n\t\tURL:    rpURL,\n\t\tBody:   newDummyReadCloser(\"\"),\n\t}\n\tmustEnd := time.After(time.Duration(150) * time.Millisecond)\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Millisecond)\n\tdefer cancel()\n\tresponse, err := httpProxy(&backend)(ctx, &request)\n\tif err == nil || err.Error() != \"booom\" {\n\t\tt.Errorf(\"The proxy returned an unexpected error: %s\\n\", err.Error())\n\t}\n\tif response != nil {\n\t\tt.Errorf(\"We weren't expecting a response but we got one: %v\\n\", response)\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"Error: expected response\")\n\tdefault:\n\t}\n}\n\nfunc TestNewHTTPProxy_badMethod(t *testing.T) {\n\tbackendServer := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {\n\t\tt.Error(\"The handler shouldn't be called\")\n\t}))\n\tdefer backendServer.Close()\n\n\trpURL, _ := url.Parse(backendServer.URL)\n\tbackend := config.Backend{\n\t\tDecoder: func(_ io.Reader, _ *map[string]interface{}) error {\n\t\t\tt.Error(\"The decoder shouldn't be called\")\n\t\t\treturn nil\n\t\t},\n\t}\n\trequest := Request{\n\t\tMethod: \"\\n\",\n\t\tPath:   \"/\",\n\t\tURL:    rpURL,\n\t\tBody:   newDummyReadCloser(\"\"),\n\t}\n\tmustEnd := time.After(time.Duration(150) * time.Millisecond)\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Millisecond)\n\tdefer cancel()\n\t_, err := httpProxy(&backend)(ctx, &request)\n\tif err == nil {\n\t\tt.Error(\"The proxy didn't return the expected error\")\n\t\treturn\n\t}\n\tif err.Error() != \"net/http: invalid method \\\"\\\\n\\\"\" {\n\t\tt.Errorf(\"The proxy returned an unexpected error: %s\\n\", err.Error())\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"Error: expected response\")\n\tdefault:\n\t}\n}\n\nfunc TestNewHTTPProxy_requestKo(t *testing.T) {\n\tbackendServer := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {\n\t\tt.Error(\"The handler shouldn't be called\")\n\t}))\n\tdefer backendServer.Close()\n\n\trpURL, _ := url.Parse(backendServer.URL)\n\tbackend := config.Backend{\n\t\tDecoder: func(_ io.Reader, _ *map[string]interface{}) error {\n\t\t\tt.Error(\"The decoder shouldn't be called\")\n\t\t\treturn nil\n\t\t},\n\t}\n\trequest := Request{\n\t\tMethod: \"GET\",\n\t\tPath:   \"/\",\n\t\tURL:    rpURL,\n\t\tBody:   newDummyReadCloser(\"\"),\n\t}\n\tmustEnd := time.After(time.Duration(150) * time.Millisecond)\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(100)*time.Millisecond)\n\tdefer cancel()\n\n\texpectedError := fmt.Errorf(\"MAYDAY, MAYDAY\")\n\t_, err := NewHTTPProxyWithHTTPExecutor(&backend, func(_ context.Context, _ *http.Request) (*http.Response, error) {\n\t\treturn nil, expectedError\n\t}, backend.Decoder)(ctx, &request)\n\tif err == nil {\n\t\tt.Error(\"The proxy didn't return the expected error\")\n\t\treturn\n\t}\n\tif err != expectedError {\n\t\tt.Errorf(\"The proxy returned an unexpected error: %s\\n\", err.Error())\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"Error: expected response\")\n\tdefault:\n\t}\n}\n\nfunc TestNewRequestBuilderMiddleware_ok(t *testing.T) {\n\texpected := errors.New(\"error to be propagated\")\n\texpectedMethod := \"GET\"\n\texpectedPath := \"/supu\"\n\tassertion := func(_ context.Context, request *Request) (*Response, error) {\n\t\tif request.Method != expectedMethod {\n\t\t\terr := fmt.Errorf(\"Wrong request method. Want: %s. Have: %s\", expectedMethod, request.Method)\n\t\t\tt.Error(err.Error())\n\t\t\treturn nil, err\n\t\t}\n\t\tif request.Path != expectedPath {\n\t\t\terr := fmt.Errorf(\"Wrong request path. Want: %s. Have: %s\", expectedPath, request.Path)\n\t\t\tt.Error(err.Error())\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, expected\n\t}\n\tsampleBackend := config.Backend{\n\t\tURLPattern: expectedPath,\n\t\tMethod:     expectedMethod,\n\t}\n\tmw := NewRequestBuilderMiddleware(&sampleBackend)\n\tresponse, err := mw(assertion)(context.Background(), &Request{})\n\tif err != expected {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif response != nil {\n\t\tt.Errorf(\"We weren't expecting a response but we got one: %v\\n\", response)\n\t}\n}\n\nfunc TestDefaultHTTPResponseParserConfig_nopDecoder(t *testing.T) {\n\tresult := map[string]interface{}{}\n\tif err := DefaultHTTPResponseParserConfig.Decoder(bytes.NewBufferString(\"some body\"), &result); err != nil {\n\t\tt.Error(err.Error())\n\t}\n\tif len(result) != 0 {\n\t\tt.Error(\"unexpected result\")\n\t}\n}\n\nfunc TestDefaultHTTPResponseParserConfig_nopEntityFormatter(t *testing.T) {\n\texpected := Response{Data: map[string]interface{}{\"supu\": \"tupu\"}, IsComplete: true}\n\tresult := DefaultHTTPResponseParserConfig.EntityFormatter.Format(expected)\n\tif !result.IsComplete {\n\t\tt.Error(\"unexpected result\")\n\t}\n\td, ok := result.Data[\"supu\"]\n\tif !ok {\n\t\tt.Error(\"unexpected result\")\n\t}\n\tif v, ok := d.(string); !ok || v != \"tupu\" {\n\t\tt.Error(\"unexpected result\")\n\t}\n}\n\nfunc TestNewHTTPProxy_noopDecoder(t *testing.T) {\n\texpectedcontent := \"some nice, interesting and long content\"\n\tbackendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"header1\", \"value1\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.Write([]byte(expectedcontent))\n\t}))\n\tdefer backendServer.Close()\n\n\trpURL, _ := url.Parse(backendServer.URL)\n\tbackend := config.Backend{\n\t\tEncoding: encoding.NOOP,\n\t\tDecoder:  encoding.NoOpDecoder,\n\t}\n\trequest := Request{\n\t\tMethod: \"GET\",\n\t\tPath:   \"/\",\n\t\tURL:    rpURL,\n\t\tBody:   newDummyReadCloser(\"\"),\n\t}\n\tmustEnd := time.After(time.Duration(150) * time.Millisecond)\n\n\tresult, err := HTTPProxyFactory(http.DefaultClient)(&backend)(context.Background(), &request)\n\tif err != nil {\n\t\tt.Errorf(\"The proxy returned an unexpected error: %s\\n\", err.Error())\n\t\treturn\n\t}\n\tif result == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"Error: expected response\")\n\t\treturn\n\tdefault:\n\t}\n\n\tif len(result.Data) > 0 {\n\t\tt.Error(\"unexpected data:\", result.Data)\n\t\treturn\n\t}\n\n\tif result.Metadata.StatusCode != http.StatusOK {\n\t\tt.Error(\"unexpected status code:\", result.Metadata.StatusCode)\n\t\treturn\n\t}\n\n\tif len(result.Metadata.Headers[\"Header1\"]) < 1 || result.Metadata.Headers[\"Header1\"][0] != \"value1\" {\n\t\tt.Error(\"unexpected header:\", result.Metadata.Headers)\n\t\treturn\n\t}\n\n\tb := &bytes.Buffer{}\n\tif _, err := b.ReadFrom(result.Io); err != nil {\n\t\tt.Error(err, b.String())\n\t\treturn\n\t}\n\tif content := b.String(); content != expectedcontent {\n\t\tt.Error(\"unexpected content:\", content)\n\t}\n}\n\nfunc TestNewHTTPProxy_redirectWithBody(t *testing.T) {\n\tvar executed atomic.Uint64\n\texpectedBody := `{\"message\":\"redirected\"}`\n\texpectedResponse := `{\"message\":\"ok\"}`\n\tbackendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdata, err := io.ReadAll(r.Body)\n\t\tif err != nil {\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tif string(data) != expectedBody {\n\t\t\tt.Errorf(\"invalid data: %s\", string(data))\n\t\t\treturn\n\t\t}\n\t\texecuted.Add(1)\n\t\tw.Write([]byte(`{\"message\":\"ok\"}`))\n\t}))\n\tdefer backendServer.Close()\n\tredirServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\texecuted.Add(1)\n\t\thttp.Redirect(w, r, backendServer.URL, http.StatusPermanentRedirect)\n\t}))\n\tdefer redirServer.Close()\n\n\trpURL, _ := url.Parse(redirServer.URL)\n\tbackend := config.Backend{\n\t\tDecoder: encoding.JSONDecoder,\n\t\tExtraConfig: map[string]interface{}{\n\t\t\tclientHTTPOptions: map[string]interface{}{\n\t\t\t\tclientHTTPOptionRedirectPost: true,\n\t\t\t},\n\t\t},\n\t}\n\trequest := Request{\n\t\tMethod: \"POST\",\n\t\tPath:   \"/\",\n\t\tURL:    rpURL,\n\t\tBody:   newDummyReadCloser(expectedBody),\n\t}\n\tmustEnd := time.After(time.Duration(150) * time.Millisecond)\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(100)*time.Millisecond)\n\tdefer cancel()\n\tresponse, err := httpProxy(&backend)(ctx, &request)\n\tif err != nil {\n\t\tt.Errorf(\"The proxy propagated a backend errors: %s\\n\", err)\n\t\treturn\n\t}\n\trespData, err := json.Marshal(response.Data)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif string(respData) != expectedResponse {\n\t\tt.Errorf(\"unexpected response data: '%s'\", string(respData))\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"Error: expected response\")\n\tdefault:\n\t}\n\tif executed.Load() != 2 {\n\t\tt.Errorf(\"number of executions should be 2 not %d\", executed.Load())\n\t}\n}\n"
  },
  {
    "path": "proxy/logging.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\n// NewLoggingMiddleware creates proxy middleware for logging requests and responses\nfunc NewLoggingMiddleware(logger logging.Logger, name string) Middleware {\n\tlogPrefix := \"[\" + strings.ToUpper(name) + \"]\"\n\treturn func(next ...Proxy) Proxy {\n\t\tif len(next) > 1 {\n\t\t\tlogger.Fatal(\"too many proxies for this proxy middleware: NewLoggingMiddleware only accepts 1 proxy, got %d\", len(next))\n\t\t\treturn nil\n\t\t}\n\t\treturn func(ctx context.Context, request *Request) (*Response, error) {\n\t\t\tbegin := time.Now()\n\t\t\tlogger.Info(logPrefix, \"Calling backend\")\n\t\t\tlogger.Debug(logPrefix, \"Request\", request)\n\n\t\t\tresult, err := next[0](ctx, request)\n\n\t\t\tlogger.Info(logPrefix, \"Call to backend took\", time.Since(begin).String())\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warning(logPrefix, \"Call to backend failed:\", err.Error())\n\t\t\t\treturn result, err\n\t\t\t}\n\t\t\tif result == nil {\n\t\t\t\tlogger.Warning(logPrefix, \"Call to backend returned a null response\")\n\t\t\t}\n\n\t\t\treturn result, err\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "proxy/logging_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nfunc TestNewLoggingMiddleware_ok(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, _ := logging.NewLogger(\"DEBUG\", buff, \"pref\")\n\tresp := &Response{IsComplete: true}\n\tmw := NewLoggingMiddleware(logger, \"supu\")\n\tp := mw(dummyProxy(resp))\n\tr, err := p(context.Background(), &Request{})\n\tif r != resp {\n\t\tt.Error(\"The proxy didn't return the expected response\")\n\t\treturn\n\t}\n\tif err != nil {\n\t\tt.Errorf(\"The proxy returned an unexpected error: %s\", err.Error())\n\t\treturn\n\t}\n\tlogMsg := buff.String()\n\tif strings.Count(logMsg, \"pref\") != 3 {\n\t\tt.Error(\"The logs don't have the injected prefix\")\n\t}\n\tif strings.Count(logMsg, \"INFO\") != 2 {\n\t\tt.Error(\"The logs don't have the expected INFO messages\")\n\t}\n\tif strings.Count(logMsg, \"DEBU\") != 1 {\n\t\tt.Error(\"The logs don't have the expected DEBUG messages\")\n\t}\n\tif !strings.Contains(logMsg, \"[SUPU] Calling backend\") {\n\t\tt.Error(\"The logs didn't mark the start of the execution\")\n\t}\n\tif !strings.Contains(logMsg, \"[SUPU] Call to backend took\") {\n\t\tt.Error(\"The logs didn't mark the end of the execution\")\n\t}\n}\n\nfunc TestNewLoggingMiddleware_erroredResponse(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, _ := logging.NewLogger(\"DEBUG\", buff, \"pref\")\n\tresp := &Response{IsComplete: true}\n\tmw := NewLoggingMiddleware(logger, \"supu\")\n\texpextedError := fmt.Errorf(\"NO-body expects the %s Inquisition!\", \"Spanish\")\n\tp := mw(func(_ context.Context, _ *Request) (*Response, error) {\n\t\treturn resp, expextedError\n\t})\n\tr, err := p(context.Background(), &Request{})\n\tif r != resp {\n\t\tt.Error(\"The proxy didn't return the expected response\")\n\t\treturn\n\t}\n\tif err != expextedError {\n\t\tt.Errorf(\"The proxy didn't return the expected error: %s\", err.Error())\n\t\treturn\n\t}\n\tlogMsg := buff.String()\n\tif strings.Count(logMsg, \"pref\") != 4 {\n\t\tt.Error(\"The logs don't have the injected prefix\")\n\t}\n\tif strings.Count(logMsg, \"INFO\") != 2 {\n\t\tt.Error(\"The logs don't have the expected INFO messages\")\n\t}\n\tif strings.Count(logMsg, \"DEBU\") != 1 {\n\t\tt.Error(\"The logs don't have the expected DEBUG messages\")\n\t}\n\tif strings.Count(logMsg, \"WARN\") != 1 {\n\t\tt.Error(\"The logs don't have the expected DEBUG messages\")\n\t}\n\tif !strings.Contains(logMsg, \"[SUPU] Call to backend failed: NO-body expects the Spanish Inquisition!\") {\n\t\tt.Error(\"The logs didn't mark the fail of the execution\")\n\t}\n\tif !strings.Contains(logMsg, \"[SUPU] Calling backend\") {\n\t\tt.Error(\"The logs didn't mark the start of the execution\")\n\t}\n\tif !strings.Contains(logMsg, \"[SUPU] Call to backend took\") {\n\t\tt.Error(\"The logs didn't mark the end of the execution\")\n\t}\n}\n\nfunc TestNewLoggingMiddleware_nullResponse(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, _ := logging.NewLogger(\"DEBUG\", buff, \"pref\")\n\tmw := NewLoggingMiddleware(logger, \"supu\")\n\tp := mw(dummyProxy(nil))\n\tr, err := p(context.Background(), &Request{})\n\tif r != nil {\n\t\tt.Error(\"The proxy didn't return the expected response\")\n\t\treturn\n\t}\n\tif err != nil {\n\t\tt.Errorf(\"The proxy returned an unexpected error: %s\", err.Error())\n\t\treturn\n\t}\n\tlogMsg := buff.String()\n\tif strings.Count(logMsg, \"pref\") != 4 {\n\t\tt.Error(\"The logs don't have the injected prefix\")\n\t}\n\tif strings.Count(logMsg, \"INFO\") != 2 {\n\t\tt.Error(\"The logs don't have the expected INFO messages\")\n\t}\n\tif strings.Count(logMsg, \"DEBU\") != 1 {\n\t\tt.Error(\"The logs don't have the expected DEBUG messages\")\n\t}\n\tif strings.Count(logMsg, \"WARN\") != 1 {\n\t\tt.Error(\"The logs don't have the expected DEBUG messages\")\n\t}\n\tif !strings.Contains(logMsg, \"[SUPU] Call to backend returned a null response\") {\n\t\tt.Error(\"The logs didn't mark the fail of the execution\")\n\t}\n\tif !strings.Contains(logMsg, \"[SUPU] Calling backend\") {\n\t\tt.Error(\"The logs didn't mark the start of the execution\")\n\t}\n\tif !strings.Contains(logMsg, \"[SUPU] Call to backend took\") {\n\t\tt.Error(\"The logs didn't mark the end of the execution\")\n\t}\n}\n"
  },
  {
    "path": "proxy/merging.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\n// NewMergeDataMiddleware creates proxy middleware for merging responses from several backends\nfunc NewMergeDataMiddleware(logger logging.Logger, endpointConfig *config.EndpointConfig) Middleware { // skipcq: GO-R1005\n\ttotalBackends := len(endpointConfig.Backend)\n\tif totalBackends == 0 {\n\t\tlogger.Fatal(\"all endpoints must have at least one backend: NewMergeDataMiddleware\")\n\t\treturn nil\n\t}\n\tif totalBackends == 1 {\n\t\treturn emptyMiddlewareFallback(logger)\n\t}\n\tserviceTimeout := time.Duration(85*endpointConfig.Timeout.Nanoseconds()/100) * time.Nanosecond\n\tcombiner := getResponseCombiner(endpointConfig.ExtraConfig)\n\tisSequential, sequentialReplacements := sequentialMergerConfig(endpointConfig)\n\n\tlogger.Debug(\n\t\tfmt.Sprintf(\n\t\t\t\"[ENDPOINT: %s][Merge] Backends: %d, sequential: %t, combiner: %s\",\n\t\t\tendpointConfig.Endpoint,\n\t\t\ttotalBackends,\n\t\t\tisSequential,\n\t\t\tgetResponseCombinerName(endpointConfig.ExtraConfig),\n\t\t),\n\t)\n\n\tbfFactory := backendFiltererFactory.filtererFactory\n\n\treturn func(next ...Proxy) Proxy {\n\t\tif len(next) != totalBackends {\n\t\t\t// we leave the panic here, because we do not want to continue\n\t\t\t// if this configuration is wrong, as it would lead to unexpected\n\t\t\t// behaviour.\n\t\t\tlogger.Fatal(\"not enough proxies for this endpoint: NewMergeDataMiddleware\")\n\t\t\treturn nil\n\t\t}\n\t\treqClone := func(r *Request) *Request { res := r.Clone(); return &res }\n\n\t\tfilters, err := bfFactory(endpointConfig)\n\t\tif err != nil {\n\t\t\tlogger.Error(fmt.Sprintf(\"[ENDPOINT: %s]%s %s\", endpointConfig.Endpoint, backendFiltererFactory.logPrefix, err))\n\t\t\treturn func(_ context.Context, _ *Request) (*Response, error) { return nil, err }\n\t\t}\n\n\t\tif hasUnsafeBackends(endpointConfig) {\n\t\t\treqClone = CloneRequest\n\t\t}\n\n\t\tif !isSequential {\n\t\t\treturn parallelMerge(reqClone, serviceTimeout, combiner, filters, next...)\n\t\t}\n\n\t\treturn sequentialMerge(reqClone, serviceTimeout, combiner, sequentialReplacements, filters, next...)\n\t}\n}\n\n// BackendFiltererFactory is a factory function that returns a list of BackendFilterer\n// based on the provided EndpointConfig.\n// The returned list must be sorted by the backend index.\n// The list can contain nil values, which means that the backend in that index is untouched.\ntype BackendFiltererFactory func(*config.EndpointConfig) ([]BackendFilterer, error)\n\n// BackendFilterer evalutes the request and returns true if the backend should be used,\n// otherwise the backend is skipped in both normal and sequential merging.\n// If the backend is skipped, the response will not be merged into the final response.\ntype BackendFilterer func(*Request) bool\n\nfunc defaultBackendFiltererFactory(_ *config.EndpointConfig) ([]BackendFilterer, error) {\n\treturn []BackendFilterer{}, nil\n}\n\ntype backendFiltererRegistry struct {\n\tlogPrefix       string\n\tfiltererFactory BackendFiltererFactory\n}\n\nvar backendFiltererFactory = backendFiltererRegistry{\n\tfiltererFactory: defaultBackendFiltererFactory,\n}\n\n// RegisterBackendFiltererFactory registers a new backend filterer factory\n// to be used by the merging middleware.\n// This factory is used to create a list of BackendFilterer\n// functions that will be used to filter backends based on the request.\n// Important: this function should be called everytime the middleware is created.\nfunc RegisterBackendFiltererFactory(logPrefix string, f BackendFiltererFactory) {\n\tbackendFiltererFactory.logPrefix = logPrefix\n\tbackendFiltererFactory.filtererFactory = f\n}\n\nfunc ResetBackendFiltererFactory() {\n\tbackendFiltererFactory.logPrefix = \"\"\n\tbackendFiltererFactory.filtererFactory = defaultBackendFiltererFactory\n}\n\ntype sequentialBackendReplacement struct {\n\tbackendIndex int\n\tdestination  string\n\tsource       []string\n\tfullResponse bool\n}\n\nfunc sequentialMergerConfig(cfg *config.EndpointConfig) (bool, [][]sequentialBackendReplacement) { // skipcq: GO-R1005\n\tenabled := false\n\ttotalBackends := len(cfg.Backend)\n\tsequentialReplacements := make([][]sequentialBackendReplacement, totalBackends)\n\tvar propagatedParams []string\n\n\tif v, ok := cfg.ExtraConfig[Namespace]; ok {\n\t\tif e, ok := v.(map[string]interface{}); ok {\n\t\t\tif v, ok := e[isSequentialKey]; ok {\n\t\t\t\tc, ok := v.(bool)\n\t\t\t\tenabled = ok && c\n\t\t\t}\n\t\t\tif v, ok := e[sequentialPropagateKey]; ok {\n\t\t\t\tif a, ok := v.([]interface{}); ok {\n\t\t\t\t\tfor _, p := range a {\n\t\t\t\t\t\tpropagatedParams = append(propagatedParams, p.(string))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tvar rePropagatedParams = regexp.MustCompile(`[Rr]esp(\\d+)_?([\\w-.]+)?`)\n\tvar reUrlPatterns = regexp.MustCompile(`\\{\\{\\.Resp(\\d+)_([\\w-.]+)\\}\\}`)\n\tdestKeyGenerator := func(i string, t string) string {\n\t\tkey := \"Resp\" + i\n\t\tif t != \"\" {\n\t\t\tkey += \"_\" + t\n\t\t}\n\t\treturn key\n\t}\n\n\tfor i, b := range cfg.Backend {\n\t\tfor _, match := range reUrlPatterns.FindAllStringSubmatch(b.URLPattern, -1) {\n\t\t\tif len(match) > 1 {\n\t\t\t\tbackendIndex, err := strconv.Atoi(match[1])\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tsequentialReplacements[i] = append(sequentialReplacements[i], sequentialBackendReplacement{\n\t\t\t\t\tbackendIndex: backendIndex,\n\t\t\t\t\tdestination:  destKeyGenerator(match[1], match[2]),\n\t\t\t\t\tsource:       strings.Split(match[2], \".\"),\n\t\t\t\t\tfullResponse: match[2] == \"\",\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tif i > 0 {\n\t\t\tfor _, p := range propagatedParams {\n\t\t\t\tfor _, match := range rePropagatedParams.FindAllStringSubmatch(p, -1) {\n\t\t\t\t\tif len(match) > 1 {\n\t\t\t\t\t\tbackendIndex, err := strconv.Atoi(match[1])\n\t\t\t\t\t\tif err != nil || backendIndex >= totalBackends {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsequentialReplacements[i] = append(sequentialReplacements[i], sequentialBackendReplacement{\n\t\t\t\t\t\t\tbackendIndex: backendIndex,\n\t\t\t\t\t\t\tdestination:  destKeyGenerator(match[1], match[2]),\n\t\t\t\t\t\t\tsource:       strings.Split(match[2], \".\"),\n\t\t\t\t\t\t\tfullResponse: match[2] == \"\",\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn enabled, sequentialReplacements\n}\n\nfunc hasUnsafeBackends(cfg *config.EndpointConfig) bool {\n\tif len(cfg.Backend) == 1 {\n\t\treturn false\n\t}\n\n\tfor _, b := range cfg.Backend {\n\t\tif m := strings.ToUpper(b.Method); m != http.MethodGet && m != http.MethodHead {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc parallelMerge(\n\treqCloner func(*Request) *Request,\n\ttimeout time.Duration,\n\trc ResponseCombiner,\n\tfilters []BackendFilterer,\n\tnext ...Proxy,\n) Proxy {\n\treturn func(ctx context.Context, request *Request) (*Response, error) {\n\t\tlocalCtx, cancel := context.WithTimeout(ctx, timeout)\n\n\t\tproxyCount := len(next)\n\t\tfilterCount := len(filters)\n\n\t\tparts := make(chan *Response, proxyCount)\n\t\tfailed := make(chan error, proxyCount)\n\n\t\tfor i, n := range next {\n\t\t\tif (i < filterCount) && (filters[i] != nil) && !filters[i](request) {\n\t\t\t\tproxyCount--\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tgo requestPart(localCtx, n, reqCloner(request), parts, failed)\n\t\t}\n\n\t\tacc := newIncrementalMergeAccumulator(proxyCount, rc)\n\t\tfor i := 0; i < proxyCount; i++ {\n\t\t\tselect {\n\t\t\tcase err := <-failed:\n\t\t\t\tacc.Merge(nil, err)\n\t\t\tcase response := <-parts:\n\t\t\t\tacc.Merge(response, nil)\n\t\t\t}\n\t\t}\n\n\t\tresult, err := acc.Result()\n\t\tcancel()\n\t\treturn result, err\n\t}\n}\n\nfunc sequentialMerge( // skipcq: GO-R1005\n\treqCloner func(*Request) *Request,\n\ttimeout time.Duration,\n\trc ResponseCombiner,\n\tsequentialReplacements [][]sequentialBackendReplacement,\n\tfilters []BackendFilterer,\n\tnext ...Proxy,\n) Proxy {\n\treturn func(ctx context.Context, request *Request) (*Response, error) {\n\t\tlocalCtx, cancel := context.WithTimeout(ctx, timeout)\n\n\t\tfilterCount := len(filters)\n\t\tparts := make([]*Response, len(next))\n\t\tout := make(chan *Response, 1)\n\t\terrCh := make(chan error, 1)\n\t\tsequentialMergeRegistry := map[string]string{}\n\n\t\tacc := newIncrementalMergeAccumulator(len(next), rc)\n\tTxLoop:\n\t\tfor i, n := range next {\n\t\t\tif i > 0 {\n\t\t\t\tfor _, r := range sequentialReplacements[i] {\n\t\t\t\t\tif r.backendIndex >= i || parts[r.backendIndex] == nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tvar v interface{}\n\t\t\t\t\tvar ok bool\n\n\t\t\t\t\tdata := parts[r.backendIndex].Data\n\t\t\t\t\tif len(r.source) > 1 {\n\t\t\t\t\t\tfor _, k := range r.source[:len(r.source)-1] {\n\t\t\t\t\t\t\tv, ok = data[k]\n\t\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tclean, ok := v.(map[string]interface{})\n\t\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdata = clean\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif found := sequentialMergeRegistry[r.destination]; found != \"\" {\n\t\t\t\t\t\trequest.Params[r.destination] = found\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tif r.fullResponse {\n\t\t\t\t\t\tif parts[r.backendIndex].Io == nil {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbuf, err := io.ReadAll(parts[r.backendIndex].Io)\n\n\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\trequest.Params[r.destination] = string(buf)\n\t\t\t\t\t\t\tsequentialMergeRegistry[r.destination] = string(buf)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tv, ok = data[r.source[len(r.source)-1]]\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tvar param string\n\n\t\t\t\t\tswitch clean := v.(type) {\n\t\t\t\t\tcase []interface{}:\n\t\t\t\t\t\tif len(clean) == 0 {\n\t\t\t\t\t\t\trequest.Params[r.destination] = \"\"\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvar b strings.Builder\n\t\t\t\t\t\tfor i := 0; i < len(clean)-1; i++ {\n\t\t\t\t\t\t\tfmt.Fprintf(&b, \"%v,\", clean[i])\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfmt.Fprintf(&b, \"%v\", clean[len(clean)-1])\n\t\t\t\t\t\tparam = b.String()\n\t\t\t\t\tcase string:\n\t\t\t\t\t\tparam = clean\n\t\t\t\t\tcase int:\n\t\t\t\t\t\tparam = strconv.Itoa(clean)\n\t\t\t\t\tcase float64:\n\t\t\t\t\t\tparam = strconv.FormatFloat(clean, 'E', -1, 32)\n\t\t\t\t\tcase bool:\n\t\t\t\t\t\tparam = strconv.FormatBool(clean)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tparam = fmt.Sprintf(\"%v\", v)\n\t\t\t\t\t}\n\t\t\t\t\trequest.Params[r.destination] = param\n\t\t\t\t\tsequentialMergeRegistry[r.destination] = param\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (i < filterCount) && (filters[i] != nil) && !filters[i](request) {\n\t\t\t\tparts[i] = &Response{IsComplete: true, Data: make(map[string]interface{})}\n\t\t\t\tacc.pending--\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsequentialRequestPart(localCtx, n, reqCloner(request), out, errCh)\n\n\t\t\tselect {\n\t\t\tcase err := <-errCh:\n\t\t\t\tif i == 0 {\n\t\t\t\t\tcancel()\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tacc.Merge(nil, err)\n\t\t\t\tbreak TxLoop\n\t\t\tcase response := <-out:\n\t\t\t\tacc.Merge(response, nil)\n\t\t\t\tif !response.IsComplete {\n\t\t\t\t\tbreak TxLoop\n\t\t\t\t}\n\t\t\t\tparts[i] = response\n\t\t\t}\n\t\t}\n\n\t\tresult, err := acc.Result()\n\t\tcancel()\n\t\treturn result, err\n\t}\n}\n\ntype incrementalMergeAccumulator struct {\n\tpending  int\n\tdata     *Response\n\tcombiner ResponseCombiner\n\terrs     []error\n}\n\nfunc newIncrementalMergeAccumulator(total int, combiner ResponseCombiner) *incrementalMergeAccumulator {\n\treturn &incrementalMergeAccumulator{\n\t\tpending:  total,\n\t\tcombiner: combiner,\n\t\terrs:     []error{},\n\t}\n}\n\nfunc (i *incrementalMergeAccumulator) Merge(res *Response, err error) {\n\ti.pending--\n\tif err != nil {\n\t\ti.errs = append(i.errs, err)\n\t\tif i.data != nil {\n\t\t\ti.data.IsComplete = false\n\t\t}\n\t\treturn\n\t}\n\tif res == nil {\n\t\ti.errs = append(i.errs, errNullResult)\n\t\treturn\n\t}\n\tif i.data == nil {\n\t\ti.data = res\n\t\treturn\n\t}\n\ti.data = i.combiner(2, []*Response{i.data, res})\n}\n\nfunc (i *incrementalMergeAccumulator) Result() (*Response, error) {\n\tif i.data == nil {\n\t\treturn nil, newMergeError(i.errs)\n\t}\n\n\tif i.pending > 0 || len(i.errs) > 0 {\n\t\ti.data.IsComplete = false\n\t}\n\treturn i.data, newMergeError(i.errs)\n}\n\nfunc requestPart(ctx context.Context, next Proxy, request *Request, out chan<- *Response, failed chan<- error) {\n\tlocalCtx, cancel := context.WithCancel(ctx)\n\n\tin, err := next(localCtx, request)\n\tif err != nil {\n\t\tfailed <- err\n\t\tcancel()\n\t\treturn\n\t}\n\tif in == nil {\n\t\tfailed <- errNullResult\n\t\tcancel()\n\t\treturn\n\t}\n\tselect {\n\tcase out <- in:\n\tcase <-ctx.Done():\n\t\tfailed <- ctx.Err()\n\t}\n\tcancel()\n}\n\nfunc sequentialRequestPart(ctx context.Context, next Proxy, request *Request, out chan<- *Response, failed chan<- error) {\n\tcopyRequest := CloneRequest(request)\n\n\tin, err := next(ctx, request)\n\n\t*request = *copyRequest\n\n\tif err != nil {\n\t\tfailed <- err\n\t\treturn\n\t}\n\tif in == nil {\n\t\tfailed <- errNullResult\n\t\treturn\n\t}\n\tselect {\n\tcase out <- in:\n\tcase <-ctx.Done():\n\t\tfailed <- ctx.Err()\n\t}\n}\n\nfunc newMergeError(errs []error) error {\n\tif len(errs) == 0 {\n\t\treturn nil\n\t}\n\treturn mergeError{errs}\n}\n\ntype mergeError struct {\n\terrs []error\n}\n\nfunc (m mergeError) Error() string {\n\tmsg := make([]string, len(m.errs))\n\tfor i, err := range m.errs {\n\t\tmsg[i] = err.Error()\n\t}\n\treturn strings.Join(msg, \"\\n\")\n}\n\nfunc (m mergeError) Errors() []error {\n\treturn m.errs\n}\n\n// ResponseCombiner func to merge the collected responses into a single one\ntype ResponseCombiner func(int, []*Response) *Response\n\n// RegisterResponseCombiner adds a new response combiner into the internal register\nfunc RegisterResponseCombiner(name string, f ResponseCombiner) {\n\tresponseCombiners.SetResponseCombiner(name, f)\n}\n\nconst (\n\tmergeKey               = \"combiner\"\n\tisSequentialKey        = \"sequential\"\n\tsequentialPropagateKey = \"sequential_propagated_params\"\n\tdefaultCombinerName    = \"default\"\n)\n\nvar responseCombiners = initResponseCombiners()\n\nfunc initResponseCombiners() *combinerRegister {\n\treturn newCombinerRegister(map[string]ResponseCombiner{defaultCombinerName: combineData}, combineData)\n}\n\nfunc getResponseCombinerName(extra config.ExtraConfig) string {\n\tif v, ok := extra[Namespace]; ok {\n\t\tif e, ok := v.(map[string]interface{}); ok {\n\t\t\tif v, ok := e[mergeKey]; ok {\n\t\t\t\tif _, ok := responseCombiners.GetResponseCombiner(v.(string)); ok {\n\t\t\t\t\treturn v.(string)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn defaultCombinerName\n}\n\nfunc getResponseCombiner(extra config.ExtraConfig) ResponseCombiner {\n\tcombiner := getResponseCombinerName(extra)\n\tc, _ := responseCombiners.GetResponseCombiner(combiner)\n\treturn c\n}\n\nfunc combineData(total int, parts []*Response) *Response {\n\tisComplete := len(parts) == total\n\tvar retResponse *Response\n\tfor _, part := range parts {\n\t\tif part == nil || part.Data == nil {\n\t\t\tisComplete = false\n\t\t\tcontinue\n\t\t}\n\t\tisComplete = isComplete && part.IsComplete\n\t\tif retResponse == nil {\n\t\t\tretResponse = &Response{Data: part.Data, IsComplete: isComplete}\n\t\t\tcontinue\n\t\t}\n\t\tfor k, v := range part.Data {\n\t\t\tretResponse.Data[k] = v\n\t\t}\n\t}\n\n\tif nil == retResponse {\n\t\t// do not allow nil data in the response:\n\t\treturn &Response{Data: make(map[string]interface{}), IsComplete: isComplete}\n\t}\n\tretResponse.IsComplete = isComplete\n\treturn retResponse\n}\n"
  },
  {
    "path": "proxy/merging_benchmark_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nfunc BenchmarkNewMergeDataMiddleware(b *testing.B) {\n\tbackend := config.Backend{}\n\tbackends := make([]*config.Backend, 10)\n\tfor i := range backends {\n\t\tbackends[i] = &backend\n\t}\n\n\tproxies := []Proxy{\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t}\n\n\tfor _, totalParts := range []int{2, 3, 4, 5, 6, 7, 8, 9, 10} {\n\t\tb.Run(fmt.Sprintf(\"with %d parts\", totalParts), func(b *testing.B) {\n\t\t\tendpoint := config.EndpointConfig{\n\t\t\t\tBackend: backends[:totalParts],\n\t\t\t\tTimeout: time.Duration(100) * time.Millisecond,\n\t\t\t}\n\t\t\tproxy := NewMergeDataMiddleware(logging.NoOp, &endpoint)(proxies[:totalParts]...)\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\tproxy(context.Background(), &Request{Params: map[string]string{}})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkNewMergeDataMiddleware_sequential(b *testing.B) {\n\tbackends := make([]*config.Backend, 10)\n\tpattern := \"/some\"\n\tvar keys []string\n\n\tfor i := range backends {\n\t\tb := &config.Backend{\n\t\t\tURLKeys:    make([]string, 4*i),\n\t\t\tURLPattern: pattern,\n\t\t}\n\t\tcopy(b.URLKeys, keys)\n\t\tbackends[i] = b\n\t\tfor _, t := range []string{\"int\", \"float\", \"bool\", \"string\"} {\n\t\t\tnext := fmt.Sprintf(\"Resp%d_%s\", i, t)\n\t\t\tpattern += \"/{{.\" + next + \"}}\"\n\t\t\tkeys = append(keys, next)\n\t\t}\n\t}\n\n\tproxies := []Proxy{\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"int\": 1, \"float\": 3.14, \"bool\": true, \"string\": \"wwwww\"}, IsComplete: true}),\n\t}\n\n\tfor _, totalParts := range []int{2, 3, 4, 5, 6, 7, 8, 9, 10} {\n\t\tb.Run(fmt.Sprintf(\"with %d parts\", totalParts), func(b *testing.B) {\n\t\t\tendpoint := config.EndpointConfig{\n\t\t\t\tBackend: backends[:totalParts],\n\t\t\t\tTimeout: time.Duration(100) * time.Millisecond,\n\t\t\t\tExtraConfig: config.ExtraConfig{\n\t\t\t\t\tNamespace: map[string]interface{}{\n\t\t\t\t\t\tisSequentialKey: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tproxy := NewMergeDataMiddleware(logging.NoOp, &endpoint)(proxies[:totalParts]...)\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\tproxy(context.Background(), &Request{Params: map[string]string{}})\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "proxy/merging_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nfunc TestNewMergeDataMiddleware(t *testing.T) {\n\ttests := []func(t *testing.T){\n\t\ttestNewMergeDataMiddleware_simpleFiltering,\n\t\ttestNewMergeDataMiddleware_sequentialFiltering,\n\t\ttestNewMergeDataMiddleware_empty,\n\t\ttestNewMergeDataMiddleware_ok,\n\t\ttestNewMergeDataMiddleware_sequential,\n\t\ttestNewMergeDataMiddleware_sequential_unavailableParams,\n\t\ttestNewMergeDataMiddleware_sequential_erroredBackend,\n\t\ttestNewMergeDataMiddleware_sequential_erroredFirstBackend,\n\t\ttestNewMergeDataMiddleware_mergeIncompleteResults,\n\t\ttestNewMergeDataMiddleware_mergeEmptyResults,\n\t\ttestNewMergeDataMiddleware_partialTimeout,\n\t\ttestNewMergeDataMiddleware_partial,\n\t\ttestNewMergeDataMiddleware_nullResponse,\n\t\ttestNewMergeDataMiddleware_timeout,\n\t\ttestRegisterResponseCombiner,\n\t}\n\tfor _, test := range tests {\n\t\tResetBackendFiltererFactory()\n\t\ttest(t)\n\t}\n}\n\nfunc testNewMergeDataMiddleware_empty(t *testing.T) {\n\ttimeout := 500 * time.Millisecond\n\tbackend := config.Backend{}\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{&backend, &backend},\n\t\tTimeout: timeout,\n\t}\n\n\texpectedErr := errors.New(\"wait for me\")\n\n\terroredProxy := func(_ context.Context, _ *Request) (*Response, error) {\n\t\treturn nil, expectedErr\n\t}\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\tp := mw(erroredProxy, erroredProxy)\n\n\tmustEnd := time.After(2 * timeout)\n\tout, err := p(context.Background(), &Request{})\n\tmErr, ok := err.(mergeError)\n\tif !ok {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err)\n\t\treturn\n\t}\n\tif len(mErr.errs) != 2 {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err)\n\t\treturn\n\t}\n\tif mErr.errs[0] != mErr.errs[1] || mErr.errs[0] != expectedErr {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err)\n\t\treturn\n\t}\n\tif out != nil {\n\t\tt.Errorf(\"The proxy returned a result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t}\n\n}\n\nfunc testNewMergeDataMiddleware_ok(t *testing.T) {\n\ttimeout := 500\n\tbackend := config.Backend{}\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{&backend, &backend},\n\t\tTimeout: time.Duration(timeout) * time.Millisecond,\n\t}\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\tp := mw(\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"supu\": 42}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"tupu\": true}, IsComplete: true}))\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\tout, err := p(context.Background(), &Request{})\n\tif err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif out == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t\tif len(out.Data) != 2 {\n\t\t\tt.Errorf(\"We weren't expecting a partial response but we got %v!\\n\", out)\n\t\t}\n\t\tif !out.IsComplete {\n\t\t\tt.Errorf(\"We were expecting a completed response but we got an incompleted one!\\n\")\n\t\t}\n\t}\n}\n\nfunc testNewMergeDataMiddleware_sequential(t *testing.T) {\n\ttimeout := 1000\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{\n\t\t\t{URLPattern: \"/\"},\n\t\t\t{URLPattern: \"/aaa/{{.Resp0_array}}\"},\n\t\t\t{URLPattern: \"/aaa/{{.Resp0_int}}/{{.Resp0_string}}/{{.Resp0_bool}}/{{.Resp0_float}}/{{.Resp0_struct.foo}}\"},\n\t\t\t{URLPattern: \"/aaa/{{.Resp0_int}}/{{.Resp0_string}}/{{.Resp0_bool}}/{{.Resp0_float}}/{{.Resp0_struct.foo}}?x={{.Resp1_tupu}}\"},\n\t\t\t{URLPattern: \"/aaa/{{.Resp0_struct.foo}}/{{.Resp0_struct.struct.foo}}/{{.Resp0_struct.struct.struct.foo}}\"},\n\t\t\t{URLPattern: \"/zzz\", Encoding: \"no-op\"},\n\t\t\t{URLPattern: \"/hit-me\"},\n\t\t},\n\t\tTimeout: time.Duration(timeout) * time.Millisecond,\n\t\tExtraConfig: config.ExtraConfig{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\tisSequentialKey:        true,\n\t\t\t\tsequentialPropagateKey: []interface{}{\"resp0_propagated\", \"resp5\"},\n\t\t\t},\n\t\t},\n\t}\n\n\texpectedBody := \"foo\"\n\tcheckBody := func(t *testing.T, r *Request) {\n\t\tif r.Body == nil {\n\t\t\tt.Error(\"empty body\")\n\t\t\treturn\n\t\t}\n\t\tb, err := io.ReadAll(r.Body)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\n\t\tr.Body.Close()\n\n\t\tif string(b) != expectedBody {\n\t\t\tt.Errorf(\"unexpected body '%s'\", string(b))\n\t\t}\n\t}\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\tp := mw(\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\n\t\t\t\"int\":    42,\n\t\t\t\"string\": \"some\",\n\t\t\t\"bool\":   true,\n\t\t\t\"float\":  3.14,\n\t\t\t\"struct\": map[string]interface{}{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t\"struct\": map[string]interface{}{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t\t\"struct\": map[string]interface{}{\n\t\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"array\":      []interface{}{\"1\", \"2\"},\n\t\t\t\"propagated\": \"everywhere\",\n\t\t}, IsComplete: true}),\n\t\tfunc(_ context.Context, r *Request) (*Response, error) {\n\t\t\tcheckBody(t, r)\n\t\t\tcheckRequestParam(t, r, \"Resp0_array\", \"1,2\")\n\t\t\tcheckRequestParam(t, r, \"Resp0_propagated\", \"everywhere\")\n\t\t\treturn &Response{Data: map[string]interface{}{\"tupu\": \"foo\"}, IsComplete: true}, nil\n\t\t},\n\t\tfunc(_ context.Context, r *Request) (*Response, error) {\n\t\t\tcheckBody(t, r)\n\t\t\tcheckRequestParam(t, r, \"Resp0_int\", \"42\")\n\t\t\tcheckRequestParam(t, r, \"Resp0_string\", \"some\")\n\t\t\tcheckRequestParam(t, r, \"Resp0_float\", \"3.14E+00\")\n\t\t\tcheckRequestParam(t, r, \"Resp0_bool\", \"true\")\n\t\t\tcheckRequestParam(t, r, \"Resp0_struct.foo\", \"bar\")\n\t\t\tcheckRequestParam(t, r, \"Resp0_propagated\", \"everywhere\")\n\t\t\treturn &Response{Data: map[string]interface{}{\"tupu\": \"foo\"}, IsComplete: true}, nil\n\t\t},\n\t\tfunc(_ context.Context, r *Request) (*Response, error) {\n\t\t\tcheckBody(t, r)\n\t\t\tcheckRequestParam(t, r, \"Resp0_int\", \"42\")\n\t\t\tcheckRequestParam(t, r, \"Resp0_string\", \"some\")\n\t\t\tcheckRequestParam(t, r, \"Resp0_float\", \"3.14E+00\")\n\t\t\tcheckRequestParam(t, r, \"Resp0_bool\", \"true\")\n\t\t\tcheckRequestParam(t, r, \"Resp0_struct.foo\", \"bar\")\n\t\t\tcheckRequestParam(t, r, \"Resp1_tupu\", \"foo\")\n\t\t\tcheckRequestParam(t, r, \"Resp0_propagated\", \"everywhere\")\n\t\t\treturn &Response{Data: map[string]interface{}{\"aaaa\": []int{1, 2, 3}}, IsComplete: true}, nil\n\t\t},\n\t\tfunc(_ context.Context, r *Request) (*Response, error) {\n\t\t\tcheckBody(t, r)\n\t\t\tcheckRequestParam(t, r, \"Resp0_struct.foo\", \"bar\")\n\t\t\tcheckRequestParam(t, r, \"Resp0_struct.struct.foo\", \"bar\")\n\t\t\tcheckRequestParam(t, r, \"Resp0_struct.struct.struct.foo\", \"bar\")\n\t\t\tcheckRequestParam(t, r, \"Resp0_propagated\", \"everywhere\")\n\t\t\treturn &Response{Data: map[string]interface{}{\"bbbb\": []bool{true, false}}, IsComplete: true}, nil\n\t\t},\n\t\tfunc(_ context.Context, r *Request) (*Response, error) {\n\t\t\tcheckBody(t, r)\n\t\t\tcheckRequestParam(t, r, \"Resp0_propagated\", \"everywhere\")\n\t\t\treturn &Response{Data: map[string]interface{}{}, Io: io.NopCloser(strings.NewReader(\"hello\")), IsComplete: true}, nil\n\t\t},\n\t\tfunc(_ context.Context, r *Request) (*Response, error) {\n\t\t\tcheckBody(t, r)\n\t\t\tcheckRequestParam(t, r, \"Resp0_propagated\", \"everywhere\")\n\t\t\tcheckRequestParam(t, r, \"Resp5\", \"hello\")\n\t\t\treturn &Response{Data: map[string]interface{}{}, IsComplete: true}, nil\n\t\t},\n\t)\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\tout, err := p(context.Background(), &Request{\n\t\tParams: map[string]string{},\n\t\tBody:   io.NopCloser(strings.NewReader(expectedBody)),\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif out == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t\tif len(out.Data) != 10 {\n\t\t\tt.Errorf(\"We weren't expecting a partial response but we got %v!\\n\", out)\n\t\t}\n\t\tif !out.IsComplete {\n\t\t\tt.Errorf(\"We were expecting a completed response but we got an incompleted one!\\n\")\n\t\t}\n\t}\n}\n\nfunc checkRequestParam(t *testing.T, r *Request, k, v string) {\n\tif r.Params[k] != v {\n\t\tt.Errorf(\"request without the expected set of params: %s - %+v\", k, r.Params)\n\t}\n}\n\nfunc testNewMergeDataMiddleware_sequential_unavailableParams(t *testing.T) {\n\ttimeout := 500\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{\n\t\t\t{URLPattern: \"/\"},\n\t\t\t{URLPattern: \"/aaa/{{.Resp2_supu}\"},\n\t\t\t{URLPattern: \"/aaa/{{.Resp0_tupu}}?x={{.Resp1_tupu}}\"},\n\t\t},\n\t\tTimeout: time.Duration(timeout) * time.Millisecond,\n\t\tExtraConfig: config.ExtraConfig{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\tisSequentialKey: true,\n\t\t\t},\n\t\t},\n\t}\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\tp := mw(\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"supu\": 42}, IsComplete: true}),\n\t\tfunc(_ context.Context, r *Request) (*Response, error) {\n\t\t\tif v, ok := r.Params[\"Resp0_supu\"]; ok || v != \"\" {\n\t\t\t\tt.Errorf(\"request with unexpected set of params\")\n\t\t\t}\n\t\t\treturn &Response{Data: map[string]interface{}{\"tupu\": \"foo\"}, IsComplete: true}, nil\n\t\t},\n\t\tfunc(_ context.Context, r *Request) (*Response, error) {\n\t\t\tif v, ok := r.Params[\"Resp0_supu\"]; ok || v != \"\" {\n\t\t\t\tt.Errorf(\"request with unexpected set of params\")\n\t\t\t}\n\t\t\tif r.Params[\"Respo_tupu\"] != \"\" {\n\t\t\t\tt.Errorf(\"request without the expected set of params\")\n\t\t\t}\n\t\t\tif r.Params[\"Resp1_tupu\"] != \"foo\" {\n\t\t\t\tt.Errorf(\"request without the expected set of params\")\n\t\t\t}\n\t\t\treturn &Response{Data: map[string]interface{}{\"aaaa\": []int{1, 2, 3}}, IsComplete: true}, nil\n\t\t},\n\t)\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\tout, err := p(context.Background(), &Request{Params: map[string]string{}})\n\tif err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif out == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t\tif len(out.Data) != 3 {\n\t\t\tt.Errorf(\"We weren't expecting a partial response but we got %v!\\n\", out)\n\t\t}\n\t\tif !out.IsComplete {\n\t\t\tt.Errorf(\"We were expecting a completed response but we got an incompleted one!\\n\")\n\t\t}\n\t}\n}\n\nfunc testNewMergeDataMiddleware_sequential_erroredBackend(t *testing.T) {\n\ttimeout := 500\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{\n\t\t\t{URLPattern: \"/\"},\n\t\t\t{URLPattern: \"/aaa/{{.Resp0_supu}}\"},\n\t\t\t{URLPattern: \"/aaa/{{.Resp0_supu}}?x={{.Resp1_tupu}}\"},\n\t\t},\n\t\tTimeout: time.Duration(timeout) * time.Millisecond,\n\t\tExtraConfig: config.ExtraConfig{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\tisSequentialKey: true,\n\t\t\t},\n\t\t},\n\t}\n\texpecterErr := errors.New(\"wait for me\")\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\tp := mw(\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"supu\": 42}, IsComplete: true}),\n\t\tfunc(_ context.Context, r *Request) (*Response, error) {\n\t\t\tif r.Params[\"Resp0_supu\"] != \"42\" {\n\t\t\t\tt.Errorf(\"request without the expected set of params\")\n\t\t\t}\n\t\t\treturn nil, expecterErr\n\t\t},\n\t\tfunc(_ context.Context, _ *Request) (*Response, error) {\n\t\t\treturn nil, nil\n\t\t},\n\t)\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\tout, err := p(context.Background(), &Request{Params: map[string]string{}})\n\tif err == nil {\n\t\tt.Errorf(\"The middleware did not propagate an error\\n\")\n\t\treturn\n\t}\n\tif out == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t\tif len(out.Data) != 1 {\n\t\t\tt.Errorf(\"We weren't expecting a partial response but we got %v!\\n\", out)\n\t\t}\n\t\tif out.IsComplete {\n\t\t\tt.Errorf(\"We were not expecting a completed response!\\n\")\n\t\t}\n\t}\n}\n\nfunc testNewMergeDataMiddleware_sequential_erroredFirstBackend(t *testing.T) {\n\ttimeout := 500\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{\n\t\t\t{URLPattern: \"/\"},\n\t\t\t{URLPattern: \"/aaa/{{.Resp0_supu}}\"},\n\t\t\t{URLPattern: \"/aaa/{{.Resp0_supu}}?x={{.Resp1_tupu}}\"},\n\t\t},\n\t\tTimeout: time.Duration(timeout) * time.Millisecond,\n\t\tExtraConfig: config.ExtraConfig{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\tisSequentialKey: true,\n\t\t\t},\n\t\t},\n\t}\n\texpecterErr := errors.New(\"wait for me\")\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\tp := mw(\n\t\tfunc(_ context.Context, _ *Request) (*Response, error) {\n\t\t\treturn nil, expecterErr\n\t\t},\n\t\tfunc(_ context.Context, _ *Request) (*Response, error) {\n\t\t\tt.Error(\"this backend should never be called\")\n\t\t\treturn nil, nil\n\t\t},\n\t\tfunc(_ context.Context, _ *Request) (*Response, error) {\n\t\t\tt.Error(\"this backend should never be called\")\n\t\t\treturn nil, nil\n\t\t},\n\t)\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\tout, err := p(context.Background(), &Request{Params: map[string]string{}})\n\tif err != expecterErr {\n\t\tt.Errorf(\"The middleware did not propagate the expected error: %v\\n\", err)\n\t\treturn\n\t}\n\tif out != nil {\n\t\tt.Errorf(\"The proxy returned a not null result %v\", out)\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t}\n}\n\nfunc testNewMergeDataMiddleware_mergeIncompleteResults(t *testing.T) {\n\ttimeout := 500\n\tbackend := config.Backend{}\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{&backend, &backend},\n\t\tTimeout: time.Duration(timeout) * time.Millisecond,\n\t}\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\tp := mw(\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"supu\": 42}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"tupu\": true}, IsComplete: false}))\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\tout, err := p(context.Background(), &Request{})\n\tif err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif out == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t\tif len(out.Data) != 2 {\n\t\t\tt.Errorf(\"We were expecting incomplete results merged but we got %v!\\n\", out.Data)\n\t\t}\n\t\tif out.IsComplete {\n\t\t\tt.Errorf(\"We were expecting an incomplete response but we got a completed one!\\n\")\n\t\t}\n\t}\n}\n\nfunc testNewMergeDataMiddleware_mergeEmptyResults(t *testing.T) {\n\ttimeout := 500\n\tbackend := config.Backend{}\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{&backend, &backend},\n\t\tTimeout: time.Duration(timeout) * time.Millisecond,\n\t}\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\tp := mw(\n\t\tdummyProxy(&Response{Data: nil, IsComplete: false}),\n\t\tdummyProxy(&Response{Data: nil, IsComplete: false}))\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\tout, err := p(context.Background(), &Request{})\n\tif err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif out == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t\tif len(out.Data) != 0 {\n\t\t\tt.Errorf(\"We were expecting empty data but we got %v!\\n\", out)\n\t\t}\n\t\tif out.IsComplete {\n\t\t\tt.Errorf(\"We were expecting an incomplete response but we got an incompleted one!\\n\")\n\t\t}\n\t}\n}\n\nfunc testNewMergeDataMiddleware_partialTimeout(t *testing.T) {\n\ttimeout := 100\n\tbackend := config.Backend{Timeout: time.Duration(timeout) * time.Millisecond}\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{&backend, &backend},\n\t\tTimeout: time.Duration(timeout) * time.Millisecond,\n\t}\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\tp := mw(\n\t\tdelayedProxy(t, time.Duration(timeout/2)*time.Millisecond, &Response{Data: map[string]interface{}{\"supu\": 42}, IsComplete: true}),\n\t\tdelayedProxy(t, time.Duration(5*timeout)*time.Millisecond, nil))\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\tout, err := p(context.Background(), &Request{})\n\tif err == nil || err.Error() != \"context deadline exceeded\" {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif out == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t\tif len(out.Data) != 1 {\n\t\t\tt.Errorf(\"We were expecting a partial response but we got %v!\\n\", out)\n\t\t}\n\t\tif out.IsComplete {\n\t\t\tt.Errorf(\"We were expecting an incompleted response but we got a completed one!\\n\")\n\t\t}\n\t}\n}\n\nfunc testNewMergeDataMiddleware_partial(t *testing.T) {\n\ttimeout := 100\n\tbackend := config.Backend{Timeout: time.Duration(timeout) * time.Millisecond}\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{&backend, &backend},\n\t\tTimeout: time.Duration(timeout) * time.Millisecond,\n\t}\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\tp := mw(\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"supu\": 42}, IsComplete: true}),\n\t\tdummyProxy(&Response{}))\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\tout, err := p(context.Background(), &Request{})\n\tif err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif out == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t\tif len(out.Data) != 1 {\n\t\t\tt.Errorf(\"We were expecting a partial response but we got %v!\\n\", out)\n\t\t}\n\t\tif out.IsComplete {\n\t\t\tt.Errorf(\"We were expecting an incompleted response but we got a completed one!\\n\")\n\t\t}\n\t}\n}\n\nfunc testNewMergeDataMiddleware_nullResponse(t *testing.T) {\n\ttimeout := 100\n\tbackend := config.Backend{Timeout: time.Duration(timeout) * time.Millisecond}\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{&backend, &backend},\n\t}\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\tout, err := mw(NoopProxy, NoopProxy)(context.Background(), &Request{})\n\tif err == nil {\n\t\tt.Errorf(\"The middleware did not propagate the expected error\")\n\t}\n\tswitch mergeErr := err.(type) {\n\tcase mergeError:\n\t\tif len(mergeErr.errs) != 2 {\n\t\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t\t}\n\t\tif mergeErr.errs[0] != mergeErr.errs[1] {\n\t\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t\t}\n\t\tif mergeErr.errs[0] != errNullResult {\n\t\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t\t}\n\tdefault:\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t}\n\tif out != nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t}\n}\n\nfunc testNewMergeDataMiddleware_timeout(t *testing.T) {\n\ttimeout := 100\n\tbackend := config.Backend{Timeout: time.Duration(timeout) * time.Millisecond}\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{&backend, &backend},\n\t\tTimeout: time.Duration(timeout) * time.Millisecond,\n\t}\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\tp := mw(\n\t\tdelayedProxy(t, time.Duration(5*timeout)*time.Millisecond, nil),\n\t\tdelayedProxy(t, time.Duration(5*timeout)*time.Millisecond, nil))\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\tout, err := p(context.Background(), &Request{})\n\tif err == nil {\n\t\tt.Errorf(\"The middleware did not propagate the expected error\")\n\t}\n\tswitch mergeErr := err.(type) {\n\tcase mergeError:\n\t\tif len(mergeErr.errs) != 2 {\n\t\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t\t}\n\t\tif mergeErr.errs[0].Error() != mergeErr.errs[1].Error() {\n\t\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t\t}\n\t\tif mergeErr.errs[0].Error() != \"context deadline exceeded\" {\n\t\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t\t}\n\tdefault:\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t}\n\tif out != nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t}\n}\n\nfunc testRegisterResponseCombiner(t *testing.T) {\n\tsubject := \"test combiner\"\n\tif len(responseCombiners.data.Clone()) != 1 {\n\t\tt.Error(\"unexpected initial size of the response combiner list:\", responseCombiners.data.Clone())\n\t}\n\tRegisterResponseCombiner(subject, getResponseCombiner(config.ExtraConfig{}))\n\tdefer func() { responseCombiners = initResponseCombiners() }()\n\n\tif len(responseCombiners.data.Clone()) != 2 {\n\t\tt.Error(\"unexpected size of the response combiner list:\", responseCombiners.data.Clone())\n\t}\n\ttimeout := 500\n\tbackend := config.Backend{}\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{&backend, &backend},\n\t\tTimeout: time.Duration(timeout) * time.Millisecond,\n\t\tExtraConfig: config.ExtraConfig{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\tmergeKey: defaultCombinerName,\n\t\t\t},\n\t\t},\n\t}\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\tp := mw(\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"supu\": 42}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"tupu\": true}, IsComplete: true}))\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\tout, err := p(context.Background(), &Request{})\n\tif err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif out == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t\tif len(out.Data) != 2 {\n\t\t\tt.Errorf(\"We weren't expecting a partial response but we got %v!\\n\", out)\n\t\t}\n\t\tif !out.IsComplete {\n\t\t\tt.Errorf(\"We were expecting a completed response but we got an incompleted one!\\n\")\n\t\t}\n\t}\n}\n\nfunc Test_incrementalMergeAccumulator_invalidResponse(t *testing.T) {\n\tacc := newIncrementalMergeAccumulator(3, combineData)\n\tacc.Merge(nil, nil)\n\tacc.Merge(nil, nil)\n\tacc.Merge(nil, nil)\n\tres, err := acc.Result()\n\tif res != nil {\n\t\tt.Error(\"response should be nil\")\n\t\treturn\n\t}\n\tif err == nil {\n\t\tt.Error(\"expecting error\")\n\t\treturn\n\t}\n\tswitch mergeErr := err.(type) {\n\tcase mergeError:\n\t\tif len(mergeErr.errs) != 3 {\n\t\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t\t}\n\t\tif mergeErr.errs[0] != mergeErr.errs[1] {\n\t\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t\t}\n\t\tif mergeErr.errs[0] != mergeErr.errs[2] {\n\t\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t\t}\n\t\tif mergeErr.errs[0] != errNullResult {\n\t\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t\t}\n\tdefault:\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t}\n}\n\nfunc Test_incrementalMergeAccumulator_incompleteResponse(t *testing.T) {\n\tacc := newIncrementalMergeAccumulator(3, combineData)\n\tacc.Merge(&Response{Data: make(map[string]interface{}), IsComplete: true}, nil)\n\tacc.Merge(&Response{Data: make(map[string]interface{}), IsComplete: false}, nil)\n\tacc.Merge(&Response{Data: make(map[string]interface{}), IsComplete: true}, nil)\n\tres, err := acc.Result()\n\tif res == nil {\n\t\tt.Error(\"response should not be nil\")\n\t\treturn\n\t}\n\tif err != nil {\n\t\tt.Errorf(\"unexpected error: %s\", err.Error())\n\t\treturn\n\t}\n\tif res.IsComplete {\n\t\tt.Error(\"response should not be completed\")\n\t}\n}\n\nfunc testNewMergeDataMiddleware_simpleFiltering(t *testing.T) {\n\ttimeout := 500\n\tbackend := config.Backend{}\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{&backend, &backend},\n\t\tTimeout: time.Duration(timeout) * time.Millisecond,\n\t}\n\n\tRegisterBackendFiltererFactory(\"\", func(_ *config.EndpointConfig) ([]BackendFilterer, error) {\n\t\treturn []BackendFilterer{\n\t\t\tfunc(_ *Request) bool {\n\t\t\t\treturn true\n\t\t\t},\n\t\t\tfunc(r *Request) bool {\n\t\t\t\treturn r.Headers[\"X-Filter\"][0] == \"supu\"\n\t\t\t},\n\t\t}, nil\n\t})\n\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\tp := mw(\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"supu\": 42}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"tupu\": true}, IsComplete: true}))\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\tout, err := p(context.Background(), &Request{Headers: map[string][]string{\"X-Filter\": {\"meh\"}}})\n\tif err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif out == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t\tif len(out.Data) != 1 || out.Data[\"supu\"] != 42 {\n\t\t\tt.Errorf(\"We were expecting a response from just a backend, but we got %v!\\n\", out)\n\t\t}\n\t\tif !out.IsComplete {\n\t\t\tt.Errorf(\"We were expecting a completed response but we got an incompleted one!\\n\")\n\t\t}\n\t}\n}\n\nfunc testNewMergeDataMiddleware_sequentialFiltering(t *testing.T) {\n\ttimeout := 1000\n\tendpoint := config.EndpointConfig{\n\t\tBackend: []*config.Backend{\n\t\t\t{URLPattern: \"/\"},\n\t\t\t{URLPattern: \"/aaa/{{.Resp0_string}}\"},\n\t\t\t{URLPattern: \"/hit-me/{{.Resp1_tupu}}\"},\n\t\t},\n\t\tTimeout: time.Duration(timeout) * time.Millisecond,\n\t\tExtraConfig: config.ExtraConfig{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\tisSequentialKey: true,\n\t\t\t},\n\t\t},\n\t}\n\n\tRegisterBackendFiltererFactory(\"\", func(_ *config.EndpointConfig) ([]BackendFilterer, error) {\n\t\treturn []BackendFilterer{\n\t\t\tfunc(_ *Request) bool {\n\t\t\t\treturn true\n\t\t\t},\n\t\t\tfunc(_ *Request) bool {\n\t\t\t\treturn false\n\t\t\t},\n\t\t\tfunc(_ *Request) bool {\n\t\t\t\treturn true\n\t\t\t},\n\t\t}, nil\n\t})\n\n\tmw := NewMergeDataMiddleware(logging.NoOp, &endpoint)\n\tp := mw(\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"string\": \"some\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"tupu\": \"foo\"}, IsComplete: true}),\n\t\tdummyProxy(&Response{Data: map[string]interface{}{\"final\": \"meh\"}, IsComplete: true}),\n\t)\n\tmustEnd := time.After(time.Duration(2*timeout) * time.Millisecond)\n\tout, err := p(context.Background(), &Request{\n\t\tParams: map[string]string{},\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif out == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t\tif len(out.Data) != 2 {\n\t\t\tt.Errorf(\"We were expecting a response from just two backends, but we got %v!\\n\", out)\n\t\t}\n\t\tif !out.IsComplete {\n\t\t\tt.Errorf(\"We were expecting a completed response but we got an incompleted one!\\n\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "proxy/plugin/modifier.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\nPackage plugin provides tools for loading and registering proxy plugins\n*/\npackage plugin\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"plugin\"\n\t\"strings\"\n\n\t\"github.com/luraproject/lura/v2/logging\"\n\tluraplugin \"github.com/luraproject/lura/v2/plugin\"\n\t\"github.com/luraproject/lura/v2/register\"\n)\n\nconst (\n\t// Namespace is the namespace for the extra_config section\n\tNamespace = \"github.com/devopsfaith/krakend/proxy/plugin\"\n\t// requestNamespace is the internal namespace for the register to be used with request modifiers\n\trequestNamespace = \"github.com/devopsfaith/krakend/proxy/plugin/request\"\n\t// responseNamespace is the internal namespace for the register to be used with response modifiers\n\tresponseNamespace = \"github.com/devopsfaith/krakend/proxy/plugin/response\"\n)\n\nvar modifierRegister = register.New()\n\n// ModifierFactory is a function that, given a config passed as a map, returns a modifier\ntype ModifierFactory func(map[string]interface{}) func(interface{}) (interface{}, error)\n\n// GetRequestModifier returns a ModifierFactory from the request namespace by name\nfunc GetRequestModifier(name string) (ModifierFactory, bool) {\n\treturn getModifier(requestNamespace, name)\n}\n\n// GetResponseModifier returns a ModifierFactory from the response namespace by name\nfunc GetResponseModifier(name string) (ModifierFactory, bool) {\n\treturn getModifier(responseNamespace, name)\n}\n\nfunc getModifier(namespace, name string) (ModifierFactory, bool) {\n\tr, ok := modifierRegister.Get(namespace)\n\tif !ok {\n\t\treturn nil, ok\n\t}\n\tm, ok := r.Get(name)\n\tif !ok {\n\t\treturn nil, ok\n\t}\n\tres, ok := m.(func(map[string]interface{}) func(interface{}) (interface{}, error))\n\tif !ok {\n\t\treturn nil, ok\n\t}\n\treturn ModifierFactory(res), ok\n}\n\n// RegisterModifier registers the injected modifier factory with the given name at the selected namespace\nfunc RegisterModifier(\n\tname string,\n\tmodifierFactory func(map[string]interface{}) func(interface{}) (interface{}, error),\n\tappliesToRequest bool,\n\tappliesToResponse bool,\n) {\n\tif appliesToRequest {\n\t\tmodifierRegister.Register(requestNamespace, name, modifierFactory)\n\t}\n\tif appliesToResponse {\n\t\tmodifierRegister.Register(responseNamespace, name, modifierFactory)\n\t}\n}\n\n// Registerer defines the interface for the plugins to expose in order to be able to be loaded/registered\ntype Registerer interface {\n\tRegisterModifiers(func(\n\t\tname string,\n\t\tmodifierFactory func(map[string]interface{}) func(interface{}) (interface{}, error),\n\t\tappliesToRequest bool,\n\t\tappliesToResponse bool,\n\t))\n}\n\ntype LoggerRegisterer interface {\n\tRegisterLogger(interface{})\n}\n\ntype ContextRegisterer interface {\n\tRegisterContext(context.Context)\n}\n\n// RegisterModifierFunc type is the function passed to the loaded Registerers\ntype RegisterModifierFunc func(\n\tname string,\n\tmodifierFactory func(map[string]interface{}) func(interface{}) (interface{}, error),\n\tappliesToRequest bool,\n\tappliesToResponse bool,\n)\n\n// Load scans the given path using the pattern and registers all the found modifier plugins into the rmf\nfunc Load(path, pattern string, rmf RegisterModifierFunc) (int, error) {\n\treturn LoadWithLogger(path, pattern, rmf, nil)\n}\n\n// LoadWithLogger scans the given path using the pattern and registers all the found modifier plugins into the rmf\nfunc LoadWithLogger(path, pattern string, rmf RegisterModifierFunc, logger logging.Logger) (int, error) {\n\treturn LoadWithLoggerAndContext(context.Background(), path, pattern, rmf, logger)\n}\n\nfunc LoadWithLoggerAndContext(ctx context.Context, path, pattern string, rmf RegisterModifierFunc, logger logging.Logger) (int, error) {\n\tplugins, err := luraplugin.Scan(path, pattern)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn load(ctx, plugins, rmf, logger)\n}\n\nfunc load(ctx context.Context, plugins []string, rmf RegisterModifierFunc, logger logging.Logger) (int, error) {\n\tvar errors []error\n\n\tloadedPlugins := 0\n\tfor k, pluginName := range plugins {\n\t\tif err := open(ctx, pluginName, rmf, logger); err != nil {\n\t\t\terrors = append(errors, fmt.Errorf(\"plugin #%d (%s): %s\", k, pluginName, err.Error()))\n\t\t\tcontinue\n\t\t}\n\t\tloadedPlugins++\n\t}\n\n\tif len(errors) > 0 {\n\t\treturn loadedPlugins, loaderError{errors: errors}\n\t}\n\treturn loadedPlugins, nil\n}\n\nfunc open(ctx context.Context, pluginName string, rmf RegisterModifierFunc, logger logging.Logger) (err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tvar ok bool\n\t\t\terr, ok = r.(error)\n\t\t\tif !ok {\n\t\t\t\terr = fmt.Errorf(\"%v\", r)\n\t\t\t}\n\t\t}\n\t}()\n\n\tvar p Plugin\n\tp, err = pluginOpener(pluginName)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar r interface{}\n\tr, err = p.Lookup(\"ModifierRegisterer\")\n\tif err != nil {\n\t\treturn\n\t}\n\tregisterer, ok := r.(Registerer)\n\tif !ok {\n\t\treturn fmt.Errorf(\"modifier plugin loader: unknown type\")\n\t}\n\n\tif logger != nil {\n\t\tif lr, ok := r.(LoggerRegisterer); ok {\n\t\t\tlr.RegisterLogger(logger)\n\t\t}\n\t}\n\n\tif lr, ok := r.(ContextRegisterer); ok {\n\t\tlr.RegisterContext(ctx)\n\t}\n\n\tRegisterExtraComponents(r)\n\n\tregisterer.RegisterModifiers(rmf)\n\treturn\n}\n\nvar RegisterExtraComponents = func(interface{}) {}\n\n// Plugin is the interface of the loaded plugins\ntype Plugin interface {\n\tLookup(name string) (plugin.Symbol, error)\n}\n\n// pluginOpener keeps the plugin open function in a var for easy testing\nvar pluginOpener = defaultPluginOpener\n\nfunc defaultPluginOpener(name string) (Plugin, error) {\n\treturn plugin.Open(name)\n}\n\ntype loaderError struct {\n\terrors []error\n}\n\n// Error implements the error interface\nfunc (l loaderError) Error() string {\n\tmsgs := make([]string, len(l.errors))\n\tfor i, err := range l.errors {\n\t\tmsgs[i] = err.Error()\n\t}\n\treturn fmt.Sprintf(\"plugin loader found %d error(s): \\n%s\", len(msgs), strings.Join(msgs, \"\\n\"))\n}\n\nfunc (l loaderError) Len() int {\n\treturn len(l.errors)\n}\n\nfunc (l loaderError) Errs() []error {\n\treturn l.errors\n}\n"
  },
  {
    "path": "proxy/plugin/modifier_test.go",
    "content": "//go:build integration || !race\n// +build integration !race\n\n// SPDX-License-Identifier: Apache-2.0\n\npackage plugin\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nfunc ExampleLoadWithLoggerAndContext() {\n\tvar data []byte\n\n\tbuf := bytes.NewBuffer(data)\n\tlogger, err := logging.NewLogger(\"DEBUG\", buf, \"\")\n\tif err != nil {\n\t\tfmt.Println(err.Error())\n\t\treturn\n\t}\n\ttotal, err := LoadWithLoggerAndContext(context.Background(), \"./tests\", \".so\", RegisterModifier, logger)\n\tif err != nil {\n\t\tfmt.Println(err.Error())\n\t\treturn\n\t}\n\tif total != 2 {\n\t\tfmt.Printf(\"unexpected number of loaded plugins!. have %d, want 2\\n\", total)\n\t\treturn\n\t}\n\n\tmodFactory, ok := GetRequestModifier(\"lura-request-modifier-example-request\")\n\tif !ok {\n\t\tfmt.Println(\"modifier factory not found in the register\")\n\t\treturn\n\t}\n\n\tmodifier := modFactory(map[string]interface{}{})\n\n\tinput := requestWrapper{\n\t\tctx:     context.WithValue(context.Background(), \"myCtxKey\", \"some\"),\n\t\tpath:    \"/bar\",\n\t\tmethod:  \"GET\",\n\t\theaders: map[string][]string{\"X-Foo\": {\"bar\"}},\n\t}\n\n\ttmp, err := modifier(input)\n\tif err != nil {\n\t\tfmt.Println(err.Error())\n\t\treturn\n\t}\n\n\toutput, ok := tmp.(RequestWrapper)\n\tif !ok {\n\t\tfmt.Println(\"unexpected result type\")\n\t\treturn\n\t}\n\n\tif res := output.Path(); res != \"/bar/fooo\" {\n\t\tfmt.Printf(\"unexpected result path. have %s, want /bar/fooo\\n\", res)\n\t\treturn\n\t}\n\n\tmodFactory, ok = GetResponseModifier(\"lura-request-modifier-example-response\")\n\tif !ok {\n\t\tfmt.Println(\"modifier factory not found in the register\")\n\t\treturn\n\t}\n\n\tmodifier = modFactory(map[string]interface{}{})\n\n\tresponse := responseWrapper{\n\t\tctx:     context.WithValue(context.Background(), \"myCtxKey\", \"other\"),\n\t\trequest: input,\n\t\tdata:    map[string]interface{}{\"foo\": \"bar\"},\n\t}\n\n\tif _, err = modifier(response); err != nil {\n\t\tfmt.Println(err.Error())\n\t\treturn\n\t}\n\n\tlines := strings.Split(buf.String(), \"\\n\")\n\tfor i := range lines[:len(lines)-1] {\n\t\tfmt.Println(lines[i][21:])\n\t}\n\n\t// output:\n\t// DEBUG: [PLUGIN: lura-error-example] Logger loaded\n\t// DEBUG: [PLUGIN: lura-request-modifier-example] Logger loaded\n\t// DEBUG: [PLUGIN: lura-request-modifier-example] Context loaded\n\t// DEBUG: [PLUGIN: lura-request-modifier-example] Request modifier injected\n\t// DEBUG: context key: some\n\t// DEBUG: params: map[]\n\t// DEBUG: headers: map[X-Foo:[bar]]\n\t// DEBUG: method: GET\n\t// DEBUG: url: <nil>\n\t// DEBUG: query: map[]\n\t// DEBUG: path: /bar/fooo\n\t// DEBUG: [PLUGIN: lura-request-modifier-example] Response modifier injected\n\t// DEBUG: Header X-Foo value: bar\n\t// DEBUG: context key: other\n\t// DEBUG: data: map[foo:bar]\n\t// DEBUG: is complete: false\n\t// DEBUG: headers: map[]\n\t// DEBUG: status code: 0\n\t// DEBUG: original headers: map[X-Foo:[bar]]\n\n}\n\nfunc TestLoad(t *testing.T) {\n\ttotal, err := LoadWithLogger(\"./tests\", \".so\", RegisterModifier, logging.NoOp)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\tt.Fail()\n\t}\n\tif total != 2 {\n\t\tt.Errorf(\"unexpected number of loaded plugins!. have %d, want 2\", total)\n\t}\n\n\tmodFactory, ok := GetRequestModifier(\"lura-request-modifier-example-request\")\n\tif !ok {\n\t\tt.Error(\"modifier factory not found in the register\")\n\t\treturn\n\t}\n\n\tmodifier := modFactory(map[string]interface{}{})\n\n\tinput := requestWrapper{ctx: context.WithValue(context.Background(), \"myCtxKey\", \"some\"), path: \"/bar\"}\n\n\ttmp, err := modifier(input)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\toutput, ok := tmp.(RequestWrapper)\n\tif !ok {\n\t\tt.Error(\"unexpected result type\")\n\t\treturn\n\t}\n\n\tif res := output.Path(); res != \"/bar/fooo\" {\n\t\tt.Errorf(\"unexpected result path. have %s, want /bar/fooo\", res)\n\t}\n}\n\ntype RequestWrapper interface {\n\tParams() map[string]string\n\tHeaders() map[string][]string\n\tBody() io.ReadCloser\n\tMethod() string\n\tURL() *url.URL\n\tQuery() url.Values\n\tPath() string\n}\n\ntype requestWrapper struct {\n\tctx     context.Context\n\tmethod  string\n\turl     *url.URL\n\tquery   url.Values\n\tpath    string\n\tbody    io.ReadCloser\n\tparams  map[string]string\n\theaders map[string][]string\n}\n\nfunc (r requestWrapper) Context() context.Context     { return r.ctx }\nfunc (r requestWrapper) Method() string               { return r.method }\nfunc (r requestWrapper) URL() *url.URL                { return r.url }\nfunc (r requestWrapper) Query() url.Values            { return r.query }\nfunc (r requestWrapper) Path() string                 { return r.path }\nfunc (r requestWrapper) Body() io.ReadCloser          { return r.body }\nfunc (r requestWrapper) Params() map[string]string    { return r.params }\nfunc (r requestWrapper) Headers() map[string][]string { return r.headers }\n\ntype metadataWrapper struct {\n\theaders    map[string][]string\n\tstatusCode int\n}\n\nfunc (m metadataWrapper) Headers() map[string][]string { return m.headers }\nfunc (m metadataWrapper) StatusCode() int              { return m.statusCode }\n\ntype responseWrapper struct {\n\tctx        context.Context\n\trequest    interface{}\n\tdata       map[string]interface{}\n\tisComplete bool\n\tmetadata   metadataWrapper\n\tio         io.Reader\n}\n\nfunc (r responseWrapper) Context() context.Context     { return r.ctx }\nfunc (r responseWrapper) Request() interface{}         { return r.request }\nfunc (r responseWrapper) Data() map[string]interface{} { return r.data }\nfunc (r responseWrapper) IsComplete() bool             { return r.isComplete }\nfunc (r responseWrapper) Io() io.Reader                { return r.io }\nfunc (r responseWrapper) Headers() map[string][]string { return r.metadata.headers }\nfunc (r responseWrapper) StatusCode() int              { return r.metadata.statusCode }\n"
  },
  {
    "path": "proxy/plugin/tests/error/main.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\nfunc main() {}\n\nvar ModifierRegisterer = registerer(\"lura-error-example\")\n\nvar logger Logger = nil\n\ntype registerer string\n\nfunc (r registerer) RegisterModifiers(f func(\n\tname string,\n\tmodifierFactory func(map[string]interface{}) func(interface{}) (interface{}, error),\n\tappliesToRequest bool,\n\tappliesToResponse bool,\n)) {\n\tf(string(r)+\"-request\", r.requestModifierFactory, true, false)\n\tf(string(r)+\"-response\", r.reqsponseModifierFactory, false, true)\n}\n\nfunc (registerer) RegisterLogger(in interface{}) {\n\tl, ok := in.(Logger)\n\tif !ok {\n\t\treturn\n\t}\n\tlogger = l\n\tlogger.Debug(fmt.Sprintf(\"[PLUGIN: %s] Logger loaded\", ModifierRegisterer))\n}\n\nfunc (registerer) requestModifierFactory(_ map[string]interface{}) func(interface{}) (interface{}, error) {\n\tlogger.Debug(fmt.Sprintf(\"[PLUGIN: %s] Request modifier injected\", ModifierRegisterer))\n\treturn func(_ interface{}) (interface{}, error) {\n\t\tlogger.Debug(fmt.Sprintf(\"[PLUGIN: %s] Rejecting request\", ModifierRegisterer))\n\t\treturn nil, requestErr\n\t}\n}\n\nfunc (registerer) reqsponseModifierFactory(_ map[string]interface{}) func(interface{}) (interface{}, error) {\n\tlogger.Debug(fmt.Sprintf(\"[PLUGIN: %s] Response modifier injected\", ModifierRegisterer))\n\treturn func(_ interface{}) (interface{}, error) {\n\t\tlogger.Debug(fmt.Sprintf(\"[PLUGIN: %s] Replacing response\", ModifierRegisterer))\n\t\treturn nil, responseErr\n\t}\n}\n\ntype customError struct {\n\terror\n\tstatusCode int\n}\n\nfunc (r customError) StatusCode() int { return r.statusCode }\n\nvar (\n\trequestErr = customError{\n\t\terror:      errors.New(\"request rejected just because\"),\n\t\tstatusCode: http.StatusTeapot,\n\t}\n\tresponseErr = customError{\n\t\terror:      errors.New(\"response replaced because reasons\"),\n\t\tstatusCode: http.StatusTeapot,\n\t}\n)\n\ntype Logger interface {\n\tDebug(v ...interface{})\n\tInfo(v ...interface{})\n\tWarning(v ...interface{})\n\tError(v ...interface{})\n\tCritical(v ...interface{})\n\tFatal(v ...interface{})\n}\n"
  },
  {
    "path": "proxy/plugin/tests/logger/main.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"path\"\n)\n\nfunc main() {}\n\nvar ModifierRegisterer = registerer(\"lura-request-modifier-example\")\n\nvar logger Logger = nil\nvar ctx context.Context = context.Background()\n\ntype registerer string\n\nfunc (r registerer) RegisterModifiers(f func(\n\tname string,\n\tmodifierFactory func(map[string]interface{}) func(interface{}) (interface{}, error),\n\tappliesToRequest bool,\n\tappliesToResponse bool,\n)) {\n\tf(string(r)+\"-request\", r.requestModifierFactory, true, false)\n\tf(string(r)+\"-response\", r.reqsponseModifierFactory, false, true)\n}\n\nfunc (registerer) RegisterLogger(in interface{}) {\n\tl, ok := in.(Logger)\n\tif !ok {\n\t\treturn\n\t}\n\tlogger = l\n\tlogger.Debug(fmt.Sprintf(\"[PLUGIN: %s] Logger loaded\", ModifierRegisterer))\n}\n\nfunc (registerer) RegisterContext(c context.Context) {\n\tctx = c\n\tlogger.Debug(fmt.Sprintf(\"[PLUGIN: %s] Context loaded\", ModifierRegisterer))\n}\n\nfunc (registerer) requestModifierFactory(_ map[string]interface{}) func(interface{}) (interface{}, error) {\n\t// check the config\n\t// return the modifier\n\n\t// Graceful shutdown of any service or connection managed by the plugin\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tlogger.Debug(\"Shuting down the service\")\n\t}()\n\n\tif logger == nil {\n\t\tfmt.Println(\"request modifier loaded without logger\")\n\t\treturn func(input interface{}) (interface{}, error) {\n\t\t\treq, ok := input.(RequestWrapper)\n\t\t\tif !ok {\n\t\t\t\treturn nil, unkownTypeErr\n\t\t\t}\n\n\t\t\treturn modifier(req), nil\n\t\t}\n\t}\n\n\tlogger.Debug(fmt.Sprintf(\"[PLUGIN: %s] Request modifier injected\", ModifierRegisterer))\n\treturn func(input interface{}) (interface{}, error) {\n\t\treq, ok := input.(RequestWrapper)\n\t\tif !ok {\n\t\t\treturn nil, unkownTypeErr\n\t\t}\n\n\t\tr := modifier(req)\n\n\t\trequestCtx := req.Context()\n\t\tlogger.Debug(\"context key:\", requestCtx.Value(\"myCtxKey\"))\n\t\tlogger.Debug(\"params:\", r.params)\n\t\tlogger.Debug(\"headers:\", r.headers)\n\t\tlogger.Debug(\"method:\", r.method)\n\t\tlogger.Debug(\"url:\", r.url)\n\t\tlogger.Debug(\"query:\", r.query)\n\t\tlogger.Debug(\"path:\", r.path)\n\n\t\treturn r, nil\n\t}\n}\n\nfunc (registerer) reqsponseModifierFactory(_ map[string]interface{}) func(interface{}) (interface{}, error) {\n\t// check the cfg. If the modifier requires some configuration,\n\t// it should be under the name of the plugin.\n\t// ex: if this modifier required some A and B config params\n\t/*\n\t   \"extra_config\":{\n\t       \"plugin/req-resp-modifier\":{\n\t           \"name\":[\"krakend-debugger\"],\n\t           \"krakend-debugger\":{\n\t               \"A\":\"foo\",\n\t               \"B\":42\n\t           }\n\t       }\n\t   }\n\t*/\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tlogger.Debug(\"Shuting down the service\")\n\t}()\n\n\t// return the modifier\n\tif logger == nil {\n\t\tfmt.Println(\"response modifier loaded without logger\")\n\t\treturn func(input interface{}) (interface{}, error) {\n\t\t\tresp, ok := input.(ResponseWrapper)\n\t\t\tif !ok {\n\t\t\t\treturn nil, unkownTypeErr\n\t\t\t}\n\n\t\t\tfmt.Println(\"data:\", resp.Data())\n\t\t\tfmt.Println(\"is complete:\", resp.IsComplete())\n\t\t\tfmt.Println(\"headers:\", resp.Headers())\n\t\t\tfmt.Println(\"status code:\", resp.StatusCode())\n\n\t\t\treturn input, nil\n\t\t}\n\t}\n\n\tlogger.Debug(fmt.Sprintf(\"[PLUGIN: %s] Response modifier injected\", ModifierRegisterer))\n\treturn func(input interface{}) (interface{}, error) {\n\t\tresp, ok := input.(ResponseWrapper)\n\t\tif !ok {\n\t\t\treturn nil, unkownTypeErr\n\t\t}\n\n\t\tif req, ok := resp.Request().(RequestWrapper); ok {\n\t\t\tfor k, v := range req.Headers() {\n\t\t\t\tlogger.Debug(fmt.Sprintf(\"Header %s value: %s\", k, v[0]))\n\t\t\t}\n\t\t}\n\n\t\trespCtx := resp.Context()\n\t\tlogger.Debug(\"context key:\", respCtx.Value(\"myCtxKey\"))\n\t\tlogger.Debug(\"data:\", resp.Data())\n\t\tlogger.Debug(\"is complete:\", resp.IsComplete())\n\t\tlogger.Debug(\"headers:\", resp.Headers())\n\t\tlogger.Debug(\"status code:\", resp.StatusCode())\n\n\t\treq, ok := resp.Request().(RequestWrapper)\n\t\tif ok {\n\t\t\tlogger.Debug(\"original headers:\", req.Headers())\n\t\t}\n\n\t\treturn resp, nil\n\t}\n}\n\nfunc modifier(req RequestWrapper) requestWrapper {\n\treturn requestWrapper{\n\t\tparams:  req.Params(),\n\t\theaders: req.Headers(),\n\t\tbody:    req.Body(),\n\t\tmethod:  req.Method(),\n\t\turl:     req.URL(),\n\t\tquery:   req.Query(),\n\t\tpath:    path.Join(req.Path(), \"/fooo\"),\n\t}\n}\n\nvar unkownTypeErr = errors.New(\"unknown request type\")\n\ntype ResponseWrapper interface {\n\tContext() context.Context\n\tRequest() interface{}\n\tData() map[string]interface{}\n\tIsComplete() bool\n\tHeaders() map[string][]string\n\tStatusCode() int\n}\n\ntype RequestWrapper interface {\n\tContext() context.Context\n\tParams() map[string]string\n\tHeaders() map[string][]string\n\tBody() io.ReadCloser\n\tMethod() string\n\tURL() *url.URL\n\tQuery() url.Values\n\tPath() string\n}\n\ntype requestWrapper struct {\n\tctx     context.Context\n\tmethod  string\n\turl     *url.URL\n\tquery   url.Values\n\tpath    string\n\tbody    io.ReadCloser\n\tparams  map[string]string\n\theaders map[string][]string\n}\n\nfunc (r requestWrapper) Context() context.Context     { return r.ctx }\nfunc (r requestWrapper) Method() string               { return r.method }\nfunc (r requestWrapper) URL() *url.URL                { return r.url }\nfunc (r requestWrapper) Query() url.Values            { return r.query }\nfunc (r requestWrapper) Path() string                 { return r.path }\nfunc (r requestWrapper) Body() io.ReadCloser          { return r.body }\nfunc (r requestWrapper) Params() map[string]string    { return r.params }\nfunc (r requestWrapper) Headers() map[string][]string { return r.headers }\n\ntype Logger interface {\n\tDebug(v ...interface{})\n\tInfo(v ...interface{})\n\tWarning(v ...interface{})\n\tError(v ...interface{})\n\tCritical(v ...interface{})\n\tFatal(v ...interface{})\n}\n"
  },
  {
    "path": "proxy/plugin.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy/plugin\"\n)\n\n// NewPluginMiddleware returns an endpoint middleware wrapped (if required) with the plugin middleware.\n// The plugin middleware will try to load all the required plugins from the register and execute them in order.\n// RequestModifiers are executed before passing the request to the next middlware. ResponseModifiers are executed\n// once the response is returned from the next middleware.\nfunc NewPluginMiddleware(logger logging.Logger, endpoint *config.EndpointConfig) Middleware {\n\tcfg, ok := endpoint.ExtraConfig[plugin.Namespace].(map[string]interface{})\n\n\tif !ok {\n\t\treturn emptyMiddlewareFallback(logger)\n\t}\n\n\treturn newPluginMiddleware(logger, \"ENDPOINT\", endpoint.Endpoint, cfg)\n}\n\n// NewBackendPluginMiddleware returns a backend middleware wrapped (if required) with the plugin middleware.\n// The plugin middleware will try to load all the required plugins from the register and execute them in order.\n// RequestModifiers are executed before passing the request to the next middlware. ResponseModifiers are executed\n// once the response is returned from the next middleware.\nfunc NewBackendPluginMiddleware(logger logging.Logger, remote *config.Backend) Middleware {\n\tcfg, ok := remote.ExtraConfig[plugin.Namespace].(map[string]interface{})\n\n\tif !ok {\n\t\treturn emptyMiddlewareFallback(logger)\n\t}\n\n\treturn newPluginMiddleware(logger, \"BACKEND\",\n\t\tfmt.Sprintf(\"%s %s -> %s\", remote.ParentEndpointMethod, remote.ParentEndpoint, remote.URLPattern), cfg)\n}\n\nfunc newPluginMiddleware(logger logging.Logger, tag, pattern string, cfg map[string]interface{}) Middleware {\n\tplugins, ok := cfg[\"name\"].([]interface{})\n\tif !ok {\n\t\treturn emptyMiddlewareFallback(logger)\n\t}\n\n\tvar reqModifiers []func(interface{}) (interface{}, error)\n\n\tvar respModifiers []func(interface{}) (interface{}, error)\n\n\tfor _, p := range plugins {\n\t\tname, ok := p.(string)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tif mf, ok := plugin.GetRequestModifier(name); ok {\n\t\t\tif fn := mf(cfg); fn != nil {\n\t\t\t\treqModifiers = append(reqModifiers, fn)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif mf, ok := plugin.GetResponseModifier(name); ok {\n\t\t\tif fn := mf(cfg); fn != nil {\n\t\t\t\trespModifiers = append(respModifiers, fn)\n\t\t\t}\n\t\t}\n\t}\n\n\ttotReqModifiers, totRespModifiers := len(reqModifiers), len(respModifiers)\n\tif totReqModifiers == totRespModifiers && totRespModifiers == 0 {\n\t\treturn emptyMiddlewareFallback(logger)\n\t}\n\n\tlogger.Debug(\n\t\tfmt.Sprintf(\n\t\t\t\"[%s: %s][Modifier Plugins] Adding %d request and %d response modifiers\",\n\t\t\ttag,\n\t\t\tpattern,\n\t\t\ttotReqModifiers,\n\t\t\ttotRespModifiers,\n\t\t),\n\t)\n\n\treturn func(next ...Proxy) Proxy {\n\t\tif len(next) > 1 {\n\t\t\tlogger.Fatal(\"too many proxies for this proxy middleware: newPluginMiddleware only accepts 1 proxy, got %d tag: %s, pattern: %s\",\n\t\t\t\tlen(next), tag, pattern)\n\t\t\treturn nil\n\t\t}\n\n\t\tif totReqModifiers == 0 {\n\t\t\treturn func(ctx context.Context, r *Request) (*Response, error) {\n\t\t\t\tresp, err := next[0](ctx, r)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn resp, err\n\t\t\t\t}\n\n\t\t\t\treturn executeResponseModifiers(ctx, respModifiers, resp, newRequestWrapper(ctx, r))\n\t\t\t}\n\t\t}\n\n\t\tif totRespModifiers == 0 {\n\t\t\treturn func(ctx context.Context, r *Request) (*Response, error) {\n\t\t\t\tvar err error\n\t\t\t\tr, err = executeRequestModifiers(ctx, reqModifiers, r)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\treturn next[0](ctx, r)\n\t\t\t}\n\t\t}\n\n\t\treturn func(ctx context.Context, r *Request) (*Response, error) {\n\t\t\tvar err error\n\t\t\tr, err = executeRequestModifiers(ctx, reqModifiers, r)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tresp, err := next[0](ctx, r)\n\t\t\tif err != nil {\n\t\t\t\treturn resp, err\n\t\t\t}\n\n\t\t\treturn executeResponseModifiers(ctx, respModifiers, resp, newRequestWrapper(ctx, r))\n\t\t}\n\t}\n}\n\nfunc executeRequestModifiers(ctx context.Context, reqModifiers []func(interface{}) (interface{}, error), r *Request) (*Request, error) {\n\tvar tmp RequestWrapper\n\ttmp = newRequestWrapper(ctx, r)\n\n\tfor _, f := range reqModifiers {\n\t\tres, err := f(tmp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tt, ok := res.(RequestWrapper)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\ttmp = t\n\t}\n\n\tr.Method = tmp.Method()\n\tr.URL = tmp.URL()\n\tr.Query = tmp.Query()\n\tr.Path = tmp.Path()\n\tr.Body = tmp.Body()\n\tr.Params = tmp.Params()\n\tr.Headers = tmp.Headers()\n\n\treturn r, nil\n}\n\nfunc executeResponseModifiers(ctx context.Context, respModifiers []func(interface{}) (interface{}, error), r *Response, req RequestWrapper) (*Response, error) {\n\tvar tmp ResponseWrapper\n\ttmp = responseWrapper{\n\t\tctx:        ctx,\n\t\trequest:    req,\n\t\tdata:       r.Data,\n\t\tisComplete: r.IsComplete,\n\t\tmetadata: metadataWrapper{\n\t\t\theaders:    r.Metadata.Headers,\n\t\t\tstatusCode: r.Metadata.StatusCode,\n\t\t},\n\t\tio: r.Io,\n\t}\n\n\tfor _, f := range respModifiers {\n\t\tres, err := f(tmp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tt, ok := res.(ResponseWrapper)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\ttmp = t\n\t}\n\n\tr.Data = tmp.Data()\n\tr.IsComplete = tmp.IsComplete()\n\tr.Io = tmp.Io()\n\tr.Metadata = Metadata{}\n\tr.Metadata.Headers = tmp.Headers()\n\tr.Metadata.StatusCode = tmp.StatusCode()\n\treturn r, nil\n}\n\n// RequestWrapper is an interface for passing proxy request between the lura pipe and the loaded plugins\ntype RequestWrapper interface {\n\tParams() map[string]string\n\tHeaders() map[string][]string\n\tBody() io.ReadCloser\n\tMethod() string\n\tURL() *url.URL\n\tQuery() url.Values\n\tPath() string\n}\n\n// ResponseWrapper is an interface for passing proxy response between the lura pipe and the loaded plugins\ntype ResponseWrapper interface {\n\tData() map[string]interface{}\n\tIo() io.Reader\n\tIsComplete() bool\n\tHeaders() map[string][]string\n\tStatusCode() int\n}\n\nfunc newRequestWrapper(ctx context.Context, r *Request) *requestWrapper {\n\treturn &requestWrapper{\n\t\tctx:     ctx,\n\t\tmethod:  r.Method,\n\t\turl:     r.URL,\n\t\tquery:   r.Query,\n\t\tpath:    r.Path,\n\t\tbody:    r.Body,\n\t\tparams:  r.Params,\n\t\theaders: r.Headers,\n\t}\n}\n\ntype requestWrapper struct {\n\tctx     context.Context\n\tmethod  string\n\turl     *url.URL\n\tquery   url.Values\n\tpath    string\n\tbody    io.ReadCloser\n\tparams  map[string]string\n\theaders map[string][]string\n}\n\nfunc (r *requestWrapper) Context() context.Context     { return r.ctx }\nfunc (r *requestWrapper) Method() string               { return r.method }\nfunc (r *requestWrapper) URL() *url.URL                { return r.url }\nfunc (r *requestWrapper) Query() url.Values            { return r.query }\nfunc (r *requestWrapper) Path() string                 { return r.path }\nfunc (r *requestWrapper) Body() io.ReadCloser          { return r.body }\nfunc (r *requestWrapper) Params() map[string]string    { return r.params }\nfunc (r *requestWrapper) Headers() map[string][]string { return r.headers }\n\ntype metadataWrapper struct {\n\theaders    map[string][]string\n\tstatusCode int\n}\n\nfunc (m metadataWrapper) Headers() map[string][]string { return m.headers }\nfunc (m metadataWrapper) StatusCode() int              { return m.statusCode }\n\ntype responseWrapper struct {\n\tctx        context.Context\n\trequest    interface{}\n\tdata       map[string]interface{}\n\tisComplete bool\n\tmetadata   metadataWrapper\n\tio         io.Reader\n}\n\nfunc (r responseWrapper) Context() context.Context     { return r.ctx }\nfunc (r responseWrapper) Request() interface{}         { return r.request }\nfunc (r responseWrapper) Data() map[string]interface{} { return r.data }\nfunc (r responseWrapper) IsComplete() bool             { return r.isComplete }\nfunc (r responseWrapper) Io() io.Reader                { return r.io }\nfunc (r responseWrapper) Headers() map[string][]string { return r.metadata.headers }\nfunc (r responseWrapper) StatusCode() int              { return r.metadata.statusCode }\n"
  },
  {
    "path": "proxy/plugin_test.go",
    "content": "//go:build integration || !race\n// +build integration !race\n\n// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy/plugin\"\n)\n\nfunc TestNewPluginMiddleware_logger(t *testing.T) {\n\tplugin.LoadWithLogger(\"./plugin/tests\", \".so\", plugin.RegisterModifier, logging.NoOp)\n\n\tvalidator := func(ctx context.Context, r *Request) (*Response, error) {\n\t\tif r.Path != \"/bar/fooo/fooo\" {\n\t\t\treturn nil, fmt.Errorf(\"unexpected path %s\", r.Path)\n\t\t}\n\t\treturn &Response{\n\t\t\tData:       map[string]interface{}{\"foo\": \"bar\"},\n\t\t\tIsComplete: true,\n\t\t\tMetadata: Metadata{\n\t\t\t\tHeaders:    map[string][]string{},\n\t\t\t\tStatusCode: 0,\n\t\t\t},\n\t\t}, nil\n\t}\n\n\tbknd := NewBackendPluginMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.Backend{\n\t\t\tExtraConfig: map[string]interface{}{\n\t\t\t\tplugin.Namespace: map[string]interface{}{\n\t\t\t\t\t\"name\": []interface{}{\"lura-request-modifier-example-request\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)(validator)\n\n\tp := NewPluginMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.EndpointConfig{\n\t\t\tExtraConfig: map[string]interface{}{\n\t\t\t\tplugin.Namespace: map[string]interface{}{\n\t\t\t\t\t\"name\": []interface{}{\n\t\t\t\t\t\t\"lura-request-modifier-example-request\",\n\t\t\t\t\t\t\"lura-request-modifier-example-response\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)(bknd)\n\n\tresp, err := p(context.Background(), &Request{Path: \"/bar\"})\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t}\n\n\tif resp == nil {\n\t\tt.Errorf(\"unexpected response: %v\", resp)\n\t\treturn\n\t}\n\n\tif v, ok := resp.Data[\"foo\"].(string); !ok || v != \"bar\" {\n\t\tt.Errorf(\"unexpected foo value: %v\", resp.Data[\"foo\"])\n\t}\n}\n\nfunc TestNewPluginMiddleware_error_request(t *testing.T) {\n\tplugin.LoadWithLogger(\"./plugin/tests\", \".so\", plugin.RegisterModifier, logging.NoOp)\n\n\tvalidator := func(ctx context.Context, r *Request) (*Response, error) {\n\t\tt.Error(\"the backend should not be called\")\n\t\treturn nil, nil\n\t}\n\n\tbknd := NewBackendPluginMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.Backend{},\n\t)(validator)\n\n\tp := NewPluginMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.EndpointConfig{\n\t\t\tExtraConfig: map[string]interface{}{\n\t\t\t\tplugin.Namespace: map[string]interface{}{\n\t\t\t\t\t\"name\": []interface{}{\n\t\t\t\t\t\t\"lura-error-example-request\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)(bknd)\n\n\tresp, err := p(context.Background(), &Request{Path: \"/bar\"})\n\n\tif resp != nil {\n\t\tt.Errorf(\"unexpected response: %v\", resp)\n\t\treturn\n\t}\n\n\tif err == nil {\n\t\tt.Error(\"error expected\")\n\t\treturn\n\t}\n\n\tcustomErr, ok := err.(statusCodeError)\n\n\tif !ok {\n\t\tt.Errorf(\"unexpected error: %+v (%T)\", err, err)\n\t\treturn\n\t}\n\n\tif sc := customErr.StatusCode(); sc != http.StatusTeapot {\n\t\tt.Errorf(\"unexpected status code: %d\", sc)\n\t}\n\n\tif errorMsg := err.Error(); errorMsg != \"request rejected just because\" {\n\t\tt.Errorf(\"unexpected error message. have: '%s'\", errorMsg)\n\t}\n}\n\nfunc TestNewPluginMiddleware_error_response(t *testing.T) {\n\tplugin.LoadWithLogger(\"./plugin/tests\", \".so\", plugin.RegisterModifier, logging.NoOp)\n\n\tvar hit bool\n\n\tvalidator := func(ctx context.Context, r *Request) (*Response, error) {\n\t\thit = true\n\t\treturn &Response{\n\t\t\tData:       map[string]interface{}{\"foo\": \"bar\"},\n\t\t\tIsComplete: true,\n\t\t\tMetadata: Metadata{\n\t\t\t\tHeaders: map[string][]string{},\n\t\t\t},\n\t\t}, nil\n\t}\n\n\tbknd := NewBackendPluginMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.Backend{},\n\t)(validator)\n\n\tp := NewPluginMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.EndpointConfig{\n\t\t\tExtraConfig: map[string]interface{}{\n\t\t\t\tplugin.Namespace: map[string]interface{}{\n\t\t\t\t\t\"name\": []interface{}{\n\t\t\t\t\t\t\"lura-error-example-response\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)(bknd)\n\n\tresp, err := p(context.Background(), &Request{Path: \"/bar\"})\n\n\tif resp != nil {\n\t\tt.Errorf(\"unexpected response: %v\", resp)\n\t\treturn\n\t}\n\n\tif err == nil {\n\t\tt.Error(\"error expected\")\n\t\treturn\n\t}\n\n\tcustomErr, ok := err.(statusCodeError)\n\n\tif !ok {\n\t\tt.Errorf(\"unexpected error: %+v (%T)\", err, err)\n\t\treturn\n\t}\n\n\tif sc := customErr.StatusCode(); sc != http.StatusTeapot {\n\t\tt.Errorf(\"unexpected status code: %d\", sc)\n\t}\n\n\tif errorMsg := err.Error(); errorMsg != \"response replaced because reasons\" {\n\t\tt.Errorf(\"unexpected error message. have: '%s'\", errorMsg)\n\t}\n\n\tif !hit {\n\t\tt.Error(\"the backend has not been called\")\n\t}\n}\n\nfunc TestNewPluginMiddleware_PoisonedPlugin(t *testing.T) {\n\tplugin.RegisterModifier(\"poisoned\", func(map[string]interface{}) func(interface{}) (interface{}, error) {\n\t\treturn nil\n\t}, false, true)\n\n\texpectedResp := &Response{\n\t\tData:       map[string]interface{}{\"foo\": \"bar\"},\n\t\tIsComplete: true,\n\t\tMetadata: Metadata{\n\t\t\tHeaders: map[string][]string{},\n\t\t},\n\t}\n\n\tvalidator := func(ctx context.Context, r *Request) (*Response, error) {\n\t\treturn expectedResp, nil\n\t}\n\n\tbknd := NewBackendPluginMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.Backend{},\n\t)(validator)\n\n\tp := NewPluginMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.EndpointConfig{\n\t\t\tExtraConfig: map[string]interface{}{\n\t\t\t\tplugin.Namespace: map[string]interface{}{\n\t\t\t\t\t\"name\": []interface{}{\n\t\t\t\t\t\t\"poisoned\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)(bknd)\n\n\tresp, err := p(context.Background(), &Request{Path: \"/bar\"})\n\n\tif resp != expectedResp {\n\t\tt.Errorf(\"unexpected response: %v\", resp)\n\t\treturn\n\t}\n\n\tif err != nil {\n\t\tt.Error(\"error expected\")\n\t\treturn\n\t}\n}\n\ntype statusCodeError interface {\n\terror\n\tStatusCode() int\n}\n"
  },
  {
    "path": "proxy/proxy.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\nPackage proxy provides proxy and proxy middleware interfaces and implementations.\n*/\npackage proxy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\n// Namespace to be used in extra config\nconst Namespace = \"github.com/devopsfaith/krakend/proxy\"\n\n// Metadata is the Metadata of the Response which contains Headers and StatusCode\ntype Metadata struct {\n\tHeaders    map[string][]string\n\tStatusCode int\n}\n\n// Response is the entity returned by the proxy\ntype Response struct {\n\tData       map[string]interface{}\n\tIsComplete bool\n\tMetadata   Metadata\n\tIo         io.Reader\n}\n\n// readCloserWrapper is Io.Reader which is closed when the Context is closed or canceled\ntype readCloserWrapper struct {\n\tctx context.Context\n\trc  io.ReadCloser\n}\n\n// NewReadCloserWrapper Creates a new closeable io.Read\nfunc NewReadCloserWrapper(ctx context.Context, in io.ReadCloser) io.Reader {\n\twrapper := readCloserWrapper{ctx, in}\n\tgo wrapper.closeOnCancel()\n\treturn wrapper\n}\n\nfunc (w readCloserWrapper) Read(b []byte) (int, error) {\n\treturn w.rc.Read(b)\n}\n\n// closeOnCancel closes the io.Reader when the context is Done\nfunc (w readCloserWrapper) closeOnCancel() {\n\t<-w.ctx.Done()\n\tw.rc.Close()\n}\n\nvar (\n\t// ErrNoBackends is the error returned when an endpoint has no backends defined\n\tErrNoBackends = errors.New(\"all endpoints must have at least one backend\")\n\t// ErrTooManyBackends is the error returned when an endpoint has too many backends defined\n\tErrTooManyBackends = errors.New(\"too many backends for this proxy\")\n\t// ErrTooManyProxies is the error returned when a middleware has too many proxies defined\n\tErrTooManyProxies = errors.New(\"too many proxies for this proxy middleware\")\n\t// ErrNotEnoughProxies is the error returned when an endpoint has not enough proxies defined\n\tErrNotEnoughProxies = errors.New(\"not enough proxies for this endpoint\")\n)\n\n// Proxy processes a request in a given context and returns a response and an error\ntype Proxy func(ctx context.Context, request *Request) (*Response, error)\n\n// BackendFactory creates a proxy based on the received backend configuration\ntype BackendFactory func(remote *config.Backend) Proxy\n\n// Middleware adds a middleware, decorator or wrapper over a collection of proxies,\n// exposing a proxy interface.\n//\n// Proxy middlewares can be stacked:\n//\n//\tvar p Proxy\n//\tp := EmptyMiddleware(NoopProxy)\n//\tresponse, err := p(ctx, r)\ntype Middleware func(next ...Proxy) Proxy\n\n// EmptyMiddlewareWithLoggger is a dummy middleware, useful for testing and fallback\nfunc EmptyMiddlewareWithLogger(logger logging.Logger, next ...Proxy) Proxy {\n\tif len(next) > 1 {\n\t\tlogger.Fatal(\"too many proxies for this proxy middleware: EmptyMiddleware only accepts 1 proxy, got %d\", len(next))\n\t\treturn nil\n\t}\n\treturn next[0]\n}\n\nfunc EmptyMiddleware(next ...Proxy) Proxy {\n\treturn EmptyMiddlewareWithLogger(logging.NoOp, next...)\n}\n\nfunc emptyMiddlewareFallback(logger logging.Logger) Middleware {\n\treturn func(next ...Proxy) Proxy {\n\t\treturn EmptyMiddlewareWithLogger(logger, next...)\n\t}\n}\n\n// NoopProxy is a do nothing proxy, useful for testing\nfunc NoopProxy(_ context.Context, _ *Request) (*Response, error) { return nil, nil }\n"
  },
  {
    "path": "proxy/proxy_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestEmptyMiddleware_ok(t *testing.T) {\n\texpected := Response{}\n\tresult, err := EmptyMiddleware(dummyProxy(&expected))(context.Background(), &Request{})\n\tif err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif result != &expected {\n\t\tt.Errorf(\"The middleware returned an unexpected result: %v\\n\", result)\n\t}\n}\n\nfunc explosiveProxy(t *testing.T) Proxy {\n\treturn func(ctx context.Context, _ *Request) (*Response, error) {\n\t\tt.Error(\"This proxy shouldn't been executed!\")\n\t\treturn &Response{}, nil\n\t}\n}\n\nfunc dummyProxy(r *Response) Proxy {\n\treturn func(_ context.Context, _ *Request) (*Response, error) {\n\t\treturn r, nil\n\t}\n}\n\nfunc delayedProxy(_ *testing.T, timeout time.Duration, r *Response) Proxy {\n\treturn func(ctx context.Context, _ *Request) (*Response, error) {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tcase <-time.After(timeout):\n\t\t\treturn r, nil\n\t\t}\n\t}\n}\n\nfunc newDummyReadCloser(content string) io.ReadCloser {\n\treturn dummyReadCloser{strings.NewReader(content)}\n}\n\ntype dummyReadCloser struct {\n\treader io.Reader\n}\n\nfunc (d dummyReadCloser) Read(p []byte) (int, error) {\n\treturn d.reader.Read(p)\n}\n\nfunc (dummyReadCloser) Close() error {\n\treturn nil\n}\n\nfunc TestWrapper(t *testing.T) {\n\texpected := \"supu\"\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\treadCloser := &dummyRC{\n\t\tr:  bytes.NewBufferString(expected),\n\t\tmu: &sync.Mutex{},\n\t}\n\n\tr := NewReadCloserWrapper(ctx, readCloser)\n\tvar out bytes.Buffer\n\ttot, err := out.ReadFrom(r)\n\tif err != nil {\n\t\tt.Errorf(\"Total bits read: %d. Err: %s\", tot, err.Error())\n\t\treturn\n\t}\n\tif readCloser.IsClosed() {\n\t\tt.Error(\"The subject shouldn't be closed yet\")\n\t\treturn\n\t}\n\tif tot != 4 {\n\t\tt.Errorf(\"Unexpected number of bits read: %d\", tot)\n\t\treturn\n\t}\n\tif v := out.String(); v != expected {\n\t\tt.Errorf(\"Unexpected content: %s\", v)\n\t\treturn\n\t}\n\n\tcancel()\n\t<-time.After(100 * time.Millisecond)\n\tif !readCloser.IsClosed() {\n\t\tt.Error(\"The subject should be already closed\")\n\t\treturn\n\t}\n}\n\ntype dummyRC struct {\n\tr      io.Reader\n\tclosed bool\n\tmu     *sync.Mutex\n}\n\nfunc (d *dummyRC) Read(b []byte) (int, error) {\n\td.mu.Lock()\n\tdefer d.mu.Unlock()\n\n\tif d.closed {\n\t\treturn -1, fmt.Errorf(\"Reading from a closed source\")\n\t}\n\treturn d.r.Read(b)\n}\n\nfunc (d *dummyRC) Close() error {\n\td.mu.Lock()\n\tdefer d.mu.Unlock()\n\n\td.closed = true\n\treturn nil\n}\n\nfunc (d *dummyRC) IsClosed() bool {\n\td.mu.Lock()\n\tdefer d.mu.Unlock()\n\n\tres := d.closed\n\treturn res\n}\n"
  },
  {
    "path": "proxy/query_strings_filter.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"net/url\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\n// NewFilterQueryStringsMiddleware returns a middleware with or without a header filtering\n// proxy wrapping the next element (depending on the configuration).\nfunc NewFilterQueryStringsMiddleware(logger logging.Logger, remote *config.Backend) Middleware {\n\tif len(remote.QueryStringsToPass) == 0 {\n\t\treturn emptyMiddlewareFallback(logger)\n\t}\n\n\treturn func(next ...Proxy) Proxy {\n\t\tif len(next) > 1 {\n\t\t\tlogger.Fatal(\"too many proxies for this %s %s -> %s proxy middleware: NewFilterQueryStringsMiddleware only accepts 1 proxy, got %d\", remote.ParentEndpointMethod, remote.ParentEndpoint, remote.URLPattern, len(next))\n\t\t\treturn nil\n\t\t}\n\t\tnextProxy := next[0]\n\t\treturn func(ctx context.Context, request *Request) (*Response, error) {\n\t\t\tif len(request.Query) == 0 {\n\t\t\t\treturn nextProxy(ctx, request)\n\t\t\t}\n\t\t\tnumQueryStringsToPass := 0\n\t\t\tfor _, v := range remote.QueryStringsToPass {\n\t\t\t\tif _, ok := request.Query[v]; ok {\n\t\t\t\t\tnumQueryStringsToPass++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif numQueryStringsToPass == len(request.Query) {\n\t\t\t\t// all the query strings should pass, no need to clone the headers\n\t\t\t\treturn nextProxy(ctx, request)\n\t\t\t}\n\t\t\t// ATTENTION: this is not a clone of query strings!\n\t\t\t// this just filters the query strings we do not want to send:\n\t\t\t// issues and race conditions could happen the same way as when we\n\t\t\t// do not filter the headers. This is a design decission, and if we\n\t\t\t// want to clone the query string values (because of write modifications),\n\t\t\t// that should be done at an upper level (so the approach is the same\n\t\t\t// for non filtered parallel requests).\n\t\t\tnewQueryStrings := make(url.Values, numQueryStringsToPass)\n\t\t\tfor _, v := range remote.QueryStringsToPass {\n\t\t\t\tif values, ok := request.Query[v]; ok {\n\t\t\t\t\tnewQueryStrings[v] = values\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nextProxy(ctx, &Request{\n\t\t\t\tMethod:  request.Method,\n\t\t\t\tURL:     request.URL,\n\t\t\t\tQuery:   newQueryStrings,\n\t\t\t\tPath:    request.Path,\n\t\t\t\tBody:    request.Body,\n\t\t\t\tParams:  request.Params,\n\t\t\t\tHeaders: request.Headers,\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "proxy/query_strings_filter_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nfunc TestNewFilterQueryStringsMiddleware(t *testing.T) {\n\tmw := NewFilterQueryStringsMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.Backend{\n\t\t\tQueryStringsToPass: []string{\n\t\t\t\t\"oak\",\n\t\t\t\t\"cedar\",\n\t\t\t},\n\t\t},\n\t)\n\n\tvar receivedReq *Request\n\tprxy := mw(func(ctx context.Context, req *Request) (*Response, error) {\n\t\treceivedReq = req\n\t\treturn nil, nil\n\t})\n\n\tsentReq := &Request{\n\t\tBody:   nil,\n\t\tParams: map[string]string{},\n\t\tQuery: map[string][]string{\n\t\t\t\"oak\":    []string{\"acorn\", \"evergreen\"},\n\t\t\t\"maple\":  []string{\"tree\", \"shrub\"},\n\t\t\t\"cedar\":  []string{\"mediterranean\", \"himalayas\"},\n\t\t\t\"willow\": []string{\"350\"},\n\t\t},\n\t}\n\n\tprxy(context.Background(), sentReq)\n\n\tif receivedReq == sentReq {\n\t\tt.Errorf(\"request should be different\")\n\t\treturn\n\t}\n\n\toak, ok := receivedReq.Query[\"oak\"]\n\tif !ok {\n\t\tt.Errorf(\"missing 'oak'\")\n\t\treturn\n\t}\n\tif len(oak) != len(sentReq.Query[\"oak\"]) {\n\t\tt.Errorf(\"want len(oak): %d, got %d\",\n\t\t\tlen(sentReq.Query[\"oak\"]), len(oak))\n\t\treturn\n\t}\n\n\tfor idx, expected := range sentReq.Query[\"oak\"] {\n\t\tif expected != oak[idx] {\n\t\t\tt.Errorf(\"want oak[%d] = %s, got %s\",\n\t\t\t\tidx, expected, oak[idx])\n\t\t\treturn\n\t\t}\n\t}\n\n\tif _, ok := receivedReq.Query[\"cedar\"]; !ok {\n\t\tt.Errorf(\"missing 'cedar'\")\n\t\treturn\n\t}\n\n\tif _, ok := receivedReq.Query[\"mapple\"]; ok {\n\t\tt.Errorf(\"should not be there: 'mapple'\")\n\t\treturn\n\t}\n\n\tif _, ok := receivedReq.Query[\"willow\"]; ok {\n\t\tt.Errorf(\"should not be there: 'willow'\")\n\t\treturn\n\t}\n\n\t// check that when query strings are all the expected, no need to copy\n\tsentReq = &Request{\n\t\tBody:   nil,\n\t\tParams: map[string]string{},\n\t\tQuery: map[string][]string{\n\t\t\t\"oak\":   []string{\"acorn\", \"evergreen\"},\n\t\t\t\"cedar\": []string{\"mediterranean\", \"himalayas\"},\n\t\t},\n\t}\n\n\tprxy(context.Background(), sentReq)\n\n\tif receivedReq != sentReq {\n\t\tt.Errorf(\"request should be the same, no modification of query string expected\")\n\t\treturn\n\t}\n}\n\nfunc TestFilterQueryStringsBlockAll(t *testing.T) {\n\t// In order to block all the query strings, we must only let pass\n\t// the 'empty' string \"\"\n\tmw := NewFilterQueryStringsMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.Backend{\n\t\t\tQueryStringsToPass: []string{\"\"},\n\t\t},\n\t)\n\n\tvar receivedReq *Request\n\tprxy := mw(func(ctx context.Context, req *Request) (*Response, error) {\n\t\treceivedReq = req\n\t\treturn nil, nil\n\t})\n\n\tsentReq := &Request{\n\t\tBody:   nil,\n\t\tParams: map[string]string{},\n\t\tQuery: map[string][]string{\n\t\t\t\"oak\":   []string{\"acorn\", \"evergreen\"},\n\t\t\t\"maple\": []string{\"tree\", \"shrub\"},\n\t\t},\n\t}\n\n\tprxy(context.Background(), sentReq)\n\n\tif receivedReq == sentReq {\n\t\tt.Errorf(\"request should be different\")\n\t\treturn\n\t}\n\n\tif len(receivedReq.Query) != 0 {\n\t\tt.Errorf(\"should have blocked all query strings\")\n\t\treturn\n\t}\n}\n\nfunc TestFilterQueryStringsAllowAll(t *testing.T) {\n\t// Empty backend query strings to passa everything\n\tmw := NewFilterQueryStringsMiddleware(\n\t\tlogging.NoOp,\n\t\t&config.Backend{\n\t\t\tQueryStringsToPass: []string{},\n\t\t},\n\t)\n\n\tvar receivedReq *Request\n\tprxy := mw(func(ctx context.Context, req *Request) (*Response, error) {\n\t\treceivedReq = req\n\t\treturn nil, nil\n\t})\n\n\tsentReq := &Request{\n\t\tBody:   nil,\n\t\tParams: map[string]string{},\n\t\tQuery: map[string][]string{\n\t\t\t\"oak\":   []string{\"acorn\", \"evergreen\"},\n\t\t\t\"maple\": []string{\"tree\", \"shrub\"},\n\t\t},\n\t}\n\n\tprxy(context.Background(), sentReq)\n\n\tif len(receivedReq.Query) != 2 {\n\t\tt.Errorf(\"should have passed all query strings\")\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "proxy/register.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"github.com/luraproject/lura/v2/register\"\n)\n\nfunc NewRegister() *Register {\n\treturn &Register{\n\t\tresponseCombiners,\n\t}\n}\n\ntype Register struct {\n\t*combinerRegister\n}\n\ntype combinerRegister struct {\n\tdata     *register.Untyped\n\tfallback ResponseCombiner\n}\n\nfunc newCombinerRegister(data map[string]ResponseCombiner, fallback ResponseCombiner) *combinerRegister {\n\tr := register.NewUntyped()\n\tfor k, v := range data {\n\t\tr.Register(k, v)\n\t}\n\treturn &combinerRegister{r, fallback}\n}\n\nfunc (r *combinerRegister) GetResponseCombiner(name string) (ResponseCombiner, bool) {\n\tv, ok := r.data.Get(name)\n\tif !ok {\n\t\treturn r.fallback, ok\n\t}\n\tif rc, ok := v.(ResponseCombiner); ok {\n\t\treturn rc, ok\n\t}\n\treturn r.fallback, ok\n}\n\nfunc (r *combinerRegister) SetResponseCombiner(name string, rc ResponseCombiner) {\n\tr.data.Register(name, rc)\n}\n"
  },
  {
    "path": "proxy/register_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestNewRegister_responseCombiner_ok(t *testing.T) {\n\tr := NewRegister()\n\tr.SetResponseCombiner(\"name1\", func(total int, parts []*Response) *Response {\n\t\tif total < 0 || total >= len(parts) {\n\t\t\treturn nil\n\t\t}\n\t\treturn parts[total]\n\t})\n\n\trc, ok := r.GetResponseCombiner(\"name1\")\n\tif !ok {\n\t\tt.Error(\"expecting response combiner\")\n\t\treturn\n\t}\n\n\tresult := rc(0, []*Response{{IsComplete: true, Data: map[string]interface{}{\"a\": 42}}})\n\n\tif result == nil {\n\t\tt.Error(\"expecting result\")\n\t\treturn\n\t}\n\n\tif !result.IsComplete {\n\t\tt.Error(\"expecting a complete result\")\n\t\treturn\n\t}\n\n\tif len(result.Data) != 1 {\n\t\tt.Error(\"unexpected result size:\", len(result.Data))\n\t\treturn\n\t}\n}\n\nfunc TestNewRegister_responseCombiner_fallbackIfErrored(t *testing.T) {\n\tr := NewRegister()\n\n\tr.data.Register(\"errored\", true)\n\n\trc, ok := r.GetResponseCombiner(\"errored\")\n\tif !ok {\n\t\tt.Error(\"expecting response combiner\")\n\t\treturn\n\t}\n\n\toriginal := &Response{IsComplete: true, Data: map[string]interface{}{\"a\": 42}}\n\n\tresult := rc(1, []*Response{{Data: original.Data, IsComplete: original.IsComplete}})\n\n\tif !reflect.DeepEqual(original.Data, result.Data) {\n\t\tt.Errorf(\"unexpected data, want=%+v | have=%+v\", original.Data, result.Data)\n\t\treturn\n\t}\n\tif result.IsComplete != original.IsComplete {\n\t\tt.Errorf(\"unexpected complete flag, want=%+v | have=%+v\", original.IsComplete, result.IsComplete)\n\t\treturn\n\t}\n}\n\nfunc TestNewRegister_responseCombiner_fallbackIfUnknown(t *testing.T) {\n\tr := NewRegister()\n\n\trc, ok := r.GetResponseCombiner(\"unknown\")\n\tif ok {\n\t\tt.Error(\"the response combiner should not be found\")\n\t\treturn\n\t}\n\n\toriginal := &Response{IsComplete: true, Data: map[string]interface{}{\"a\": 42}}\n\n\tresult := rc(1, []*Response{{Data: original.Data, IsComplete: original.IsComplete}})\n\n\tif !reflect.DeepEqual(original.Data, result.Data) {\n\t\tt.Errorf(\"unexpected data, want=%+v | have=%+v\", original.Data, result.Data)\n\t\treturn\n\t}\n\tif result.IsComplete != original.IsComplete {\n\t\tt.Errorf(\"unexpected complete flag, want=%+v | have=%+v\", original.IsComplete, result.IsComplete)\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "proxy/request.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/url\"\n)\n\n// Request contains the data to send to the backend\ntype Request struct {\n\tMethod  string\n\tURL     *url.URL\n\tQuery   url.Values\n\tPath    string\n\tBody    io.ReadCloser\n\tParams  map[string]string\n\tHeaders map[string][]string\n}\n\n// GeneratePath takes a pattern and updates the path of the request\nfunc (r *Request) GeneratePath(URLPattern string) {\n\tif len(r.Params) == 0 {\n\t\tr.Path = URLPattern\n\t\treturn\n\t}\n\tbuff := []byte(URLPattern)\n\tfor k, v := range r.Params {\n\t\tvar key []byte\n\n\t\tkey = append(key, \"{{.\"...)\n\t\tkey = append(key, k...)\n\t\tkey = append(key, \"}}\"...)\n\t\tbuff = bytes.ReplaceAll(buff, key, []byte(v))\n\t}\n\tr.Path = string(buff)\n}\n\n// Clone clones itself into a new request. The returned cloned request is not\n// thread-safe, so changes on request.Params and request.Headers could generate\n// race-conditions depending on the part of the pipe they are being executed.\n// For thread-safe request headers and/or params manipulation, use the proxy.CloneRequest\n// function.\nfunc (r *Request) Clone() Request {\n\tvar clonedURL *url.URL\n\tif r.URL != nil {\n\t\tclonedURL, _ = url.Parse(r.URL.String())\n\t}\n\treturn Request{\n\t\tMethod:  r.Method,\n\t\tURL:     clonedURL,\n\t\tQuery:   r.Query,\n\t\tPath:    r.Path,\n\t\tBody:    r.Body,\n\t\tParams:  r.Params,\n\t\tHeaders: r.Headers,\n\t}\n}\n\n// CloneRequest returns a deep copy of the received request, so the received and the\n// returned proxy.Request do not share a pointer\nfunc CloneRequest(r *Request) *Request {\n\tclone := r.Clone()\n\tclone.Headers = CloneRequestHeaders(r.Headers)\n\tclone.Params = CloneRequestParams(r.Params)\n\tif r.Body == nil {\n\t\treturn &clone\n\t}\n\tbuf := new(bytes.Buffer)\n\tbuf.ReadFrom(r.Body)\n\tr.Body.Close()\n\n\tr.Body = io.NopCloser(bytes.NewReader(buf.Bytes()))\n\tclone.Body = io.NopCloser(buf)\n\n\treturn &clone\n}\n\n// CloneRequestHeaders returns a copy of the received request headers\nfunc CloneRequestHeaders(headers map[string][]string) map[string][]string {\n\tm := make(map[string][]string, len(headers))\n\tfor k, vs := range headers {\n\t\ttmp := make([]string, len(vs))\n\t\tcopy(tmp, vs)\n\t\tm[k] = tmp\n\t}\n\treturn m\n}\n\n// CloneRequestParams returns a copy of the received request params\nfunc CloneRequestParams(params map[string]string) map[string]string {\n\tm := make(map[string]string, len(params))\n\tfor k, v := range params {\n\t\tm[k] = v\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "proxy/request_benchmark_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport \"testing\"\n\nfunc BenchmarkRequestGeneratePath(b *testing.B) {\n\tr := Request{\n\t\tMethod: \"GET\",\n\t\tParams: map[string]string{\n\t\t\t\"Supu\": \"42\",\n\t\t\t\"Tupu\": \"false\",\n\t\t\t\"Foo\":  \"bar\",\n\t\t},\n\t}\n\n\tfor _, testCase := range []string{\n\t\t\"/a\",\n\t\t\"/a/{{.Supu}}\",\n\t\t\"/a?b={{.Tupu}}\",\n\t\t\"/a/{{.Supu}}/foo/{{.Foo}}\",\n\t\t\"/a/{{.Supu}}/foo/{{.Foo}}/b?c={{.Tupu}}\",\n\t} {\n\t\tb.Run(testCase, func(b *testing.B) {\n\t\t\tb.ReportAllocs()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\tr.GeneratePath(testCase)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "proxy/request_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRequestGeneratePath(t *testing.T) {\n\tr := Request{\n\t\tMethod: \"GET\",\n\t\tParams: map[string]string{\n\t\t\t\"Supu\": \"42\",\n\t\t\t\"Tupu\": \"false\",\n\t\t\t\"Foo\":  \"bar\",\n\t\t},\n\t}\n\n\tfor i, testCase := range [][]string{\n\t\t{\"/a/{{.Supu}}\", \"/a/42\"},\n\t\t{\"/a?b={{.Tupu}}\", \"/a?b=false\"},\n\t\t{\"/a/{{.Supu}}/foo/{{.Foo}}\", \"/a/42/foo/bar\"},\n\t\t{\"/a\", \"/a\"},\n\t} {\n\t\tr.GeneratePath(testCase[0])\n\t\tif r.Path != testCase[1] {\n\t\t\tt.Errorf(\"%d: want %s, have %s\", i, testCase[1], r.Path)\n\t\t}\n\t}\n}\n\nfunc TestRequest_Clone(t *testing.T) {\n\tr := Request{\n\t\tMethod: \"GET\",\n\t\tParams: map[string]string{\n\t\t\t\"Supu\": \"42\",\n\t\t\t\"Tupu\": \"false\",\n\t\t\t\"Foo\":  \"bar\",\n\t\t},\n\t\tHeaders: map[string][]string{\n\t\t\t\"Content-Type\": {\"application/json\"},\n\t\t},\n\t}\n\tclone := r.Clone()\n\n\tif len(r.Params) != len(clone.Params) {\n\t\tt.Errorf(\"wrong num of params. have: %d, want: %d\", len(clone.Params), len(r.Params))\n\t\treturn\n\t}\n\tfor k, v := range r.Params {\n\t\tif res, ok := clone.Params[k]; !ok {\n\t\t\tt.Errorf(\"param %s not cloned\", k)\n\t\t} else if res != v {\n\t\t\tt.Errorf(\"unexpected param %s. have: %s, want: %s\", k, res, v)\n\t\t}\n\t}\n\n\tif len(r.Headers) != len(clone.Headers) {\n\t\tt.Errorf(\"wrong num of headers. have: %d, want: %d\", len(clone.Headers), len(r.Headers))\n\t\treturn\n\t}\n\n\tfor k, vs := range r.Headers {\n\t\tif res, ok := clone.Headers[k]; !ok {\n\t\t\tt.Errorf(\"header %s not cloned\", k)\n\t\t} else if len(res) != len(vs) {\n\t\t\tt.Errorf(\"unexpected header %s. have: %v, want: %v\", k, res, vs)\n\t\t}\n\t}\n\n\tr.Headers[\"extra\"] = []string{\"supu\"}\n\n\tif len(r.Headers) != len(clone.Headers) {\n\t\tt.Errorf(\"wrong num of headers. have: %d, want: %d\", len(clone.Headers), len(r.Headers))\n\t\treturn\n\t}\n\n\tfor k, vs := range r.Headers {\n\t\tif res, ok := clone.Headers[k]; !ok {\n\t\t\tt.Errorf(\"header %s not cloned\", k)\n\t\t} else if len(res) != len(vs) {\n\t\t\tt.Errorf(\"unexpected header %s. have: %v, want: %v\", k, res, vs)\n\t\t}\n\t}\n}\n\nfunc TestCloneRequest(t *testing.T) {\n\tbody := `{\"a\":1,\"b\":2}`\n\tr := Request{\n\t\tMethod: \"POST\",\n\t\tParams: map[string]string{\n\t\t\t\"Supu\": \"42\",\n\t\t\t\"Tupu\": \"false\",\n\t\t\t\"Foo\":  \"bar\",\n\t\t},\n\t\tHeaders: map[string][]string{\n\t\t\t\"Content-Type\": {\"application/json\"},\n\t\t},\n\t\tBody: io.NopCloser(strings.NewReader(body)),\n\t}\n\tclone := CloneRequest(&r)\n\n\tif len(r.Params) != len(clone.Params) {\n\t\tt.Errorf(\"wrong num of params. have: %d, want: %d\", len(clone.Params), len(r.Params))\n\t\treturn\n\t}\n\tfor k, v := range r.Params {\n\t\tif res, ok := clone.Params[k]; !ok {\n\t\t\tt.Errorf(\"param %s not cloned\", k)\n\t\t} else if res != v {\n\t\t\tt.Errorf(\"unexpected param %s. have: %s, want: %s\", k, res, v)\n\t\t}\n\t}\n\n\tif len(r.Headers) != len(clone.Headers) {\n\t\tt.Errorf(\"wrong num of headers. have: %d, want: %d\", len(clone.Headers), len(r.Headers))\n\t\treturn\n\t}\n\n\tfor k, vs := range r.Headers {\n\t\tif res, ok := clone.Headers[k]; !ok {\n\t\t\tt.Errorf(\"header %s not cloned\", k)\n\t\t} else if len(res) != len(vs) {\n\t\t\tt.Errorf(\"unexpected header %s. have: %v, want: %v\", k, res, vs)\n\t\t}\n\t}\n\n\tr.Headers[\"extra\"] = []string{\"supu\"}\n\n\tif _, ok := clone.Headers[\"extra\"]; ok {\n\t\tt.Error(\"the cloned instance shares its headers with the original one\")\n\t}\n\n\tdelete(r.Params, \"Supu\")\n\n\tif _, ok := clone.Params[\"Supu\"]; !ok {\n\t\tt.Error(\"the cloned instance shares its params with the original one\")\n\t}\n\n\trb, _ := io.ReadAll(r.Body)\n\tcb, _ := io.ReadAll(clone.Body)\n\n\tif !bytes.Equal(cb, rb) || body != string(rb) {\n\t\tt.Errorf(\"unexpected bodies. original: %s, returned: %s\", string(rb), string(cb))\n\t}\n}\n"
  },
  {
    "path": "proxy/shadow.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nconst (\n\tshadowKey        = \"shadow\"\n\tshadowTimeoutKey = \"shadow_timeout\"\n)\n\ntype shadowFactory struct {\n\tf Factory\n}\n\n// New check the Backends for an ExtraConfig with the \"shadow\" param to true\n// implements the Factory interface. Sets the \"shadow_timeout\" defined in the\n// config; uses the backend timeout as fallback.\nfunc (s shadowFactory) New(cfg *config.EndpointConfig) (p Proxy, err error) {\n\tif len(cfg.Backend) == 0 {\n\t\terr = ErrNoBackends\n\t\treturn\n\t}\n\n\tcfgCopy := *cfg\n\n\tvar shadow []*config.Backend\n\tvar regular []*config.Backend\n\tvar maxTimeout time.Duration\n\tfor _, b := range cfgCopy.Backend {\n\t\tif d, ok := isShadowBackend(b); ok {\n\t\t\tif maxTimeout < d {\n\t\t\t\tmaxTimeout = d\n\t\t\t}\n\t\t\tshadow = append(shadow, b)\n\t\t\tcontinue\n\t\t}\n\t\tregular = append(regular, b)\n\t}\n\n\tcfgCopy.Backend = regular\n\tp, err = s.f.New(&cfgCopy)\n\n\tif len(shadow) > 0 {\n\t\tcfgCopy.Backend = shadow\n\t\tpShadow, _ := s.f.New(&cfgCopy)\n\t\tp = ShadowMiddlewareWithTimeout(maxTimeout, p, pShadow)\n\t}\n\n\treturn\n}\n\n// NewShadowFactory creates a new shadowFactory using the provided Factory\nfunc NewShadowFactory(f Factory) Factory {\n\treturn shadowFactory{f}\n}\n\n// ShadowMiddlewareWithLogger is a Middleware that creates a shadowProxy\nfunc ShadowMiddlewareWithLogger(logger logging.Logger, next ...Proxy) Proxy {\n\tswitch len(next) {\n\tcase 0:\n\t\tlogger.Fatal(\"not enough proxies for this endpoint: ShadowMiddlewareWithLogger only accepts 1 or 2 proxies, got 0\")\n\t\treturn nil\n\tcase 1:\n\t\treturn next[0]\n\tcase 2:\n\t\treturn NewShadowProxy(next[0], next[1])\n\tdefault:\n\t\tlogger.Fatal(\"too many proxies for this proxy middleware: ShadowMiddlewareWithLogger only accepts 1 or 2 proxies, got %d\", len(next))\n\t\treturn nil\n\t}\n}\n\n// ShadowMiddleware is a Middleware that creates a shadowProxy\nfunc ShadowMiddleware(next ...Proxy) Proxy {\n\treturn ShadowMiddlewareWithLogger(logging.NoOp, next...)\n}\n\n// ShadowMiddlewareWithTimeoutAndLogger is a Middleware that creates a shadowProxy with a timeout in the context\nfunc ShadowMiddlewareWithTimeoutAndLogger(logger logging.Logger, timeout time.Duration, next ...Proxy) Proxy {\n\tswitch len(next) {\n\tcase 0:\n\t\tlogger.Fatal(\"not enough proxies for this endpoint: ShadowMiddlewareWithTimeoutAndLogger only accepts 1 or 2 proxies, got 0\")\n\t\treturn nil\n\tcase 1:\n\t\treturn next[0]\n\tcase 2:\n\t\treturn NewShadowProxyWithTimeout(timeout, next[0], next[1])\n\tdefault:\n\t\tlogger.Fatal(\"too many proxies for this proxy middleware: ShadowMiddlewareWithTimeoutAndLogger only accepts 1 or 2 proxies, got %d\", len(next))\n\t\treturn nil\n\t}\n}\n\n// ShadowMiddlewareWithTimeout is a Middleware that creates a shadowProxy with a timeout in the context\nfunc ShadowMiddlewareWithTimeout(timeout time.Duration, next ...Proxy) Proxy {\n\treturn ShadowMiddlewareWithTimeoutAndLogger(logging.NoOp, timeout, next...)\n}\n\n// NewShadowProxy returns a Proxy that sends requests to p1 and p2 but ignores\n// the response of p2.\nfunc NewShadowProxy(p1, p2 Proxy) Proxy {\n\treturn NewShadowProxyWithTimeout(config.DefaultTimeout, p1, p2)\n}\n\n// NewShadowProxyWithTimeout returns a Proxy that sends requests to p1 and p2 but ignores\n// the response of p2. Sets a timeout in the context.\nfunc NewShadowProxyWithTimeout(timeout time.Duration, p1, p2 Proxy) Proxy {\n\treturn func(ctx context.Context, request *Request) (*Response, error) {\n\t\tshadowCtx, cancel := newContextWrapperWithTimeout(ctx, timeout)\n\t\tshadowRequest := CloneRequest(request)\n\t\tgo func() {\n\t\t\tp2(shadowCtx, shadowRequest)\n\t\t\tcancel()\n\t\t}()\n\t\treturn p1(ctx, request)\n\t}\n}\n\nfunc isShadowBackend(c *config.Backend) (time.Duration, bool) {\n\tduration := c.Timeout\n\tv, ok := c.ExtraConfig[Namespace]\n\tif !ok {\n\t\treturn duration, false\n\t}\n\n\te, ok := v.(map[string]interface{})\n\tif !ok {\n\t\treturn duration, false\n\t}\n\n\tk, ok := e[shadowKey]\n\tif !ok {\n\t\treturn duration, false\n\t}\n\n\tif s, ok := k.(bool); !ok || !s {\n\t\treturn duration, false\n\t}\n\n\tt, ok := e[shadowTimeoutKey].(string)\n\tif !ok {\n\t\treturn duration, true\n\t}\n\n\tif d, err := time.ParseDuration(t); err == nil {\n\t\tduration = d\n\t}\n\n\treturn duration, true\n}\n\ntype contextWrapper struct {\n\tcontext.Context\n\tdata context.Context\n}\n\nfunc (c contextWrapper) Value(key interface{}) interface{} {\n\treturn c.data.Value(key)\n}\n\nfunc newContextWrapperWithTimeout(data context.Context, timeout time.Duration) (contextWrapper, context.CancelFunc) {\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\treturn contextWrapper{\n\t\tContext: ctx,\n\t\tdata:    data,\n\t}, cancel\n}\n"
  },
  {
    "path": "proxy/shadow_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nvar (\n\textraCfg = config.ExtraConfig{\n\t\tNamespace: map[string]interface{}{\n\t\t\t\"shadow\":         true,\n\t\t\t\"shadow_timeout\": \"10s\",\n\t\t},\n\t}\n\tbadExtra = config.ExtraConfig{\n\t\tNamespace: map[string]interface{}{\n\t\t\t\"shadow\": \"string\",\n\t\t},\n\t}\n)\n\nfunc newAssertionProxy(counter *uint64) Proxy {\n\treturn func(ctx context.Context, request *Request) (*Response, error) {\n\t\tatomic.AddUint64(counter, 1)\n\t\treturn nil, nil\n\t}\n}\n\nfunc TestIsShadowBackend(t *testing.T) {\n\n\tcfg := &config.Backend{ExtraConfig: extraCfg}\n\tbadCfg := &config.Backend{ExtraConfig: badExtra}\n\n\td, ok := isShadowBackend(cfg)\n\tif !ok {\n\t\tt.Error(\"The shadow backend should be true\")\n\t}\n\tif d != 10*time.Second {\n\t\tt.Errorf(\"Invalid duration %s\", d)\n\t}\n\n\tif _, ok := isShadowBackend(&config.Backend{}); ok {\n\t\tt.Error(\"The shadow backend should be false\")\n\t}\n\n\tif _, ok := isShadowBackend(badCfg); ok {\n\t\tt.Error(\"The shadow backend should be false\")\n\t}\n}\n\nfunc TestShadowMiddleware(t *testing.T) {\n\tvar counter uint64\n\tassertProxy := newAssertionProxy(&counter)\n\tp := ShadowMiddleware(assertProxy, assertProxy)\n\tp(context.Background(), &Request{})\n\ttime.Sleep(100 * time.Millisecond)\n\tif atomic.LoadUint64(&counter) != 2 {\n\t\tt.Errorf(\"The shadow proxy should have been called 2 times, not %d\", counter)\n\t}\n}\n\nfunc TestShadowFactory_noBackends(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\tfactory := DefaultFactory(logger)\n\n\tsFactory := NewShadowFactory(factory)\n\n\tif _, err := sFactory.New(&config.EndpointConfig{}); err != ErrNoBackends {\n\t\tt.Errorf(\"Expecting ErrNoBackends. Got: %v\\n\", err)\n\t}\n}\n\nfunc TestNewShadowFactory(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\tvar counter uint64\n\tassertProxy := newAssertionProxy(&counter)\n\tfactory := NewDefaultFactory(func(_ *config.Backend) Proxy { return assertProxy }, logger)\n\tf := NewShadowFactory(factory)\n\tsBackend := &config.Backend{ExtraConfig: extraCfg}\n\tbackend := &config.Backend{}\n\tendpointConfig := &config.EndpointConfig{Backend: []*config.Backend{sBackend, backend}}\n\tserviceConfig := config.ServiceConfig{\n\t\tVersion:   config.ConfigVersion,\n\t\tEndpoints: []*config.EndpointConfig{endpointConfig},\n\t\tTimeout:   100 * time.Millisecond,\n\t\tHost:      []string{\"dummy\"},\n\t}\n\tif err = serviceConfig.Init(); err != nil {\n\t\tt.Errorf(\"Error during the config init: %s\\n\", err.Error())\n\t}\n\n\tp, err := f.New(endpointConfig)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\t_, err = p(context.Background(), &Request{})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\ttime.Sleep(100 * time.Millisecond)\n\tif atomic.LoadUint64(&counter) != 2 {\n\t\tt.Errorf(\"The shadow proxy should have been called 2 times, not %d\", counter)\n\t}\n}\n\nfunc TestShadowMiddleware_erroredBackend(t *testing.T) {\n\ttimeout := 100 * time.Millisecond\n\tp := ShadowMiddleware(\n\t\tdelayedProxy(t, timeout, &Response{Data: map[string]interface{}{\"supu\": 42}, IsComplete: true}),\n\t\tfunc(_ context.Context, _ *Request) (*Response, error) {\n\t\t\treturn nil, errors.New(\"ignore me\")\n\t\t},\n\t)\n\tmustEnd := time.After(time.Duration(5 * timeout))\n\tout, err := p(context.Background(), &Request{Params: map[string]string{}})\n\tif err != nil {\n\t\tt.Errorf(\"unexpected error: %s\\n\", err.Error())\n\t\treturn\n\t}\n\tif out == nil {\n\t\tt.Errorf(\"The proxy returned a null result\\n\")\n\t\treturn\n\t}\n\tselect {\n\tcase <-mustEnd:\n\t\tt.Errorf(\"We were expecting a response but we got none\\n\")\n\tdefault:\n\t\tif len(out.Data) != 1 {\n\t\t\tt.Errorf(\"We weren't expecting a partial response but we got %v!\\n\", out)\n\t\t}\n\t\tif !out.IsComplete {\n\t\t\tt.Errorf(\"We were expecting a completed response!\\n\")\n\t\t}\n\t}\n}\n\nfunc TestShadowMiddleware_partialTimeout(t *testing.T) {\n\ttimeout := 200 * time.Millisecond\n\tp := ShadowMiddleware(\n\t\tdelayedProxy(t, time.Duration(5*timeout), &Response{Data: map[string]interface{}{\"supu\": 42}}),\n\t\tdelayedProxy(t, time.Duration(timeout/2), &Response{Data: map[string]interface{}{\"supu\": 42}, IsComplete: true}))\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\tout, err := p(ctx, &Request{})\n\tif err == nil || err.Error() != \"context deadline exceeded\" {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\\n\", err.Error())\n\t}\n\tif out != nil {\n\t\tt.Errorf(\"The proxy did not return a null result: %+v\\n\", out)\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "proxy/stack_benchmark_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nvar result interface{}\n\nfunc BenchmarkProxyStack_single(b *testing.B) {\n\tbackend := &config.Backend{\n\t\tConcurrentCalls: 3,\n\t\tTimeout:         time.Duration(100) * time.Millisecond,\n\t\tHost:            []string{\"supu:8080\"},\n\t\tMethod:          \"GET\",\n\t\tURLPattern:      \"/a/{{.Tupu}}\",\n\t\tDenyList:        []string{\"map.aaaa\"},\n\t\tMapping:         map[string]string{\"supu\": \"SUPUUUUU\"},\n\t}\n\tcfg := &config.EndpointConfig{\n\t\tBackend: []*config.Backend{backend},\n\t\tExtraConfig: map[string]interface{}{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\tstaticKey: map[string]interface{}{\n\t\t\t\t\t\"data\": map[string]interface{}{\n\t\t\t\t\t\t\"status\": \"errored\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"strategy\": \"incomplete\",\n\t\t\t},\n\t\t},\n\t}\n\n\tef := NewEntityFormatter(backend)\n\tp := func(_ context.Context, _ *Request) (*Response, error) {\n\t\tres := ef.Format(Response{\n\t\t\tData: map[string]interface{}{\n\t\t\t\t\"supu\": 42,\n\t\t\t\t\"tupu\": true,\n\t\t\t\t\"foo\":  \"bar\",\n\t\t\t\t\"map\":  map[string]interface{}{\"aaaa\": false},\n\t\t\t\t\"col\": []interface{}{\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"a\": 1,\n\t\t\t\t\t\t\"b\": 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tIsComplete: true,\n\t\t})\n\t\treturn &res, nil\n\t}\n\tp = NewRoundRobinLoadBalancedMiddleware(backend)(p)\n\tp = NewConcurrentMiddleware(backend)(p)\n\tp = NewRequestBuilderMiddleware(backend)(p)\n\tp = NewStaticMiddleware(logging.NoOp, cfg)(p)\n\n\trequest := &Request{\n\t\tMethod:  \"GET\",\n\t\tBody:    newDummyReadCloser(\"\"),\n\t\tParams:  map[string]string{\"Tupu\": \"true\"},\n\t\tHeaders: map[string][]string{},\n\t}\n\n\tvar r *Response\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor i := 0; i < b.N; i++ {\n\t\tr, _ = p(context.Background(), request)\n\t}\n\tresult = r\n}\n\nfunc BenchmarkProxyStack_multi(b *testing.B) {\n\tbackend := &config.Backend{\n\t\tConcurrentCalls: 3,\n\t\tTimeout:         time.Duration(100) * time.Millisecond,\n\t\tHost:            []string{\"supu:8080\"},\n\t\tMethod:          \"GET\",\n\t\tURLPattern:      \"/a/{{.Tupu}}\",\n\t\tDenyList:        []string{\"map.aaaa\"},\n\t\tMapping:         map[string]string{\"supu\": \"SUPUUUUU\"},\n\t}\n\n\trequest := &Request{\n\t\tMethod:  \"GET\",\n\t\tBody:    newDummyReadCloser(\"\"),\n\t\tParams:  map[string]string{\"Tupu\": \"true\"},\n\t\tHeaders: map[string][]string{},\n\t}\n\n\tfor _, testCase := range [][]*config.Backend{\n\t\t{backend},\n\t\t{backend, backend},\n\t\t{backend, backend, backend},\n\t\t{backend, backend, backend, backend},\n\t\t{backend, backend, backend, backend, backend},\n\t} {\n\t\tb.Run(fmt.Sprintf(\"with %d backends\", len(testCase)), func(b *testing.B) {\n\n\t\t\tcfg := &config.EndpointConfig{\n\t\t\t\tBackend: testCase,\n\t\t\t}\n\n\t\t\tbackendProxy := make([]Proxy, len(cfg.Backend))\n\n\t\t\tfor i, backend := range cfg.Backend {\n\t\t\t\tef := NewEntityFormatter(backend)\n\t\t\t\tbackendProxy[i] = func(_ context.Context, _ *Request) (*Response, error) {\n\t\t\t\t\tres := ef.Format(Response{\n\t\t\t\t\t\tData: map[string]interface{}{\n\t\t\t\t\t\t\t\"supu\": 42,\n\t\t\t\t\t\t\t\"tupu\": true,\n\t\t\t\t\t\t\t\"foo\":  \"bar\",\n\t\t\t\t\t\t\t\"map\":  map[string]interface{}{\"aaaa\": false},\n\t\t\t\t\t\t\t\"col\": []interface{}{\n\t\t\t\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\t\t\t\"a\": 1,\n\t\t\t\t\t\t\t\t\t\"b\": 2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tIsComplete: true,\n\t\t\t\t\t})\n\t\t\t\t\treturn &res, nil\n\t\t\t\t}\n\t\t\t\tbackendProxy[i] = NewRoundRobinLoadBalancedMiddleware(backend)(backendProxy[i])\n\t\t\t\tbackendProxy[i] = NewConcurrentMiddleware(backend)(backendProxy[i])\n\t\t\t\tbackendProxy[i] = NewRequestBuilderMiddleware(backend)(backendProxy[i])\n\t\t\t}\n\t\t\tp := NewMergeDataMiddleware(logging.NoOp, cfg)(backendProxy...)\n\t\t\tp = NewStaticMiddleware(logging.NoOp, cfg)(p)\n\n\t\t\tvar r *Response\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\tr, _ = p(context.Background(), request)\n\t\t\t}\n\t\t\tresult = r\n\t\t})\n\t}\n}\n\nfunc BenchmarkProxyStack_multipost(b *testing.B) {\n\tbackendGET := &config.Backend{\n\t\tConcurrentCalls: 3,\n\t\tTimeout:         time.Duration(100) * time.Millisecond,\n\t\tHost:            []string{\"supu:8080\"},\n\t\tMethod:          \"GET\",\n\t\tURLPattern:      \"/a/{{.Tupu}}\",\n\t\tDenyList:        []string{\"map.aaaa\"},\n\t\tMapping:         map[string]string{\"supu\": \"SUPUUUUU\"},\n\t}\n\n\tbackendPOST := &config.Backend{\n\t\tConcurrentCalls: 3,\n\t\tTimeout:         time.Duration(100) * time.Millisecond,\n\t\tHost:            []string{\"supu:8080\"},\n\t\tMethod:          \"POST\",\n\t\tURLPattern:      \"/a/{{.Tupu}}\",\n\t\tDenyList:        []string{\"map.aaaa\"},\n\t\tMapping:         map[string]string{\"supu\": \"SUPUUUUU\"},\n\t}\n\n\trequest := &Request{\n\t\tMethod:  \"POST\",\n\t\tBody:    newDummyReadCloser(\"\"),\n\t\tParams:  map[string]string{\"Tupu\": \"true\"},\n\t\tHeaders: map[string][]string{},\n\t}\n\n\tfor _, testCase := range [][]*config.Backend{\n\t\t{backendGET},\n\t\t{backendGET, backendPOST},\n\t\t{backendGET, backendPOST, backendGET},\n\t\t{backendGET, backendPOST, backendGET, backendPOST},\n\t\t{backendGET, backendPOST, backendGET, backendPOST, backendPOST},\n\t} {\n\t\tb.Run(fmt.Sprintf(\"with %d backends\", len(testCase)), func(b *testing.B) {\n\n\t\t\tcfg := &config.EndpointConfig{\n\t\t\t\tBackend: testCase,\n\t\t\t}\n\n\t\t\tbackendProxy := make([]Proxy, len(cfg.Backend))\n\n\t\t\tfor i, backend := range cfg.Backend {\n\t\t\t\tef := NewEntityFormatter(backend)\n\t\t\t\tbackendProxy[i] = func(_ context.Context, _ *Request) (*Response, error) {\n\t\t\t\t\tres := ef.Format(Response{\n\t\t\t\t\t\tData: map[string]interface{}{\n\t\t\t\t\t\t\t\"supu\": 42,\n\t\t\t\t\t\t\t\"tupu\": true,\n\t\t\t\t\t\t\t\"foo\":  \"bar\",\n\t\t\t\t\t\t\t\"map\":  map[string]interface{}{\"aaaa\": false},\n\t\t\t\t\t\t\t\"col\": []interface{}{\n\t\t\t\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\t\t\t\"a\": 1,\n\t\t\t\t\t\t\t\t\t\"b\": 2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tIsComplete: true,\n\t\t\t\t\t})\n\t\t\t\t\treturn &res, nil\n\t\t\t\t}\n\t\t\t\tbackendProxy[i] = NewRoundRobinLoadBalancedMiddleware(backend)(backendProxy[i])\n\t\t\t\tbackendProxy[i] = NewConcurrentMiddleware(backend)(backendProxy[i])\n\t\t\t\tbackendProxy[i] = NewRequestBuilderMiddleware(backend)(backendProxy[i])\n\t\t\t}\n\t\t\tp := NewMergeDataMiddleware(logging.NoOp, cfg)(backendProxy...)\n\t\t\tp = NewStaticMiddleware(logging.NoOp, cfg)(p)\n\n\t\t\tvar r *Response\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\tr, _ = p(context.Background(), request)\n\t\t\t}\n\t\t\tresult = r\n\t\t})\n\t}\n}\n\nfunc BenchmarkProxyStack_single_flatmap(b *testing.B) {\n\tbackend := &config.Backend{\n\t\tConcurrentCalls: 3,\n\t\tTimeout:         time.Duration(100) * time.Millisecond,\n\t\tHost:            []string{\"supu:8080\"},\n\t\tMethod:          \"GET\",\n\t\tURLPattern:      \"/a/{{.Tupu}}\",\n\t\tExtraConfig: config.ExtraConfig{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\tflatmapKey: []interface{}{\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"del\",\n\t\t\t\t\t\t\"args\": []interface{}{\"map.aaaa\"},\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"move\",\n\t\t\t\t\t\t\"args\": []interface{}{\"supu\", \"SUPUUUUU\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tcfg := &config.EndpointConfig{\n\t\tBackend: []*config.Backend{backend},\n\t\tExtraConfig: map[string]interface{}{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\tstaticKey: map[string]interface{}{\n\t\t\t\t\t\"data\": map[string]interface{}{\n\t\t\t\t\t\t\"status\": \"errored\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"strategy\": \"incomplete\",\n\t\t\t},\n\t\t},\n\t}\n\n\tef := NewEntityFormatter(backend)\n\tp := func(_ context.Context, _ *Request) (*Response, error) {\n\t\tres := ef.Format(Response{\n\t\t\tData: map[string]interface{}{\n\t\t\t\t\"supu\": 42,\n\t\t\t\t\"tupu\": true,\n\t\t\t\t\"foo\":  \"bar\",\n\t\t\t\t\"map\":  map[string]interface{}{\"aaaa\": false},\n\t\t\t\t\"col\": []interface{}{\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"a\": 1,\n\t\t\t\t\t\t\"b\": 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tIsComplete: true,\n\t\t})\n\t\treturn &res, nil\n\t}\n\tp = NewRoundRobinLoadBalancedMiddleware(backend)(p)\n\tp = NewConcurrentMiddleware(backend)(p)\n\tp = NewRequestBuilderMiddleware(backend)(p)\n\tp = NewStaticMiddleware(logging.NoOp, cfg)(p)\n\n\trequest := &Request{\n\t\tMethod:  \"GET\",\n\t\tBody:    newDummyReadCloser(\"\"),\n\t\tParams:  map[string]string{\"Tupu\": \"true\"},\n\t\tHeaders: map[string][]string{},\n\t}\n\n\tvar r *Response\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor i := 0; i < b.N; i++ {\n\t\tr, _ = p(context.Background(), request)\n\t}\n\tresult = r\n}\n\nfunc BenchmarkProxyStack_multi_flatmap(b *testing.B) {\n\tbackend := &config.Backend{\n\t\tConcurrentCalls: 3,\n\t\tTimeout:         time.Duration(100) * time.Millisecond,\n\t\tHost:            []string{\"supu:8080\"},\n\t\tMethod:          \"GET\",\n\t\tURLPattern:      \"/a/{{.Tupu}}\",\n\t\tExtraConfig: config.ExtraConfig{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\tflatmapKey: []interface{}{\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"del\",\n\t\t\t\t\t\t\"args\": []interface{}{\"map.aaaa\"},\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\"type\": \"move\",\n\t\t\t\t\t\t\"args\": []interface{}{\"supu\", \"SUPUUUUU\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\trequest := &Request{\n\t\tMethod:  \"GET\",\n\t\tBody:    newDummyReadCloser(\"\"),\n\t\tParams:  map[string]string{\"Tupu\": \"true\"},\n\t\tHeaders: map[string][]string{},\n\t}\n\n\tfor _, testCase := range [][]*config.Backend{\n\t\t{backend},\n\t\t{backend, backend},\n\t\t{backend, backend, backend},\n\t\t{backend, backend, backend, backend},\n\t\t{backend, backend, backend, backend, backend},\n\t} {\n\t\tb.Run(fmt.Sprintf(\"with %d backends\", len(testCase)), func(b *testing.B) {\n\n\t\t\tcfg := &config.EndpointConfig{\n\t\t\t\tBackend: testCase,\n\t\t\t}\n\n\t\t\tbackendProxy := make([]Proxy, len(cfg.Backend))\n\n\t\t\tfor i, backend := range cfg.Backend {\n\t\t\t\tef := NewEntityFormatter(backend)\n\t\t\t\tbackendProxy[i] = func(_ context.Context, _ *Request) (*Response, error) {\n\t\t\t\t\tres := ef.Format(Response{\n\t\t\t\t\t\tData: map[string]interface{}{\n\t\t\t\t\t\t\t\"supu\": 42,\n\t\t\t\t\t\t\t\"tupu\": true,\n\t\t\t\t\t\t\t\"foo\":  \"bar\",\n\t\t\t\t\t\t\t\"map\":  map[string]interface{}{\"aaaa\": false},\n\t\t\t\t\t\t\t\"col\": []interface{}{\n\t\t\t\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\t\t\t\"a\": 1,\n\t\t\t\t\t\t\t\t\t\"b\": 2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tIsComplete: true,\n\t\t\t\t\t})\n\t\t\t\t\treturn &res, nil\n\t\t\t\t}\n\t\t\t\tbackendProxy[i] = NewRoundRobinLoadBalancedMiddleware(backend)(backendProxy[i])\n\t\t\t\tbackendProxy[i] = NewConcurrentMiddleware(backend)(backendProxy[i])\n\t\t\t\tbackendProxy[i] = NewRequestBuilderMiddleware(backend)(backendProxy[i])\n\t\t\t}\n\t\t\tp := NewMergeDataMiddleware(logging.NoOp, cfg)(backendProxy...)\n\t\t\tp = NewStaticMiddleware(logging.NoOp, cfg)(p)\n\n\t\t\tvar r *Response\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\tp(context.Background(), request)\n\t\t\t}\n\t\t\tresult = r\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "proxy/stack_test.go",
    "content": "//go:build integration || !race\n// +build integration !race\n\n// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nfunc TestProxyStack_multi(t *testing.T) {\n\tresults := map[string]int{}\n\tm := new(sync.Mutex)\n\ttotal := 100000\n\tcfgPath := \".config.json\"\n\n\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tm.Lock()\n\t\tresults[r.URL.String()]++\n\t\tm.Unlock()\n\t\tw.Write([]byte(\"{\\\"foo\\\":42}\"))\n\t}))\n\tdefer s.Close()\n\n\t{\n\t\tcfgContent := `{\n\t\t\t\"version\":3,\n\t\t\t\"endpoints\":[{\n\t\t\t\t\"endpoint\":\"/{foo}\",\n\t\t\t\t\"backend\":[\n\t\t\t\t\t{\n\t\t\t\t\t\t\"host\":       [\"%s\"],\n\t\t\t\t\t\t\"url_pattern\": \"/first/{foo}\",\n\t\t\t\t\t\t\"group\":      \"1\"\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"host\":       [\"%s\"],\n\t\t\t\t\t\t\"url_pattern\": \"/second/{foo}\",\n\t\t\t\t\t\t\"group\":      \"2\"\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"host\":       [\"%s\"],\n\t\t\t\t\t\t\"url_pattern\": \"/third/{foo}\",\n\t\t\t\t\t\t\"group\":      \"3\"\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}]\n\t\t}`\n\t\tif err := os.WriteFile(cfgPath, []byte(fmt.Sprintf(cfgContent, s.URL, s.URL, s.URL)), 0666); err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t\tdefer os.Remove(cfgPath)\n\t}\n\n\tcfg, err := config.NewParser().Parse(cfgPath)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tcfg.Normalize()\n\n\tfactory := NewDefaultFactory(httpProxy, logging.NoOp)\n\tp, err := factory.New(cfg.Endpoints[0])\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\tfor i := 0; i < total; i++ {\n\t\tp(context.Background(), &Request{\n\t\t\tMethod:  \"GET\",\n\t\t\tParams:  map[string]string{\"Foo\": \"42\"},\n\t\t\tHeaders: map[string][]string{},\n\t\t\tPath:    \"/\",\n\t\t\tQuery:   url.Values{},\n\t\t\tBody:    io.NopCloser(strings.NewReader(\"\")),\n\t\t\tURL:     new(url.URL),\n\t\t})\n\t}\n\n\tfor k, v := range results {\n\t\tif v != total {\n\t\t\tt.Errorf(\"the url %s was consumed %d times\", k, v)\n\t\t}\n\t}\n\n\tif len(results) != 3 {\n\t\tt.Errorf(\"unexpected number of consumed urls. have %d, want 3\", len(results))\n\t}\n}\n"
  },
  {
    "path": "proxy/static.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\n// NewStaticMiddleware creates proxy middleware for adding static values to the processed responses\nfunc NewStaticMiddleware(logger logging.Logger, endpointConfig *config.EndpointConfig) Middleware {\n\tcfg, ok := getStaticMiddlewareCfg(endpointConfig.ExtraConfig)\n\tif !ok {\n\t\treturn emptyMiddlewareFallback(logger)\n\t}\n\n\tb, _ := json.Marshal(cfg.Data)\n\n\tlogger.Debug(\n\t\tfmt.Sprintf(\n\t\t\t\"[ENDPOINT: %s][Static] Adding a static response using '%s' strategy. Data: %s\",\n\t\t\tendpointConfig.Endpoint,\n\t\t\tcfg.Strategy,\n\t\t\tstring(b),\n\t\t),\n\t)\n\n\treturn func(next ...Proxy) Proxy {\n\t\tif len(next) > 1 {\n\t\t\tlogger.Fatal(\"too many proxies for this proxy middleware: NewStaticMiddleware only accepts 1 proxy, got %d\", len(next))\n\t\t\treturn nil\n\t\t}\n\t\treturn func(ctx context.Context, request *Request) (*Response, error) {\n\t\t\tresult, err := next[0](ctx, request)\n\t\t\tif !cfg.Match(result, err) {\n\t\t\t\treturn result, err\n\t\t\t}\n\n\t\t\tif result == nil {\n\t\t\t\tresult = &Response{Data: map[string]interface{}{}}\n\t\t\t} else if result.Data == nil {\n\t\t\t\tresult.Data = map[string]interface{}{}\n\t\t\t}\n\n\t\t\tfor k, v := range cfg.Data {\n\t\t\t\tresult.Data[k] = v\n\t\t\t}\n\n\t\t\treturn result, err\n\t\t}\n\t}\n}\n\nconst (\n\tstaticKey = \"static\"\n\n\tstaticAlwaysStrategy       = \"always\"\n\tstaticIfSuccessStrategy    = \"success\"\n\tstaticIfErroredStrategy    = \"errored\"\n\tstaticIfCompleteStrategy   = \"complete\"\n\tstaticIfIncompleteStrategy = \"incomplete\"\n)\n\ntype staticConfig struct {\n\tData     map[string]interface{}\n\tStrategy string\n\tMatch    func(*Response, error) bool\n}\n\nfunc getStaticMiddlewareCfg(extra config.ExtraConfig) (staticConfig, bool) {\n\tv, ok := extra[Namespace]\n\tif !ok {\n\t\treturn staticConfig{}, ok\n\t}\n\te, ok := v.(map[string]interface{})\n\tif !ok {\n\t\treturn staticConfig{}, ok\n\t}\n\tv, ok = e[staticKey]\n\tif !ok {\n\t\treturn staticConfig{}, ok\n\t}\n\ttmp, ok := v.(map[string]interface{})\n\tif !ok {\n\t\treturn staticConfig{}, ok\n\t}\n\tdata, ok := tmp[\"data\"].(map[string]interface{})\n\tif !ok {\n\t\treturn staticConfig{}, ok\n\t}\n\n\tname, ok := tmp[\"strategy\"].(string)\n\tif !ok {\n\t\tname = staticAlwaysStrategy\n\t}\n\tcfg := staticConfig{\n\t\tData:     data,\n\t\tStrategy: name,\n\t\tMatch:    staticAlwaysMatch,\n\t}\n\tswitch name {\n\tcase staticAlwaysStrategy:\n\t\tcfg.Match = staticAlwaysMatch\n\tcase staticIfSuccessStrategy:\n\t\tcfg.Match = staticIfSuccessMatch\n\tcase staticIfErroredStrategy:\n\t\tcfg.Match = staticIfErroredMatch\n\tcase staticIfCompleteStrategy:\n\t\tcfg.Match = staticIfCompleteMatch\n\tcase staticIfIncompleteStrategy:\n\t\tcfg.Match = staticIfIncompleteMatch\n\t}\n\treturn cfg, true\n}\n\nfunc staticAlwaysMatch(_ *Response, _ error) bool      { return true }\nfunc staticIfSuccessMatch(_ *Response, err error) bool { return err == nil }\nfunc staticIfErroredMatch(_ *Response, err error) bool { return err != nil }\nfunc staticIfCompleteMatch(r *Response, err error) bool {\n\treturn err == nil && r != nil && r.IsComplete\n}\nfunc staticIfIncompleteMatch(r *Response, _ error) bool { return r == nil || !r.IsComplete }\n"
  },
  {
    "path": "proxy/static_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage proxy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nfunc TestNewStaticMiddleware_ok(t *testing.T) {\n\tendpoint := config.EndpointConfig{\n\t\tExtraConfig: config.ExtraConfig{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\tstaticKey: map[string]interface{}{\n\t\t\t\t\t\"data\": map[string]interface{}{\n\t\t\t\t\t\t\"new-1\": true,\n\t\t\t\t\t\t\"new-2\": map[string]interface{}{\"k1\": 42},\n\t\t\t\t\t\t\"new-3\": \"42\",\n\t\t\t\t\t},\n\t\t\t\t\t\"strategy\": \"incomplete\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tmw := NewStaticMiddleware(logging.NoOp, &endpoint)\n\n\tp := mw(dummyProxy(&Response{Data: map[string]interface{}{\"supu\": 42}, IsComplete: true}))\n\tout1, err := p(context.Background(), &Request{})\n\tif err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t}\n\tif out1 == nil {\n\t\tt.Error(\"The proxy returned a null result\")\n\t\treturn\n\t}\n\tif len(out1.Data) != 1 {\n\t\tt.Errorf(\"We weren't expecting an extra partial response but we got %v!\", out1)\n\t}\n\tif !out1.IsComplete {\n\t\tt.Errorf(\"We were expecting a completed response but we got an incompleted one!\")\n\t}\n\n\tp = mw(dummyProxy(&Response{Data: map[string]interface{}{\"supu\": 42}}))\n\tout2, err := p(context.Background(), &Request{})\n\tif err != nil {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err.Error())\n\t}\n\tif out2 == nil {\n\t\tt.Error(\"The proxy returned a null result\")\n\t\treturn\n\t}\n\tif len(out2.Data) != 4 {\n\t\tt.Errorf(\"We weren't expecting a partial response but we got %v!\", out2)\n\t}\n\n\texpectedError := errors.New(\"expect me\")\n\tp = mw(func(_ context.Context, _ *Request) (*Response, error) {\n\t\treturn nil, expectedError\n\t})\n\tout3, err := p(context.Background(), &Request{})\n\tif err != expectedError {\n\t\tt.Errorf(\"The middleware propagated an unexpected error: %s\", err)\n\t}\n\tif out3 == nil {\n\t\tt.Error(\"The proxy returned a null result\")\n\t\treturn\n\t}\n\tif len(out3.Data) != 3 {\n\t\tt.Errorf(\"We weren't expecting a partial response but we got %v!\", out3)\n\t}\n}\n\ntype staticMatcherTestCase struct {\n\tname     string\n\tresponse *Response\n\terr      error\n\texpected bool\n}\n\nfunc TestNewStaticMiddleware(t *testing.T) {\n\tdata := map[string]interface{}{\n\t\t\"new-1\": true,\n\t\t\"new-2\": map[string]interface{}{\"k1\": 42},\n\t\t\"new-3\": \"42\",\n\t}\n\textra := config.ExtraConfig{\n\t\tNamespace: map[string]interface{}{\n\t\t\tstaticKey: map[string]interface{}{\n\t\t\t\t\"data\":     data,\n\t\t\t\t\"strategy\": staticIfCompleteStrategy,\n\t\t\t},\n\t\t},\n\t}\n\n\tmw := NewStaticMiddleware(logging.NoOp, &config.EndpointConfig{ExtraConfig: extra})\n\n\tp := mw(func(_ context.Context, r *Request) (*Response, error) {\n\t\treturn &Response{IsComplete: true}, nil\n\t})\n\n\tresp, err := p(context.Background(), nil)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\tif !reflect.DeepEqual(data, resp.Data) {\n\t\tt.Errorf(\"unexpected data: %+v\", resp.Data)\n\t}\n}\n\nfunc Test_staticAlwaysMatch(t *testing.T) {\n\textra := config.ExtraConfig{\n\t\tNamespace: map[string]interface{}{\n\t\t\tstaticKey: map[string]interface{}{\n\t\t\t\t\"data\": map[string]interface{}{\n\t\t\t\t\t\"new-1\": true,\n\t\t\t\t\t\"new-2\": map[string]interface{}{\"k1\": 42},\n\t\t\t\t\t\"new-3\": \"42\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tcfg, _ := getStaticMiddlewareCfg(extra)\n\tfor _, testCase := range []staticMatcherTestCase{\n\t\t{\n\t\t\tname:     \"nil & nil\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"nil & error\",\n\t\t\terr:      errors.New(\"ignore me\"),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"complete & nil\",\n\t\t\tresponse: &Response{Data: map[string]interface{}{}, IsComplete: true},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"complete & error\",\n\t\t\tresponse: &Response{Data: map[string]interface{}{}, IsComplete: true},\n\t\t\terr:      errors.New(\"ignore me\"),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"incomplete\",\n\t\t\tresponse: &Response{},\n\t\t\texpected: true,\n\t\t},\n\t} {\n\t\ttestStaticMatcher(t, cfg.Match, testCase)\n\t}\n}\n\nfunc Test_staticIfSuccessMatch(t *testing.T) {\n\textra := config.ExtraConfig{\n\t\tNamespace: map[string]interface{}{\n\t\t\tstaticKey: map[string]interface{}{\n\t\t\t\t\"data\": map[string]interface{}{\n\t\t\t\t\t\"new-1\": true,\n\t\t\t\t\t\"new-2\": map[string]interface{}{\"k1\": 42},\n\t\t\t\t\t\"new-3\": \"42\",\n\t\t\t\t},\n\t\t\t\t\"strategy\": staticIfSuccessStrategy,\n\t\t\t},\n\t\t},\n\t}\n\tcfg, _ := getStaticMiddlewareCfg(extra)\n\tfor _, testCase := range []staticMatcherTestCase{\n\t\t{\n\t\t\tname:     \"nil & nil\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"nil & error\",\n\t\t\terr:      errors.New(\"ignore me\"),\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"success & nil\",\n\t\t\tresponse: &Response{},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"success & error\",\n\t\t\tresponse: &Response{},\n\t\t\terr:      errors.New(\"ignore me\"),\n\t\t},\n\t} {\n\t\ttestStaticMatcher(t, cfg.Match, testCase)\n\t}\n}\n\nfunc Test_staticIfErroredMatch(t *testing.T) {\n\textra := config.ExtraConfig{\n\t\tNamespace: map[string]interface{}{\n\t\t\tstaticKey: map[string]interface{}{\n\t\t\t\t\"data\": map[string]interface{}{\n\t\t\t\t\t\"new-1\": true,\n\t\t\t\t\t\"new-2\": map[string]interface{}{\"k1\": 42},\n\t\t\t\t\t\"new-3\": \"42\",\n\t\t\t\t},\n\t\t\t\t\"strategy\": staticIfErroredStrategy,\n\t\t\t},\n\t\t},\n\t}\n\tcfg, _ := getStaticMiddlewareCfg(extra)\n\tfor _, testCase := range []staticMatcherTestCase{\n\t\t{\n\t\t\tname: \"nil & nil\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nil & error\",\n\t\t\terr:      errors.New(\"ignore me\"),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"success & nil\",\n\t\t\tresponse: &Response{},\n\t\t},\n\t\t{\n\t\t\tname:     \"success & error\",\n\t\t\tresponse: &Response{},\n\t\t\terr:      errors.New(\"ignore me\"),\n\t\t\texpected: true,\n\t\t},\n\t} {\n\t\ttestStaticMatcher(t, cfg.Match, testCase)\n\t}\n}\n\nfunc Test_staticIfCompleteMatch(t *testing.T) {\n\textra := config.ExtraConfig{\n\t\tNamespace: map[string]interface{}{\n\t\t\tstaticKey: map[string]interface{}{\n\t\t\t\t\"data\": map[string]interface{}{\n\t\t\t\t\t\"new-1\": true,\n\t\t\t\t\t\"new-2\": map[string]interface{}{\"k1\": 42},\n\t\t\t\t\t\"new-3\": \"42\",\n\t\t\t\t},\n\t\t\t\t\"strategy\": staticIfCompleteStrategy,\n\t\t\t},\n\t\t},\n\t}\n\tcfg, _ := getStaticMiddlewareCfg(extra)\n\tfor _, testCase := range []staticMatcherTestCase{\n\t\t{\n\t\t\tname: \"nil & nil\",\n\t\t},\n\t\t{\n\t\t\tname: \"nil & error\",\n\t\t\terr:  errors.New(\"ignore me\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"success & nil\",\n\t\t\tresponse: &Response{},\n\t\t},\n\t\t{\n\t\t\tname:     \"success & error\",\n\t\t\tresponse: &Response{},\n\t\t\terr:      errors.New(\"ignore me\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"complete\",\n\t\t\tresponse: &Response{IsComplete: true},\n\t\t\texpected: true,\n\t\t},\n\t} {\n\t\ttestStaticMatcher(t, cfg.Match, testCase)\n\t}\n}\n\nfunc Test_staticIfIncompleteMatch(t *testing.T) {\n\textra := config.ExtraConfig{\n\t\tNamespace: map[string]interface{}{\n\t\t\tstaticKey: map[string]interface{}{\n\t\t\t\t\"data\": map[string]interface{}{\n\t\t\t\t\t\"new-1\": true,\n\t\t\t\t\t\"new-2\": map[string]interface{}{\"k1\": 42},\n\t\t\t\t\t\"new-3\": \"42\",\n\t\t\t\t},\n\t\t\t\t\"strategy\": staticIfIncompleteStrategy,\n\t\t\t},\n\t\t},\n\t}\n\tcfg, _ := getStaticMiddlewareCfg(extra)\n\tfor _, testCase := range []staticMatcherTestCase{\n\t\t{\n\t\t\tname:     \"nil & nil\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"nil & error\",\n\t\t\terr:      errors.New(\"ignore me\"),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"success & nil\",\n\t\t\tresponse: &Response{},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"success & error\",\n\t\t\tresponse: &Response{},\n\t\t\terr:      errors.New(\"ignore me\"),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"complete\",\n\t\t\tresponse: &Response{IsComplete: true},\n\t\t},\n\t} {\n\t\ttestStaticMatcher(t, cfg.Match, testCase)\n\t}\n}\n\nfunc testStaticMatcher(t *testing.T, marcher func(*Response, error) bool, testCase staticMatcherTestCase) {\n\tif marcher(testCase.response, testCase.err) != testCase.expected {\n\t\tt.Errorf(\n\t\t\t\"[%s] unexepecting match result (%v) with: %v, %v\",\n\t\t\ttestCase.name,\n\t\t\ttestCase.expected,\n\t\t\ttestCase.response,\n\t\t\ttestCase.err,\n\t\t)\n\t}\n}\n\nfunc Test_getStaticMiddlewareCfg_ko(t *testing.T) {\n\tfor i, cfg := range []config.ExtraConfig{\n\t\t{\"a\": 42},\n\t\t{Namespace: true},\n\t\t{Namespace: map[string]interface{}{}},\n\t\t{Namespace: map[string]interface{}{staticKey: 42}},\n\t\t{Namespace: map[string]interface{}{staticKey: map[string]interface{}{}}},\n\t} {\n\t\tif _, ok := getStaticMiddlewareCfg(cfg); ok {\n\t\t\tt.Errorf(\"expecting error on test #%d\", i)\n\t\t}\n\t}\n}\n\nfunc Test_getStaticMiddlewareCfg_strategy(t *testing.T) {\n\tfor _, strategy := range []string{\n\t\tstaticAlwaysStrategy,\n\t\tstaticIfSuccessStrategy,\n\t\tstaticIfErroredStrategy,\n\t\tstaticIfCompleteStrategy,\n\t\tstaticIfIncompleteStrategy,\n\t} {\n\t\tcfg := config.ExtraConfig{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\tstaticKey: map[string]interface{}{\n\t\t\t\t\t\"data\":     map[string]interface{}{},\n\t\t\t\t\t\"strategy\": strategy,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tstaticCfg, ok := getStaticMiddlewareCfg(cfg)\n\t\tif !ok {\n\t\t\tt.Errorf(\"unexpecting error on test %s\", strategy)\n\t\t}\n\t\tif strategy != staticCfg.Strategy {\n\t\t\tt.Errorf(\"wrong parsing on test %s\", strategy)\n\t\t}\n\t}\n\n\tcfg := config.ExtraConfig{\n\t\tNamespace: map[string]interface{}{\n\t\t\tstaticKey: map[string]interface{}{\n\t\t\t\t\"data\": map[string]interface{}{},\n\t\t\t},\n\t\t},\n\t}\n\tstaticCfg, ok := getStaticMiddlewareCfg(cfg)\n\tif !ok {\n\t\tt.Error(\"unexpecting error parsing default strategy\")\n\t}\n\tif staticAlwaysStrategy != staticCfg.Strategy {\n\t\tt.Error(\"wrong parsing default strategy\")\n\t}\n}\n"
  },
  {
    "path": "register/register.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\n\tPackage register offers tools for creating and managing registers.\n*/\npackage register\n\nimport \"sync\"\n\n// New returns an initialized Namespaced register\nfunc New() *Namespaced {\n\treturn &Namespaced{data: NewUntyped()}\n}\n\n// Namespaced is a register able to keep track of elements stored\n// under namespaces and keys\ntype Namespaced struct {\n\tdata *Untyped\n}\n\n// Get returns the Untyped register stored under the namespace\nfunc (n *Namespaced) Get(namespace string) (*Untyped, bool) {\n\tv, ok := n.data.Get(namespace)\n\tif !ok {\n\t\treturn nil, ok\n\t}\n\tregister, ok := v.(*Untyped)\n\treturn register, ok\n}\n\n// Register stores v at the key name of the Untyped register named namespace\nfunc (n *Namespaced) Register(namespace, name string, v interface{}) {\n\tif register, ok := n.Get(namespace); ok {\n\t\tregister.Register(name, v)\n\t\treturn\n\t}\n\n\tregister := NewUntyped()\n\tregister.Register(name, v)\n\tn.data.Register(namespace, register)\n}\n\n// AddNamespace adds a new, empty Untyped register under the give namespace (if\n// it did not exist)\nfunc (n *Namespaced) AddNamespace(namespace string) {\n\tif _, ok := n.Get(namespace); ok {\n\t\treturn\n\t}\n\tn.data.Register(namespace, NewUntyped())\n}\n\n// NewUntyped returns an empty Untyped register\nfunc NewUntyped() *Untyped {\n\treturn &Untyped{\n\t\tdata:  map[string]interface{}{},\n\t\tmutex: &sync.RWMutex{},\n\t}\n}\n\n// Untyped is a simple register, safe for concurrent access\ntype Untyped struct {\n\tdata  map[string]interface{}\n\tmutex *sync.RWMutex\n}\n\n// Register stores v under the key name\nfunc (u *Untyped) Register(name string, v interface{}) {\n\tu.mutex.Lock()\n\tu.data[name] = v\n\tu.mutex.Unlock()\n}\n\n// Get returns the value stored at the key name\nfunc (u *Untyped) Get(name string) (interface{}, bool) {\n\tu.mutex.RLock()\n\tv, ok := u.data[name]\n\tu.mutex.RUnlock()\n\treturn v, ok\n}\n\n// Clone returns a snapshot of the register\nfunc (u *Untyped) Clone() map[string]interface{} {\n\tu.mutex.RLock()\n\tres := make(map[string]interface{}, len(u.data))\n\tfor k, v := range u.data {\n\t\tres[k] = v\n\t}\n\tu.mutex.RUnlock()\n\treturn res\n}\n"
  },
  {
    "path": "register/register_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage register\n\nimport \"testing\"\n\nfunc TestNamespaced(t *testing.T) {\n\tr := New()\n\tr.Register(\"namespace1\", \"name1\", 42)\n\tr.AddNamespace(\"namespace1\")\n\tr.AddNamespace(\"namespace2\")\n\tr.Register(\"namespace2\", \"name2\", true)\n\n\tnr, ok := r.Get(\"namespace1\")\n\tif !ok {\n\t\tt.Error(\"namespace1 not found\")\n\t\treturn\n\t}\n\tif _, ok := nr.Get(\"name2\"); ok {\n\t\tt.Error(\"name2 found into namespace1\")\n\t\treturn\n\t}\n\tv1, ok := nr.Get(\"name1\")\n\tif !ok {\n\t\tt.Error(\"name1 not found\")\n\t\treturn\n\t}\n\tif i, ok := v1.(int); !ok || i != 42 {\n\t\tt.Error(\"unexpected value:\", v1)\n\t}\n\n\tnr, ok = r.Get(\"namespace2\")\n\tif !ok {\n\t\tt.Error(\"namespace2 not found\")\n\t\treturn\n\t}\n\tif _, ok := nr.Get(\"name1\"); ok {\n\t\tt.Error(\"name1 found into namespace2\")\n\t\treturn\n\t}\n\tv2, ok := nr.Get(\"name2\")\n\tif !ok {\n\t\tt.Error(\"name2 not found\")\n\t\treturn\n\t}\n\tif b, ok := v2.(bool); !ok || !b {\n\t\tt.Error(\"unexpected value:\", v2)\n\t}\n}\n"
  },
  {
    "path": "router/chi/endpoint.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage chi\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/router/mux\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n)\n\n// HandlerFactory creates a handler function that adapts the chi router with the injected proxy\ntype HandlerFactory func(*config.EndpointConfig, proxy.Proxy) http.HandlerFunc\n\n// NewEndpointHandler implements the HandleFactory interface using the default ToHTTPError function\nfunc NewEndpointHandler(cfg *config.EndpointConfig, prxy proxy.Proxy) http.HandlerFunc {\n\thf := mux.CustomEndpointHandler(\n\t\tmux.NewRequestBuilder(extractParamsFromEndpoint),\n\t)\n\treturn hf(cfg, prxy)\n}\n\nfunc extractParamsFromEndpoint(r *http.Request) map[string]string {\n\tctx := r.Context()\n\trctx := chi.RouteContext(ctx)\n\n\tparams := map[string]string{}\n\tif len(rctx.URLParams.Keys) > 0 {\n\t\ttitle := cases.Title(language.Und)\n\t\tfor _, param := range rctx.URLParams.Keys {\n\t\t\tparams[title.String(param[:1])+param[1:]] = chi.URLParam(r, param)\n\t\t}\n\t}\n\treturn params\n}\n"
  },
  {
    "path": "router/chi/endpoint_benchmark_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage chi\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n)\n\nfunc BenchmarkEndpointHandler_ko(b *testing.B) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, fmt.Errorf(\"This is %s\", \"a dummy error\")\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:     time.Second,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\"},\n\t}\n\n\trouter := chi.NewRouter()\n\trouter.Handle(\"/_chi_endpoint/\", NewEndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_chi_endpoint/a?b=1\", http.NoBody)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tb.ReportAllocs()\n\tfor i := 0; i < b.N; i++ {\n\t\tw := httptest.NewRecorder()\n\t\trouter.ServeHTTP(w, req)\n\t}\n}\n\nfunc BenchmarkEndpointHandler_ok(b *testing.B) {\n\tpResp := proxy.Response{\n\t\tData:       map[string]interface{}{},\n\t\tIo:         io.NopCloser(&bytes.Buffer{}),\n\t\tIsComplete: true,\n\t\tMetadata:   proxy.Metadata{},\n\t}\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &pResp, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:     time.Second,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\"},\n\t}\n\n\trouter := chi.NewRouter()\n\trouter.Handle(\"/_chi_endpoint/\", NewEndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_chi_endpoint/a?b=1\", http.NoBody)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tb.ReportAllocs()\n\tfor i := 0; i < b.N; i++ {\n\t\tw := httptest.NewRecorder()\n\t\trouter.ServeHTTP(w, req)\n\t}\n}\n\nfunc BenchmarkEndpointHandler_ko_Parallel(b *testing.B) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, fmt.Errorf(\"This is %s\", \"a dummy error\")\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:     time.Second,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\"},\n\t}\n\n\trouter := chi.NewRouter()\n\trouter.Handle(\"/_chi_endpoint/\", NewEndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_chi_endpoint/a?b=1\", http.NoBody)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tw := httptest.NewRecorder()\n\t\t\trouter.ServeHTTP(w, req)\n\t\t}\n\t})\n}\n\nfunc BenchmarkEndpointHandler_ok_Parallel(b *testing.B) {\n\tpResp := proxy.Response{\n\t\tData:       map[string]interface{}{},\n\t\tIo:         io.NopCloser(&bytes.Buffer{}),\n\t\tIsComplete: true,\n\t\tMetadata:   proxy.Metadata{},\n\t}\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &pResp, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:     time.Second,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\"},\n\t}\n\n\trouter := chi.NewRouter()\n\trouter.Handle(\"/_chi_endpoint/\", NewEndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_chi_endpoint/a?b=1\", http.NoBody)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tw := httptest.NewRecorder()\n\t\t\trouter.ServeHTTP(w, req)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "router/chi/endpoint_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage chi\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\nfunc TestEndpointHandler_ok(t *testing.T) {\n\tp := func(ctx context.Context, req *proxy.Request) (*proxy.Response, error) {\n\t\tdata, _ := json.Marshal(req.Query)\n\t\tif string(data) != `{\"b\":[\"1\"],\"c[]\":[\"x\",\"y\"],\"d\":[\"1\",\"2\"]}` {\n\t\t\tt.Errorf(\"unexpected querystring: %s\", data)\n\t\t}\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       map[string]interface{}{\"supu\": \"tupu\"},\n\t\t\tMetadata: proxy.Metadata{\n\t\t\t\tHeaders: map[string][]string{\"a\": {\"a1\", \"a2\"}},\n\t\t\t},\n\t\t}, nil\n\t}\n\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{\\\"supu\\\":\\\"tupu\\\"}\",\n\t\texpectedCache:      \"public, max-age=21600\",\n\t\texpectedContent:    \"application/json\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          true,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_okAllParams(t *testing.T) {\n\tp := func(_ context.Context, req *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       map[string]interface{}{\"query\": req.Query, \"headers\": req.Headers, \"params\": req.Params},\n\t\t\tMetadata: proxy.Metadata{\n\t\t\t\tHeaders:    map[string][]string{\"X-YZ\": {\"something\"}},\n\t\t\t\tStatusCode: 200,\n\t\t\t},\n\t\t}, nil\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       `{\"headers\":{\"Content-Type\":[\"application/json\"],\"User-Agent\":[\"KrakenD Version undefined\"],\"X-Forwarded-For\":[\"\"],\"X-Forwarded-Host\":[\"127.0.0.1:8080\"]},\"params\":{\"Param\":\"a\"},\"query\":{\"a\":[\"42\"],\"b\":[\"1\"],\"c[]\":[\"x\",\"y\"],\"d\":[\"1\",\"2\"]}}`,\n\t\texpectedCache:      \"public, max-age=21600\",\n\t\texpectedContent:    \"application/json\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          true,\n\t\tqueryString:        []string{\"*\"},\n\t\theaders:            []string{\"*\"},\n\t\texpectedHeaders:    map[string][]string{\"X-YZ\": {\"something\"}},\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_incomplete(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: false,\n\t\t\tData:       map[string]interface{}{\"foo\": \"bar\"},\n\t\t}, nil\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{\\\"foo\\\":\\\"bar\\\"}\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"application/json\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_errored(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, errors.New(\"this is a dummy error\")\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"this is a dummy error\\n\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"text/plain; charset=utf-8\",\n\t\texpectedStatusCode: http.StatusInternalServerError,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_errored_responseError(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, dummyResponseError{err: \"this is a dummy error\", status: http.StatusTeapot}\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"this is a dummy error\\n\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"text/plain; charset=utf-8\",\n\t\texpectedStatusCode: http.StatusTeapot,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\ntype dummyResponseError struct {\n\terr    string\n\tstatus int\n}\n\nfunc (d dummyResponseError) Error() string {\n\treturn d.err\n}\n\nfunc (d dummyResponseError) StatusCode() int {\n\treturn d.status\n}\n\nfunc TestEndpointHandler_incompleteAndErrored(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: false,\n\t\t\tData:       map[string]interface{}{\"foo\": \"bar\"},\n\t\t}, errors.New(\"This is a dummy error\")\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{\\\"foo\\\":\\\"bar\\\"}\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"application/json\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_cancelEmpty(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\treturn nil, nil\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            0,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"internal server error\\n\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"text/plain; charset=utf-8\",\n\t\texpectedStatusCode: http.StatusInternalServerError,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_cancel(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: false,\n\t\t\tData:       map[string]interface{}{\"foo\": \"bar\"},\n\t\t}, nil\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            0,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{\\\"foo\\\":\\\"bar\\\"}\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"application/json\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_noop(t *testing.T) {\n\tendpointHandlerTestCase{\n\t\ttimeout:            time.Minute,\n\t\tproxy:              proxy.NoopProxy,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{}\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"application/json\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\ntype endpointHandlerTestCase struct {\n\ttimeout            time.Duration\n\tproxy              proxy.Proxy\n\tmethod             string\n\texpectedBody       string\n\texpectedCache      string\n\texpectedContent    string\n\texpectedHeaders    map[string][]string\n\texpectedStatusCode int\n\tcompleted          bool\n\tqueryString        []string\n\theaders            []string\n}\n\nfunc (tc endpointHandlerTestCase) test(t *testing.T) {\n\tendpoint := &config.EndpointConfig{\n\t\tMethod:      \"GET\",\n\t\tTimeout:     tc.timeout,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\", \"c[]\", \"d\"},\n\t}\n\tif len(tc.queryString) > 0 {\n\t\tendpoint.QueryString = tc.queryString\n\t}\n\tif len(tc.headers) > 0 {\n\t\tendpoint.HeadersToPass = tc.headers\n\t}\n\n\ts := startChiServer(NewEndpointHandler(endpoint, tc.proxy))\n\n\treq, _ := http.NewRequest(tc.method, \"http://127.0.0.1:8080/_chi_endpoint/a?a=42&b=1&c[]=x&c[]=y&d=1&d=2\", io.NopCloser(&bytes.Buffer{}))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tw := httptest.NewRecorder()\n\ts.ServeHTTP(w, req)\n\n\tbody, ioerr := io.ReadAll(w.Result().Body)\n\tif ioerr != nil {\n\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\treturn\n\t}\n\tw.Result().Body.Close()\n\tcontent := string(body)\n\tresp := w.Result()\n\tif resp.Header.Get(\"Cache-Control\") != tc.expectedCache {\n\t\tt.Error(\"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t}\n\tif tc.completed && resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderCompleteResponseValue {\n\t\tt.Error(server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t}\n\tif !tc.completed && resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderIncompleteResponseValue {\n\t\tt.Error(server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t}\n\tif resp.Header.Get(\"Content-Type\") != tc.expectedContent {\n\t\tt.Error(\"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t}\n\tif resp.Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\tt.Error(\"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t}\n\tif resp.StatusCode != tc.expectedStatusCode {\n\t\tt.Error(\"Unexpected status code:\", resp.StatusCode)\n\t}\n\tif content != tc.expectedBody {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", tc.expectedBody)\n\t}\n\tfor k, v := range tc.expectedHeaders {\n\t\tif header := resp.Header.Get(k); v[0] != header {\n\t\t\tt.Error(\"Unexpected value for header:\", k, header, \"expected:\", v[0])\n\t\t}\n\t}\n}\n\nfunc startChiServer(handlerFunc http.HandlerFunc) *chi.Mux {\n\tr := chi.NewRouter()\n\tr.Handle(\"/_chi_endpoint/{param}\", handlerFunc)\n\treturn r\n}\n"
  },
  {
    "path": "router/chi/router.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\n\tPackage chi provides some basic implementations for building routers based on go-chi/chi\n*/\npackage chi\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/router\"\n\t\"github.com/luraproject/lura/v2/router/mux\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\n// ChiDefaultDebugPattern is the default pattern used to define the debug endpoint\nconst ChiDefaultDebugPattern = \"/__debug/\"\n\nconst logPrefix = \"[SERVICE: Chi]\"\n\n// RunServerFunc is a func that will run the http Server with the given params.\ntype RunServerFunc func(context.Context, config.ServiceConfig, http.Handler) error\n\n// Config is the struct that collects the parts the router should be builded from\ntype Config struct {\n\tEngine         chi.Router\n\tMiddlewares    chi.Middlewares\n\tHandlerFactory HandlerFactory\n\tProxyFactory   proxy.Factory\n\tLogger         logging.Logger\n\tDebugPattern   string\n\tRunServer      RunServerFunc\n}\n\n// DefaultFactory returns a chi router factory with the injected proxy factory and logger.\n// It also uses a default chi router and the default HandlerFactory\nfunc DefaultFactory(proxyFactory proxy.Factory, logger logging.Logger) router.Factory {\n\treturn NewFactory(\n\t\tConfig{\n\t\t\tEngine:         chi.NewRouter(),\n\t\t\tMiddlewares:    chi.Middlewares{middleware.Logger},\n\t\t\tHandlerFactory: NewEndpointHandler,\n\t\t\tProxyFactory:   proxyFactory,\n\t\t\tLogger:         logger,\n\t\t\tDebugPattern:   ChiDefaultDebugPattern,\n\t\t\tRunServer:      server.RunServer,\n\t\t},\n\t)\n}\n\n// NewFactory returns a chi router factory with the injected configuration\nfunc NewFactory(cfg Config) router.Factory {\n\tif cfg.DebugPattern == \"\" {\n\t\tcfg.DebugPattern = ChiDefaultDebugPattern\n\t}\n\treturn factory{cfg}\n}\n\ntype factory struct {\n\tcfg Config\n}\n\n// New implements the factory interface\nfunc (rf factory) New() router.Router {\n\treturn rf.NewWithContext(context.Background())\n}\n\n// NewWithContext implements the factory interface\nfunc (rf factory) NewWithContext(ctx context.Context) router.Router {\n\treturn chiRouter{rf.cfg, ctx, rf.cfg.RunServer}\n}\n\ntype chiRouter struct {\n\tcfg       Config\n\tctx       context.Context\n\tRunServer RunServerFunc\n}\n\n// Run implements the router interface\nfunc (r chiRouter) Run(cfg config.ServiceConfig) {\n\tr.cfg.Engine.Use(r.cfg.Middlewares...)\n\tif cfg.Debug {\n\t\tr.registerDebugEndpoints()\n\t}\n\n\tr.cfg.Engine.Get(\"/__health\", mux.HealthHandler)\n\n\tserver.InitHTTPDefaultTransport(cfg)\n\n\tr.registerKrakendEndpoints(cfg.Endpoints)\n\n\tr.cfg.Engine.NotFound(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(server.CompleteResponseHeaderName, server.HeaderIncompleteResponseValue)\n\t\thttp.NotFound(w, r)\n\t})\n\n\tif err := r.RunServer(r.ctx, cfg, r.cfg.Engine); err != nil {\n\t\tr.cfg.Logger.Error(logPrefix, err.Error())\n\t}\n\n\tr.cfg.Logger.Info(logPrefix, \"Router execution ended\")\n}\n\nfunc (r chiRouter) registerDebugEndpoints() {\n\tdebugHandler := mux.DebugHandler(r.cfg.Logger)\n\tr.cfg.Engine.Get(r.cfg.DebugPattern, debugHandler)\n\tr.cfg.Engine.Post(r.cfg.DebugPattern, debugHandler)\n\tr.cfg.Engine.Put(r.cfg.DebugPattern, debugHandler)\n\tr.cfg.Engine.Patch(r.cfg.DebugPattern, debugHandler)\n\tr.cfg.Engine.Delete(r.cfg.DebugPattern, debugHandler)\n}\n\nfunc (r chiRouter) registerKrakendEndpoints(endpoints []*config.EndpointConfig) {\n\tfor _, c := range endpoints {\n\t\tproxyStack, err := r.cfg.ProxyFactory.New(c)\n\t\tif err != nil {\n\t\t\tr.cfg.Logger.Error(logPrefix, \"calling the ProxyFactory\", err.Error())\n\t\t\tcontinue\n\t\t}\n\n\t\tr.registerKrakendEndpoint(c.Method, c, r.cfg.HandlerFactory(c, proxyStack), len(c.Backend))\n\t}\n}\n\nfunc (r chiRouter) registerKrakendEndpoint(method string, endpoint *config.EndpointConfig, handler http.HandlerFunc, totBackends int) {\n\tmethod = strings.ToTitle(method)\n\tpath := endpoint.Endpoint\n\n\tif method != http.MethodGet && totBackends > 1 {\n\t\tif !router.IsValidSequentialEndpoint(endpoint) {\n\t\t\tr.cfg.Logger.Error(logPrefix, method, \"endpoints with sequential proxy enabled only allow a non-GET in the last backend! Ignoring\", path)\n\t\t\treturn\n\t\t}\n\t}\n\n\tswitch method {\n\tcase http.MethodGet:\n\t\tr.cfg.Engine.Get(path, handler)\n\tcase http.MethodPost:\n\t\tr.cfg.Engine.Post(path, handler)\n\tcase http.MethodPut:\n\t\tr.cfg.Engine.Put(path, handler)\n\tcase http.MethodPatch:\n\t\tr.cfg.Engine.Patch(path, handler)\n\tcase http.MethodDelete:\n\t\tr.cfg.Engine.Delete(path, handler)\n\tdefault:\n\t\tr.cfg.Logger.Error(logPrefix, \"Unsupported method\", method)\n\t\treturn\n\t}\n\tr.cfg.Logger.Debug(logPrefix, \"registering the endpoint\", method, path)\n}\n"
  },
  {
    "path": "router/chi/router_test.go",
    "content": "//go:build !race\n// +build !race\n\n// SPDX-License-Identifier: Apache-2.0\n\npackage chi\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\nfunc TestDefaultFactory_ok(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"}), logger).NewWithContext(ctx)\n\texpectedBody := \"{\\\"supu\\\":\\\"tupu\\\"}\"\n\n\tserviceCfg := config.ServiceConfig{\n\t\tPort: 8062,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/get\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/get\",\n\t\t\t\tMethod:   \"POST\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/post\",\n\t\t\t\tMethod:   \"Post\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/put\",\n\t\t\t\tMethod:   \"put\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/patch\",\n\t\t\t\tMethod:   \"PATCH\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/delete\",\n\t\t\t\tMethod:   \"DELETE\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, endpoint := range serviceCfg.Endpoints {\n\t\treq, _ := http.NewRequest(strings.ToTitle(endpoint.Method), fmt.Sprintf(\"http://127.0.0.1:8062%s\", endpoint.Endpoint), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tresp, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\tt.Error(\"Making the request:\", err.Error())\n\t\t\treturn\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(resp.Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\t\treturn\n\t\t}\n\t\tcontent := string(body)\n\t\tif resp.Header.Get(\"Cache-Control\") != \"\" {\n\t\t\tt.Error(endpoint.Endpoint, \"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t\t}\n\t\tif resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderCompleteResponseValue {\n\t\t\tt.Error(server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t\t}\n\t\tif resp.Header.Get(\"Content-Type\") != \"application/json\" {\n\t\t\tt.Error(endpoint.Endpoint, \"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif resp.Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(endpoint.Endpoint, \"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Error(endpoint.Endpoint, \"Unexpected status code:\", resp.StatusCode)\n\t\t}\n\t\tif content != expectedBody {\n\t\t\tt.Error(endpoint.Endpoint, \"Unexpected body:\", content, \"expected:\", expectedBody)\n\t\t}\n\t}\n}\n\nfunc TestDefaultFactory_ko(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := NewFactory(Config{\n\t\tEngine:         chi.NewRouter(),\n\t\tMiddlewares:    chi.Middlewares{},\n\t\tHandlerFactory: NewEndpointHandler,\n\t\tProxyFactory:   noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"}),\n\t\tLogger:         logger,\n\t\tRunServer:      server.RunServer,\n\t}).NewWithContext(ctx)\n\n\tserviceCfg := config.ServiceConfig{\n\t\tDebug: true,\n\t\tPort:  8063,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/ignored\",\n\t\t\t\tMethod:   \"GETTT\",\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/empty\",\n\t\t\t\tMethod:   \"GETTT\",\n\t\t\t\tBackend:  []*config.Backend{},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/also-ignored\",\n\t\t\t\tMethod:   \"PUT\",\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, subject := range [][]string{\n\t\t{\"GET\", \"ignored\"},\n\t\t{\"GET\", \"empty\"},\n\t\t{\"PUT\", \"also-ignored\"},\n\t} {\n\t\treq, _ := http.NewRequest(subject[0], fmt.Sprintf(\"http://127.0.0.1:8063/%s\", subject[1]), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tcheckResponseIs404(t, req)\n\t}\n}\n\nfunc TestDefaultFactory_proxyFactoryCrash(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(erroredProxyFactory{fmt.Errorf(\"%s\", \"crash!!!\")}, logger).NewWithContext(ctx)\n\n\tserviceCfg := config.ServiceConfig{\n\t\tDebug: true,\n\t\tPort:  8064,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/ignored\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, subject := range [][]string{{\"GET\", \"ignored\"}, {\"PUT\", \"also-ignored\"}} {\n\t\treq, _ := http.NewRequest(subject[0], fmt.Sprintf(\"http://127.0.0.1:8064/%s\", subject[1]), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tcheckResponseIs404(t, req)\n\t}\n}\n\nfunc TestRunServer_ko(t *testing.T) {\n\tbuff := new(bytes.Buffer)\n\tlogger, err := logging.NewLogger(\"DEBUG\", buff, \"\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\terrorMsg := \"runServer error\"\n\trunServerFunc := func(_ context.Context, _ config.ServiceConfig, _ http.Handler) error {\n\t\treturn errors.New(errorMsg)\n\t}\n\n\tpf := noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"})\n\tr := NewFactory(\n\t\tConfig{\n\t\t\tEngine:         chi.NewRouter(),\n\t\t\tMiddlewares:    chi.Middlewares{},\n\t\t\tHandlerFactory: NewEndpointHandler,\n\t\t\tProxyFactory:   pf,\n\t\t\tLogger:         logger,\n\t\t\tDebugPattern:   ChiDefaultDebugPattern,\n\t\t\tRunServer:      runServerFunc,\n\t\t},\n\t).New()\n\n\tserviceCfg := config.ServiceConfig{}\n\tr.Run(serviceCfg)\n\tre := regexp.MustCompile(errorMsg)\n\tif !re.MatchString(buff.String()) {\n\t\tt.Errorf(\"the logger doesn't contain the expected msg: %s\", buff.Bytes())\n\t}\n}\n\nfunc checkResponseIs404(t *testing.T, req *http.Request) {\n\texpectedBody := \"404 page not found\\n\"\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tt.Error(\"Making the request:\", err.Error())\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\tbody, ioerr := io.ReadAll(resp.Body)\n\tif ioerr != nil {\n\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\treturn\n\t}\n\tcontent := string(body)\n\n\tif resp.Header.Get(\"Cache-Control\") != \"\" {\n\t\tt.Error(\"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t}\n\tif resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderIncompleteResponseValue {\n\t\tt.Error(req.URL.String(), server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t}\n\tif resp.Header.Get(\"Content-Type\") != \"text/plain; charset=utf-8\" {\n\t\tt.Error(\"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t}\n\tif resp.Header.Get(\"X-Krakend\") != \"\" {\n\t\tt.Error(\"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t}\n\tif resp.StatusCode != http.StatusNotFound {\n\t\tt.Error(\"Unexpected status code:\", resp.StatusCode)\n\t}\n\tif content != expectedBody {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", expectedBody)\n\t}\n}\n\ntype noopProxyFactory map[string]interface{}\n\nfunc (n noopProxyFactory) New(_ *config.EndpointConfig) (proxy.Proxy, error) {\n\treturn func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       n,\n\t\t}, nil\n\t}, nil\n}\n\ntype erroredProxyFactory struct {\n\tError error\n}\n\nfunc (e erroredProxyFactory) New(_ *config.EndpointConfig) (proxy.Proxy, error) {\n\treturn proxy.NoopProxy, e.Error\n}\n"
  },
  {
    "path": "router/gin/debug.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage gin\n\nimport (\n\t\"io\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\n// DebugHandler creates a dummy handler function, useful for quick integration tests\nfunc DebugHandler(logger logging.Logger) gin.HandlerFunc {\n\tlogPrefixSecondary := \"[ENDPOINT: /__debug/*]\"\n\treturn func(c *gin.Context) {\n\t\tlogger.Debug(logPrefixSecondary, \"Method:\", c.Request.Method)\n\t\tlogger.Debug(logPrefixSecondary, \"URL:\", c.Request.RequestURI)\n\t\tlogger.Debug(logPrefixSecondary, \"Query:\", c.Request.URL.Query())\n\t\tlogger.Debug(logPrefixSecondary, \"Params:\", c.Params)\n\t\tlogger.Debug(logPrefixSecondary, \"Headers:\", c.Request.Header)\n\t\tbody, _ := io.ReadAll(c.Request.Body)\n\t\tc.Request.Body.Close()\n\t\tlogger.Debug(logPrefixSecondary, \"Body:\", string(body))\n\t\tc.JSON(200, gin.H{\n\t\t\t\"message\": \"pong\",\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "router/gin/debug_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage gin\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nfunc TestDebugHandler(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\trouter := gin.New()\n\trouter.GET(\"/_gin_endpoint/:param\", DebugHandler(logger))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8088/_gin_endpoint/a?b=1\", io.NopCloser(&bytes.Buffer{}))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tw := httptest.NewRecorder()\n\n\trouter.ServeHTTP(w, req)\n\n\tbody, ioerr := io.ReadAll(w.Result().Body)\n\tif ioerr != nil {\n\t\tt.Error(\"reading a response:\", ioerr.Error())\n\t\treturn\n\t}\n\tw.Result().Body.Close()\n\n\texpectedBody := \"{\\\"message\\\":\\\"pong\\\"}\"\n\n\tcontent := string(body)\n\tif w.Result().Header.Get(\"Cache-Control\") != \"\" {\n\t\tt.Error(\"Cache-Control error:\", w.Result().Header.Get(\"Cache-Control\"))\n\t}\n\tif w.Result().Header.Get(\"Content-Type\") != \"application/json; charset=utf-8\" {\n\t\tt.Error(\"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t}\n\tif w.Result().Header.Get(\"X-Krakend\") != \"\" {\n\t\tt.Error(\"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t}\n\tif w.Result().StatusCode != http.StatusOK {\n\t\tt.Error(\"Unexpected status code:\", w.Result().StatusCode)\n\t}\n\tif content != expectedBody {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", expectedBody)\n\t}\n}\n"
  },
  {
    "path": "router/gin/echo.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage gin\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype echoResponse struct {\n\tUri         string              `json:\"req_uri\"`\n\tUriDetails  map[string]string   `json:\"req_uri_details\"`\n\tMethod      string              `json:\"req_method\"`\n\tQuerystring map[string][]string `json:\"req_querystring\"`\n\tBody        string              `json:\"req_body\"`\n\tHeaders     map[string][]string `json:\"req_headers\"`\n}\n\n// EchoHandler creates a dummy handler function, useful for quick integration tests\nfunc EchoHandler() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tvar body string\n\t\tif c.Request.Body != nil {\n\t\t\ttmp, _ := io.ReadAll(c.Request.Body)\n\t\t\tc.Request.Body.Close()\n\t\t\tbody = string(tmp)\n\t\t}\n\t\tresp := echoResponse{\n\t\t\tUri: c.Request.RequestURI,\n\t\t\tUriDetails: map[string]string{\n\t\t\t\t\"user\":     c.Request.URL.User.String(),\n\t\t\t\t\"host\":     c.Request.Host,\n\t\t\t\t\"path\":     c.Request.URL.Path,\n\t\t\t\t\"query\":    c.Request.URL.Query().Encode(),\n\t\t\t\t\"fragment\": c.Request.URL.Fragment,\n\t\t\t},\n\t\t\tMethod:      c.Request.Method,\n\t\t\tQuerystring: c.Request.URL.Query(),\n\t\t\tBody:        body,\n\t\t\tHeaders:     c.Request.Header,\n\t\t}\n\n\t\tc.JSON(http.StatusOK, resp)\n\t}\n}\n"
  },
  {
    "path": "router/gin/echo_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage gin\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc TestEchoHandler(t *testing.T) {\n\treqBody := `{\"message\":\"some body to send\"}`\n\texpectedRespBody := `{\"req_uri\":\"http://127.0.0.1:8088/_gin_endpoint/a?b=1\",\"req_uri_details\":{\"fragment\":\"\",\"host\":\"127.0.0.1:8088\",\"path\":\"/_gin_endpoint/a\",\"query\":\"b=1\",\"user\":\"\"},\"req_method\":\"GET\",\"req_querystring\":{\"b\":[\"1\"]},\"req_body\":\"{\\\"message\\\":\\\"some body to send\\\"}\",\"req_headers\":{\"Content-Type\":[\"application/json\"]}}`\n\texpectedRespNoBody := `{\"req_uri\":\"http://127.0.0.1:8088/_gin_endpoint/a?b=1\",\"req_uri_details\":{\"fragment\":\"\",\"host\":\"127.0.0.1:8088\",\"path\":\"/_gin_endpoint/a\",\"query\":\"b=1\",\"user\":\"\"},\"req_method\":\"GET\",\"req_querystring\":{\"b\":[\"1\"]},\"req_body\":\"\",\"req_headers\":{\"Content-Type\":[\"application/json\"]}}`\n\texpectedRespString := `{\"req_uri\":\"http://127.0.0.1:8088/_gin_endpoint/a?b=1\",\"req_uri_details\":{\"fragment\":\"\",\"host\":\"127.0.0.1:8088\",\"path\":\"/_gin_endpoint/a\",\"query\":\"b=1\",\"user\":\"\"},\"req_method\":\"GET\",\"req_querystring\":{\"b\":[\"1\"]},\"req_body\":\"Hello lura\",\"req_headers\":{\"Content-Type\":[\"application/json\"]}}`\n\n\tgin.SetMode(gin.TestMode)\n\trouter := gin.New()\n\trouter.GET(\"/_gin_endpoint/:param\", EchoHandler())\n\n\tfor _, tc := range []struct {\n\t\tname string\n\t\tbody io.Reader\n\t\tresp string\n\t}{\n\t\t{\n\t\t\tname: \"json body\",\n\t\t\tbody: strings.NewReader(reqBody),\n\t\t\tresp: expectedRespBody,\n\t\t},\n\t\t{\n\t\t\tname: \"no body\",\n\t\t\tbody: http.NoBody,\n\t\t\tresp: expectedRespNoBody,\n\t\t},\n\t\t{\n\t\t\tname: \"string body\",\n\t\t\tbody: strings.NewReader(\"Hello lura\"),\n\t\t\tresp: expectedRespString,\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\techoRunTestRequest(t, router, tc.body, tc.resp)\n\t\t})\n\t}\n\n}\n\nfunc echoRunTestRequest(t *testing.T, e *gin.Engine, body io.Reader, expected string) {\n\treq := httptest.NewRequest(\"GET\", \"http://127.0.0.1:8088/_gin_endpoint/a?b=1\", body)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tw := httptest.NewRecorder()\n\n\te.ServeHTTP(w, req)\n\n\trespBody, ioerr := io.ReadAll(w.Result().Body)\n\tif ioerr != nil {\n\t\tt.Error(\"reading a response:\", ioerr.Error())\n\t\treturn\n\t}\n\tw.Result().Body.Close()\n\n\tcontent := string(respBody)\n\tif w.Result().Header.Get(\"Cache-Control\") != \"\" {\n\t\tt.Error(\"Cache-Control error:\", w.Result().Header.Get(\"Cache-Control\"))\n\t}\n\tif w.Result().Header.Get(\"Content-Type\") != \"application/json; charset=utf-8\" {\n\t\tt.Error(\"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t}\n\tif w.Result().Header.Get(\"X-Krakend\") != \"\" {\n\t\tt.Error(\"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t}\n\tif w.Result().StatusCode != http.StatusOK {\n\t\tt.Error(\"Unexpected status code:\", w.Result().StatusCode)\n\t}\n\tif content != expected {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", expected)\n\t}\n}\n"
  },
  {
    "path": "router/gin/endpoint.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage gin\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/textproto\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/core\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\nconst requestParamsAsterisk string = \"*\"\n\n// HandlerFactory creates a handler function that adapts the gin router with the injected proxy\ntype HandlerFactory func(*config.EndpointConfig, proxy.Proxy) gin.HandlerFunc\n\n// ErrorResponseWriter writes the string representation of an error into the response body\n// and sets a Content-Type header for errors that implement the encodedResponseError interface.\nvar ErrorResponseWriter = func(c *gin.Context, err error) {\n\tif te, ok := err.(encodedResponseError); ok && te.Encoding() != \"\" {\n\t\tc.Header(\"Content-Type\", te.Encoding())\n\t}\n\tc.Writer.WriteString(err.Error())\n}\n\n// EndpointHandler implements the HandlerFactory interface using the default ToHTTPError function\nvar EndpointHandler = CustomErrorEndpointHandler(logging.NoOp, server.DefaultToHTTPError)\n\n// CustomErrorEndpointHandler returns a HandlerFactory using the injected ToHTTPError function and logger\nfunc CustomErrorEndpointHandler(logger logging.Logger, errF server.ToHTTPError) HandlerFactory { // skipcq: GO-R1005\n\treturn func(configuration *config.EndpointConfig, prxy proxy.Proxy) gin.HandlerFunc {\n\t\tcacheControlHeaderValue := fmt.Sprintf(\"public, max-age=%d\", int(configuration.CacheTTL.Seconds()))\n\t\tisCacheEnabled := configuration.CacheTTL.Seconds() != 0\n\t\trequestGenerator := NewRequest(configuration.HeadersToPass)\n\t\trender := getRender(configuration)\n\t\tlogPrefix := \"[ENDPOINT: \" + configuration.Endpoint + \"]\"\n\n\t\treturn func(c *gin.Context) {\n\t\t\trequestCtx, cancel := context.WithTimeout(c, configuration.Timeout)\n\n\t\t\tc.Header(core.KrakendHeaderName, core.KrakendHeaderValue)\n\n\t\t\tresponse, err := prxy(requestCtx, requestGenerator(c, configuration.QueryString))\n\n\t\t\tselect {\n\t\t\tcase <-requestCtx.Done():\n\t\t\t\tif err == nil {\n\t\t\t\t\terr = server.ErrInternalError\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tcomplete := server.HeaderIncompleteResponseValue\n\n\t\t\tif response != nil && len(response.Data) > 0 {\n\t\t\t\tif response.IsComplete {\n\t\t\t\t\tcomplete = server.HeaderCompleteResponseValue\n\t\t\t\t\tif isCacheEnabled {\n\t\t\t\t\t\tc.Header(\"Cache-Control\", cacheControlHeaderValue)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor k, vs := range response.Metadata.Headers {\n\t\t\t\t\tfor _, v := range vs {\n\t\t\t\t\t\tc.Writer.Header().Add(k, v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tc.Header(server.CompleteResponseHeaderName, complete)\n\n\t\t\tfor _, err := range c.Errors {\n\t\t\t\tlogger.Error(logPrefix, err.Error())\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tif t, ok := err.(multiError); ok {\n\t\t\t\t\tfor i, errN := range t.Errors() {\n\t\t\t\t\t\tc.Error(errN)\n\t\t\t\t\t\tlogger.Error(fmt.Sprintf(\"%s Error #%d: %s\", logPrefix, i, errN.Error()))\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tc.Error(err)\n\t\t\t\t\tlogger.Error(logPrefix, err.Error())\n\t\t\t\t}\n\n\t\t\t\tif response == nil {\n\t\t\t\t\tif t, ok := err.(headerResponseError); ok {\n\t\t\t\t\t\tfor k, vs := range t.Headers() {\n\t\t\t\t\t\t\tfor _, v := range vs {\n\t\t\t\t\t\t\t\tc.Writer.Header().Add(k, v)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif t, ok := err.(responseError); ok {\n\t\t\t\t\t\tc.Status(t.StatusCode())\n\t\t\t\t\t} else {\n\t\t\t\t\t\tc.Status(errF(err))\n\t\t\t\t\t}\n\n\t\t\t\t\tif returnErrorMsg {\n\t\t\t\t\t\tErrorResponseWriter(c, err)\n\t\t\t\t\t}\n\t\t\t\t\tcancel()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trender(c, response)\n\t\t\tcancel()\n\t\t}\n\t}\n}\n\n// NewRequest gets a request from the current gin context and the received query string\nfunc NewRequest(headersToSend []string) func(*gin.Context, []string) *proxy.Request {\n\tif len(headersToSend) == 0 {\n\t\theadersToSend = server.HeadersToSend\n\t}\n\n\treturn func(c *gin.Context, queryString []string) *proxy.Request {\n\t\tparams := make(map[string]string, len(c.Params))\n\t\tfor _, param := range c.Params {\n\t\t\tparams[textproto.CanonicalMIMEHeaderKey(param.Key[:1])+param.Key[1:]] = param.Value\n\t\t}\n\n\t\theaders := make(map[string][]string, 3+len(headersToSend))\n\n\t\tfor _, k := range headersToSend {\n\t\t\tif k == requestParamsAsterisk {\n\t\t\t\theaders = c.Request.Header\n\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif h, ok := c.Request.Header[textproto.CanonicalMIMEHeaderKey(k)]; ok {\n\t\t\t\theaders[k] = h\n\t\t\t}\n\t\t}\n\n\t\theaders[\"X-Forwarded-For\"] = []string{c.ClientIP()}\n\t\theaders[\"X-Forwarded-Host\"] = []string{c.Request.Host}\n\t\t// if User-Agent is not forwarded using headersToSend, we set\n\t\t// the KrakenD router User Agent value\n\t\tif _, ok := headers[\"User-Agent\"]; !ok {\n\t\t\theaders[\"User-Agent\"] = server.UserAgentHeaderValue\n\t\t} else {\n\t\t\theaders[\"X-Forwarded-Via\"] = server.UserAgentHeaderValue\n\t\t}\n\n\t\tquery := make(map[string][]string, len(queryString))\n\t\tqueryValues := c.Request.URL.Query()\n\t\tfor i := range queryString {\n\t\t\tif queryString[i] == requestParamsAsterisk {\n\t\t\t\tquery = c.Request.URL.Query()\n\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif v, ok := queryValues[queryString[i]]; ok && len(v) > 0 {\n\t\t\t\tquery[queryString[i]] = v\n\t\t\t}\n\t\t}\n\n\t\treturn &proxy.Request{\n\t\t\tPath:    c.Request.URL.Path,\n\t\t\tMethod:  c.Request.Method,\n\t\t\tQuery:   query,\n\t\t\tBody:    c.Request.Body,\n\t\t\tParams:  params,\n\t\t\tHeaders: headers,\n\t\t}\n\t}\n}\n\ntype encodedResponseError interface {\n\tresponseError\n\tEncoding() string\n}\n\ntype responseError interface {\n\terror\n\tStatusCode() int\n}\n\ntype headerResponseError interface {\n\tresponseError\n\tHeaders() map[string][]string\n}\n\ntype multiError interface {\n\terror\n\tErrors() []error\n}\n"
  },
  {
    "path": "router/gin/endpoint_benchmark_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage gin\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n)\n\nfunc BenchmarkEndpointHandler_ko(b *testing.B) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, fmt.Errorf(\"This is %s\", \"a dummy error\")\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:     time.Second,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\"},\n\t}\n\n\tgin.SetMode(gin.TestMode)\n\trouter := gin.New()\n\trouter.GET(\"/_gin_endpoint/:param\", EndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", http.NoBody)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tb.ReportAllocs()\n\tfor i := 0; i < b.N; i++ {\n\t\tw := httptest.NewRecorder()\n\t\trouter.ServeHTTP(w, req)\n\t}\n}\n\nfunc BenchmarkEndpointHandler_ok(b *testing.B) {\n\tpResp := proxy.Response{\n\t\tData:       map[string]interface{}{},\n\t\tIo:         io.NopCloser(&bytes.Buffer{}),\n\t\tIsComplete: true,\n\t\tMetadata:   proxy.Metadata{},\n\t}\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &pResp, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:     time.Second,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\"},\n\t}\n\n\tgin.SetMode(gin.TestMode)\n\trouter := gin.New()\n\trouter.GET(\"/_gin_endpoint/:param\", EndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", http.NoBody)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tb.ReportAllocs()\n\tfor i := 0; i < b.N; i++ {\n\t\tw := httptest.NewRecorder()\n\t\trouter.ServeHTTP(w, req)\n\t}\n}\n\nfunc BenchmarkEndpointHandler_ko_Parallel(b *testing.B) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, fmt.Errorf(\"This is %s\", \"a dummy error\")\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:     time.Second,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\"},\n\t}\n\n\tgin.SetMode(gin.TestMode)\n\trouter := gin.New()\n\trouter.GET(\"/_gin_endpoint/:param\", EndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", http.NoBody)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tw := httptest.NewRecorder()\n\t\t\trouter.ServeHTTP(w, req)\n\t\t}\n\t})\n}\n\nfunc BenchmarkEndpointHandler_ok_Parallel(b *testing.B) {\n\tpResp := proxy.Response{\n\t\tData:       map[string]interface{}{},\n\t\tIo:         io.NopCloser(&bytes.Buffer{}),\n\t\tIsComplete: true,\n\t\tMetadata:   proxy.Metadata{},\n\t}\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &pResp, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:     time.Second,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\"},\n\t}\n\n\tgin.SetMode(gin.TestMode)\n\trouter := gin.New()\n\trouter.GET(\"/_gin_endpoint/:param\", EndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", http.NoBody)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tw := httptest.NewRecorder()\n\t\t\trouter.ServeHTTP(w, req)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "router/gin/endpoint_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage gin\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\nfunc TestEndpointHandler_ok(t *testing.T) {\n\tp := func(ctx context.Context, req *proxy.Request) (*proxy.Response, error) {\n\t\tif v, ok := ctx.Value(\"bool\").(bool); !ok || !v {\n\t\t\tt.Errorf(\"unexpected bool context value: %v\", v)\n\t\t}\n\t\tif v, ok := ctx.Value(\"int\").(int); !ok || v != 42 {\n\t\t\tt.Errorf(\"unexpected int context value: %v\", v)\n\t\t}\n\t\tif v, ok := ctx.Value(\"string\").(string); !ok || v != \"supu\" {\n\t\t\tt.Errorf(\"unexpected string context value: %v\", v)\n\t\t}\n\t\tdata, _ := json.Marshal(req.Query)\n\t\tif string(data) != `{\"b\":[\"1\"],\"c[]\":[\"x\",\"y\"],\"d\":[\"1\",\"2\"]}` {\n\t\t\tt.Errorf(\"unexpected querystring: %s\", data)\n\t\t}\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       map[string]interface{}{\"supu\": \"tupu\"},\n\t\t\tMetadata: proxy.Metadata{\n\t\t\t\tHeaders: map[string][]string{\"a\": {\"a1\", \"a2\"}},\n\t\t\t},\n\t\t}, nil\n\t}\n\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{\\\"supu\\\":\\\"tupu\\\"}\",\n\t\texpectedCache:      \"public, max-age=21600\",\n\t\texpectedContent:    \"application/json; charset=utf-8\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          true,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_okAllParams(t *testing.T) {\n\tp := func(_ context.Context, req *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       map[string]interface{}{\"query\": req.Query, \"headers\": req.Headers, \"params\": req.Params},\n\t\t\tMetadata: proxy.Metadata{\n\t\t\t\tHeaders:    map[string][]string{\"X-YZ\": {\"something\"}},\n\t\t\t\tStatusCode: 200,\n\t\t\t},\n\t\t}, nil\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       `{\"headers\":{\"Content-Type\":[\"application/json\"],\"User-Agent\":[\"KrakenD Version undefined\"],\"X-Forwarded-For\":[\"\"],\"X-Forwarded-Host\":[\"127.0.0.1:8080\"]},\"params\":{\"Param\":\"a\"},\"query\":{\"a\":[\"42\"],\"b\":[\"1\"],\"c[]\":[\"x\",\"y\"],\"d\":[\"1\",\"2\"]}}`,\n\t\texpectedCache:      \"public, max-age=21600\",\n\t\texpectedContent:    \"application/json; charset=utf-8\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          true,\n\t\tqueryString:        []string{\"*\"},\n\t\theaders:            []string{\"*\"},\n\t\texpectedHeaders:    map[string][]string{\"X-YZ\": {\"something\"}},\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nvar ctxContent = map[string]interface{}{\n\t\"bool\":   true,\n\t\"int\":    42,\n\t\"string\": \"supu\",\n}\n\nfunc TestEndpointHandler_incomplete(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: false,\n\t\t\tData:       map[string]interface{}{\"foo\": \"bar\"},\n\t\t}, nil\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{\\\"foo\\\":\\\"bar\\\"}\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"application/json; charset=utf-8\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_errored(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, errors.New(\"this is a dummy error\")\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"\",\n\t\texpectedStatusCode: http.StatusInternalServerError,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_errored_responseError(t *testing.T) {\n\texpectedBody := \"this is a dummy error\"\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, dummyResponseError{err: expectedBody, status: http.StatusTeapot}\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"\",\n\t\texpectedStatusCode: http.StatusTeapot,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n\n\t// Same test case but with body (return_error_msg enabled)\n\treturnErrorMsg = true\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       expectedBody,\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"\",\n\t\texpectedStatusCode: http.StatusTeapot,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n\treturnErrorMsg = false\n}\n\nfunc TestEndpointHandler_errored_withHeaders(t *testing.T) {\n\texpectedBody := \"this is a dummy error\"\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, dummyHeadersResponseError{\n\t\t\tdummyResponseError: dummyResponseError{err: expectedBody, status: http.StatusTeapot},\n\t\t\theaders: map[string][]string{\n\t\t\t\t\"X-Header\": {\"header1\", \"header2\"},\n\t\t\t},\n\t\t}\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"\",\n\t\texpectedHeaders:    map[string][]string{\"X-Header\": {\"header1\", \"header2\"}},\n\t\texpectedStatusCode: http.StatusTeapot,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_errored_encodedResponseError(t *testing.T) {\n\texpectedBody := `{ \"message\": \"this is a dummy error\" }`\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, dummyEncodedResponseError{dummyResponseError: dummyResponseError{err: expectedBody, status: http.StatusTeapot}, encoding: \"application/json\"}\n\t}\n\treturnErrorMsg = true\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       expectedBody,\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"application/json\",\n\t\texpectedStatusCode: http.StatusTeapot,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n\treturnErrorMsg = false\n}\n\ntype dummyResponseError struct {\n\terr    string\n\tstatus int\n}\n\nfunc (d dummyResponseError) Error() string {\n\treturn d.err\n}\n\nfunc (d dummyResponseError) StatusCode() int {\n\treturn d.status\n}\n\ntype dummyEncodedResponseError struct {\n\tdummyResponseError\n\tencoding string\n}\n\nfunc (d dummyEncodedResponseError) Encoding() string {\n\treturn d.encoding\n}\n\ntype dummyHeadersResponseError struct {\n\tdummyResponseError\n\theaders map[string][]string\n}\n\nfunc (d dummyHeadersResponseError) Headers() map[string][]string {\n\treturn d.headers\n}\n\nfunc TestEndpointHandler_incompleteAndErrored(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: false,\n\t\t\tData:       map[string]interface{}{\"foo\": \"bar\"},\n\t\t}, errors.New(\"This is a dummy error\")\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{\\\"foo\\\":\\\"bar\\\"}\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"application/json; charset=utf-8\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_cancelEmpty(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\treturn nil, nil\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            0,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"\",\n\t\texpectedStatusCode: http.StatusInternalServerError,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_cancel(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: false,\n\t\t\tData:       map[string]interface{}{\"foo\": \"bar\"},\n\t\t}, nil\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            0,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{\\\"foo\\\":\\\"bar\\\"}\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"application/json; charset=utf-8\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_noop(t *testing.T) {\n\tendpointHandlerTestCase{\n\t\ttimeout:            time.Minute,\n\t\tproxy:              proxy.NoopProxy,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{}\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"application/json; charset=utf-8\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestCustomErrorEndpointHandler(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\thf := CustomErrorEndpointHandler(logger, server.DefaultToHTTPError)\n\n\tendpoint := &config.EndpointConfig{\n\t\tMethod:      \"GET\",\n\t\tEndpoint:    \"/\",\n\t\tTimeout:     time.Minute,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\", \"c[]\", \"d\"},\n\t}\n\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, errors.New(\"this is a dummy error\")\n\t}\n\n\ts := startGinServer(hf(endpoint, p))\n\n\treq, _ := http.NewRequest(\n\t\t\"GET\",\n\t\t\"http://127.0.0.1:8080/_gin_endpoint/a?a=42&b=1&c[]=x&c[]=y&d=1&d=2\",\n\t\tio.NopCloser(&bytes.Buffer{}),\n\t)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tw := httptest.NewRecorder()\n\ts.ServeHTTP(w, req)\n\n\tif content := buff.String(); !strings.Contains(content, \"pref ERROR: [ENDPOINT: /] this is a dummy error\") {\n\t\tt.Error(\"unexpected log content\", content)\n\t}\n}\n\ntype endpointHandlerTestCase struct {\n\ttimeout            time.Duration\n\tproxy              proxy.Proxy\n\tmethod             string\n\texpectedBody       string\n\texpectedCache      string\n\texpectedContent    string\n\texpectedHeaders    map[string][]string\n\texpectedStatusCode int\n\tcompleted          bool\n\tqueryString        []string\n\theaders            []string\n}\n\nfunc (tc endpointHandlerTestCase) test(t *testing.T) {\n\tendpoint := &config.EndpointConfig{\n\t\tMethod:      \"GET\",\n\t\tTimeout:     tc.timeout,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\", \"c[]\", \"d\"},\n\t}\n\tif len(tc.queryString) > 0 {\n\t\tendpoint.QueryString = tc.queryString\n\t}\n\tif len(tc.headers) > 0 {\n\t\tendpoint.HeadersToPass = tc.headers\n\t}\n\n\ts := startGinServer(EndpointHandler(endpoint, tc.proxy))\n\n\treq, _ := http.NewRequest(\n\t\ttc.method,\n\t\t\"http://127.0.0.1:8080/_gin_endpoint/a?a=42&b=1&c[]=x&c[]=y&d=1&d=2\",\n\t\tio.NopCloser(&bytes.Buffer{}),\n\t)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tw := httptest.NewRecorder()\n\ts.ServeHTTP(w, req)\n\n\tbody, ioerr := io.ReadAll(w.Result().Body)\n\tif ioerr != nil {\n\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\treturn\n\t}\n\tw.Result().Body.Close()\n\tcontent := string(body)\n\tresp := w.Result()\n\tif resp.Header.Get(\"Cache-Control\") != tc.expectedCache {\n\t\tt.Error(\"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t}\n\tif tc.completed && resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderCompleteResponseValue {\n\t\tt.Error(server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t}\n\tif !tc.completed && resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderIncompleteResponseValue {\n\t\tt.Error(server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t}\n\tif resp.Header.Get(\"Content-Type\") != tc.expectedContent {\n\t\tt.Error(\"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t}\n\tif resp.Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\tt.Error(\"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t}\n\tif resp.StatusCode != tc.expectedStatusCode {\n\t\tt.Error(\"Unexpected status code:\", resp.StatusCode)\n\t}\n\tif content != tc.expectedBody {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", tc.expectedBody)\n\t}\n\tfor k, v := range tc.expectedHeaders {\n\t\th := resp.Header.Values(k)\n\t\tif !reflect.DeepEqual(h, v) {\n\t\t\tt.Error(\"Unexpected value for header:\", k, h, \"expected:\", v)\n\t\t}\n\t}\n}\n\nfunc startGinServer(handlerFunc gin.HandlerFunc) *gin.Engine {\n\tgin.SetMode(gin.TestMode)\n\tr := gin.New()\n\tr.GET(\"/_gin_endpoint/:param\", ctxMiddleware, handlerFunc)\n\n\treturn r\n}\n\nfunc ctxMiddleware(c *gin.Context) {\n\tfor k, v := range ctxContent {\n\t\tc.Set(k, v)\n\t}\n}\n"
  },
  {
    "path": "router/gin/engine.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage gin\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/core\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\nconst Namespace = \"github_com/luraproject/lura/router/gin\"\n\ntype EngineOptions struct {\n\tLogger    logging.Logger\n\tWriter    io.Writer\n\tFormatter gin.LogFormatter\n\tHealth    <-chan string\n}\n\n// NewEngine returns an initialized gin engine\nfunc NewEngine(cfg config.ServiceConfig, opt EngineOptions) *gin.Engine {\n\tgin.SetMode(gin.ReleaseMode)\n\tif cfg.Debug {\n\t\topt.Logger.Debug(logPrefix, \"Debug enabled\")\n\t}\n\tengine := gin.New()\n\n\tengine.RedirectTrailingSlash = true\n\tengine.RedirectFixedPath = true\n\tengine.HandleMethodNotAllowed = true\n\tengine.ContextWithFallback = true\n\n\tvar paths []string\n\n\tginOptions := engineConfiguration{}\n\tif v, ok := cfg.ExtraConfig[Namespace]; ok {\n\t\tif b, err := json.Marshal(v); err == nil {\n\t\t\tif err := json.Unmarshal(b, &ginOptions); err == nil {\n\t\t\t\tif ginOptions.DisableRedirectTrailingSlash != nil {\n\t\t\t\t\tengine.RedirectTrailingSlash = !*ginOptions.DisableRedirectTrailingSlash\n\t\t\t\t}\n\t\t\t\tif ginOptions.DisableRedirectFixedPath != nil {\n\t\t\t\t\tengine.RedirectFixedPath = !*ginOptions.DisableRedirectFixedPath\n\t\t\t\t}\n\t\t\t\tif ginOptions.DisableHandleMethodNotAllowed != nil {\n\t\t\t\t\tengine.HandleMethodNotAllowed = !*ginOptions.DisableHandleMethodNotAllowed\n\t\t\t\t}\n\t\t\t\tif ginOptions.ForwardedByClientIP != nil {\n\t\t\t\t\tengine.ForwardedByClientIP = *ginOptions.ForwardedByClientIP\n\t\t\t\t}\n\t\t\t\tif len(ginOptions.RemoteIPHeaders) > 0 {\n\t\t\t\t\tengine.RemoteIPHeaders = ginOptions.RemoteIPHeaders\n\t\t\t\t\tfor k, h := range engine.RemoteIPHeaders {\n\t\t\t\t\t\tengine.RemoteIPHeaders[k] = textproto.CanonicalMIMEHeaderKey(h)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif len(ginOptions.TrustedProxies) > 0 {\n\t\t\t\t\tengine.SetTrustedProxies(ginOptions.TrustedProxies)\n\t\t\t\t}\n\t\t\t\tengine.AppEngine = ginOptions.AppEngine\n\t\t\t\tengine.MaxMultipartMemory = ginOptions.MaxMultipartMemory\n\t\t\t\tengine.RemoveExtraSlash = ginOptions.RemoveExtraSlash\n\t\t\t\tengine.UseH2C = ginOptions.UseH2C\n\t\t\t\tpaths = ginOptions.LoggerSkipPaths\n\n\t\t\t\treturnErrorMsg = ginOptions.ReturnErrorMsg\n\n\t\t\t\tif ginOptions.ObfuscateVersionHeader {\n\t\t\t\t\tcore.KrakendHeaderValue = \"Version undefined\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tengine.NoRoute(func(c *gin.Context) {\n\t\tc.Header(server.CompleteResponseHeaderName, server.HeaderIncompleteResponseValue)\n\t})\n\n\tif !ginOptions.DisableAccessLog {\n\t\tengine.Use(\n\t\t\tgin.LoggerWithConfig(gin.LoggerConfig{\n\t\t\t\tOutput:    opt.Writer,\n\t\t\t\tSkipPaths: paths,\n\t\t\t\tFormatter: opt.Formatter,\n\t\t\t}),\n\t\t)\n\t}\n\tengine.Use(gin.Recovery())\n\n\tif !ginOptions.DisablePathDecoding {\n\t\tengine.Use(paramChecker())\n\t}\n\n\tif !ginOptions.DisableHealthEndpoint {\n\t\tpath := \"/__health\"\n\t\tif ginOptions.HealthPath != \"\" {\n\t\t\tpath = ginOptions.HealthPath\n\t\t}\n\n\t\tengine.GET(path, healthEndpoint(opt.Health))\n\t}\n\n\treturn engine\n}\n\nfunc healthEndpoint(health <-chan string) func(*gin.Context) {\n\tmu := new(sync.RWMutex)\n\treports := map[string]string{}\n\n\tgo func() {\n\t\tfor name := range health {\n\t\t\tmu.Lock()\n\t\t\treports[name] = time.Now().String()\n\t\t\tmu.Unlock()\n\t\t}\n\t}()\n\n\treturn func(c *gin.Context) {\n\t\tmu.RLock()\n\t\tdefer mu.RUnlock()\n\n\t\tc.JSON(200, gin.H{\"status\": \"ok\", \"agents\": reports, \"now\": time.Now().String()})\n\t}\n}\n\nfunc paramChecker() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tfor _, param := range c.Params {\n\t\t\ts, err := url.PathUnescape(param.Value)\n\t\t\tif err != nil {\n\t\t\t\tc.Status(http.StatusBadRequest)\n\t\t\t\tErrorResponseWriter(c, fmt.Errorf(\"error: %s\", err))\n\t\t\t\tc.Abort()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif s != param.Value || strings.Contains(s, \"?\") || strings.Contains(s, \"#\") {\n\t\t\t\tc.Status(http.StatusBadRequest)\n\t\t\t\tErrorResponseWriter(c, errors.New(\"error: encoded url params\"))\n\t\t\t\tc.Abort()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype engineConfiguration struct {\n\t// Disables automatic redirection if the current route can't be matched but a\n\t// handler for the path with (without) the trailing slash exists.\n\t// For example if /foo/ is requested but a route only exists for /foo, the\n\t// client is redirected to /foo with http status code 301 for GET requests\n\t// and 307 for all other request methods.\n\tDisableRedirectTrailingSlash *bool `json:\"disable_redirect_trailing_slash\"`\n\n\t// If enabled, the router tries to fix the current request path, if no\n\t// handle is registered for it.\n\t// First superfluous path elements like ../ or // are removed.\n\t// Afterwards the router does a case-insensitive lookup of the cleaned path.\n\t// If a handle can be found for this route, the router makes a redirection\n\t// to the corrected path with status code 301 for GET requests and 307 for\n\t// all other request methods.\n\t// For example /FOO and /..//Foo could be redirected to /foo.\n\t// RedirectTrailingSlash is independent of this option.\n\tDisableRedirectFixedPath *bool `json:\"disable_redirect_fixed_path\"`\n\n\t// If enabled, the router checks if another method is allowed for the\n\t// current route, if the current request can not be routed.\n\t// If this is the case, the request is answered with 'Method Not Allowed'\n\t// and HTTP status code 405.\n\t// If no other Method is allowed, the request is delegated to the NotFound\n\t// handler.\n\tDisableHandleMethodNotAllowed *bool `json:\"disable_handle_method_not_allowed\"`\n\n\t// If enabled, client IP will be parsed from the request's headers that\n\t// match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was\n\t// fetched, it falls back to the IP obtained from\n\t// `(*gin.Context).Request.RemoteAddr`.\n\tForwardedByClientIP *bool `json:\"forwarded_by_client_ip\"`\n\n\t// List of headers used to obtain the client IP when\n\t// `(*gin.Engine).ForwardedByClientIP` is `true` and\n\t// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the\n\t// network origins of `(*gin.Engine).TrustedProxies`.\n\tRemoteIPHeaders []string `json:\"remote_ip_headers\"`\n\n\t// List of network origins (IPv4 addresses, IPv4 CIDRs, IPv6 addresses or\n\t// IPv6 CIDRs) from which to trust request's headers that contain\n\t// alternative client IP when `(*gin.Engine).ForwardedByClientIP` is\n\t// `true`.\n\tTrustedProxies []string `json:\"trusted_proxies\"`\n\n\t// #726 #755 If enabled, it will trust some headers starting with\n\t// 'X-AppEngine...' for better integration with that PaaS.\n\tAppEngine bool `json:\"app_engine\"`\n\n\t// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm\n\t// method call.\n\tMaxMultipartMemory int64 `json:\"max_multipart_memory\"`\n\n\t// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.\n\t// See the PR #1817 and issue #1644\n\tRemoveExtraSlash bool `json:\"remove_extra_slash\"`\n\n\t// LoggerSkipPaths defines the set of path to avoid logging\n\tLoggerSkipPaths []string `json:\"logger_skip_paths\"`\n\n\t// AutoOptions enables the autogenerated OPTIONS endpoint for all the registered paths\n\tAutoOptions bool `json:\"auto_options\"`\n\n\t// ReturnErrorMsg flags if the error msg should be returned to the client as response body\n\tReturnErrorMsg bool `json:\"return_error_msg\"`\n\n\t// DisableHealthEndpoint marks if the health check endpoint should be exposed\n\tDisableHealthEndpoint bool `json:\"disable_health\"`\n\n\t// HealthPath allows users to define a custom path for the health check endpoint\n\tHealthPath string `json:\"health_path\"`\n\n\t// DisableAccessLog blocks the injection of the router logger\n\tDisableAccessLog bool `json:\"disable_access_log\"`\n\n\t// DisablePathDecoding disables automatic validation of the url params looking for url encoded ones.\n\t// For example if /foo/..%252Fbar is requested and this flag is set to false, the router will\n\t// reject the request with http status code 400.\n\tDisablePathDecoding bool `json:\"disable_path_decoding\"`\n\n\t// ObfuscateVersionHeader flags if the version header returned by the router should replace the actual\n\t// version with the value \"undefined\"\n\tObfuscateVersionHeader bool `json:\"hide_version_header\"`\n\n\t// UseH2C enable h2c support.\n\tUseH2C bool `json:\"use_h2c\"`\n}\n\nvar returnErrorMsg bool\n"
  },
  {
    "path": "router/gin/engine_test.go",
    "content": "package gin\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/luraproject/lura/v2/config\"\n)\n\nfunc TestNewEngine_contextIsPropagated(t *testing.T) {\n\tengine := NewEngine(\n\t\tconfig.ServiceConfig{},\n\t\tEngineOptions{},\n\t)\n\n\ttype ctxKeyType string\n\n\tctxKey := ctxKeyType(\"foo\")\n\tctxValue := \"bar\"\n\n\tengine.GET(\"/some/path\", func(c *gin.Context) {\n\t\tc.String(http.StatusOK, \"%v\", c.Value(ctxKey))\n\t})\n\n\treq, _ := http.NewRequest(\"GET\", \"/some/path\", http.NoBody)\n\treq = req.WithContext(context.WithValue(req.Context(), ctxKey, ctxValue))\n\n\tw := httptest.NewRecorder()\n\n\tengine.ServeHTTP(w, req)\n\n\tresp := w.Result()\n\n\tif sc := resp.StatusCode; sc != http.StatusOK {\n\t\tt.Errorf(\"unexpected status code: %d\", sc)\n\t\treturn\n\t}\n\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.Errorf(\"reading the response body: %s\", err.Error())\n\t\treturn\n\t}\n\n\tif string(b) != ctxValue {\n\t\tt.Errorf(\"unexpected value: %s\", string(b))\n\t}\n}\n\nfunc TestNewEngine_paramsAreChecked(t *testing.T) {\n\tengine := NewEngine(\n\t\tconfig.ServiceConfig{},\n\t\tEngineOptions{},\n\t)\n\n\tengine.GET(\"/user/:id/public\", func(c *gin.Context) {\n\t\tc.String(http.StatusOK, \"ok\")\n\t})\n\n\tassertResponse := func(path string, statusCode int, body string) {\n\t\treq, _ := http.NewRequest(\"GET\", path, http.NoBody)\n\t\tw := httptest.NewRecorder()\n\t\tengine.ServeHTTP(w, req)\n\t\tresp := w.Result()\n\n\t\tif sc := resp.StatusCode; sc != statusCode {\n\t\t\tt.Errorf(\"unexpected status code: %d (expected %d)\", sc, statusCode)\n\t\t\treturn\n\t\t}\n\n\t\tb, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"reading the response body: %s\", err.Error())\n\t\t\treturn\n\t\t}\n\n\t\tif string(b) != body {\n\t\t\tt.Errorf(\"unexpected response body: '%s' (expected '%s')\", string(b), body)\n\t\t}\n\t}\n\n\tassertResponse(\"/user/123/public\", http.StatusOK, \"ok\")\n\tassertResponse(\"/user/123%3f/public\", http.StatusBadRequest, \"error: encoded url params\")\n\tassertResponse(\"/user/123%23/public\", http.StatusBadRequest, \"error: encoded url params\")\n}\n"
  },
  {
    "path": "router/gin/render.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage gin\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/encoding\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n)\n\n// Render defines the signature of the functions to be use for the final response\n// encoding and rendering\ntype Render func(*gin.Context, *proxy.Response)\n\n// NEGOTIATE defines the value of the OutputEncoding for the negotiated render\nconst NEGOTIATE = \"negotiate\"\nconst XML = \"xml\"\nconst YAML = \"yaml\"\n\nvar (\n\tmutex          = &sync.RWMutex{}\n\trenderRegister = map[string]Render{\n\t\tencoding.STRING:   stringRender,\n\t\tencoding.JSON:     jsonRender,\n\t\tencoding.NOOP:     noopRender,\n\t\t\"json-collection\": jsonCollectionRender,\n\t\tXML:               xmlRender,\n\t\tYAML:              yamlRender,\n\t}\n)\n\nfunc init() {\n\t// the negotiated render must be registered at the init function in order\n\t// to avoid a cyclical dependency\n\trenderRegister[NEGOTIATE] = negotiatedRender\n}\n\n// RegisterRender allows clients to register their custom renders\nfunc RegisterRender(name string, r Render) {\n\tmutex.Lock()\n\trenderRegister[name] = r\n\tmutex.Unlock()\n}\n\nfunc getRender(cfg *config.EndpointConfig) Render {\n\tfallback := jsonRender\n\tif len(cfg.Backend) == 1 {\n\t\tfallback = getWithFallback(cfg.Backend[0].Encoding, fallback)\n\t}\n\n\tif cfg.OutputEncoding == \"\" {\n\t\treturn fallback\n\t}\n\n\treturn getWithFallback(cfg.OutputEncoding, fallback)\n}\n\nfunc getWithFallback(key string, fallback Render) Render {\n\tmutex.RLock()\n\tr, ok := renderRegister[key]\n\tmutex.RUnlock()\n\tif !ok {\n\t\treturn fallback\n\t}\n\treturn r\n}\n\nfunc negotiatedRender(c *gin.Context, response *proxy.Response) {\n\tswitch c.NegotiateFormat(gin.MIMEJSON, gin.MIMEPlain, gin.MIMEXML, gin.MIMEYAML) {\n\tcase gin.MIMEXML:\n\t\tgetWithFallback(XML, jsonRender)(c, response)\n\tcase gin.MIMEPlain, gin.MIMEYAML:\n\t\tgetWithFallback(YAML, jsonRender)(c, response)\n\tdefault:\n\t\tgetWithFallback(encoding.JSON, jsonRender)(c, response)\n\t}\n}\n\nfunc stringRender(c *gin.Context, response *proxy.Response) {\n\tstatus := c.Writer.Status()\n\n\tif response == nil {\n\t\tc.String(status, \"\")\n\t\treturn\n\t}\n\td, ok := response.Data[\"content\"]\n\tif !ok {\n\t\tc.String(status, \"\")\n\t\treturn\n\t}\n\tmsg, ok := d.(string)\n\tif !ok {\n\t\tc.String(status, \"\")\n\t\treturn\n\t}\n\tc.String(status, msg)\n}\n\nfunc jsonRender(c *gin.Context, response *proxy.Response) {\n\tstatus := c.Writer.Status()\n\tif response == nil {\n\t\tc.JSON(status, emptyResponse)\n\t\treturn\n\t}\n\tc.JSON(status, response.Data)\n}\n\nfunc jsonCollectionRender(c *gin.Context, response *proxy.Response) {\n\tstatus := c.Writer.Status()\n\tif response == nil {\n\t\tc.JSON(status, []struct{}{})\n\t\treturn\n\t}\n\tcol, ok := response.Data[\"collection\"]\n\tif !ok {\n\t\tc.JSON(status, []struct{}{})\n\t\treturn\n\t}\n\tc.JSON(status, col)\n}\n\nfunc xmlRender(c *gin.Context, response *proxy.Response) {\n\tstatus := c.Writer.Status()\n\tif response == nil {\n\t\tc.XML(status, nil)\n\t\treturn\n\t}\n\td, ok := response.Data[\"content\"]\n\tif !ok {\n\t\tc.XML(status, nil)\n\t\treturn\n\t}\n\tc.XML(status, d)\n}\n\nfunc yamlRender(c *gin.Context, response *proxy.Response) {\n\tstatus := c.Writer.Status()\n\tif response == nil {\n\t\tc.YAML(status, emptyResponse)\n\t\treturn\n\t}\n\tc.YAML(status, response.Data)\n}\n\nfunc noopRender(c *gin.Context, response *proxy.Response) {\n\tif response == nil {\n\t\tc.Status(http.StatusInternalServerError)\n\t\treturn\n\t}\n\tfor k, vs := range response.Metadata.Headers {\n\t\tfor _, v := range vs {\n\t\t\tc.Writer.Header().Add(k, v)\n\t\t}\n\t}\n\tc.Status(response.Metadata.StatusCode)\n\tif response.Io == nil {\n\t\treturn\n\t}\n\tio.Copy(c.Writer, response.Io)\n}\n\nvar emptyResponse = gin.H{}\n"
  },
  {
    "path": "router/gin/render_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage gin\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/encoding\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n)\n\nfunc TestRender_Negotiated_ok(t *testing.T) {\n\ttype A struct {\n\t\tB string\n\t}\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       map[string]interface{}{\"content\": A{B: \"supu\"}},\n\t\t}, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:        time.Second,\n\t\tCacheTTL:       6 * time.Hour,\n\t\tQueryString:    []string{\"b\"},\n\t\tOutputEncoding: NEGOTIATE,\n\t}\n\n\tgin.SetMode(gin.TestMode)\n\tserver := gin.New()\n\tserver.GET(\"/_gin_endpoint/:param\", EndpointHandler(endpoint, p))\n\n\tfor _, testData := range [][]string{\n\t\t{\"plain\", \"text/plain\", \"application/yaml; charset=utf-8\", \"content:\\n  b: supu\\n\"},\n\t\t{\"none\", \"\", \"application/json; charset=utf-8\", `{\"content\":{\"B\":\"supu\"}}`},\n\t\t{\"json\", \"application/json\", \"application/json; charset=utf-8\", `{\"content\":{\"B\":\"supu\"}}`},\n\t\t{\"xml\", \"application/xml\", \"application/xml; charset=utf-8\", `<A><B>supu</B></A>`},\n\t} {\n\t\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", io.NopCloser(&bytes.Buffer{}))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\treq.Header.Set(\"Accept\", testData[1])\n\n\t\tw := httptest.NewRecorder()\n\t\tserver.ServeHTTP(w, req)\n\n\t\tdefer w.Result().Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(w.Result().Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"reading response body:\", ioerr)\n\t\t\treturn\n\t\t}\n\n\t\tcontent := string(body)\n\t\tif w.Result().Header.Get(\"Cache-Control\") != \"public, max-age=21600\" {\n\t\t\tt.Error(testData[0], \"Cache-Control error:\", w.Result().Header.Get(\"Cache-Control\"))\n\t\t}\n\t\tif w.Result().Header.Get(\"Content-Type\") != testData[2] {\n\t\t\tt.Error(testData[0], \"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(testData[0], \"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif w.Result().StatusCode != http.StatusOK {\n\t\t\tt.Error(testData[0], \"Unexpected status code:\", w.Result().StatusCode)\n\t\t}\n\t\tif content != testData[3] {\n\t\t\tt.Error(testData[0], fmt.Sprintf(\"Unexpected body: '%s'\\nexpected: '%s'\", content, testData[3]))\n\t\t}\n\t}\n}\n\nfunc TestRender_Negotiated_noData(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tData: map[string]interface{}{},\n\t\t}, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:        time.Second,\n\t\tCacheTTL:       6 * time.Hour,\n\t\tQueryString:    []string{\"b\"},\n\t\tOutputEncoding: NEGOTIATE,\n\t}\n\n\tgin.SetMode(gin.TestMode)\n\tserver := gin.New()\n\tserver.GET(\"/_gin_endpoint/:param\", EndpointHandler(endpoint, p))\n\n\tfor _, testData := range [][]string{\n\t\t{\"plain\", \"text/plain\", \"application/yaml; charset=utf-8\", \"{}\\n\"},\n\t\t{\"none\", \"\", \"application/json; charset=utf-8\", \"{}\"},\n\t\t{\"json\", \"application/json\", \"application/json; charset=utf-8\", \"{}\"},\n\t\t{\"xml\", \"application/xml\", \"application/xml; charset=utf-8\", \"\"},\n\t} {\n\t\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", io.NopCloser(&bytes.Buffer{}))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\treq.Header.Set(\"Accept\", testData[1])\n\n\t\tw := httptest.NewRecorder()\n\t\tserver.ServeHTTP(w, req)\n\n\t\tdefer w.Result().Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(w.Result().Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"reading response body:\", ioerr)\n\t\t\treturn\n\t\t}\n\n\t\tcontent := string(body)\n\t\tif w.Result().Header.Get(\"Content-Type\") != testData[2] {\n\t\t\tt.Error(testData[0], \"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(testData[0], \"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif w.Result().StatusCode != http.StatusOK {\n\t\t\tt.Error(testData[0], \"Unexpected status code:\", w.Result().StatusCode)\n\t\t}\n\t\tif content != testData[3] {\n\t\t\tt.Error(testData[0], \"Unexpected body:\", content, \"expected:\", testData[3])\n\t\t}\n\t}\n}\n\nfunc TestRender_Negotiated_noResponse(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:        time.Second,\n\t\tCacheTTL:       6 * time.Hour,\n\t\tQueryString:    []string{\"b\"},\n\t\tOutputEncoding: NEGOTIATE,\n\t}\n\n\tgin.SetMode(gin.TestMode)\n\tserver := gin.New()\n\tserver.GET(\"/_gin_endpoint/:param\", EndpointHandler(endpoint, p))\n\n\tfor _, testData := range [][]string{\n\t\t{\"plain\", \"text/plain\", \"application/yaml; charset=utf-8\", \"{}\\n\"},\n\t\t{\"none\", \"\", \"application/json; charset=utf-8\", \"{}\"},\n\t\t{\"json\", \"application/json\", \"application/json; charset=utf-8\", \"{}\"},\n\t\t{\"xml\", \"application/xml\", \"application/xml; charset=utf-8\", \"\"},\n\t} {\n\t\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", io.NopCloser(&bytes.Buffer{}))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\treq.Header.Set(\"Accept\", testData[1])\n\n\t\tw := httptest.NewRecorder()\n\t\tserver.ServeHTTP(w, req)\n\n\t\tdefer w.Result().Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(w.Result().Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"reading response body:\", ioerr)\n\t\t\treturn\n\t\t}\n\n\t\tcontent := string(body)\n\t\tif w.Result().Header.Get(\"Content-Type\") != testData[2] {\n\t\t\tt.Error(testData[0], \"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(testData[0], \"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif w.Result().StatusCode != http.StatusOK {\n\t\t\tt.Error(testData[0], \"Unexpected status code:\", w.Result().StatusCode)\n\t\t}\n\t\tif content != testData[3] {\n\t\t\tt.Error(testData[0], \"Unexpected body:\", content, \"expected:\", testData[3])\n\t\t}\n\t}\n}\n\nfunc TestRender_unknown(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       map[string]interface{}{\"supu\": \"tupu\"},\n\t\t}, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:        time.Second,\n\t\tCacheTTL:       6 * time.Hour,\n\t\tQueryString:    []string{\"b\"},\n\t\tOutputEncoding: \"unknown\",\n\t}\n\n\tgin.SetMode(gin.TestMode)\n\tserver := gin.New()\n\tserver.GET(\"/_gin_endpoint/:param\", EndpointHandler(endpoint, p))\n\n\texpectedHeader := \"application/json; charset=utf-8\"\n\texpectedBody := `{\"supu\":\"tupu\"}`\n\n\tfor _, testData := range [][]string{\n\t\t{\"plain\", \"text/plain\"},\n\t\t{\"none\", \"\"},\n\t\t{\"json\", \"application/json\"},\n\t\t{\"unknown\", \"unknown\"},\n\t} {\n\t\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", io.NopCloser(&bytes.Buffer{}))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\treq.Header.Set(\"Accept\", testData[1])\n\n\t\tw := httptest.NewRecorder()\n\t\tserver.ServeHTTP(w, req)\n\n\t\tdefer w.Result().Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(w.Result().Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"reading response body:\", ioerr)\n\t\t\treturn\n\t\t}\n\n\t\tcontent := string(body)\n\t\tif w.Result().Header.Get(\"Cache-Control\") != \"public, max-age=21600\" {\n\t\t\tt.Error(testData[0], \"Cache-Control error:\", w.Result().Header.Get(\"Cache-Control\"))\n\t\t}\n\t\tif w.Result().Header.Get(\"Content-Type\") != expectedHeader {\n\t\t\tt.Error(testData[0], \"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(testData[0], \"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif w.Result().StatusCode != http.StatusOK {\n\t\t\tt.Error(testData[0], \"Unexpected status code:\", w.Result().StatusCode)\n\t\t}\n\t\tif content != expectedBody {\n\t\t\tt.Error(testData[0], \"Unexpected body:\", content, \"expected:\", expectedBody)\n\t\t}\n\t}\n}\n\nfunc TestRender_string(t *testing.T) {\n\texpectedContent := \"supu\"\n\texpectedHeader := \"text/plain; charset=utf-8\"\n\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       map[string]interface{}{\"content\": expectedContent},\n\t\t}, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:        time.Second,\n\t\tCacheTTL:       6 * time.Hour,\n\t\tQueryString:    []string{\"b\"},\n\t\tOutputEncoding: encoding.STRING,\n\t}\n\n\tgin.SetMode(gin.TestMode)\n\tserver := gin.New()\n\tserver.GET(\"/_gin_endpoint/:param\", EndpointHandler(endpoint, p))\n\n\tfor _, testData := range [][]string{\n\t\t{\"plain\", \"text/plain\"},\n\t\t{\"none\", \"\"},\n\t\t{\"json\", \"application/json\"},\n\t\t{\"unknown\", \"unknown\"},\n\t} {\n\t\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", io.NopCloser(&bytes.Buffer{}))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\treq.Header.Set(\"Accept\", testData[1])\n\n\t\tw := httptest.NewRecorder()\n\t\tserver.ServeHTTP(w, req)\n\n\t\tdefer w.Result().Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(w.Result().Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"reading response body:\", ioerr)\n\t\t\treturn\n\t\t}\n\n\t\tcontent := string(body)\n\t\tif w.Result().Header.Get(\"Cache-Control\") != \"public, max-age=21600\" {\n\t\t\tt.Error(testData[0], \"Cache-Control error:\", w.Result().Header.Get(\"Cache-Control\"))\n\t\t}\n\t\tif w.Result().Header.Get(\"Content-Type\") != expectedHeader {\n\t\t\tt.Error(testData[0], \"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(testData[0], \"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif w.Result().StatusCode != http.StatusOK {\n\t\t\tt.Error(testData[0], \"Unexpected status code:\", w.Result().StatusCode)\n\t\t}\n\t\tif content != expectedContent {\n\t\t\tt.Error(testData[0], \"Unexpected body:\", content, \"expected:\", expectedContent)\n\t\t}\n\t}\n}\n\nfunc TestRender_string_noData(t *testing.T) {\n\texpectedContent := \"\"\n\texpectedHeader := \"text/plain; charset=utf-8\"\n\n\tfor k, p := range []proxy.Proxy{\n\t\tfunc(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\t\treturn &proxy.Response{\n\t\t\t\tIsComplete: false,\n\t\t\t\tData:       map[string]interface{}{\"content\": 42},\n\t\t\t}, nil\n\t\t},\n\t\tfunc(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\t\treturn &proxy.Response{\n\t\t\t\tIsComplete: false,\n\t\t\t\tData:       map[string]interface{}{},\n\t\t\t}, nil\n\t\t},\n\t\tfunc(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\t\treturn nil, nil\n\t\t},\n\t} {\n\t\tendpoint := &config.EndpointConfig{\n\t\t\tTimeout:        time.Second,\n\t\t\tCacheTTL:       6 * time.Hour,\n\t\t\tQueryString:    []string{\"b\"},\n\t\t\tOutputEncoding: encoding.STRING,\n\t\t}\n\n\t\tgin.SetMode(gin.TestMode)\n\t\tserver := gin.New()\n\t\tserver.GET(\"/_gin_endpoint/:param\", EndpointHandler(endpoint, p))\n\n\t\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", io.NopCloser(&bytes.Buffer{}))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tw := httptest.NewRecorder()\n\t\tserver.ServeHTTP(w, req)\n\n\t\tdefer w.Result().Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(w.Result().Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"reading response body:\", ioerr)\n\t\t\treturn\n\t\t}\n\n\t\tcontent := string(body)\n\t\tif w.Result().Header.Get(\"Content-Type\") != expectedHeader {\n\t\t\tt.Error(k, \"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(k, \"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif w.Result().StatusCode != http.StatusOK {\n\t\t\tt.Error(k, \"Unexpected status code:\", w.Result().StatusCode)\n\t\t}\n\t\tif content != expectedContent {\n\t\t\tt.Error(k, \"Unexpected body:\", content, \"expected:\", expectedContent)\n\t\t}\n\t}\n}\n\nfunc TestRegisterRender(t *testing.T) {\n\tvar total int\n\texpected := &proxy.Response{IsComplete: true, Data: map[string]interface{}{\"a\": \"b\"}}\n\tname := \"test render\"\n\n\tRegisterRender(name, func(_ *gin.Context, resp *proxy.Response) {\n\t\t*resp = *expected\n\t\ttotal++\n\t})\n\n\tsubject := getRender(&config.EndpointConfig{OutputEncoding: name})\n\n\tvar c *gin.Context\n\tresp := proxy.Response{}\n\tsubject(c, &resp)\n\n\tif !reflect.DeepEqual(resp, *expected) {\n\t\tt.Error(\"unexpected response\", resp)\n\t}\n\n\tif total != 1 {\n\t\tt.Error(\"the render was called an unexpected amount of times:\", total)\n\t}\n}\n\nfunc TestRender_noop(t *testing.T) {\n\texpectedContent := \"supu\"\n\texpectedHeader := \"text/plain; charset=utf-8\"\n\texpectedSetCookieValue := []string{\"test1=test1\", \"test2=test2\"}\n\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tMetadata: proxy.Metadata{\n\t\t\t\tStatusCode: 200,\n\t\t\t\tHeaders: map[string][]string{\n\t\t\t\t\t\"Content-Type\": {expectedHeader},\n\t\t\t\t\t\"Set-Cookie\":   {\"test1=test1\", \"test2=test2\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tIo: bytes.NewBufferString(expectedContent),\n\t\t}, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:        time.Second,\n\t\tCacheTTL:       6 * time.Hour,\n\t\tQueryString:    []string{\"b\"},\n\t\tOutputEncoding: encoding.NOOP,\n\t}\n\n\tgin.SetMode(gin.TestMode)\n\tserver := gin.New()\n\tserver.GET(\"/_gin_endpoint/:param\", EndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", io.NopCloser(&bytes.Buffer{}))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tw := httptest.NewRecorder()\n\tserver.ServeHTTP(w, req)\n\n\tdefer w.Result().Body.Close()\n\n\tbody, ioerr := io.ReadAll(w.Result().Body)\n\tif ioerr != nil {\n\t\tt.Error(\"reading response body:\", ioerr)\n\t\treturn\n\t}\n\n\tcontent := string(body)\n\tif w.Result().Header.Get(\"Content-Type\") != expectedHeader {\n\t\tt.Error(\"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t}\n\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\tt.Error(\"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t}\n\tif w.Result().StatusCode != http.StatusOK {\n\t\tt.Error(\"Unexpected status code:\", w.Result().StatusCode)\n\t}\n\tif content != expectedContent {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", expectedContent)\n\t}\n\tgotCookie := w.Header()[\"Set-Cookie\"]\n\tif !reflect.DeepEqual(gotCookie, expectedSetCookieValue) {\n\t\tt.Error(\"Unexpected Set-Cookie header:\", gotCookie, \"expected:\", expectedSetCookieValue)\n\t}\n}\n\nfunc TestRender_noop_nilBody(t *testing.T) {\n\texpectedContent := \"\"\n\texpectedHeader := \"\"\n\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{IsComplete: true}, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:        time.Second,\n\t\tCacheTTL:       6 * time.Hour,\n\t\tQueryString:    []string{\"b\"},\n\t\tOutputEncoding: encoding.NOOP,\n\t}\n\n\tgin.SetMode(gin.TestMode)\n\tserver := gin.New()\n\tserver.GET(\"/_gin_endpoint/:param\", EndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", io.NopCloser(&bytes.Buffer{}))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tw := httptest.NewRecorder()\n\tserver.ServeHTTP(w, req)\n\n\tdefer w.Result().Body.Close()\n\n\tbody, ioerr := io.ReadAll(w.Result().Body)\n\tif ioerr != nil {\n\t\tt.Error(\"reading response body:\", ioerr)\n\t\treturn\n\t}\n\n\tcontent := string(body)\n\tif w.Result().Header.Get(\"Content-Type\") != expectedHeader {\n\t\tt.Error(\"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t}\n\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\tt.Error(\"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t}\n\tif w.Result().StatusCode != http.StatusOK {\n\t\tt.Error(\"Unexpected status code:\", w.Result().StatusCode)\n\t}\n\tif content != expectedContent {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", expectedContent)\n\t}\n}\n\nfunc TestRender_noop_nilResponse(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:        time.Second,\n\t\tCacheTTL:       6 * time.Hour,\n\t\tQueryString:    []string{\"b\"},\n\t\tOutputEncoding: encoding.NOOP,\n\t}\n\n\tgin.SetMode(gin.TestMode)\n\tserver := gin.New()\n\tserver.GET(\"/_gin_endpoint/:param\", EndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", io.NopCloser(&bytes.Buffer{}))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tw := httptest.NewRecorder()\n\tserver.ServeHTTP(w, req)\n\n\tif w.Result().Header.Get(\"Content-Type\") != \"\" {\n\t\tt.Error(\"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t}\n\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\tt.Error(\"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t}\n\tif w.Result().StatusCode != http.StatusInternalServerError {\n\t\tt.Error(\"Unexpected status code:\", w.Result().StatusCode)\n\t}\n}\n"
  },
  {
    "path": "router/gin/router.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\nPackage gin provides some basic implementations for building routers based on gin-gonic/gin\n*/\npackage gin\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/core\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/router\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\nconst logPrefix = \"[SERVICE: Gin]\"\n\n// RunServerFunc is a func that will run the http Server with the given params.\ntype RunServerFunc func(context.Context, config.ServiceConfig, http.Handler) error\n\n// Config is the struct that collects the parts the router should be builded from\ntype Config struct {\n\tEngine         *gin.Engine\n\tMiddlewares    []gin.HandlerFunc\n\tHandlerFactory HandlerFactory\n\tProxyFactory   proxy.Factory\n\tLogger         logging.Logger\n\tRunServer      RunServerFunc\n}\n\n// DefaultFactory returns a gin router factory with the injected proxy factory and logger.\n// It also uses a default gin router and the default HandlerFactory\nfunc DefaultFactory(proxyFactory proxy.Factory, logger logging.Logger) router.Factory {\n\treturn NewFactory(\n\t\tConfig{\n\t\t\tEngine:         gin.Default(),\n\t\t\tMiddlewares:    []gin.HandlerFunc{},\n\t\t\tHandlerFactory: EndpointHandler,\n\t\t\tProxyFactory:   proxyFactory,\n\t\t\tLogger:         logger,\n\t\t\tRunServer:      server.RunServer,\n\t\t},\n\t)\n}\n\n// NewFactory returns a gin router factory with the injected configuration\nfunc NewFactory(cfg Config) router.Factory {\n\treturn factory{cfg}\n}\n\ntype factory struct {\n\tcfg Config\n}\n\n// New implements the factory interface\nfunc (rf factory) New() router.Router {\n\treturn rf.NewWithContext(context.Background())\n}\n\n// NewWithContext implements the factory interface\nfunc (rf factory) NewWithContext(ctx context.Context) router.Router {\n\treturn ginRouter{\n\t\tcfg:        rf.cfg,\n\t\tctx:        ctx,\n\t\trunServerF: rf.cfg.RunServer,\n\t\tmu:         new(sync.Mutex),\n\t\turlCatalog: urlCatalog{\n\t\t\tmu:      new(sync.Mutex),\n\t\t\tcatalog: map[string][]string{},\n\t\t},\n\t}\n}\n\ntype ginRouter struct {\n\tcfg        Config\n\tctx        context.Context\n\trunServerF RunServerFunc\n\tmu         *sync.Mutex\n\turlCatalog urlCatalog\n}\n\ntype urlCatalog struct {\n\tmu      *sync.Mutex\n\tcatalog map[string][]string\n}\n\n// Run completes the router initialization and executes it\nfunc (r ginRouter) Run(cfg config.ServiceConfig) {\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\n\tserver.InitHTTPDefaultTransport(cfg)\n\n\tr.registerEndpointsAndMiddlewares(cfg)\n\n\tr.cfg.Logger.Info(\"[SERVICE: Gin] Listening on port:\", cfg.Port)\n\tif err := r.runServerF(r.ctx, cfg, &safeCaster{h: r.cfg.Engine.Handler()}); err != nil && err != http.ErrServerClosed {\n\t\tr.cfg.Logger.Error(logPrefix, err.Error())\n\t}\n\n\tr.cfg.Logger.Info(logPrefix, \"Router execution ended\")\n}\n\nfunc (r ginRouter) registerEndpointsAndMiddlewares(cfg config.ServiceConfig) {\n\tif cfg.Debug {\n\t\tr.cfg.Engine.Any(\"/__debug/*param\", DebugHandler(r.cfg.Logger))\n\t}\n\n\tif cfg.Echo {\n\t\tr.cfg.Engine.Any(\"/__echo/*param\", EchoHandler())\n\t}\n\n\tendpointGroup := r.cfg.Engine.Group(\"/\")\n\tendpointGroup.Use(r.cfg.Middlewares...)\n\n\tr.registerKrakendEndpoints(endpointGroup, cfg)\n\n\tif opts, ok := cfg.ExtraConfig[Namespace].(map[string]interface{}); ok {\n\t\tif v, ok := opts[\"auto_options\"].(bool); ok && v {\n\t\t\tr.cfg.Logger.Debug(logPrefix, \"Enabling the auto options endpoints\")\n\t\t\tr.registerOptionEndpoints(endpointGroup)\n\t\t}\n\t}\n}\n\nfunc (r ginRouter) registerKrakendEndpoints(rg *gin.RouterGroup, cfg config.ServiceConfig) {\n\tproxyBuildFailedHandler := func(c *gin.Context) {\n\t\tc.AbortWithStatus(http.StatusInternalServerError)\n\t}\n\t// build and register the pipes and endpoints sequentially\n\tfor _, c := range cfg.Endpoints {\n\t\tproxyStack, err := r.cfg.ProxyFactory.New(c)\n\t\tif err != nil {\n\t\t\tr.cfg.Logger.Error(logPrefix, \"Calling the ProxyFactory\", err.Error())\n\t\t\tr.registerKrakendEndpoint(rg, c.Method, c,\n\t\t\t\tproxyBuildFailedHandler, 1)\n\t\t\tcontinue\n\t\t}\n\t\tr.registerKrakendEndpoint(rg, c.Method, c, r.cfg.HandlerFactory(c, proxyStack), len(c.Backend))\n\t}\n}\n\nfunc (r ginRouter) registerKrakendEndpoint(rg *gin.RouterGroup, method string, e *config.EndpointConfig, h gin.HandlerFunc, total int) {\n\tmethod = strings.ToTitle(method)\n\tpath := e.Endpoint\n\tif method != http.MethodGet && total > 1 {\n\t\tif !router.IsValidSequentialEndpoint(e) {\n\t\t\tr.cfg.Logger.Error(logPrefix, method, \"endpoints with sequential proxy enabled only allow a non-GET in the last backend! Ignoring\", path)\n\t\t\treturn\n\t\t}\n\t}\n\n\tswitch method {\n\tcase http.MethodGet:\n\t\trg.GET(path, h)\n\tcase http.MethodPost:\n\t\trg.POST(path, h)\n\tcase http.MethodPut:\n\t\trg.PUT(path, h)\n\tcase http.MethodPatch:\n\t\trg.PATCH(path, h)\n\tcase http.MethodDelete:\n\t\trg.DELETE(path, h)\n\tdefault:\n\t\tr.cfg.Logger.Error(logPrefix, \"[ENDPOINT:\", path, \"] Unsupported method\", method)\n\t\treturn\n\t}\n\n\tr.urlCatalog.mu.Lock()\n\tdefer r.urlCatalog.mu.Unlock()\n\n\tmethods, ok := r.urlCatalog.catalog[path]\n\tif !ok {\n\t\tr.urlCatalog.catalog[path] = []string{method}\n\t\treturn\n\t}\n\tr.urlCatalog.catalog[path] = append(methods, method)\n}\n\nfunc (r ginRouter) registerOptionEndpoints(rg *gin.RouterGroup) {\n\tr.urlCatalog.mu.Lock()\n\tdefer r.urlCatalog.mu.Unlock()\n\n\tfor path, methods := range r.urlCatalog.catalog {\n\t\tsort.Strings(methods)\n\t\tallowed := strings.Join(methods, \", \")\n\n\t\trg.OPTIONS(path, func(c *gin.Context) {\n\t\t\tc.Header(\"Allow\", allowed)\n\t\t\tc.Header(core.KrakendHeaderName, core.KrakendHeaderValue)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "router/gin/router_test.go",
    "content": "//go:build !race\n// +build !race\n\n// SPDX-License-Identifier: Apache-2.0\n\npackage gin\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\nfunc TestDefaultFactory_ok(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"}), logger).NewWithContext(ctx)\n\texpectedBody := \"{\\\"supu\\\":\\\"tupu\\\"}\"\n\n\tserviceCfg := config.ServiceConfig{\n\t\tPort: 8072,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/some\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/some\",\n\t\t\t\tMethod:   \"post\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/some\",\n\t\t\t\tMethod:   \"put\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/some\",\n\t\t\t\tMethod:   \"PATCH\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/some\",\n\t\t\t\tMethod:   \"DELETE\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tExtraConfig: map[string]interface{}{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\t\"auto_options\": true,\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, endpoint := range serviceCfg.Endpoints {\n\t\treq, _ := http.NewRequest(strings.ToTitle(endpoint.Method), fmt.Sprintf(\"http://127.0.0.1:8072%s\", endpoint.Endpoint), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tresp, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\tt.Error(\"Making the request:\", err.Error())\n\t\t\treturn\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(resp.Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\t\treturn\n\t\t}\n\t\tcontent := string(body)\n\t\tif resp.Header.Get(\"Cache-Control\") != \"\" {\n\t\t\tt.Error(\"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t\t}\n\t\tif resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderCompleteResponseValue {\n\t\t\tt.Error(server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t\t}\n\t\tif resp.Header.Get(\"Content-Type\") != \"application/json; charset=utf-8\" {\n\t\t\tt.Error(\"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif resp.Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(\"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Error(\"Unexpected status code:\", resp.StatusCode)\n\t\t}\n\t\tif content != expectedBody {\n\t\t\tt.Error(\"Unexpected body:\", content, \"expected:\", expectedBody)\n\t\t}\n\t}\n\n\treq, _ := http.NewRequest(\"OPTIONS\", \"http://127.0.0.1:8072/some\", http.NoBody)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tt.Error(\"Making the request:\", err.Error())\n\t\treturn\n\t}\n\n\tif allow := resp.Header.Get(\"Allow\"); allow != \"DELETE, GET, PATCH, POST, PUT\" {\n\t\tt.Errorf(\"unexpected options response: '%s'\", allow)\n\t}\n}\n\nfunc TestDefaultFactory_ko(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"}), logger).NewWithContext(ctx)\n\n\tserviceCfg := config.ServiceConfig{\n\t\tDebug: true,\n\t\tEcho:  true,\n\t\tPort:  8073,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/ignored\",\n\t\t\t\tMethod:   \"GETTT\",\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/empty\",\n\t\t\t\tMethod:   \"GETTT\",\n\t\t\t\tBackend:  []*config.Backend{},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/also-ignored\",\n\t\t\t\tMethod:   \"PUTT\",\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, subject := range [][]string{\n\t\t{\"GET\", \"ignored\"},\n\t\t{\"GET\", \"empty\"},\n\t\t{\"PUT\", \"also-ignored\"},\n\t} {\n\t\treq, _ := http.NewRequest(subject[0], fmt.Sprintf(\"http://127.0.0.1:8073/%s\", subject[1]), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tcheckResponseIs404(t, req)\n\t}\n}\n\nfunc TestDefaultFactory_proxyFactoryCrash(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(erroredProxyFactory{fmt.Errorf(\"%s\", \"crash!!!\")}, logger).NewWithContext(ctx)\n\n\tserviceCfg := config.ServiceConfig{\n\t\tDebug: true,\n\t\tEcho:  true,\n\t\tPort:  8074,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/ignored\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8074/ignored\", http.NoBody)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tt.Error(\"Making the request:\", err.Error())\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\tbody, ioerr := io.ReadAll(resp.Body)\n\tif ioerr != nil {\n\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\treturn\n\t}\n\tif resp.Header.Get(\"Cache-Control\") != \"\" {\n\t\tt.Error(req.URL.String(), \"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t}\n\tif resp.Header.Get(\"Content-Type\") != \"\" {\n\t\tt.Error(req.URL.String(), \"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t}\n\tif resp.Header.Get(\"X-Krakend\") != \"\" {\n\t\tt.Error(req.URL.String(), \"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t}\n\tif resp.StatusCode != http.StatusInternalServerError {\n\t\tt.Error(req.URL.String(), \"Unexpected status code:\", resp.StatusCode)\n\t}\n\tif string(body) != \"\" {\n\t\tt.Error(req.URL.String(), \"Unexpected body:\", string(body))\n\t}\n}\n\nfunc TestRunServer_ko(t *testing.T) {\n\tbuff := new(bytes.Buffer)\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\terrorMsg := \"runServer error\"\n\trunServerFunc := func(_ context.Context, _ config.ServiceConfig, _ http.Handler) error {\n\t\treturn errors.New(errorMsg)\n\t}\n\n\tpf := noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"})\n\tr := NewFactory(\n\t\tConfig{\n\t\t\tEngine:         gin.Default(),\n\t\t\tMiddlewares:    []gin.HandlerFunc{},\n\t\t\tHandlerFactory: EndpointHandler,\n\t\t\tProxyFactory:   pf,\n\t\t\tLogger:         logger,\n\t\t\tRunServer:      runServerFunc,\n\t\t},\n\t).New()\n\n\tserviceCfg := config.ServiceConfig{}\n\tr.Run(serviceCfg)\n\tre := regexp.MustCompile(errorMsg)\n\tif !re.MatchString(buff.String()) {\n\t\tt.Errorf(\"the logger doesn't contain the expected msg: %s\", buff.Bytes())\n\t}\n}\n\nfunc checkResponseIs404(t *testing.T, req *http.Request) {\n\texpectedBody := \"404 page not found\"\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tt.Error(\"Making the request:\", err.Error())\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\tbody, ioerr := io.ReadAll(resp.Body)\n\tif ioerr != nil {\n\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\treturn\n\t}\n\tcontent := string(body)\n\tif resp.Header.Get(\"Cache-Control\") != \"\" {\n\t\tt.Error(req.URL.String(), \"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t}\n\tif resp.Header.Get(\"Content-Type\") != \"text/plain\" {\n\t\tt.Error(req.URL.String(), \"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t}\n\tif resp.Header.Get(\"X-Krakend\") != \"\" {\n\t\tt.Error(req.URL.String(), \"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t}\n\tif resp.StatusCode != http.StatusNotFound {\n\t\tt.Error(req.URL.String(), \"Unexpected status code:\", resp.StatusCode)\n\t}\n\tif content != expectedBody {\n\t\tt.Error(req.URL.String(), \"Unexpected body:\", content, \"expected:\", expectedBody)\n\t}\n}\n\ntype noopProxyFactory map[string]interface{}\n\nfunc (n noopProxyFactory) New(_ *config.EndpointConfig) (proxy.Proxy, error) {\n\treturn func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       n,\n\t\t}, nil\n\t}, nil\n}\n\ntype erroredProxyFactory struct {\n\tError error\n}\n\nfunc (e erroredProxyFactory) New(_ *config.EndpointConfig) (proxy.Proxy, error) {\n\treturn proxy.NoopProxy, e.Error\n}\n"
  },
  {
    "path": "router/gin/safecast.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\nPackage gin provides some basic implementations for building routers based on gin-gonic/gin\n*/\npackage gin\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"net\"\n\t\"net/http\"\n)\n\nvar _ http.ResponseWriter = (*safeCast)(nil)\nvar _ http.Flusher = (*safeCast)(nil)\nvar _ http.Hijacker = (*safeCast)(nil)\nvar _ http.CloseNotifier = (*safeCast)(nil)\n\nvar _ http.Handler = (*safeCaster)(nil)\n\n// safeCast provides fallback implementation for interfaces that are not\n// checked internally by gin, and that can cause a panic:\n// - Flusher\n// - Hijacker\n// - Notifier\ntype safeCast struct {\n\tw http.ResponseWriter\n}\n\nfunc (s *safeCast) Header() http.Header {\n\treturn s.w.Header()\n}\n\nfunc (s *safeCast) Write(b []byte) (int, error) {\n\treturn s.w.Write(b)\n}\n\nfunc (s *safeCast) WriteHeader(statusCode int) {\n\ts.w.WriteHeader(statusCode)\n}\n\nfunc (s *safeCast) Flush() {\n\tif f, ok := s.w.(http.Flusher); ok {\n\t\tf.Flush()\n\t}\n}\n\nfunc (s *safeCast) Hijack() (net.Conn, *bufio.ReadWriter, error) {\n\tif h, ok := s.w.(http.Hijacker); ok {\n\t\treturn h.Hijack()\n\t}\n\treturn nil, nil, errors.New(\"not supported\")\n}\n\nfunc (s *safeCast) CloseNotify() <-chan bool {\n\tif h, ok := s.w.(http.CloseNotifier); ok {\n\t\treturn h.CloseNotify()\n\t}\n\treturn make(<-chan bool, 1)\n}\n\ntype safeCaster struct {\n\th http.Handler\n}\n\nfunc (s *safeCaster) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\ts.h.ServeHTTP(&safeCast{w}, r)\n}\n"
  },
  {
    "path": "router/gorilla/router.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\nPackage gorilla provides some basic implementations for building routers based on gorilla/mux\n*/\npackage gorilla\n\nimport (\n\t\"net/http\"\n\n\tgorilla \"github.com/gorilla/mux\"\n\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/router\"\n\t\"github.com/luraproject/lura/v2/router/mux\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n)\n\n// DefaultFactory returns a net/http mux router factory with the injected proxy factory and logger\nfunc DefaultFactory(pf proxy.Factory, logger logging.Logger) router.Factory {\n\treturn mux.NewFactory(DefaultConfig(pf, logger))\n}\n\n// DefaultConfig returns the struct that collects the parts the router should be builded from\nfunc DefaultConfig(pf proxy.Factory, logger logging.Logger) mux.Config {\n\treturn mux.Config{\n\t\tEngine:         gorillaEngine{gorilla.NewRouter()},\n\t\tMiddlewares:    []mux.HandlerMiddleware{},\n\t\tHandlerFactory: mux.CustomEndpointHandler(mux.NewRequestBuilder(gorillaParamsExtractor)),\n\t\tProxyFactory:   pf,\n\t\tLogger:         logger,\n\t\tDebugPattern:   \"/__debug/{params}\",\n\t\tEchoPattern:    \"/__echo/{params}\",\n\t\tRunServer:      server.RunServer,\n\t}\n}\n\nfunc gorillaParamsExtractor(r *http.Request) map[string]string {\n\tparams := map[string]string{}\n\ttitle := cases.Title(language.Und)\n\tfor key, value := range gorilla.Vars(r) {\n\t\tparams[title.String(key)] = value\n\t}\n\treturn params\n}\n\ntype gorillaEngine struct {\n\tr *gorilla.Router\n}\n\n// Handle implements the mux.Engine interface from the lura router package\nfunc (g gorillaEngine) Handle(pattern, method string, handler http.Handler) {\n\tg.r.Handle(pattern, handler).Methods(method)\n}\n\n// ServeHTTP implements the http:Handler interface from the stdlib\nfunc (g gorillaEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tg.r.ServeHTTP(mux.NewHTTPErrorInterceptor(w), r)\n}\n"
  },
  {
    "path": "router/gorilla/router_test.go",
    "content": "//go:build !race\n// +build !race\n\n// SPDX-License-Identifier: Apache-2.0\n\npackage gorilla\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\nfunc TestDefaultFactory_ok(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"}), logger).NewWithContext(ctx)\n\texpectedBody := \"{\\\"supu\\\":\\\"tupu\\\"}\"\n\n\tserviceCfg := config.ServiceConfig{\n\t\tPort: 8082,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/get/{id}\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/post\",\n\t\t\t\tMethod:   \"POST\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/put\",\n\t\t\t\tMethod:   \"PUT\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/patch\",\n\t\t\t\tMethod:   \"PATCH\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/delete\",\n\t\t\t\tMethod:   \"DELETE\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, endpoint := range serviceCfg.Endpoints {\n\t\treq, _ := http.NewRequest(endpoint.Method, fmt.Sprintf(\"http://127.0.0.1:8082%s\", endpoint.Endpoint), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tresp, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\tt.Error(\"Making the request:\", err.Error())\n\t\t\treturn\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(resp.Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\t\treturn\n\t\t}\n\t\tcontent := string(body)\n\t\tif resp.Header.Get(\"Cache-Control\") != \"\" {\n\t\t\tt.Error(endpoint.Endpoint, \"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t\t}\n\t\tif resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderCompleteResponseValue {\n\t\t\tt.Error(server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t\t}\n\t\tif resp.Header.Get(\"Content-Type\") != \"application/json\" {\n\t\t\tt.Error(endpoint.Endpoint, \"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif resp.Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(endpoint.Endpoint, \"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Error(endpoint.Endpoint, \"Unexpected status code:\", resp.StatusCode)\n\t\t}\n\t\tif content != expectedBody {\n\t\t\tt.Error(endpoint.Endpoint, \"Unexpected body:\", content, \"expected:\", expectedBody)\n\t\t}\n\t}\n}\n\nfunc TestDefaultFactory_ko(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"}), logger).NewWithContext(ctx)\n\n\tserviceCfg := config.ServiceConfig{\n\t\tDebug: true,\n\t\tPort:  8083,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/ignored\",\n\t\t\t\tMethod:   \"GETTT\",\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/empty\",\n\t\t\t\tMethod:   \"GETTT\",\n\t\t\t\tBackend:  []*config.Backend{},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/also-ignored\",\n\t\t\t\tMethod:   \"PUT\",\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, subject := range [][]string{\n\t\t{\"GET\", \"ignored\"},\n\t\t{\"GET\", \"empty\"},\n\t\t{\"PUT\", \"also-ignored\"},\n\t} {\n\t\treq, _ := http.NewRequest(subject[0], fmt.Sprintf(\"http://127.0.0.1:8083/%s\", subject[1]), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tcheckResponseIs404(t, req)\n\t}\n}\n\nfunc TestDefaultFactory_proxyFactoryCrash(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(erroredProxyFactory{fmt.Errorf(\"%s\", \"crash!!!\")}, logger).NewWithContext(ctx)\n\n\tserviceCfg := config.ServiceConfig{\n\t\tDebug: true,\n\t\tEcho:  true,\n\t\tPort:  8084,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/ignored\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, subject := range [][]string{{\"GET\", \"ignored\"}, {\"PUT\", \"also-ignored\"}} {\n\t\treq, _ := http.NewRequest(subject[0], fmt.Sprintf(\"http://127.0.0.1:8084/%s\", subject[1]), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tcheckResponseIs404(t, req)\n\t}\n}\n\nfunc checkResponseIs404(t *testing.T, req *http.Request) {\n\texpectedBody := \"404 page not found\\n\"\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tt.Error(\"Making the request:\", err.Error())\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\tbody, ioerr := io.ReadAll(resp.Body)\n\tif ioerr != nil {\n\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\treturn\n\t}\n\tcontent := string(body)\n\tif resp.Header.Get(\"Cache-Control\") != \"\" {\n\t\tt.Error(\"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t}\n\tif resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderIncompleteResponseValue {\n\t\tt.Error(req.URL.String(), server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t}\n\tif resp.Header.Get(\"Content-Type\") != \"text/plain; charset=utf-8\" {\n\t\tt.Error(\"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t}\n\tif resp.Header.Get(\"X-Krakend\") != \"\" {\n\t\tt.Error(\"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t}\n\tif resp.StatusCode != http.StatusNotFound {\n\t\tt.Error(\"Unexpected status code:\", resp.StatusCode)\n\t}\n\tif content != expectedBody {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", expectedBody)\n\t}\n}\n\ntype noopProxyFactory map[string]interface{}\n\nfunc (n noopProxyFactory) New(_ *config.EndpointConfig) (proxy.Proxy, error) {\n\treturn func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       n,\n\t\t}, nil\n\t}, nil\n}\n\ntype erroredProxyFactory struct {\n\tError error\n}\n\nfunc (e erroredProxyFactory) New(_ *config.EndpointConfig) (proxy.Proxy, error) {\n\treturn proxy.NoopProxy, e.Error\n}\n\ntype identityMiddleware struct{}\n\nfunc (identityMiddleware) Handler(h http.Handler) http.Handler {\n\treturn h\n}\n"
  },
  {
    "path": "router/helper.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage router\n\nimport (\n\t\"github.com/luraproject/lura/v2/config\"\n)\n\nfunc IsValidSequentialEndpoint(_ *config.EndpointConfig) bool {\n\t// if endpoint.ExtraConfig[proxy.Namespace] == nil {\n\t// \treturn false\n\t// }\n\n\t// proxyCfg := endpoint.ExtraConfig[proxy.Namespace].(map[string]interface{})\n\t// if proxyCfg[\"sequential\"] == false {\n\t// \treturn false\n\t// }\n\n\t// for i, backend := range endpoint.Backend {\n\t// \tif backend.Method != http.MethodGet && (i+1) != len(endpoint.Backend) {\n\t// \t\treturn false\n\t// \t}\n\t// }\n\n\treturn true\n}\n"
  },
  {
    "path": "router/helper_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage router\n\n// func TestIsValidSequentialEndpoint_ok(t *testing.T) {\n\n// \tendpoint := &config.EndpointConfig{\n// \t\tEndpoint: \"/correct\",\n// \t\tMethod:   \"PUT\",\n// \t\tBackend: []*config.Backend{\n// \t\t\t{\n// \t\t\t\tMethod: \"GET\",\n// \t\t\t},\n// \t\t\t{\n// \t\t\t\tMethod: \"PUT\",\n// \t\t\t},\n// \t\t},\n// \t\tExtraConfig: map[string]interface{}{\n// \t\t\tproxy.Namespace: map[string]interface{}{\n// \t\t\t\t\"sequential\": true,\n// \t\t\t},\n// \t\t},\n// \t}\n\n// \tsuccess := IsValidSequentialEndpoint(endpoint)\n\n// \tif !success {\n// \t\tt.Error(\"Endpoint expected valid but receive invalid\")\n// \t}\n// }\n\n// func TestIsValidSequentialEndpoint_wrong_config_not_given(t *testing.T) {\n\n// \tendpoint := &config.EndpointConfig{\n// \t\tEndpoint: \"/correct\",\n// \t\tMethod:   \"PUT\",\n// \t\tBackend: []*config.Backend{\n// \t\t\t{\n// \t\t\t\tMethod: \"GET\",\n// \t\t\t},\n// \t\t\t{\n// \t\t\t\tMethod: \"PUT\",\n// \t\t\t},\n// \t\t},\n// \t\tExtraConfig: map[string]interface{}{},\n// \t}\n\n// \tsuccess := IsValidSequentialEndpoint(endpoint)\n\n// \tif success {\n// \t\tt.Error(\"Endpoint expected invalid but receive valid\")\n// \t}\n// }\n\n// func TestIsValidSequentialEndpoint_wrong_config_set_false(t *testing.T) {\n\n// \tendpoint := &config.EndpointConfig{\n// \t\tEndpoint: \"/correct\",\n// \t\tMethod:   \"PUT\",\n// \t\tBackend: []*config.Backend{\n// \t\t\t{\n// \t\t\t\tMethod: \"GET\",\n// \t\t\t},\n// \t\t\t{\n// \t\t\t\tMethod: \"PUT\",\n// \t\t\t},\n// \t\t},\n// \t\tExtraConfig: map[string]interface{}{\n// \t\t\tproxy.Namespace: map[string]interface{}{\n// \t\t\t\t\"sequential\": false,\n// \t\t\t},\n// \t\t}}\n\n// \tsuccess := IsValidSequentialEndpoint(endpoint)\n\n// \tif success {\n// \t\tt.Error(\"Endpoint expected invalid but receive valid\")\n// \t}\n// }\n\n// func TestIsValidSequentialEndpoint_wrong_order(t *testing.T) {\n\n// \tendpoint := &config.EndpointConfig{\n// \t\tEndpoint: \"/correct\",\n// \t\tMethod:   \"PUT\",\n// \t\tBackend: []*config.Backend{\n// \t\t\t{\n// \t\t\t\tMethod: \"PUT\",\n// \t\t\t},\n// \t\t\t{\n// \t\t\t\tMethod: \"GET\",\n// \t\t\t},\n// \t\t},\n// \t\tExtraConfig: map[string]interface{}{\n// \t\t\tproxy.Namespace: map[string]interface{}{\n// \t\t\t\t\"sequential\": true,\n// \t\t\t},\n// \t\t},\n// \t}\n\n// \tsuccess := IsValidSequentialEndpoint(endpoint)\n\n// \tif success {\n// \t\tt.Error(\"Endpoint expected invalid but receive valid\")\n// \t}\n// }\n\n// func TestIsValidSequentialEndpoint_wrong_all_non_get(t *testing.T) {\n\n// \tendpoint := &config.EndpointConfig{\n// \t\tEndpoint: \"/correct\",\n// \t\tMethod:   \"PUT\",\n// \t\tBackend: []*config.Backend{\n// \t\t\t{\n// \t\t\t\tMethod: \"POST\",\n// \t\t\t},\n// \t\t\t{\n// \t\t\t\tMethod: \"PUT\",\n// \t\t\t},\n// \t\t},\n// \t\tExtraConfig: map[string]interface{}{\n// \t\t\tproxy.Namespace: map[string]interface{}{\n// \t\t\t\t\"sequential\": true,\n// \t\t\t},\n// \t\t},\n// \t}\n\n// \tsuccess := IsValidSequentialEndpoint(endpoint)\n\n// \tif success {\n// \t\tt.Error(\"Endpoint expected invalid but receive valid\")\n// \t}\n// }\n"
  },
  {
    "path": "router/httptreemux/router.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\n\tPackage httptreemux provides some basic implementations for building routers based on dimfeld/httptreemux\n*/\npackage httptreemux\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/dimfeld/httptreemux/v5\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/router\"\n\t\"github.com/luraproject/lura/v2/router/mux\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n)\n\n// DefaultFactory returns a net/http mux router factory with the injected proxy factory and logger\nfunc DefaultFactory(pf proxy.Factory, logger logging.Logger) router.Factory {\n\treturn mux.NewFactory(DefaultConfig(pf, logger))\n}\n\n// DefaultConfig returns the struct that collects the parts the router should be built from\nfunc DefaultConfig(pf proxy.Factory, logger logging.Logger) mux.Config {\n\treturn mux.Config{\n\t\tEngine:         NewEngine(httptreemux.NewContextMux()),\n\t\tMiddlewares:    []mux.HandlerMiddleware{},\n\t\tHandlerFactory: mux.CustomEndpointHandler(mux.NewRequestBuilder(ParamsExtractor)),\n\t\tProxyFactory:   pf,\n\t\tLogger:         logger,\n\t\tDebugPattern:   \"/__debug/{params}\",\n\t\tRunServer:      server.RunServer,\n\t}\n}\n\nfunc ParamsExtractor(r *http.Request) map[string]string {\n\tparams := map[string]string{}\n\ttitle := cases.Title(language.Und)\n\tfor key, value := range httptreemux.ContextParams(r.Context()) {\n\t\tparams[title.String(key)] = value\n\t}\n\treturn params\n}\n\nfunc NewEngine(m *httptreemux.ContextMux) Engine {\n\treturn Engine{m}\n}\n\ntype Engine struct {\n\tr *httptreemux.ContextMux\n}\n\n// Handle implements the mux.Engine interface from the lura router package\nfunc (g Engine) Handle(pattern, method string, handler http.Handler) {\n\tg.r.Handle(method, pattern, handler.(http.HandlerFunc))\n}\n\n// ServeHTTP implements the http:Handler interface from the stdlib\nfunc (g Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tg.r.ServeHTTP(mux.NewHTTPErrorInterceptor(w), r)\n}\n"
  },
  {
    "path": "router/httptreemux/router_test.go",
    "content": "//go:build !race\n// +build !race\n\n// SPDX-License-Identifier: Apache-2.0\n\npackage httptreemux\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\nfunc TestDefaultFactory_ok(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"}), logger).NewWithContext(ctx)\n\texpectedBody := \"{\\\"supu\\\":\\\"tupu\\\"}\"\n\n\tserviceCfg := config.ServiceConfig{\n\t\tPort: 8082,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/get/:id\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/post\",\n\t\t\t\tMethod:   \"POST\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/put\",\n\t\t\t\tMethod:   \"PUT\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/patch\",\n\t\t\t\tMethod:   \"PATCH\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/delete\",\n\t\t\t\tMethod:   \"DELETE\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\t<-time.After(5 * time.Millisecond)\n\n\tfor _, endpoint := range serviceCfg.Endpoints {\n\t\treq, _ := http.NewRequest(endpoint.Method, fmt.Sprintf(\"http://127.0.0.1:8082%s\", endpoint.Endpoint), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tresp, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\tt.Error(\"Making the request:\", err.Error())\n\t\t\treturn\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(resp.Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\t\treturn\n\t\t}\n\t\tcontent := string(body)\n\t\tif resp.Header.Get(\"Cache-Control\") != \"\" {\n\t\t\tt.Error(endpoint.Endpoint, \"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t\t}\n\t\tif resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderCompleteResponseValue {\n\t\t\tt.Error(server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t\t}\n\t\tif resp.Header.Get(\"Content-Type\") != \"application/json\" {\n\t\t\tt.Error(endpoint.Endpoint, \"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif resp.Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(endpoint.Endpoint, \"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Error(endpoint.Endpoint, \"Unexpected status code:\", resp.StatusCode)\n\t\t}\n\t\tif content != expectedBody {\n\t\t\tt.Error(endpoint.Endpoint, \"Unexpected body:\", content, \"expected:\", expectedBody)\n\t\t}\n\t}\n}\n\nfunc TestDefaultFactory_ko(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"}), logger).NewWithContext(ctx)\n\n\tserviceCfg := config.ServiceConfig{\n\t\tDebug: true,\n\t\tPort:  8083,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/ignored\",\n\t\t\t\tMethod:   \"GETTT\",\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/empty\",\n\t\t\t\tMethod:   \"GETTT\",\n\t\t\t\tBackend:  []*config.Backend{},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/also-ignored\",\n\t\t\t\tMethod:   \"PUT\",\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, subject := range [][]string{\n\t\t{\"GET\", \"ignored\"},\n\t\t{\"GET\", \"empty\"},\n\t\t{\"PUT\", \"also-ignored\"},\n\t} {\n\t\treq, _ := http.NewRequest(subject[0], fmt.Sprintf(\"http://127.0.0.1:8083/%s\", subject[1]), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tcheckResponseIs404(t, req)\n\t}\n}\n\nfunc TestDefaultFactory_proxyFactoryCrash(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(erroredProxyFactory{fmt.Errorf(\"%s\", \"crash!!!\")}, logger).NewWithContext(ctx)\n\n\tserviceCfg := config.ServiceConfig{\n\t\tDebug: true,\n\t\tPort:  8084,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/ignored\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, subject := range [][]string{{\"GET\", \"ignored\"}, {\"PUT\", \"also-ignored\"}} {\n\t\treq, _ := http.NewRequest(subject[0], fmt.Sprintf(\"http://127.0.0.1:8084/%s\", subject[1]), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tcheckResponseIs404(t, req)\n\t}\n}\n\nfunc checkResponseIs404(t *testing.T, req *http.Request) {\n\texpectedBody := \"404 page not found\\n\"\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tt.Error(\"Making the request:\", err.Error())\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\tbody, ioerr := io.ReadAll(resp.Body)\n\tif ioerr != nil {\n\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\treturn\n\t}\n\tcontent := string(body)\n\tif resp.Header.Get(\"Cache-Control\") != \"\" {\n\t\tt.Error(\"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t}\n\tif resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderIncompleteResponseValue {\n\t\tt.Error(req.URL.String(), server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t}\n\tif resp.Header.Get(\"Content-Type\") != \"text/plain; charset=utf-8\" {\n\t\tt.Error(\"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t}\n\tif resp.Header.Get(\"X-Krakend\") != \"\" {\n\t\tt.Error(\"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t}\n\tif resp.StatusCode != http.StatusNotFound {\n\t\tt.Error(\"Unexpected status code:\", resp.StatusCode)\n\t}\n\tif content != expectedBody {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", expectedBody)\n\t}\n}\n\ntype noopProxyFactory map[string]interface{}\n\nfunc (n noopProxyFactory) New(_ *config.EndpointConfig) (proxy.Proxy, error) {\n\treturn func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       n,\n\t\t}, nil\n\t}, nil\n}\n\ntype erroredProxyFactory struct {\n\tError error\n}\n\nfunc (e erroredProxyFactory) New(_ *config.EndpointConfig) (proxy.Proxy, error) {\n\treturn proxy.NoopProxy, e.Error\n}\n\ntype identityMiddleware struct{}\n\nfunc (identityMiddleware) Handler(h http.Handler) http.Handler {\n\treturn h\n}\n"
  },
  {
    "path": "router/mux/debug.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage mux\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\n// DebugHandler creates a dummy handler function, useful for quick integration tests\nfunc DebugHandler(logger logging.Logger) http.HandlerFunc {\n\tlogPrefixSecondary := \"[ENDPOINT /__debug/*]\"\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tlogger.Debug(logPrefixSecondary, \"Method:\", r.Method)\n\t\tlogger.Debug(logPrefixSecondary, \"URL:\", r.RequestURI)\n\t\tlogger.Debug(logPrefixSecondary, \"Query:\", r.URL.Query())\n\t\t// logger.Debug(logPrefixSecondary, \"Params:\", c.Params)\n\t\tlogger.Debug(logPrefixSecondary, \"Headers:\", r.Header)\n\t\tbody, _ := io.ReadAll(r.Body)\n\t\tr.Body.Close()\n\t\tlogger.Debug(logPrefixSecondary, \"Body:\", string(body))\n\n\t\tjs, _ := json.Marshal(map[string]string{\"message\": \"pong\"})\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.Write(js)\n\t}\n}\n"
  },
  {
    "path": "router/mux/debug_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage mux\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nfunc TestDebugHandler(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\thandler := DebugHandler(logger)\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8089/_mux_debug?b=1\", io.NopCloser(&bytes.Buffer{}))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tw := httptest.NewRecorder()\n\n\thandler.ServeHTTP(w, req)\n\n\tbody, ioerr := io.ReadAll(w.Result().Body)\n\tif ioerr != nil {\n\t\tt.Error(\"reading a response:\", err.Error())\n\t\treturn\n\t}\n\tw.Result().Body.Close()\n\n\texpectedBody := \"{\\\"message\\\":\\\"pong\\\"}\"\n\n\tcontent := string(body)\n\tif w.Result().Header.Get(\"Cache-Control\") != \"\" {\n\t\tt.Error(\"Cache-Control error:\", w.Result().Header.Get(\"Cache-Control\"))\n\t}\n\tif w.Result().Header.Get(\"Content-Type\") != \"application/json\" {\n\t\tt.Error(\"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t}\n\tif w.Result().Header.Get(\"X-Krakend\") != \"\" {\n\t\tt.Error(\"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t}\n\tif w.Result().StatusCode != http.StatusOK {\n\t\tt.Error(\"Unexpected status code:\", w.Result().StatusCode)\n\t}\n\tif content != expectedBody {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", expectedBody)\n\t}\n}\n"
  },
  {
    "path": "router/mux/echo.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage mux\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n)\n\ntype echoResponse struct {\n\tUri         string              `json:\"req_uri\"`\n\tUriDetails  map[string]string   `json:\"req_uri_details\"`\n\tMethod      string              `json:\"req_method\"`\n\tQuerystring map[string][]string `json:\"req_querystring\"`\n\tBody        string              `json:\"req_body\"`\n\tHeaders     map[string][]string `json:\"req_headers\"`\n}\n\n// EchoHandler creates a dummy handler function, useful for quick integration tests\nfunc EchoHandler() http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tvar body string\n\t\tif r.Body != nil {\n\t\t\ttmp, _ := io.ReadAll(r.Body)\n\t\t\tr.Body.Close()\n\t\t\tbody = string(tmp)\n\t\t}\n\t\tresp, err := json.Marshal(echoResponse{\n\t\t\tUri: r.RequestURI,\n\t\t\tUriDetails: map[string]string{\n\t\t\t\t\"user\":     r.URL.User.String(),\n\t\t\t\t\"host\":     r.Host,\n\t\t\t\t\"path\":     r.URL.Path,\n\t\t\t\t\"query\":    r.URL.Query().Encode(),\n\t\t\t\t\"fragment\": r.URL.Fragment,\n\t\t\t},\n\t\t\tMethod:      r.Method,\n\t\t\tQuerystring: r.URL.Query(),\n\t\t\tBody:        body,\n\t\t\tHeaders:     r.Header,\n\t\t})\n\t\tif err != nil {\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.Write(resp)\n\t}\n}\n"
  },
  {
    "path": "router/mux/echo_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage mux\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestEchoHandlerNew(t *testing.T) {\n\treqBody := `{\"message\":\"some body to send\"}`\n\texpectedRespBody := `{\"req_uri\":\"http://127.0.0.1:8088/_gin_endpoint/a?b=1\",\"req_uri_details\":{\"fragment\":\"\",\"host\":\"127.0.0.1:8088\",\"path\":\"/_gin_endpoint/a\",\"query\":\"b=1\",\"user\":\"\"},\"req_method\":\"GET\",\"req_querystring\":{\"b\":[\"1\"]},\"req_body\":\"{\\\"message\\\":\\\"some body to send\\\"}\",\"req_headers\":{\"Content-Type\":[\"application/json\"]}}`\n\texpectedRespNoBody := `{\"req_uri\":\"http://127.0.0.1:8088/_gin_endpoint/a?b=1\",\"req_uri_details\":{\"fragment\":\"\",\"host\":\"127.0.0.1:8088\",\"path\":\"/_gin_endpoint/a\",\"query\":\"b=1\",\"user\":\"\"},\"req_method\":\"GET\",\"req_querystring\":{\"b\":[\"1\"]},\"req_body\":\"\",\"req_headers\":{\"Content-Type\":[\"application/json\"]}}`\n\texpectedRespString := `{\"req_uri\":\"http://127.0.0.1:8088/_gin_endpoint/a?b=1\",\"req_uri_details\":{\"fragment\":\"\",\"host\":\"127.0.0.1:8088\",\"path\":\"/_gin_endpoint/a\",\"query\":\"b=1\",\"user\":\"\"},\"req_method\":\"GET\",\"req_querystring\":{\"b\":[\"1\"]},\"req_body\":\"Hello lura\",\"req_headers\":{\"Content-Type\":[\"application/json\"]}}`\n\te := EchoHandler()\n\n\tfor _, tc := range []struct {\n\t\tname string\n\t\tbody io.Reader\n\t\tresp string\n\t}{\n\t\t{\n\t\t\tname: \"json body\",\n\t\t\tbody: strings.NewReader(reqBody),\n\t\t\tresp: expectedRespBody,\n\t\t},\n\t\t{\n\t\t\tname: \"no body\",\n\t\t\tbody: http.NoBody,\n\t\t\tresp: expectedRespNoBody,\n\t\t},\n\t\t{\n\t\t\tname: \"string body\",\n\t\t\tbody: strings.NewReader(\"Hello lura\"),\n\t\t\tresp: expectedRespString,\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\techoRunTestRequest(t, e, tc.body, tc.resp)\n\t\t})\n\t}\n\n}\n\nfunc echoRunTestRequest(t *testing.T, e http.HandlerFunc, body io.Reader, expected string) {\n\treq := httptest.NewRequest(\"GET\", \"http://127.0.0.1:8088/_gin_endpoint/a?b=1\", body)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tw := httptest.NewRecorder()\n\n\te.ServeHTTP(w, req)\n\n\trespBody, ioerr := io.ReadAll(w.Result().Body)\n\tif ioerr != nil {\n\t\tt.Error(\"reading a response:\", ioerr.Error())\n\t\treturn\n\t}\n\tw.Result().Body.Close()\n\n\tcontent := string(respBody)\n\tif w.Result().Header.Get(\"Cache-Control\") != \"\" {\n\t\tt.Error(\"Cache-Control error:\", w.Result().Header.Get(\"Cache-Control\"))\n\t}\n\tif w.Result().Header.Get(\"Content-Type\") != \"application/json\" {\n\t\tt.Error(\"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t}\n\tif w.Result().Header.Get(\"X-Krakend\") != \"\" {\n\t\tt.Error(\"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t}\n\tif w.Result().StatusCode != http.StatusOK {\n\t\tt.Error(\"Unexpected status code:\", w.Result().StatusCode)\n\t}\n\tif content != expected {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", expected)\n\t}\n}\n"
  },
  {
    "path": "router/mux/endpoint.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage mux\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"strings\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/core\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\nconst requestParamsAsterisk string = \"*\"\n\n// HandlerFactory creates a handler function that adapts the mux router with the injected proxy\ntype HandlerFactory func(*config.EndpointConfig, proxy.Proxy) http.HandlerFunc\n\n// EndpointHandler is a HandlerFactory that adapts the mux router with the injected proxy\n// and the default RequestBuilder\nvar EndpointHandler = CustomEndpointHandler(NewRequest)\n\n// CustomEndpointHandler returns a HandlerFactory with the received RequestBuilder using the default ToHTTPError function\nfunc CustomEndpointHandler(rb RequestBuilder) HandlerFactory {\n\treturn CustomEndpointHandlerWithHTTPError(rb, server.DefaultToHTTPError)\n}\n\n// CustomEndpointHandlerWithHTTPError returns a HandlerFactory with the received RequestBuilder\nfunc CustomEndpointHandlerWithHTTPError(rb RequestBuilder, errF server.ToHTTPError) HandlerFactory {\n\treturn func(configuration *config.EndpointConfig, prxy proxy.Proxy) http.HandlerFunc {\n\t\tcacheControlHeaderValue := fmt.Sprintf(\"public, max-age=%d\", int(configuration.CacheTTL.Seconds()))\n\t\tisCacheEnabled := configuration.CacheTTL.Seconds() != 0\n\t\trender := getRender(configuration)\n\n\t\theadersToSend := configuration.HeadersToPass\n\t\tif len(headersToSend) == 0 {\n\t\t\theadersToSend = server.HeadersToSend\n\t\t}\n\t\tmethod := strings.ToTitle(configuration.Method)\n\n\t\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(core.KrakendHeaderName, core.KrakendHeaderValue)\n\t\t\tif r.Method != method {\n\t\t\t\tw.Header().Set(server.CompleteResponseHeaderName, server.HeaderIncompleteResponseValue)\n\t\t\t\thttp.Error(w, \"\", http.StatusMethodNotAllowed)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequestCtx, cancel := context.WithTimeout(r.Context(), configuration.Timeout)\n\n\t\t\tresponse, err := prxy(requestCtx, rb(r, configuration.QueryString, headersToSend))\n\n\t\t\tselect {\n\t\t\tcase <-requestCtx.Done():\n\t\t\t\tif err == nil {\n\t\t\t\t\terr = server.ErrInternalError\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tif response != nil && len(response.Data) > 0 {\n\t\t\t\tif response.IsComplete {\n\t\t\t\t\tw.Header().Set(server.CompleteResponseHeaderName, server.HeaderCompleteResponseValue)\n\t\t\t\t\tif isCacheEnabled {\n\t\t\t\t\t\tw.Header().Set(\"Cache-Control\", cacheControlHeaderValue)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tw.Header().Set(server.CompleteResponseHeaderName, server.HeaderIncompleteResponseValue)\n\t\t\t\t}\n\n\t\t\t\tfor k, vs := range response.Metadata.Headers {\n\t\t\t\t\tfor _, v := range vs {\n\t\t\t\t\t\tw.Header().Add(k, v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tw.Header().Set(server.CompleteResponseHeaderName, server.HeaderIncompleteResponseValue)\n\t\t\t\tif err != nil {\n\t\t\t\t\tif t, ok := err.(responseError); ok {\n\t\t\t\t\t\thttp.Error(w, err.Error(), t.StatusCode())\n\t\t\t\t\t} else {\n\t\t\t\t\t\thttp.Error(w, err.Error(), errF(err))\n\t\t\t\t\t}\n\t\t\t\t\tcancel()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trender(w, response)\n\t\t\tcancel()\n\t\t}\n\t}\n}\n\n// RequestBuilder is a function that creates a proxy.Request from the received http request\ntype RequestBuilder func(r *http.Request, queryString, headersToSend []string) *proxy.Request\n\n// ParamExtractor is a function that extracts query params from the requested uri\ntype ParamExtractor func(r *http.Request) map[string]string\n\n// NoopParamExtractor is a No Op ParamExtractor (returns an empty map of params)\nfunc NoopParamExtractor(_ *http.Request) map[string]string { return map[string]string{} }\n\n// NewRequest is a RequestBuilder that creates a proxy request from the received http request without\n// processing the uri params\nvar NewRequest = NewRequestBuilder(NoopParamExtractor)\n\n// NewRequestBuilder gets a RequestBuilder with the received ParamExtractor as a query param\n// extraction mechanism\nfunc NewRequestBuilder(paramExtractor ParamExtractor) RequestBuilder {\n\treturn func(r *http.Request, queryString, headersToSend []string) *proxy.Request {\n\t\tparams := paramExtractor(r)\n\t\theaders := make(map[string][]string, 3+len(headersToSend))\n\n\t\tfor _, k := range headersToSend {\n\t\t\tif k == requestParamsAsterisk {\n\t\t\t\theaders = r.Header\n\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif h, ok := r.Header[textproto.CanonicalMIMEHeaderKey(k)]; ok {\n\t\t\t\theaders[k] = h\n\t\t\t}\n\t\t}\n\n\t\theaders[\"X-Forwarded-For\"] = []string{clientIP(r)}\n\t\theaders[\"X-Forwarded-Host\"] = []string{r.Host}\n\t\t// if User-Agent is not forwarded using headersToSend, we set\n\t\t// the KrakenD router User Agent value\n\t\tif _, ok := headers[\"User-Agent\"]; !ok {\n\t\t\theaders[\"User-Agent\"] = server.UserAgentHeaderValue\n\t\t} else {\n\t\t\theaders[\"X-Forwarded-Via\"] = server.UserAgentHeaderValue\n\t\t}\n\n\t\tquery := make(map[string][]string, len(queryString))\n\t\tqueryValues := r.URL.Query()\n\t\tfor i := range queryString {\n\t\t\tif queryString[i] == requestParamsAsterisk {\n\t\t\t\tquery = queryValues\n\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif v, ok := queryValues[queryString[i]]; ok && len(v) > 0 {\n\t\t\t\tquery[queryString[i]] = v\n\t\t\t}\n\t\t}\n\n\t\treturn &proxy.Request{\n\t\t\tPath:    r.URL.Path,\n\t\t\tMethod:  r.Method,\n\t\t\tQuery:   query,\n\t\t\tBody:    r.Body,\n\t\t\tParams:  params,\n\t\t\tHeaders: headers,\n\t\t}\n\t}\n}\n\ntype responseError interface {\n\terror\n\tStatusCode() int\n}\n\n// clientIP implements a best effort algorithm to return the real client IP, it parses\n// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.\n// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.\nfunc clientIP(r *http.Request) string {\n\tclientIP := r.Header.Get(\"X-Forwarded-For\")\n\tclientIP = strings.TrimSpace(strings.Split(clientIP, \",\")[0])\n\tif clientIP == \"\" {\n\t\tclientIP = strings.TrimSpace(r.Header.Get(\"X-Real-Ip\"))\n\t}\n\tif clientIP != \"\" {\n\t\treturn clientIP\n\t}\n\n\tif addr := r.Header.Get(\"X-Appengine-Remote-Addr\"); addr != \"\" {\n\t\treturn addr\n\t}\n\n\tif ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil {\n\t\treturn ip\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "router/mux/endpoint_benchmark_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage mux\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n)\n\nfunc BenchmarkEndpointHandler_ko(b *testing.B) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, fmt.Errorf(\"This is %s\", \"a dummy error\")\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:     time.Second,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\"},\n\t}\n\n\trouter := http.NewServeMux()\n\trouter.Handle(\"/_gin_endpoint/\", EndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", http.NoBody)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tb.ReportAllocs()\n\tfor i := 0; i < b.N; i++ {\n\t\tw := httptest.NewRecorder()\n\t\trouter.ServeHTTP(w, req)\n\t}\n}\n\nfunc BenchmarkEndpointHandler_ok(b *testing.B) {\n\tpResp := proxy.Response{\n\t\tData:       map[string]interface{}{},\n\t\tIo:         io.NopCloser(&bytes.Buffer{}),\n\t\tIsComplete: true,\n\t\tMetadata:   proxy.Metadata{},\n\t}\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &pResp, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:     time.Second,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\"},\n\t}\n\n\trouter := http.NewServeMux()\n\trouter.Handle(\"/_gin_endpoint/\", EndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", http.NoBody)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tb.ReportAllocs()\n\tfor i := 0; i < b.N; i++ {\n\t\tw := httptest.NewRecorder()\n\t\trouter.ServeHTTP(w, req)\n\t}\n}\n\nfunc BenchmarkEndpointHandler_ko_Parallel(b *testing.B) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, fmt.Errorf(\"This is %s\", \"a dummy error\")\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:     time.Second,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\"},\n\t}\n\n\trouter := http.NewServeMux()\n\trouter.Handle(\"/_gin_endpoint/\", EndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", http.NoBody)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tw := httptest.NewRecorder()\n\t\t\trouter.ServeHTTP(w, req)\n\t\t}\n\t})\n}\n\nfunc BenchmarkEndpointHandler_ok_Parallel(b *testing.B) {\n\tpResp := proxy.Response{\n\t\tData:       map[string]interface{}{},\n\t\tIo:         io.NopCloser(&bytes.Buffer{}),\n\t\tIsComplete: true,\n\t\tMetadata:   proxy.Metadata{},\n\t}\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &pResp, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:     time.Second,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\"},\n\t}\n\n\trouter := http.NewServeMux()\n\trouter.Handle(\"/_gin_endpoint/\", EndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_gin_endpoint/a?b=1\", http.NoBody)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tw := httptest.NewRecorder()\n\t\t\trouter.ServeHTTP(w, req)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "router/mux/endpoint_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage mux\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\nfunc TestEndpointHandler_ok(t *testing.T) {\n\tp := func(_ context.Context, req *proxy.Request) (*proxy.Response, error) {\n\t\tdata, _ := json.Marshal(req.Query)\n\t\tif string(data) != `{\"b\":[\"1\"],\"c[]\":[\"x\",\"y\"],\"d\":[\"1\",\"2\"]}` {\n\t\t\tt.Errorf(\"unexpected querystring: %s\", data)\n\t\t}\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       map[string]interface{}{\"supu\": \"tupu\"},\n\t\t}, nil\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{\\\"supu\\\":\\\"tupu\\\"}\",\n\t\texpectedCache:      \"public, max-age=21600\",\n\t\texpectedContent:    \"application/json\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          true,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_okAllParams(t *testing.T) {\n\tp := func(_ context.Context, req *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       map[string]interface{}{\"query\": req.Query, \"headers\": req.Headers, \"params\": req.Params},\n\t\t\tMetadata: proxy.Metadata{\n\t\t\t\tHeaders:    map[string][]string{\"X-YZ\": {\"something\"}},\n\t\t\t\tStatusCode: 200,\n\t\t\t},\n\t\t}, nil\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       `{\"headers\":{\"Content-Type\":[\"application/json\"],\"User-Agent\":[\"KrakenD Version undefined\"],\"X-Forwarded-For\":[\"\"],\"X-Forwarded-Host\":[\"127.0.0.1:8081\"]},\"params\":{},\"query\":{\"a\":[\"42\"],\"b\":[\"1\"],\"c[]\":[\"x\",\"y\"],\"d\":[\"1\",\"2\"]}}`,\n\t\texpectedCache:      \"public, max-age=21600\",\n\t\texpectedContent:    \"application/json\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          true,\n\t\tqueryString:        []string{\"*\"},\n\t\theaders:            []string{\"*\"},\n\t\texpectedHeaders:    map[string][]string{\"X-YZ\": {\"something\"}},\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_incomplete(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: false,\n\t\t\tData:       map[string]interface{}{\"foo\": \"bar\"},\n\t\t}, nil\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{\\\"foo\\\":\\\"bar\\\"}\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"application/json\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_ko(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, fmt.Errorf(\"This is %s\", \"a dummy error\")\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"This is a dummy error\\n\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"text/plain; charset=utf-8\",\n\t\texpectedStatusCode: http.StatusInternalServerError,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_incompleteAndErrored(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: false,\n\t\t\tData:       map[string]interface{}{\"foo\": \"bar\"},\n\t\t}, errors.New(\"This is a dummy error\")\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{\\\"foo\\\":\\\"bar\\\"}\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"application/json\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_cancel(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: false,\n\t\t\tData:       map[string]interface{}{\"foo\": \"bar\"},\n\t\t}, nil\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            0,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{\\\"foo\\\":\\\"bar\\\"}\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"application/json\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_cancelEmpty(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\treturn nil, nil\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            0,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       server.ErrInternalError.Error() + \"\\n\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"text/plain; charset=utf-8\",\n\t\texpectedStatusCode: http.StatusInternalServerError,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_noop(t *testing.T) {\n\tendpointHandlerTestCase{\n\t\ttimeout:            time.Minute,\n\t\tproxy:              proxy.NoopProxy,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"{}\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"application/json\",\n\t\texpectedStatusCode: http.StatusOK,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_badMethod(t *testing.T) {\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              proxy.NoopProxy,\n\t\tmethod:             \"PUT\",\n\t\texpectedBody:       \"\\n\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"text/plain; charset=utf-8\",\n\t\texpectedStatusCode: http.StatusMethodNotAllowed,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestEndpointHandler_errored_responseError(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, dummyResponseError{err: \"this is a dummy error\", status: http.StatusTeapot}\n\t}\n\tendpointHandlerTestCase{\n\t\ttimeout:            10,\n\t\tproxy:              p,\n\t\tmethod:             \"GET\",\n\t\texpectedBody:       \"this is a dummy error\\n\",\n\t\texpectedCache:      \"\",\n\t\texpectedContent:    \"text/plain; charset=utf-8\",\n\t\texpectedStatusCode: http.StatusTeapot,\n\t\tcompleted:          false,\n\t}.test(t)\n\ttime.Sleep(5 * time.Millisecond)\n}\n\ntype dummyResponseError struct {\n\terr    string\n\tstatus int\n}\n\nfunc (d dummyResponseError) Error() string {\n\treturn d.err\n}\n\nfunc (d dummyResponseError) StatusCode() int {\n\treturn d.status\n}\n\ntype endpointHandlerTestCase struct {\n\ttimeout            time.Duration\n\tproxy              proxy.Proxy\n\tmethod             string\n\texpectedBody       string\n\texpectedCache      string\n\texpectedContent    string\n\texpectedHeaders    map[string][]string\n\texpectedStatusCode int\n\tcompleted          bool\n\tqueryString        []string\n\theaders            []string\n}\n\nfunc (tc endpointHandlerTestCase) test(t *testing.T) {\n\tendpoint := &config.EndpointConfig{\n\t\tMethod:      \"GET\",\n\t\tTimeout:     tc.timeout,\n\t\tCacheTTL:    6 * time.Hour,\n\t\tQueryString: []string{\"b\", \"c[]\", \"d\"},\n\t}\n\tif len(tc.queryString) > 0 {\n\t\tendpoint.QueryString = tc.queryString\n\t}\n\tif len(tc.headers) > 0 {\n\t\tendpoint.HeadersToPass = tc.headers\n\t}\n\n\ts := startMuxServer(EndpointHandler(endpoint, tc.proxy))\n\n\treq, _ := http.NewRequest(tc.method, \"http://127.0.0.1:8081/_mux_endpoint?b=1&c[]=x&c[]=y&d=1&d=2&a=42\", io.NopCloser(&bytes.Buffer{}))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tw := httptest.NewRecorder()\n\ts.ServeHTTP(w, req)\n\n\tbody, ioerr := io.ReadAll(w.Result().Body)\n\tif ioerr != nil {\n\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\treturn\n\t}\n\tw.Result().Body.Close()\n\tcontent := string(body)\n\tresp := w.Result()\n\tif resp.Header.Get(\"Cache-Control\") != tc.expectedCache {\n\t\tt.Error(\"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t}\n\tif tc.completed && resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderCompleteResponseValue {\n\t\tt.Error(server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t}\n\tif !tc.completed && resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderIncompleteResponseValue {\n\t\tt.Error(server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t}\n\tif resp.Header.Get(\"Content-Type\") != tc.expectedContent {\n\t\tt.Error(\"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t}\n\tif resp.Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\tt.Error(\"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t}\n\tif resp.StatusCode != tc.expectedStatusCode {\n\t\tt.Error(\"Unexpected status code:\", resp.StatusCode)\n\t}\n\tif content != tc.expectedBody {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", tc.expectedBody)\n\t}\n\tfor k, v := range tc.expectedHeaders {\n\t\tif header := resp.Header.Get(k); v[0] != header {\n\t\t\tt.Error(\"Unexpected value for header:\", k, header, \"expected:\", v[0])\n\t\t}\n\t}\n}\n\nfunc startMuxServer(handlerFunc http.HandlerFunc) *http.ServeMux {\n\trouter := http.NewServeMux()\n\trouter.Handle(\"/_mux_endpoint\", handlerFunc)\n\treturn router\n}\n"
  },
  {
    "path": "router/mux/engine.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage mux\n\nimport (\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\n// Engine defines the minimun required interface for the mux compatible engine\ntype Engine interface {\n\thttp.Handler\n\tHandle(pattern, method string, handler http.Handler)\n}\n\n// BasicEngine is a slightly customized http.ServeMux router\ntype BasicEngine struct {\n\thandler *http.ServeMux\n\tdict    map[string]map[string]http.HandlerFunc\n}\n\n// NewHTTPErrorInterceptor returns a HTTPErrorInterceptor over the injected response writer\nfunc NewHTTPErrorInterceptor(w http.ResponseWriter) *HTTPErrorInterceptor {\n\treturn &HTTPErrorInterceptor{w, new(sync.Once)}\n}\n\n// HTTPErrorInterceptor is a reposnse writer that adds a header signaling incomplete response in case of\n// seeing a status code not equal to 200\ntype HTTPErrorInterceptor struct {\n\thttp.ResponseWriter\n\tonce *sync.Once\n}\n\n// WriteHeader records the status code and adds a header signaling incomplete responses\nfunc (i *HTTPErrorInterceptor) WriteHeader(code int) {\n\ti.once.Do(func() {\n\t\tif code != http.StatusOK {\n\t\t\ti.ResponseWriter.Header().Set(server.CompleteResponseHeaderName, server.HeaderIncompleteResponseValue)\n\t\t}\n\t})\n\ti.ResponseWriter.WriteHeader(code)\n}\n\n// DefaultEngine returns a new engine using BasicEngine\nfunc DefaultEngine() *BasicEngine {\n\treturn &BasicEngine{\n\t\thandler: http.NewServeMux(),\n\t\tdict:    map[string]map[string]http.HandlerFunc{},\n\t}\n}\n\n// Handle registers a handler at a given url pattern and http method\nfunc (e *BasicEngine) Handle(pattern, method string, handler http.Handler) {\n\tif _, ok := e.dict[pattern]; !ok {\n\t\te.dict[pattern] = map[string]http.HandlerFunc{}\n\t\te.handler.Handle(pattern, e.registrableHandler(pattern))\n\t}\n\te.dict[pattern][method] = handler.ServeHTTP\n}\n\n// ServeHTTP adds a error interceptor and delegates the request dispatching to the\n// internal request multiplexer.\nfunc (e *BasicEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\te.handler.ServeHTTP(NewHTTPErrorInterceptor(w), r)\n}\n\nfunc (e *BasicEngine) registrableHandler(pattern string) http.Handler {\n\treturn http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\tif handler, ok := e.dict[pattern][req.Method]; ok {\n\t\t\thandler(rw, req)\n\t\t\treturn\n\t\t}\n\n\t\trw.Header().Set(server.CompleteResponseHeaderName, server.HeaderIncompleteResponseValue)\n\t\thttp.Error(rw, \"\", http.StatusMethodNotAllowed)\n\t})\n}\n"
  },
  {
    "path": "router/mux/engine_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage mux\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestEngine(t *testing.T) {\n\te := DefaultEngine()\n\n\tfor _, method := range []string{\"PUT\", \"POST\", \"DELETE\"} {\n\t\te.Handle(\"/\", method, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\thttp.Error(rw, \"hi there!\", http.StatusTeapot)\n\t\t}))\n\t}\n\n\tfor _, tc := range []struct {\n\t\tmethod string\n\t\tstatus int\n\t}{\n\t\t{status: http.StatusTeapot, method: \"PUT\"},\n\t\t{status: http.StatusTeapot, method: \"POST\"},\n\t\t{status: http.StatusTeapot, method: \"DELETE\"},\n\t\t{status: http.StatusMethodNotAllowed, method: \"GET\"},\n\t} {\n\t\treq, _ := http.NewRequest(tc.method, \"http://127.0.0.1:8081/_mux_endpoint?b=1&c[]=x&c[]=y&d=1&d=2&a=42\", io.NopCloser(&bytes.Buffer{}))\n\n\t\tw := httptest.NewRecorder()\n\t\te.ServeHTTP(w, req)\n\n\t\tif sc := w.Result().StatusCode; tc.status != sc {\n\t\t\tt.Error(\"unexpected status code:\", sc)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "router/mux/render.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage mux\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/encoding\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n)\n\n// Render defines the signature of the functions to be use for the final response\n// encoding and rendering\ntype Render func(http.ResponseWriter, *proxy.Response)\n\n// NEGOTIATE defines the value of the OutputEncoding for the negotiated render\nconst NEGOTIATE = \"negotiate\"\n\nvar (\n\tmutex          = &sync.RWMutex{}\n\trenderRegister = map[string]Render{\n\t\tencoding.STRING:   stringRender,\n\t\tencoding.JSON:     jsonRender,\n\t\tencoding.NOOP:     noopRender,\n\t\t\"json-collection\": jsonCollectionRender,\n\t}\n)\n\n// RegisterRender allows clients to register their custom renders\nfunc RegisterRender(name string, r Render) {\n\tmutex.Lock()\n\trenderRegister[name] = r\n\tmutex.Unlock()\n}\n\nfunc getRender(cfg *config.EndpointConfig) Render {\n\tfallback := jsonRender\n\tif len(cfg.Backend) == 1 {\n\t\tfallback = getWithFallback(cfg.Backend[0].Encoding, fallback)\n\t}\n\n\tif cfg.OutputEncoding == \"\" {\n\t\treturn fallback\n\t}\n\n\treturn getWithFallback(cfg.OutputEncoding, fallback)\n}\n\nfunc getWithFallback(key string, fallback Render) Render {\n\tmutex.RLock()\n\tr, ok := renderRegister[key]\n\tmutex.RUnlock()\n\tif !ok {\n\t\treturn fallback\n\t}\n\treturn r\n}\n\nvar (\n\temptyResponse   = []byte(\"{}\")\n\temptyCollection = []byte(\"[]\")\n)\n\nfunc jsonRender(w http.ResponseWriter, response *proxy.Response) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tif response == nil {\n\t\tw.Write(emptyResponse)\n\t\treturn\n\t}\n\n\tjs, err := json.Marshal(response.Data)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tw.Write(js)\n}\n\nfunc jsonCollectionRender(w http.ResponseWriter, response *proxy.Response) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tif response == nil {\n\t\tw.Write(emptyCollection)\n\t\treturn\n\t}\n\tcol, ok := response.Data[\"collection\"]\n\tif !ok {\n\t\tw.Write(emptyCollection)\n\t\treturn\n\t}\n\n\tjs, err := json.Marshal(col)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tw.Write(js)\n}\n\nfunc stringRender(w http.ResponseWriter, response *proxy.Response) {\n\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\tif response == nil {\n\t\tw.Write([]byte{})\n\t\treturn\n\t}\n\td, ok := response.Data[\"content\"]\n\tif !ok {\n\t\tw.Write([]byte{})\n\t\treturn\n\t}\n\tmsg, ok := d.(string)\n\tif !ok {\n\t\tw.Write([]byte{})\n\t\treturn\n\t}\n\tw.Write([]byte(msg))\n}\n\nfunc noopRender(w http.ResponseWriter, response *proxy.Response) {\n\tif response == nil {\n\t\thttp.Error(w, \"\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tfor k, vs := range response.Metadata.Headers {\n\t\tfor _, v := range vs {\n\t\t\tw.Header().Add(k, v)\n\t\t}\n\t}\n\tif response.Metadata.StatusCode != 0 {\n\t\tw.WriteHeader(response.Metadata.StatusCode)\n\t}\n\n\tif response.Io == nil {\n\t\treturn\n\t}\n\tio.Copy(w, response.Io)\n}\n"
  },
  {
    "path": "router/mux/render_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage mux\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/encoding\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n)\n\nfunc TestRender_unknown(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       map[string]interface{}{\"supu\": \"tupu\"},\n\t\t}, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:        time.Second,\n\t\tCacheTTL:       6 * time.Hour,\n\t\tQueryString:    []string{\"b\"},\n\t\tOutputEncoding: \"unknown\",\n\t\tMethod:         \"GET\",\n\t}\n\n\trouter := http.NewServeMux()\n\trouter.Handle(\"/_mux_endpoint\", EndpointHandler(endpoint, p))\n\n\texpectedHeader := \"application/json\"\n\texpectedBody := `{\"supu\":\"tupu\"}`\n\n\tfor _, testData := range [][]string{\n\t\t{\"plain\", \"text/plain\"},\n\t\t{\"none\", \"\"},\n\t\t{\"json\", \"application/json\"},\n\t\t{\"unknown\", \"unknown\"},\n\t} {\n\t\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_mux_endpoint?b=1\", io.NopCloser(&bytes.Buffer{}))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\treq.Header.Set(\"Accept\", testData[1])\n\n\t\tw := httptest.NewRecorder()\n\t\trouter.ServeHTTP(w, req)\n\n\t\tdefer w.Result().Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(w.Result().Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"reading response body:\", ioerr)\n\t\t\treturn\n\t\t}\n\n\t\tcontent := string(body)\n\t\tif w.Result().Header.Get(\"Cache-Control\") != \"public, max-age=21600\" {\n\t\t\tt.Error(testData[0], \"Cache-Control error:\", w.Result().Header.Get(\"Cache-Control\"))\n\t\t}\n\t\tif w.Result().Header.Get(\"Content-Type\") != expectedHeader {\n\t\t\tt.Error(testData[0], \"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(testData[0], \"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif w.Result().StatusCode != http.StatusOK {\n\t\t\tt.Error(testData[0], \"Unexpected status code:\", w.Result().StatusCode)\n\t\t}\n\t\tif content != expectedBody {\n\t\t\tt.Error(testData[0], \"Unexpected body:\", content, \"expected:\", expectedBody)\n\t\t}\n\t}\n}\n\nfunc TestRender_string(t *testing.T) {\n\texpectedContent := \"supu\"\n\texpectedHeader := \"text/plain\"\n\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       map[string]interface{}{\"content\": expectedContent},\n\t\t}, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tTimeout:        time.Second,\n\t\tCacheTTL:       6 * time.Hour,\n\t\tQueryString:    []string{\"b\"},\n\t\tOutputEncoding: encoding.STRING,\n\t\tMethod:         \"GET\",\n\t}\n\n\trouter := http.NewServeMux()\n\trouter.Handle(\"/_mux_endpoint\", EndpointHandler(endpoint, p))\n\n\tfor _, testData := range [][]string{\n\t\t{\"plain\", \"text/plain\"},\n\t\t{\"none\", \"\"},\n\t\t{\"json\", \"application/json\"},\n\t\t{\"unknown\", \"unknown\"},\n\t} {\n\t\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_mux_endpoint?b=1\", io.NopCloser(&bytes.Buffer{}))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\treq.Header.Set(\"Accept\", testData[1])\n\n\t\tw := httptest.NewRecorder()\n\t\trouter.ServeHTTP(w, req)\n\n\t\tdefer w.Result().Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(w.Result().Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"reading response body:\", ioerr)\n\t\t\treturn\n\t\t}\n\n\t\tcontent := string(body)\n\t\tif w.Result().Header.Get(\"Cache-Control\") != \"public, max-age=21600\" {\n\t\t\tt.Error(testData[0], \"Cache-Control error:\", w.Result().Header.Get(\"Cache-Control\"))\n\t\t}\n\t\tif w.Result().Header.Get(\"Content-Type\") != expectedHeader {\n\t\t\tt.Error(testData[0], \"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(testData[0], \"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif w.Result().StatusCode != http.StatusOK {\n\t\t\tt.Error(testData[0], \"Unexpected status code:\", w.Result().StatusCode)\n\t\t}\n\t\tif content != expectedContent {\n\t\t\tt.Error(testData[0], \"Unexpected body:\", content, \"expected:\", expectedContent)\n\t\t}\n\t}\n}\n\nfunc TestRender_string_noData(t *testing.T) {\n\texpectedContent := \"\"\n\texpectedHeader := \"text/plain\"\n\n\tfor k, p := range []proxy.Proxy{\n\t\tfunc(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\t\treturn &proxy.Response{\n\t\t\t\tIsComplete: false,\n\t\t\t\tData:       map[string]interface{}{\"content\": 42},\n\t\t\t}, nil\n\t\t},\n\t\tfunc(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\t\treturn &proxy.Response{\n\t\t\t\tIsComplete: false,\n\t\t\t\tData:       map[string]interface{}{},\n\t\t\t}, nil\n\t\t},\n\t\tfunc(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\t\treturn nil, nil\n\t\t},\n\t} {\n\t\tendpoint := &config.EndpointConfig{\n\t\t\tTimeout:        time.Second,\n\t\t\tCacheTTL:       6 * time.Hour,\n\t\t\tQueryString:    []string{\"b\"},\n\t\t\tOutputEncoding: encoding.STRING,\n\t\t\tMethod:         \"GET\",\n\t\t}\n\n\t\trouter := http.NewServeMux()\n\t\trouter.Handle(\"/_mux_endpoint\", EndpointHandler(endpoint, p))\n\n\t\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_mux_endpoint?b=1\", io.NopCloser(&bytes.Buffer{}))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tw := httptest.NewRecorder()\n\t\trouter.ServeHTTP(w, req)\n\n\t\tdefer w.Result().Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(w.Result().Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"reading response body:\", ioerr)\n\t\t\treturn\n\t\t}\n\n\t\tcontent := string(body)\n\t\tif w.Result().Header.Get(\"Content-Type\") != expectedHeader {\n\t\t\tt.Error(k, \"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(k, \"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif w.Result().StatusCode != http.StatusOK {\n\t\t\tt.Error(k, \"Unexpected status code:\", w.Result().StatusCode)\n\t\t}\n\t\tif content != expectedContent {\n\t\t\tt.Error(k, \"Unexpected body:\", content, \"expected:\", expectedContent)\n\t\t}\n\t}\n}\n\nfunc TestRegisterRender(t *testing.T) {\n\tvar total int\n\texpected := &proxy.Response{IsComplete: true, Data: map[string]interface{}{\"a\": \"b\"}}\n\tname := \"test render\"\n\n\tRegisterRender(name, func(_ http.ResponseWriter, resp *proxy.Response) {\n\t\t*resp = *expected\n\t\ttotal++\n\t})\n\n\tsubject := getRender(&config.EndpointConfig{OutputEncoding: name})\n\n\tw := httptest.NewRecorder()\n\tresp := proxy.Response{}\n\tsubject(w, &resp)\n\n\tif !reflect.DeepEqual(resp, *expected) {\n\t\tt.Error(\"unexpected response\", resp)\n\t}\n\n\tif total != 1 {\n\t\tt.Error(\"the render was called an unexpected amount of times:\", total)\n\t}\n}\n\nfunc TestRender_noop(t *testing.T) {\n\texpectedContent := \"supu\"\n\texpectedHeader := \"text/plain; charset=utf-8\"\n\texpectedSetCookieValue := []string{\"test1=test1\", \"test2=test2\"}\n\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tMetadata: proxy.Metadata{\n\t\t\t\tStatusCode: 200,\n\t\t\t\tHeaders: map[string][]string{\n\t\t\t\t\t\"Content-Type\": {expectedHeader},\n\t\t\t\t\t\"Set-Cookie\":   {\"test1=test1\", \"test2=test2\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tIo: bytes.NewBufferString(expectedContent),\n\t\t}, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tMethod:         \"GET\",\n\t\tTimeout:        time.Second,\n\t\tCacheTTL:       6 * time.Hour,\n\t\tQueryString:    []string{\"b\"},\n\t\tOutputEncoding: encoding.NOOP,\n\t}\n\n\trouter := http.NewServeMux()\n\trouter.Handle(\"/_mux_endpoint\", EndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_mux_endpoint?b=1\", io.NopCloser(&bytes.Buffer{}))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tw := httptest.NewRecorder()\n\trouter.ServeHTTP(w, req)\n\n\tdefer w.Result().Body.Close()\n\n\tbody, ioerr := io.ReadAll(w.Result().Body)\n\tif ioerr != nil {\n\t\tt.Error(\"reading response body:\", ioerr)\n\t\treturn\n\t}\n\n\tcontent := string(body)\n\tif w.Result().Header.Get(\"Content-Type\") != expectedHeader {\n\t\tt.Error(\"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t}\n\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\tt.Error(\"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t}\n\tif w.Result().StatusCode != http.StatusOK {\n\t\tt.Error(\"Unexpected status code:\", w.Result().StatusCode)\n\t}\n\tif content != expectedContent {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", expectedContent)\n\t}\n\tgotCookie := w.Header()[\"Set-Cookie\"]\n\tif !reflect.DeepEqual(gotCookie, expectedSetCookieValue) {\n\t\tt.Error(\"Unexpected Set-Cookie header:\", gotCookie, \"expected:\", expectedSetCookieValue)\n\t}\n}\n\nfunc TestRender_noop_nilBody(t *testing.T) {\n\texpectedContent := \"\"\n\texpectedHeader := \"\"\n\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{IsComplete: true}, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tMethod:         \"GET\",\n\t\tTimeout:        time.Second,\n\t\tCacheTTL:       6 * time.Hour,\n\t\tQueryString:    []string{\"b\"},\n\t\tOutputEncoding: encoding.NOOP,\n\t}\n\n\trouter := http.NewServeMux()\n\trouter.Handle(\"/_mux_endpoint\", EndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_mux_endpoint?b=1\", io.NopCloser(&bytes.Buffer{}))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tw := httptest.NewRecorder()\n\trouter.ServeHTTP(w, req)\n\n\tdefer w.Result().Body.Close()\n\n\tbody, ioerr := io.ReadAll(w.Result().Body)\n\tif ioerr != nil {\n\t\tt.Error(\"reading response body:\", ioerr)\n\t\treturn\n\t}\n\n\tcontent := string(body)\n\tif w.Result().Header.Get(\"Content-Type\") != expectedHeader {\n\t\tt.Error(\"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t}\n\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\tt.Error(\"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t}\n\tif w.Result().StatusCode != http.StatusOK {\n\t\tt.Error(\"Unexpected status code:\", w.Result().StatusCode)\n\t}\n\tif content != expectedContent {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", expectedContent)\n\t}\n}\n\nfunc TestRender_noop_nilResponse(t *testing.T) {\n\tp := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn nil, nil\n\t}\n\tendpoint := &config.EndpointConfig{\n\t\tMethod:         \"GET\",\n\t\tTimeout:        time.Second,\n\t\tCacheTTL:       6 * time.Hour,\n\t\tQueryString:    []string{\"b\"},\n\t\tOutputEncoding: encoding.NOOP,\n\t}\n\n\trouter := http.NewServeMux()\n\trouter.Handle(\"/_mux_endpoint\", EndpointHandler(endpoint, p))\n\n\treq, _ := http.NewRequest(\"GET\", \"http://127.0.0.1:8080/_mux_endpoint?b=1\", io.NopCloser(&bytes.Buffer{}))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tw := httptest.NewRecorder()\n\trouter.ServeHTTP(w, req)\n\n\tif w.Result().Header.Get(\"Content-Type\") != \"text/plain; charset=utf-8\" {\n\t\tt.Error(\"Content-Type error:\", w.Result().Header.Get(\"Content-Type\"))\n\t}\n\tif w.Result().Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\tt.Error(\"X-Krakend error:\", w.Result().Header.Get(\"X-Krakend\"))\n\t}\n\tif w.Result().StatusCode != http.StatusInternalServerError {\n\t\tt.Error(\"Unexpected status code:\", w.Result().StatusCode)\n\t}\n}\n"
  },
  {
    "path": "router/mux/router.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\nPackage mux provides some basic implementations for building routers based on net/http mux\n*/\npackage mux\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/router\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\n// DefaultDebugPattern is the default pattern used to define the debug endpoint\nconst (\n\tDefaultDebugPattern = \"/__debug/\"\n\tDefaultEchoPattern  = \"/__echo/\"\n\tlogPrefix           = \"[SERVICE: Mux]\"\n)\n\n// RunServerFunc is a func that will run the http Server with the given params.\ntype RunServerFunc func(context.Context, config.ServiceConfig, http.Handler) error\n\n// Config is the struct that collects the parts the router should be builded from\ntype Config struct {\n\tEngine         Engine\n\tMiddlewares    []HandlerMiddleware\n\tHandlerFactory HandlerFactory\n\tProxyFactory   proxy.Factory\n\tLogger         logging.Logger\n\tDebugPattern   string\n\tEchoPattern    string\n\tRunServer      RunServerFunc\n}\n\n// HandlerMiddleware is the interface for the decorators over the http.Handler\ntype HandlerMiddleware interface {\n\tHandler(h http.Handler) http.Handler\n}\n\n// DefaultFactory returns a net/http mux router factory with the injected proxy factory and logger\nfunc DefaultFactory(pf proxy.Factory, logger logging.Logger) router.Factory {\n\treturn factory{\n\t\tConfig{\n\t\t\tEngine:         DefaultEngine(),\n\t\t\tMiddlewares:    []HandlerMiddleware{},\n\t\t\tHandlerFactory: EndpointHandler,\n\t\t\tProxyFactory:   pf,\n\t\t\tLogger:         logger,\n\t\t\tDebugPattern:   DefaultDebugPattern,\n\t\t\tEchoPattern:    DefaultEchoPattern,\n\t\t\tRunServer:      server.RunServer,\n\t\t},\n\t}\n}\n\n// NewFactory returns a net/http mux router factory with the injected configuration\nfunc NewFactory(cfg Config) router.Factory {\n\tif cfg.DebugPattern == \"\" {\n\t\tcfg.DebugPattern = DefaultDebugPattern\n\t}\n\treturn factory{cfg}\n}\n\ntype factory struct {\n\tcfg Config\n}\n\n// New implements the factory interface\nfunc (rf factory) New() router.Router {\n\treturn rf.NewWithContext(context.Background())\n}\n\n// NewWithContext implements the factory interface\nfunc (rf factory) NewWithContext(ctx context.Context) router.Router {\n\treturn httpRouter{rf.cfg, ctx, rf.cfg.RunServer}\n}\n\ntype httpRouter struct {\n\tcfg       Config\n\tctx       context.Context\n\tRunServer RunServerFunc\n}\n\n// HealthHandler is a dummy http.HandlerFunc implementation for exposing a health check endpoint\nfunc HealthHandler(w http.ResponseWriter, _ *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.Write([]byte(`{\"status\":\"ok\"}`))\n}\n\n// Run implements the router interface\nfunc (r httpRouter) Run(cfg config.ServiceConfig) {\n\tif cfg.Debug {\n\t\tdebugHandler := DebugHandler(r.cfg.Logger)\n\t\tfor _, method := range []string{\n\t\t\thttp.MethodGet,\n\t\t\thttp.MethodPost,\n\t\t\thttp.MethodPut,\n\t\t\thttp.MethodPatch,\n\t\t\thttp.MethodDelete,\n\t\t\thttp.MethodHead,\n\t\t\thttp.MethodOptions,\n\t\t\thttp.MethodConnect,\n\t\t\thttp.MethodTrace,\n\t\t} {\n\t\t\tr.cfg.Engine.Handle(r.cfg.DebugPattern, method, debugHandler)\n\t\t}\n\t}\n\n\tif cfg.Echo {\n\t\techoHandler := EchoHandler()\n\t\tfor _, method := range []string{\n\t\t\thttp.MethodGet,\n\t\t\thttp.MethodPost,\n\t\t\thttp.MethodPut,\n\t\t\thttp.MethodPatch,\n\t\t\thttp.MethodDelete,\n\t\t\thttp.MethodHead,\n\t\t\thttp.MethodOptions,\n\t\t\thttp.MethodConnect,\n\t\t\thttp.MethodTrace,\n\t\t} {\n\t\t\tr.cfg.Engine.Handle(r.cfg.EchoPattern, method, echoHandler)\n\t\t}\n\t}\n\n\tr.cfg.Engine.Handle(\"/__health\", \"GET\", http.HandlerFunc(HealthHandler))\n\n\tserver.InitHTTPDefaultTransport(cfg)\n\n\tr.registerKrakendEndpoints(cfg.Endpoints)\n\n\tif err := r.RunServer(r.ctx, cfg, r.handler()); err != nil {\n\t\tr.cfg.Logger.Error(logPrefix, err.Error())\n\t}\n\n\tr.cfg.Logger.Info(logPrefix, \"Router execution ended\")\n}\n\nfunc (r httpRouter) registerKrakendEndpoints(endpoints []*config.EndpointConfig) {\n\tfor _, c := range endpoints {\n\t\tproxyStack, err := r.cfg.ProxyFactory.New(c)\n\t\tif err != nil {\n\t\t\tr.cfg.Logger.Error(logPrefix, \"Calling the ProxyFactory\", err.Error())\n\t\t\tcontinue\n\t\t}\n\n\t\tr.registerKrakendEndpoint(c.Method, c, r.cfg.HandlerFactory(c, proxyStack), len(c.Backend))\n\t}\n}\n\nfunc (r httpRouter) registerKrakendEndpoint(method string, endpoint *config.EndpointConfig, handler http.HandlerFunc, totBackends int) {\n\tmethod = strings.ToTitle(method)\n\tpath := endpoint.Endpoint\n\tif method != http.MethodGet && totBackends > 1 {\n\t\tif !router.IsValidSequentialEndpoint(endpoint) {\n\t\t\tr.cfg.Logger.Error(logPrefix, method, \" endpoints with sequential proxy enabled only allow a non-GET in the last backend! Ignoring\", path)\n\t\t\treturn\n\t\t}\n\t}\n\n\tswitch method {\n\tcase http.MethodGet:\n\tcase http.MethodPost:\n\tcase http.MethodPut:\n\tcase http.MethodPatch:\n\tcase http.MethodDelete:\n\tdefault:\n\t\tr.cfg.Logger.Error(logPrefix, \"Unsupported method\", method)\n\t\treturn\n\t}\n\tr.cfg.Logger.Debug(logPrefix, \"Registering the endpoint\", method, path)\n\tr.cfg.Engine.Handle(path, method, handler)\n}\n\nfunc (r httpRouter) handler() http.Handler {\n\tvar handler http.Handler = r.cfg.Engine\n\tfor _, middleware := range r.cfg.Middlewares {\n\t\tr.cfg.Logger.Debug(logPrefix, \"Adding the middleware\", middleware)\n\t\thandler = middleware.Handler(handler)\n\t}\n\treturn handler\n}\n"
  },
  {
    "path": "router/mux/router_test.go",
    "content": "//go:build !race\n// +build !race\n\n// SPDX-License-Identifier: Apache-2.0\n\npackage mux\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\nfunc TestDefaultFactory_ok(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"}), logger).NewWithContext(ctx)\n\texpectedBody := \"{\\\"supu\\\":\\\"tupu\\\"}\"\n\n\tserviceCfg := config.ServiceConfig{\n\t\tPort: 8062,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/get\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/get\",\n\t\t\t\tMethod:   \"POST\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/post\",\n\t\t\t\tMethod:   \"Post\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/put\",\n\t\t\t\tMethod:   \"put\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/patch\",\n\t\t\t\tMethod:   \"PATCH\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/delete\",\n\t\t\t\tMethod:   \"DELETE\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, endpoint := range serviceCfg.Endpoints {\n\t\treq, _ := http.NewRequest(strings.ToTitle(endpoint.Method), fmt.Sprintf(\"http://127.0.0.1:8062%s\", endpoint.Endpoint), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tresp, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\tt.Error(\"Making the request:\", err.Error())\n\t\t\treturn\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(resp.Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\t\treturn\n\t\t}\n\t\tcontent := string(body)\n\t\tif resp.Header.Get(\"Cache-Control\") != \"\" {\n\t\t\tt.Error(endpoint.Endpoint, \"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t\t}\n\t\tif resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderCompleteResponseValue {\n\t\t\tt.Error(server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t\t}\n\t\tif resp.Header.Get(\"Content-Type\") != \"application/json\" {\n\t\t\tt.Error(endpoint.Endpoint, \"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif resp.Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(endpoint.Endpoint, \"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Error(endpoint.Endpoint, \"Unexpected status code:\", resp.StatusCode)\n\t\t}\n\t\tif content != expectedBody {\n\t\t\tt.Error(endpoint.Endpoint, \"Unexpected body:\", content, \"expected:\", expectedBody)\n\t\t}\n\t}\n}\n\nfunc TestDefaultFactory_ko(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := NewFactory(Config{\n\t\tEngine:         DefaultEngine(),\n\t\tMiddlewares:    []HandlerMiddleware{identityMiddleware{}},\n\t\tHandlerFactory: EndpointHandler,\n\t\tProxyFactory:   noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"}),\n\t\tLogger:         logger,\n\t\tRunServer:      server.RunServer,\n\t}).NewWithContext(ctx)\n\n\tserviceCfg := config.ServiceConfig{\n\t\tDebug: true,\n\t\tPort:  8063,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/ignored\",\n\t\t\t\tMethod:   \"GETTT\",\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/empty\",\n\t\t\t\tMethod:   \"GETTT\",\n\t\t\t\tBackend:  []*config.Backend{},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, subject := range [][]string{\n\t\t{\"GET\", \"ignored\"},\n\t\t{\"GET\", \"empty\"},\n\t\t{\"PUT\", \"also-ignored\"},\n\t} {\n\t\treq, _ := http.NewRequest(subject[0], fmt.Sprintf(\"http://127.0.0.1:8063/%s\", subject[1]), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tcheckResponseIs404(t, req)\n\t}\n}\n\nfunc TestDefaultFactory_proxyFactoryCrash(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(erroredProxyFactory{fmt.Errorf(\"%s\", \"crash!!!\")}, logger).NewWithContext(ctx)\n\n\tserviceCfg := config.ServiceConfig{\n\t\tDebug: true,\n\t\tPort:  8064,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/ignored\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, subject := range [][]string{{\"GET\", \"ignored\"}, {\"PUT\", \"also-ignored\"}} {\n\t\treq, _ := http.NewRequest(subject[0], fmt.Sprintf(\"http://127.0.0.1:8064/%s\", subject[1]), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tcheckResponseIs404(t, req)\n\t}\n}\n\nfunc TestRunServer_ko(t *testing.T) {\n\tbuff := new(bytes.Buffer)\n\tlogger, err := logging.NewLogger(\"DEBUG\", buff, \"\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\terrorMsg := \"runServer error\"\n\trunServerFunc := func(_ context.Context, _ config.ServiceConfig, _ http.Handler) error {\n\t\treturn errors.New(errorMsg)\n\t}\n\n\tpf := noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"})\n\tr := NewFactory(\n\t\tConfig{\n\t\t\tEngine:         DefaultEngine(),\n\t\t\tMiddlewares:    []HandlerMiddleware{},\n\t\t\tHandlerFactory: EndpointHandler,\n\t\t\tProxyFactory:   pf,\n\t\t\tLogger:         logger,\n\t\t\tDebugPattern:   DefaultDebugPattern,\n\t\t\tRunServer:      runServerFunc,\n\t\t},\n\t).New()\n\n\tserviceCfg := config.ServiceConfig{}\n\tr.Run(serviceCfg)\n\tre := regexp.MustCompile(errorMsg)\n\tif !re.MatchString(buff.String()) {\n\t\tt.Errorf(\"the logger doesn't contain the expected msg: %s\", buff.Bytes())\n\t}\n}\n\nfunc checkResponseIs404(t *testing.T, req *http.Request) {\n\texpectedBody := \"404 page not found\\n\"\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tt.Error(\"Making the request:\", err.Error())\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\tbody, ioerr := io.ReadAll(resp.Body)\n\tif ioerr != nil {\n\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\treturn\n\t}\n\tcontent := string(body)\n\tif resp.Header.Get(\"Cache-Control\") != \"\" {\n\t\tt.Error(\"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t}\n\tif resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderIncompleteResponseValue {\n\t\tt.Error(req.URL.String(), server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t}\n\tif resp.Header.Get(\"Content-Type\") != \"text/plain; charset=utf-8\" {\n\t\tt.Error(\"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t}\n\tif resp.Header.Get(\"X-Krakend\") != \"\" {\n\t\tt.Error(\"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t}\n\tif resp.StatusCode != http.StatusNotFound {\n\t\tt.Error(\"Unexpected status code:\", resp.StatusCode)\n\t}\n\tif content != expectedBody {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", expectedBody)\n\t}\n}\n\ntype noopProxyFactory map[string]interface{}\n\nfunc (n noopProxyFactory) New(_ *config.EndpointConfig) (proxy.Proxy, error) {\n\treturn func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       n,\n\t\t}, nil\n\t}, nil\n}\n\ntype erroredProxyFactory struct {\n\tError error\n}\n\nfunc (e erroredProxyFactory) New(_ *config.EndpointConfig) (proxy.Proxy, error) {\n\treturn proxy.NoopProxy, e.Error\n}\n\ntype identityMiddleware struct{}\n\nfunc (identityMiddleware) Handler(h http.Handler) http.Handler {\n\treturn h\n}\n"
  },
  {
    "path": "router/negroni/router.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\n\tPackage negroni provides some basic implementations for building routers based on urfave/negroni\n*/\npackage negroni\n\nimport (\n\t\"net/http\"\n\n\tgorilla \"github.com/gorilla/mux\"\n\t\"github.com/urfave/negroni/v2\"\n\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/router\"\n\tluragorilla \"github.com/luraproject/lura/v2/router/gorilla\"\n\t\"github.com/luraproject/lura/v2/router/mux\"\n)\n\n// DefaultFactory returns a net/http mux router factory with the injected proxy factory and logger\nfunc DefaultFactory(pf proxy.Factory, logger logging.Logger, middlewares []negroni.Handler) router.Factory {\n\treturn mux.NewFactory(DefaultConfig(pf, logger, middlewares))\n}\n\n// DefaultConfig returns the struct that collects the parts the router should be builded from\nfunc DefaultConfig(pf proxy.Factory, logger logging.Logger, middlewares []negroni.Handler) mux.Config {\n\treturn DefaultConfigWithRouter(pf, logger, NewGorillaRouter(), middlewares)\n}\n\n// DefaultConfigWithRouter returns the struct that collects the parts the router should be builded from with the\n// injected gorilla mux router\nfunc DefaultConfigWithRouter(pf proxy.Factory, logger logging.Logger, muxEngine *gorilla.Router, middlewares []negroni.Handler) mux.Config {\n\tcfg := luragorilla.DefaultConfig(pf, logger)\n\tcfg.Engine = newNegroniEngine(muxEngine, middlewares...)\n\treturn cfg\n}\n\n// NewGorillaRouter is a wrapper over the default gorilla router builder\nfunc NewGorillaRouter() *gorilla.Router {\n\treturn gorilla.NewRouter()\n}\n\nfunc newNegroniEngine(muxEngine *gorilla.Router, middlewares ...negroni.Handler) negroniEngine {\n\tnegroniRouter := negroni.Classic()\n\tfor _, m := range middlewares {\n\t\tnegroniRouter.Use(m)\n\t}\n\n\tnegroniRouter.UseHandler(muxEngine)\n\n\treturn negroniEngine{muxEngine, negroniRouter}\n}\n\ntype negroniEngine struct {\n\tr *gorilla.Router\n\tn *negroni.Negroni\n}\n\n// Handle implements the mux.Engine interface from the lura router package\nfunc (e negroniEngine) Handle(pattern, method string, handler http.Handler) {\n\te.r.Handle(pattern, handler).Methods(method)\n}\n\n// ServeHTTP implements the http:Handler interface from the stdlib\nfunc (e negroniEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\te.n.ServeHTTP(mux.NewHTTPErrorInterceptor(w), r)\n}\n"
  },
  {
    "path": "router/negroni/router_test.go",
    "content": "//go:build !race\n// +build !race\n\n// SPDX-License-Identifier: Apache-2.0\n\npackage negroni\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/urfave/negroni/v2\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\nfunc TestDefaultFactory_ok(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"}), logger, []negroni.Handler{}).NewWithContext(ctx)\n\texpectedBody := \"{\\\"supu\\\":\\\"tupu\\\"}\"\n\n\tserviceCfg := config.ServiceConfig{\n\t\tPort: 8052,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/get/{id}\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/post\",\n\t\t\t\tMethod:   \"POST\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/put\",\n\t\t\t\tMethod:   \"PUT\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/patch\",\n\t\t\t\tMethod:   \"PATCH\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/delete\",\n\t\t\t\tMethod:   \"DELETE\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, endpoint := range serviceCfg.Endpoints {\n\t\treq, _ := http.NewRequest(endpoint.Method, fmt.Sprintf(\"http://127.0.0.1:8052%s\", endpoint.Endpoint), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tresp, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\tt.Error(\"Making the request:\", err.Error())\n\t\t\treturn\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(resp.Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\t\treturn\n\t\t}\n\t\tcontent := string(body)\n\t\tif resp.Header.Get(\"Cache-Control\") != \"\" {\n\t\t\tt.Error(endpoint.Endpoint, \"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t\t}\n\t\tif resp.Header.Get(\"Content-Type\") != \"application/json\" {\n\t\t\tt.Error(endpoint.Endpoint, \"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif resp.Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(endpoint.Endpoint, \"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Error(endpoint.Endpoint, \"Unexpected status code:\", resp.StatusCode)\n\t\t}\n\t\tif content != expectedBody {\n\t\t\tt.Error(endpoint.Endpoint, \"Unexpected body:\", content, \"expected:\", expectedBody)\n\t\t}\n\t}\n}\n\nfunc TestDefaultFactory_middlewares(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tcount := 0\n\n\tpf := noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"})\n\tr := DefaultFactory(pf, logger, []negroni.Handler{dummyMiddleware{&count}}).NewWithContext(ctx)\n\texpectedBody := \"{\\\"supu\\\":\\\"tupu\\\"}\"\n\n\tserviceCfg := config.ServiceConfig{\n\t\tPort: 8090,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/get/{id}\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, endpoint := range serviceCfg.Endpoints {\n\t\treq, _ := http.NewRequest(endpoint.Method, fmt.Sprintf(\"http://127.0.0.1:8090%s\", endpoint.Endpoint), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tresp, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\tt.Error(\"Making the request:\", err.Error())\n\t\t\treturn\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tbody, ioerr := io.ReadAll(resp.Body)\n\t\tif ioerr != nil {\n\t\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\t\treturn\n\t\t}\n\t\tcontent := string(body)\n\t\tif resp.Header.Get(\"Cache-Control\") != \"\" {\n\t\t\tt.Error(endpoint.Endpoint, \"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t\t}\n\t\tif resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderCompleteResponseValue {\n\t\t\tt.Error(server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t\t}\n\t\tif resp.Header.Get(\"Content-Type\") != \"application/json\" {\n\t\t\tt.Error(endpoint.Endpoint, \"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t\t}\n\t\tif resp.Header.Get(\"X-Krakend\") != \"Version undefined\" {\n\t\t\tt.Error(endpoint.Endpoint, \"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Error(endpoint.Endpoint, \"Unexpected status code:\", resp.StatusCode)\n\t\t}\n\t\tif content != expectedBody {\n\t\t\tt.Error(endpoint.Endpoint, \"Unexpected body:\", content, \"expected:\", expectedBody)\n\t\t}\n\t}\n\n\tif count != 1 {\n\t\tt.Error(\"Middleware wasn't called just one time\")\n\t}\n}\n\nfunc TestDefaultFactory_ko(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(noopProxyFactory(map[string]interface{}{\"supu\": \"tupu\"}), logger, []negroni.Handler{}).NewWithContext(ctx)\n\n\tserviceCfg := config.ServiceConfig{\n\t\tDebug: true,\n\t\tPort:  8053,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/ignored\",\n\t\t\t\tMethod:   \"GETTT\",\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/empty\",\n\t\t\t\tMethod:   \"GETTT\",\n\t\t\t\tBackend:  []*config.Backend{},\n\t\t\t},\n\t\t\t{\n\t\t\t\tEndpoint: \"/also-ignored\",\n\t\t\t\tMethod:   \"PUT\",\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, subject := range [][]string{\n\t\t{\"GET\", \"ignored\"},\n\t\t{\"GET\", \"empty\"},\n\t\t{\"PUT\", \"also-ignored\"},\n\t} {\n\t\treq, _ := http.NewRequest(subject[0], fmt.Sprintf(\"http://127.0.0.1:8053/%s\", subject[1]), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tcheckResponseIs404(t, req)\n\t}\n}\n\nfunc TestDefaultFactory_proxyFactoryCrash(t *testing.T) {\n\tbuff := bytes.NewBuffer(make([]byte, 1024))\n\tlogger, err := logging.NewLogger(\"ERROR\", buff, \"pref\")\n\tif err != nil {\n\t\tt.Error(\"building the logger:\", err.Error())\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tcancel()\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}()\n\n\tr := DefaultFactory(erroredProxyFactory{fmt.Errorf(\"%s\", \"crash!!!\")}, logger, []negroni.Handler{}).NewWithContext(ctx)\n\n\tserviceCfg := config.ServiceConfig{\n\t\tDebug: true,\n\t\tPort:  8054,\n\t\tEndpoints: []*config.EndpointConfig{\n\t\t\t{\n\t\t\t\tEndpoint: \"/ignored\",\n\t\t\t\tMethod:   \"GET\",\n\t\t\t\tTimeout:  10,\n\t\t\t\tBackend: []*config.Backend{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tgo func() { r.Run(serviceCfg) }()\n\n\ttime.Sleep(5 * time.Millisecond)\n\n\tfor _, subject := range [][]string{{\"GET\", \"ignored\"}, {\"PUT\", \"also-ignored\"}} {\n\t\treq, _ := http.NewRequest(subject[0], fmt.Sprintf(\"http://127.0.0.1:8054/%s\", subject[1]), http.NoBody)\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tcheckResponseIs404(t, req)\n\t}\n}\n\ntype dummyMiddleware struct {\n\tCount *int\n}\n\nfunc (d dummyMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {\n\t*(d.Count) = *(d.Count) + 1\n\tnext(rw, r)\n}\n\nfunc checkResponseIs404(t *testing.T, req *http.Request) {\n\texpectedBody := \"404 page not found\\n\"\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tt.Error(\"Making the request:\", err.Error())\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\tbody, ioerr := io.ReadAll(resp.Body)\n\tif ioerr != nil {\n\t\tt.Error(\"Reading the response:\", ioerr.Error())\n\t\treturn\n\t}\n\tcontent := string(body)\n\tif resp.Header.Get(\"Cache-Control\") != \"\" {\n\t\tt.Error(\"Cache-Control error:\", resp.Header.Get(\"Cache-Control\"))\n\t}\n\tif resp.Header.Get(server.CompleteResponseHeaderName) != server.HeaderIncompleteResponseValue {\n\t\tt.Error(req.URL.String(), server.CompleteResponseHeaderName, \"error:\", resp.Header.Get(server.CompleteResponseHeaderName))\n\t}\n\tif resp.Header.Get(\"Content-Type\") != \"text/plain; charset=utf-8\" {\n\t\tt.Error(\"Content-Type error:\", resp.Header.Get(\"Content-Type\"))\n\t}\n\tif resp.Header.Get(\"X-Krakend\") != \"\" {\n\t\tt.Error(\"X-Krakend error:\", resp.Header.Get(\"X-Krakend\"))\n\t}\n\tif resp.StatusCode != http.StatusNotFound {\n\t\tt.Error(\"Unexpected status code:\", resp.StatusCode)\n\t}\n\tif content != expectedBody {\n\t\tt.Error(\"Unexpected body:\", content, \"expected:\", expectedBody)\n\t}\n}\n\ntype noopProxyFactory map[string]interface{}\n\nfunc (n noopProxyFactory) New(_ *config.EndpointConfig) (proxy.Proxy, error) {\n\treturn func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) {\n\t\treturn &proxy.Response{\n\t\t\tIsComplete: true,\n\t\t\tData:       n,\n\t\t}, nil\n\t}, nil\n}\n\ntype erroredProxyFactory struct {\n\tError error\n}\n\nfunc (e erroredProxyFactory) New(_ *config.EndpointConfig) (proxy.Proxy, error) {\n\treturn proxy.NoopProxy, e.Error\n}\n\ntype identityMiddleware struct{}\n\nfunc (identityMiddleware) Handler(h http.Handler) http.Handler {\n\treturn h\n}\n"
  },
  {
    "path": "router/router.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\n\tPackage router defines some interfaces and common helpers for router adapters\n*/\npackage router\n\nimport (\n\t\"context\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n)\n\n// Router sets up the public layer exposed to the users\ntype Router interface {\n\tRun(config.ServiceConfig)\n}\n\n// RouterFunc type is an adapter to allow the use of ordinary functions as routers.\n// If f is a function with the appropriate signature, RouterFunc(f) is a Router that calls f.\ntype RouterFunc func(config.ServiceConfig)\n\n// Run implements the Router interface\nfunc (f RouterFunc) Run(cfg config.ServiceConfig) { f(cfg) }\n\n// Factory creates new routers\ntype Factory interface {\n\tNew() Router\n\tNewWithContext(context.Context) Router\n}\n"
  },
  {
    "path": "sd/dnssrv/subscriber.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\nPackage dnssrv defines some implementations for a dns based service discovery\n*/\npackage dnssrv\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/sd\"\n)\n\n// Namespace is the key for the dns sd module\nconst Namespace = \"dns\"\nconst DefaultTTL = 30 * time.Second\nconst MinTTL = time.Second\n\n// Register registers the dns sd subscriber factory under the name defined by Namespace\nfunc Register() error {\n\treturn sd.GetRegister().Register(Namespace, SubscriberFactory)\n}\n\n// TTL is the duration of the cached data\nvar TTL = DefaultTTL\n\nfunc SetTTL(d time.Duration) {\n\tif d < MinTTL {\n\t\t// in case the TTL is less than the minimum, we leave what is\n\t\t// already set.\n\t\treturn\n\t}\n\tTTL = d\n}\n\n// DefaultLookup is the function used for the DNS resolution\nvar DefaultLookup = net.LookupSRV\n\n// SubscriberFactory builds a DNS_SRV Subscriber with the received config\nfunc SubscriberFactory(cfg *config.Backend) sd.Subscriber {\n\treturn NewDetailedWithScheme(cfg.Host[0], DefaultLookup, TTL, cfg.SDScheme)\n}\n\n// New creates a DNS subscriber with the default values\nfunc New(name string) sd.Subscriber {\n\treturn NewDetailed(name, DefaultLookup, TTL)\n}\n\n// NewDetailed creates a DNS subscriber with the received values\nfunc NewDetailed(name string, lookup lookup, ttl time.Duration) sd.Subscriber {\n\treturn NewDetailedWithScheme(name, lookup, ttl, \"http\")\n}\n\n// NewDetailedWithScheme creates a DNS subscriber with the received values and the scheme to use\n// for the fetched server entries.\nfunc NewDetailedWithScheme(name string, lookup lookup, ttl time.Duration, scheme string) sd.Subscriber {\n\tif scheme == \"\" {\n\t\tscheme = \"http\"\n\t}\n\ts := subscriber{\n\t\tname:   name,\n\t\tcache:  &sd.FixedSubscriber{},\n\t\tmutex:  &sync.RWMutex{},\n\t\tttl:    ttl,\n\t\tlookup: lookup,\n\t\tscheme: scheme,\n\t}\n\n\ts.update()\n\n\tgo func() {\n\t\tfor {\n\t\t\t<-time.After(s.ttl)\n\t\t\ts.update()\n\t\t}\n\t}()\n\n\treturn s\n}\n\ntype lookup func(service, proto, name string) (cname string, addrs []*net.SRV, err error)\n\ntype subscriber struct {\n\tname   string\n\tcache  *sd.FixedSubscriber\n\tmutex  *sync.RWMutex\n\tttl    time.Duration\n\tlookup lookup\n\tscheme string\n}\n\n// Hosts returns a copy of the cached set of hosts. It is safe to call it concurrently\nfunc (s subscriber) Hosts() ([]string, error) {\n\ts.mutex.RLock()\n\tdefer s.mutex.RUnlock()\n\n\ths, err := s.cache.Hosts()\n\tif err != nil {\n\t\treturn []string{}, err\n\t}\n\n\tres := make([]string, len(hs))\n\tcopy(res, hs)\n\treturn res, nil\n}\n\nfunc (s subscriber) update() {\n\tinstances, err := s.resolve()\n\tif err != nil {\n\t\treturn\n\t}\n\n\ts.mutex.Lock()\n\tdefer s.mutex.Unlock()\n\n\tif len(instances) > 100 {\n\t\t*(s.cache) = sd.NewRandomFixedSubscriber(instances)\n\t} else {\n\t\t*(s.cache) = sd.FixedSubscriber(instances)\n\t}\n}\n\nfunc (s subscriber) resolve() ([]string, error) {\n\t_, srvs, err := s.lookup(\"\", \"\", s.name)\n\tif err != nil {\n\t\treturn []string{}, err\n\t}\n\n\tsort.Slice(\n\t\tsrvs,\n\t\tfunc(i, j int) bool {\n\t\t\tif srvs[i].Priority == srvs[j].Priority {\n\t\t\t\tif srvs[i].Weight == srvs[j].Weight {\n\t\t\t\t\tif srvs[i].Target == srvs[j].Target {\n\t\t\t\t\t\treturn srvs[i].Port < srvs[j].Port\n\t\t\t\t\t}\n\t\t\t\t\treturn srvs[i].Target < srvs[j].Target\n\t\t\t\t}\n\t\t\t\treturn srvs[i].Weight > srvs[j].Weight\n\t\t\t}\n\t\t\treturn srvs[i].Priority < srvs[j].Priority\n\t\t},\n\t)\n\n\tws := make([]uint16, 0, len(srvs))\n\thost := make([]string, 0, len(srvs))\n\n\tfor _, a := range srvs {\n\t\tif a.Priority > srvs[0].Priority {\n\t\t\tbreak\n\t\t}\n\t\tws = append(ws, a.Weight)\n\t\thost = append(host, s.scheme+\"://\"+net.JoinHostPort(a.Target, fmt.Sprint(a.Port)))\n\t}\n\n\tinstances := make([]string, 0, len(ws))\n\tfor i, times := range compact(ws) {\n\t\tfor j := uint16(0); j < times; j++ {\n\t\t\tinstances = append(instances, host[i])\n\t\t}\n\t}\n\treturn instances, nil\n}\n\nfunc compact(ws []uint16) []uint16 {\n\ttmp := normalize(ws)\n\tdiv := gcd(tmp)\n\tif div < 2 {\n\t\treturn tmp\n\t}\n\n\tres := make([]uint16, len(tmp))\n\tfor i, w := range tmp {\n\t\tres[i] = w / div\n\t}\n\n\treturn res\n}\n\nfunc normalize(ws []uint16) []uint16 {\n\tscale := 100\n\tif l := len(ws); l > scale {\n\t\tscale = l\n\t}\n\n\tvar sum int64\n\tfor _, w := range ws {\n\t\tsum += int64(w)\n\t}\n\tif sum <= int64(scale) {\n\t\treturn ws\n\t}\n\n\tres := make([]uint16, len(ws))\n\tfor i, w := range ws {\n\t\tres[i] = uint16(int64(w) * int64(scale) / sum)\n\t}\n\treturn res\n}\n\nfunc gcd(ws []uint16) uint16 {\n\tif len(ws) == 0 {\n\t\treturn 0\n\t}\n\n\tlocalGCD := func(a uint16, b uint16) uint16 {\n\t\tfor b > 0 {\n\t\t\ta, b = b, a%b\n\t\t}\n\t\treturn a\n\t}\n\n\tresult := ws[0]\n\tfor _, i := range ws[1:] {\n\t\tresult = localGCD(result, i)\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "sd/dnssrv/subscriber_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage dnssrv\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/sd\"\n)\n\nfunc ExampleRegister() {\n\tif err := Register(); err != nil {\n\t\tfmt.Println(\"registering the dns module:\", err.Error())\n\t\treturn\n\t}\n\tsrvSet := []*net.SRV{\n\t\t{\n\t\t\tPort:   90,\n\t\t\tTarget: \"foobar\",\n\t\t\tWeight: 2,\n\t\t},\n\t\t{\n\t\t\tPort:   90,\n\t\t\tTarget: \"127.0.0.1\",\n\t\t\tWeight: 2,\n\t\t},\n\t\t{\n\t\t\tPort:   80,\n\t\t\tTarget: \"127.0.0.1\",\n\t\t\tWeight: 2,\n\t\t},\n\t\t{\n\t\t\tPort:   81,\n\t\t\tTarget: \"127.0.0.1\",\n\t\t\tWeight: 4,\n\t\t},\n\t\t{\n\t\t\tPort:     82,\n\t\t\tTarget:   \"127.0.0.1\",\n\t\t\tWeight:   10,\n\t\t\tPriority: 2,\n\t\t},\n\t\t{\n\t\t\tPort:   83,\n\t\t\tTarget: \"127.0.0.1\",\n\t\t},\n\t}\n\tDefaultLookup = func(service, proto, name string) (cname string, addrs []*net.SRV, err error) {\n\t\treturn \"cname\", srvSet, nil\n\t}\n\n\ts := sd.GetRegister().Get(Namespace)(&config.Backend{Host: []string{\"some.example.tld\"}, SD: Namespace})\n\thosts, err := s.Hosts()\n\tif err != nil {\n\t\tfmt.Println(\"Getting the hosts:\", err.Error())\n\t\treturn\n\t}\n\tfor _, h := range hosts {\n\t\tfmt.Println(h)\n\t}\n\n\t// output:\n\t// http://127.0.0.1:81\n\t// http://127.0.0.1:81\n\t// http://127.0.0.1:80\n\t// http://127.0.0.1:90\n\t// http://foobar:90\n}\n\nfunc ExampleNewDetailed() {\n\tsrvSet := []*net.SRV{\n\t\t{\n\t\t\tPort:   90,\n\t\t\tTarget: \"foobar\",\n\t\t\tWeight: 2,\n\t\t},\n\t\t{\n\t\t\tPort:   90,\n\t\t\tTarget: \"127.0.0.1\",\n\t\t\tWeight: 2,\n\t\t},\n\t\t{\n\t\t\tPort:   80,\n\t\t\tTarget: \"127.0.0.1\",\n\t\t\tWeight: 2,\n\t\t},\n\t\t{\n\t\t\tPort:   81,\n\t\t\tTarget: \"127.0.0.1\",\n\t\t\tWeight: 4,\n\t\t},\n\t\t{\n\t\t\tPort:     82,\n\t\t\tTarget:   \"127.0.0.1\",\n\t\t\tWeight:   10,\n\t\t\tPriority: 2,\n\t\t},\n\t\t{\n\t\t\tPort:   83,\n\t\t\tTarget: \"127.0.0.1\",\n\t\t},\n\t}\n\tlookupFunc := func(service, proto, name string) (cname string, addrs []*net.SRV, err error) {\n\t\treturn \"cname\", srvSet, nil\n\t}\n\n\ts := NewDetailed(\"some.example.tld\", lookupFunc, 10*time.Second)\n\thosts, err := s.Hosts()\n\tif err != nil {\n\t\tfmt.Println(\"Getting the hosts:\", err.Error())\n\t\treturn\n\t}\n\tfor _, h := range hosts {\n\t\tfmt.Println(h)\n\t}\n\n\t// output:\n\t// http://127.0.0.1:81\n\t// http://127.0.0.1:81\n\t// http://127.0.0.1:80\n\t// http://127.0.0.1:90\n\t// http://foobar:90\n}\n\nfunc ExampleNewDetailedWithScheme() {\n\tsrvSet := []*net.SRV{\n\t\t{\n\t\t\tPort:   90,\n\t\t\tTarget: \"foobar\",\n\t\t\tWeight: 2,\n\t\t},\n\t\t{\n\t\t\tPort:   90,\n\t\t\tTarget: \"127.0.0.1\",\n\t\t\tWeight: 2,\n\t\t},\n\t\t{\n\t\t\tPort:   80,\n\t\t\tTarget: \"127.0.0.1\",\n\t\t\tWeight: 2,\n\t\t},\n\t\t{\n\t\t\tPort:   81,\n\t\t\tTarget: \"127.0.0.1\",\n\t\t\tWeight: 4,\n\t\t},\n\t\t{\n\t\t\tPort:     82,\n\t\t\tTarget:   \"127.0.0.1\",\n\t\t\tWeight:   10,\n\t\t\tPriority: 2,\n\t\t},\n\t\t{\n\t\t\tPort:   83,\n\t\t\tTarget: \"127.0.0.1\",\n\t\t},\n\t}\n\tlookupFunc := func(service, proto, name string) (cname string, addrs []*net.SRV, err error) {\n\t\treturn \"cname\", srvSet, nil\n\t}\n\n\ts := NewDetailedWithScheme(\"some.example.tld\", lookupFunc, 10*time.Second, \"https\")\n\thosts, err := s.Hosts()\n\tif err != nil {\n\t\tfmt.Println(\"Getting the hosts:\", err.Error())\n\t\treturn\n\t}\n\tfor _, h := range hosts {\n\t\tfmt.Println(h)\n\t}\n\n\t// output:\n\t// https://127.0.0.1:81\n\t// https://127.0.0.1:81\n\t// https://127.0.0.1:80\n\t// https://127.0.0.1:90\n\t// https://foobar:90\n}\n\nfunc TestSubscriber_LoockupError(t *testing.T) {\n\terrToReturn := errors.New(\"Some random error\")\n\tdefaultLookup := func(service, proto, name string) (cname string, addrs []*net.SRV, err error) {\n\t\treturn \"cname\", []*net.SRV{}, errToReturn\n\t}\n\tttl := 1 * time.Millisecond\n\ts := NewDetailed(\"some.example.tld\", defaultLookup, ttl)\n\thosts, err := s.Hosts()\n\tif err != nil {\n\t\tt.Error(\"Unexpected error!\", err)\n\t}\n\tif len(hosts) != 0 {\n\t\tt.Error(\"Wrong number of hosts:\", len(hosts))\n\t}\n}\n\nfunc TestSubscriber_ResolveVeryLarge(t *testing.T) {\n\tvar srvSet []*net.SRV\n\tconst max = 1000\n\tfor i := 0; i < max; i++ {\n\t\tsrvSet = append(srvSet, &net.SRV{\n\t\t\tPort:   uint16(80 + i),\n\t\t\tTarget: \"127.0.0.1\",\n\t\t\tWeight: 65535,\n\t\t})\n\t}\n\tlookupFunc := func(service, proto, name string) (cname string, addrs []*net.SRV, err error) {\n\t\treturn \"cname\", srvSet, nil\n\t}\n\ts := NewDetailed(\"large.example.tld\", lookupFunc, 10*time.Second)\n\thosts, _ := s.Hosts()\n\tif len(hosts) != max {\n\t\tt.Errorf(\"Expected %d, but got %d\", max, len(hosts))\n\t}\n}\n\nfunc Examplecompact_basicweights() {\n\tfor _, tc := range [][]uint16{\n\t\t[]uint16{25, 10000, 1000},\n\t\t[]uint16{25, 1000, 10000, 0, 65535},\n\t\t[]uint16{1, 65535},\n\t\t[]uint16{},\n\t\t[]uint16{0, 0, 0, 0},\n\t} {\n\t\tfmt.Println(tc, compact(tc))\n\t}\n\n\t// output:\n\t// [25 10000 1000] [0 10 1]\n\t// [25 1000 10000 0 65535] [0 1 13 0 85]\n\t// [1 65535] [0 1]\n\t// [] []\n\t// [0 0 0 0] [0 0 0 0]\n}\n\nfunc Examplecompact_custom_weights() {\n\ttc := make([]uint16, 200)\n\tfor i := range tc {\n\t\ttc[i] = uint16(3*5*7*11*13 + i)\n\t}\n\tfmt.Println(tc[:5], compact(tc[:5]))\n\n\tfor i := range tc {\n\t\ttc[i] = uint16(i * 3 * 5 * 7)\n\t}\n\tfmt.Println(tc[:5], compact(tc[:5]))\n\n\t// output:\n\t// [15015 15016 15017 15018 15019] [19 19 20 20 20]\n\t// [0 105 210 315 420] [0 1 2 3 4]\n}\n"
  },
  {
    "path": "sd/loadbalancing.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage sd\n\nimport (\n\t\"errors\"\n\t\"runtime\"\n\t\"sync/atomic\"\n\n\t\"github.com/valyala/fastrand\"\n)\n\n// Balancer applies a balancing stategy in order to select the backend host to be used\ntype Balancer interface {\n\tHost() (string, error)\n}\n\n// ErrNoHosts is the error the balancer must return when there are 0 hosts ready\nvar ErrNoHosts = errors.New(\"no hosts available\")\n\n// NewBalancer returns the best perfomant balancer depending on the number of available processors.\n// If GOMAXPROCS = 1, it returns a round robin LB due there is no contention over the atomic counter.\n// If GOMAXPROCS > 1, it returns a pseudo random LB optimized for scaling over the number of CPUs.\nfunc NewBalancer(subscriber Subscriber) Balancer {\n\tif p := runtime.GOMAXPROCS(-1); p == 1 {\n\t\treturn NewRoundRobinLB(subscriber)\n\t}\n\treturn NewRandomLB(subscriber)\n}\n\n// NewRoundRobinLB returns a new balancer using a round robin strategy and starting at a random\n// position in the set of hosts.\nfunc NewRoundRobinLB(subscriber Subscriber) Balancer {\n\ts, ok := subscriber.(FixedSubscriber)\n\tstart := uint64(0)\n\tif ok {\n\t\tif l := len(s); l == 1 {\n\t\t\treturn nopBalancer(s[0])\n\t\t} else if l > 1 {\n\t\t\tstart = uint64(fastrand.Uint32n(uint32(l)))\n\t\t}\n\t}\n\treturn &roundRobinLB{\n\t\tbalancer: balancer{subscriber: subscriber},\n\t\tcounter:  start,\n\t}\n}\n\ntype roundRobinLB struct {\n\tbalancer\n\tcounter uint64\n}\n\n// Host implements the balancer interface\nfunc (r *roundRobinLB) Host() (string, error) {\n\thosts, err := r.hosts()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\toffset := (atomic.AddUint64(&r.counter, 1) - 1) % uint64(len(hosts))\n\treturn hosts[offset], nil\n}\n\n// NewRandomLB returns a new balancer using a fastrand pseudorandom generator\nfunc NewRandomLB(subscriber Subscriber) Balancer {\n\tif s, ok := subscriber.(FixedSubscriber); ok && len(s) == 1 {\n\t\treturn nopBalancer(s[0])\n\t}\n\treturn &randomLB{\n\t\tbalancer: balancer{subscriber: subscriber},\n\t\trand:     fastrand.Uint32n,\n\t}\n}\n\ntype randomLB struct {\n\tbalancer\n\trand func(uint32) uint32\n}\n\n// Host implements the balancer interface\nfunc (r *randomLB) Host() (string, error) {\n\thosts, err := r.hosts()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn hosts[int(r.rand(uint32(len(hosts))))], nil\n}\n\ntype balancer struct {\n\tsubscriber Subscriber\n}\n\nfunc (b *balancer) hosts() ([]string, error) {\n\ths, err := b.subscriber.Hosts()\n\tif err != nil {\n\t\treturn hs, err\n\t}\n\tif len(hs) <= 0 {\n\t\treturn hs, ErrNoHosts\n\t}\n\treturn hs, nil\n}\n\ntype nopBalancer string\n\nfunc (b nopBalancer) Host() (string, error) { return string(b), nil }\n"
  },
  {
    "path": "sd/loadbalancing_benchmark_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage sd\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nvar balancerTestsCases = [][]string{\n\t{\"a\"},\n\t{\"a\", \"b\", \"c\"},\n\t{\"a\", \"b\", \"c\", \"e\", \"f\"},\n}\n\nfunc BenchmarkLB(b *testing.B) {\n\tfor _, tc := range []struct {\n\t\tname string\n\t\tf    func([]string) Balancer\n\t}{\n\t\t{name: \"round_robin\", f: func(hs []string) Balancer { return NewRoundRobinLB(FixedSubscriber(hs)) }},\n\t\t{name: \"random\", f: func(hs []string) Balancer { return NewRandomLB(FixedSubscriber(hs)) }},\n\t} {\n\t\tfor _, testCase := range balancerTestsCases {\n\t\t\tb.Run(fmt.Sprintf(\"%s/%d\", tc.name, len(testCase)), func(b *testing.B) {\n\t\t\t\tbalancer := tc.f(testCase)\n\t\t\t\tb.ResetTimer()\n\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\tbalancer.Host()\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc BenchmarkLB_parallel(b *testing.B) {\n\tfor _, tc := range []struct {\n\t\tname string\n\t\tf    func([]string) Balancer\n\t}{\n\t\t{name: \"round_robin\", f: func(hs []string) Balancer { return NewRoundRobinLB(FixedSubscriber(hs)) }},\n\t\t{name: \"random\", f: func(hs []string) Balancer { return NewRandomLB(FixedSubscriber(hs)) }},\n\t} {\n\t\tfor _, testCase := range balancerTestsCases {\n\t\t\tb.Run(fmt.Sprintf(\"%s/%d\", tc.name, len(testCase)), func(b *testing.B) {\n\t\t\t\tbalancer := tc.f(testCase)\n\t\t\t\tb.ResetTimer()\n\t\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\t\tfor pb.Next() {\n\t\t\t\t\t\tbalancer.Host()\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "sd/loadbalancing_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage sd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n)\n\nfunc ExampleNewRoundRobinLB() {\n\tbalancer := NewRoundRobinLB(FixedSubscriber([]string{\"a\", \"b\", \"c\"}))\n\n\t// code required in order to make the test deterministic\n\tbalancer.(*roundRobinLB).counter = 1\n\n\tfor i := 0; i < 5; i++ {\n\t\th, err := balancer.Host()\n\t\tif err != nil {\n\t\t\tfmt.Println(err.Error())\n\t\t\tcontinue\n\t\t}\n\t\tfmt.Println(h)\n\t}\n\n\t// output\n\t// b\n\t// c\n\t// a\n\t// b\n\t// a\n}\n\nfunc TestRoundRobinLB(t *testing.T) {\n\tfor _, endpoints := range balancerTestsCases {\n\t\tt.Run(fmt.Sprintf(\"%d hosts\", len(endpoints)), func(t *testing.T) {\n\t\t\tvar (\n\t\t\t\tn          = len(endpoints)\n\t\t\t\tcounts     = make(map[string]int, n)\n\t\t\t\titerations = 100000 * n\n\t\t\t\twant       = iterations / n\n\t\t\t)\n\n\t\t\tfor _, e := range endpoints {\n\t\t\t\tcounts[e] = 0\n\t\t\t}\n\n\t\t\tsubscriber := FixedSubscriber(endpoints)\n\t\t\tbalancer := NewRoundRobinLB(subscriber)\n\n\t\t\tif b, ok := balancer.(*roundRobinLB); ok {\n\t\t\t\tb.counter = 0\n\t\t\t}\n\n\t\t\tfor i := 0; i < iterations; i++ {\n\t\t\t\tendpoint, err := balancer.Host()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fail()\n\t\t\t\t}\n\t\t\t\texpected := i % n\n\t\t\t\tif v := endpoints[expected]; v != endpoint {\n\t\t\t\t\tt.Errorf(\"%d: want %s, have %s\", i, endpoints[expected], endpoint)\n\t\t\t\t}\n\t\t\t\tcounts[endpoint]++\n\t\t\t}\n\n\t\t\tfor i, have := range counts {\n\t\t\t\tif have != want {\n\t\t\t\t\tt.Errorf(\"%s: want %d, have %d\", i, want, have)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRoundRobinLB_noEndpoints(t *testing.T) {\n\tsubscriber := FixedSubscriber{}\n\tbalancer := NewRoundRobinLB(subscriber)\n\t_, err := balancer.Host()\n\tif want, have := ErrNoHosts, err; want != have {\n\t\tt.Errorf(\"want %v, have %v\", want, have)\n\t}\n}\n\nfunc ExampleNewRandomLB() {\n\tbalancer := NewRandomLB(FixedSubscriber([]string{\"a\", \"b\", \"c\"}))\n\n\t// code required in order to make the test deterministic\n\t{\n\t\tvar counter uint32\n\t\tbalancer.(*randomLB).rand = func(max uint32) uint32 {\n\t\t\tif max != 3 {\n\t\t\t\tfmt.Println(\"unexpected max:\", max)\n\t\t\t}\n\t\t\tdefer func() { counter++ }()\n\t\t\treturn counter % max\n\t\t}\n\t}\n\n\tfor i := 0; i < 5; i++ {\n\t\th, err := balancer.Host()\n\t\tif err != nil {\n\t\t\tfmt.Println(err.Error())\n\t\t\tcontinue\n\t\t}\n\t\tfmt.Println(h)\n\t}\n\n\t// output\n\t// a\n\t// b\n\t// c\n\t// a\n\t// b\n}\n\nfunc TestRandomLB(t *testing.T) {\n\tvar (\n\t\tendpoints  = []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\"}\n\t\tn          = len(endpoints)\n\t\tcounts     = make(map[string]int, n)\n\t\titerations = 1000000\n\t\twant       = iterations / n\n\t\ttolerance  = want / 100 // 1%\n\t)\n\n\tfor _, e := range endpoints {\n\t\tcounts[e] = 0\n\t}\n\n\tsubscriber := FixedSubscriber(endpoints)\n\tbalancer := NewRandomLB(subscriber)\n\n\tfor i := 0; i < iterations; i++ {\n\t\tendpoint, err := balancer.Host()\n\t\tif err != nil {\n\t\t\tt.Fail()\n\t\t}\n\t\tcounts[endpoint]++\n\t}\n\n\tfor i, have := range counts {\n\t\tdelta := int(math.Abs(float64(want - have)))\n\t\tif delta > tolerance {\n\t\t\tt.Errorf(\"%s: want %d, have %d, delta %d > %d tolerance\", i, want, have, delta, tolerance)\n\t\t}\n\t}\n}\n\nfunc TestRandomLB_single(t *testing.T) {\n\tendpoints := []string{\"a\"}\n\titerations := 1000000\n\tsubscriber := FixedSubscriber(endpoints)\n\tbalancer := NewRandomLB(subscriber)\n\n\tfor i := 0; i < iterations; i++ {\n\t\tendpoint, err := balancer.Host()\n\t\tif err != nil {\n\t\t\tt.Fail()\n\t\t}\n\t\tif endpoint != endpoints[0] {\n\t\t\tt.Errorf(\"unexpected host %s\", endpoint)\n\t\t}\n\t}\n}\n\nfunc TestRandomLB_noEndpoints(t *testing.T) {\n\tsubscriber := FixedSubscriberFactory(&config.Backend{})\n\tbalancer := NewRandomLB(subscriber)\n\t_, err := balancer.Host()\n\tif want, have := ErrNoHosts, err; want != have {\n\t\tt.Errorf(\"want %v, have %v\", want, have)\n\t}\n}\n\ntype erroredSubscriber string\n\nfunc (s erroredSubscriber) Hosts() ([]string, error) { return []string{}, errors.New(string(s)) }\n\nfunc TestRoundRobinLB_erroredSubscriber(t *testing.T) {\n\twant := \"supu\"\n\tbalancer := NewRoundRobinLB(erroredSubscriber(want))\n\thost, have := balancer.Host()\n\tif host != \"\" || want != have.Error() {\n\t\tt.Errorf(\"want %s, have %s\", want, have.Error())\n\t}\n}\n\nfunc TestRandomLB_erroredSubscriber(t *testing.T) {\n\twant := \"supu\"\n\tbalancer := NewRandomLB(erroredSubscriber(want))\n\thost, have := balancer.Host()\n\tif host != \"\" || want != have.Error() {\n\t\tt.Errorf(\"want %s, have %s\", want, have.Error())\n\t}\n}\n"
  },
  {
    "path": "sd/register.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage sd\n\nimport (\n\t\"github.com/luraproject/lura/v2/register\"\n)\n\n// GetRegister returns the package register\nfunc GetRegister() *Register {\n\treturn subscriberFactories\n}\n\ntype untypedRegister interface {\n\tRegister(name string, v interface{})\n\tGet(name string) (interface{}, bool)\n}\n\n// Register is a SD register, mapping different SD subscriber factories\n// to their respective name, so they can be accessed by name\ntype Register struct {\n\tdata untypedRegister\n}\n\nfunc initRegister() *Register {\n\treturn &Register{register.NewUntyped()}\n}\n\n// Register adds the SubscriberFactory to the internal register under the given\n// name\nfunc (r *Register) Register(name string, sf SubscriberFactory) error {\n\tr.data.Register(name, sf)\n\treturn nil\n}\n\n// Get returns the SubscriberFactory stored under the given name. It falls back to\n// a FixedSubscriberFactory if there is no factory with that name\nfunc (r *Register) Get(name string) SubscriberFactory {\n\ttmp, ok := r.data.Get(name)\n\tif !ok {\n\t\treturn FixedSubscriberFactory\n\t}\n\tsf, ok := tmp.(SubscriberFactory)\n\tif !ok {\n\t\treturn FixedSubscriberFactory\n\t}\n\treturn sf\n}\n\nvar subscriberFactories = initRegister()\n"
  },
  {
    "path": "sd/register_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage sd\n\nimport (\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n)\n\nfunc TestGetRegister_Register_ok(t *testing.T) {\n\tsf1 := func(*config.Backend) Subscriber {\n\t\treturn SubscriberFunc(func() ([]string, error) { return []string{\"one\"}, nil })\n\t}\n\tsf2 := func(*config.Backend) Subscriber {\n\t\treturn SubscriberFunc(func() ([]string, error) { return []string{\"two\", \"three\"}, nil })\n\t}\n\tif err := GetRegister().Register(\"name1\", sf1); err != nil {\n\t\tt.Error(err)\n\t}\n\tif err := GetRegister().Register(\"name2\", sf2); err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif h, err := GetRegister().Get(\"name1\")(&config.Backend{SD: \"name1\"}).Hosts(); err != nil || len(h) != 1 {\n\t\tt.Error(\"error using the sd name1\")\n\t}\n\n\tif h, err := GetRegister().Get(\"name2\")(&config.Backend{SD: \"name2\"}).Hosts(); err != nil || len(h) != 2 {\n\t\tt.Error(\"error using the sd name2\")\n\t}\n\n\tif h, err := GetRegister().Get(\"name2\")(&config.Backend{SD: \"name2\"}).Hosts(); err != nil || len(h) != 2 {\n\t\tt.Error(\"error using the sd name2\")\n\t}\n\n\tsubscriberFactories = initRegister()\n}\n\nfunc TestGetRegister_Get_unknown(t *testing.T) {\n\tif h, err := GetRegister().Get(\"name\")(&config.Backend{Host: []string{\"name\"}}).Hosts(); err != nil || len(h) != 1 {\n\t\tt.Error(\"error using the default sd\")\n\t}\n}\n\nfunc TestGetRegister_Get_errored(t *testing.T) {\n\tsubscriberFactories.data.Register(\"errored\", true)\n\tif h, err := GetRegister().Get(\"errored\")(&config.Backend{SD: \"errored\", Host: []string{\"name\"}}).Hosts(); err != nil || len(h) != 1 {\n\t\tt.Error(\"error using the default sd\")\n\t}\n\tsubscriberFactories = initRegister()\n}\n"
  },
  {
    "path": "sd/subscriber.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\n\tPackage sd defines some interfaces and implementations for service discovery\n*/\npackage sd\n\nimport (\n\t\"math/rand\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n)\n\n// Subscriber keeps the set of backend hosts up to date\ntype Subscriber interface {\n\tHosts() ([]string, error)\n}\n\n// SubscriberFunc type is an adapter to allow the use of ordinary functions as subscribers.\n// If f is a function with the appropriate signature, SubscriberFunc(f) is a Subscriber that calls f.\ntype SubscriberFunc func() ([]string, error)\n\n// Hosts implements the Subscriber interface by executing the wrapped function\nfunc (f SubscriberFunc) Hosts() ([]string, error) { return f() }\n\n// FixedSubscriber has a constant set of backend hosts and they never get updated\ntype FixedSubscriber []string\n\n// Hosts implements the subscriber interface\nfunc (s FixedSubscriber) Hosts() ([]string, error) { return s, nil }\n\n// SubscriberFactory builds subscribers with the received config\ntype SubscriberFactory func(*config.Backend) Subscriber\n\n// FixedSubscriberFactory builds a FixedSubscriber with the received config\nfunc FixedSubscriberFactory(cfg *config.Backend) Subscriber {\n\treturn FixedSubscriber(cfg.Host)\n}\n\n// NewRandomFixedSubscriber randomizes a list of hosts and builds a FixedSubscriber with it\nfunc NewRandomFixedSubscriber(hosts []string) FixedSubscriber {\n\tres := make([]string, len(hosts))\n\tj := 0\n\tfor _, i := range rand.Perm(len(hosts)) {\n\t\tres[j] = hosts[i]\n\t\tj++\n\t}\n\treturn FixedSubscriber(res)\n}\n"
  },
  {
    "path": "test/doc.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\n   Package test contains the integration tests for the KrakenD framework\n*/\npackage test\n"
  },
  {
    "path": "test/integration_test.go",
    "content": "//go:build integration || !race\n// +build integration !race\n\n// SPDX-License-Identifier: Apache-2.0\n\npackage test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"text/template\"\n\t\"time\"\n\n\tginlib \"github.com/gin-gonic/gin\"\n\t\"github.com/urfave/negroni/v2\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/proxy\"\n\t\"github.com/luraproject/lura/v2/router/chi\"\n\t\"github.com/luraproject/lura/v2/router/gin\"\n\t\"github.com/luraproject/lura/v2/router/gorilla\"\n\t\"github.com/luraproject/lura/v2/router/httptreemux\"\n\tluranegroni \"github.com/luraproject/lura/v2/router/negroni\"\n\t\"github.com/luraproject/lura/v2/transport/http/server\"\n)\n\nvar localhostIP string\n\nfunc init() {\n\tln, err := net.Listen(\"tcp\", \":8080\")\n\tif err != nil {\n\t\treturn\n\t}\n\n\tgo func() {\n\t\tconn, _ := net.Dial(\"tcp\", \"localhost:8080\")\n\t\t<-time.After(5 * time.Second)\n\t\tconn.Close()\n\t}()\n\n\tconn, err := ln.Accept()\n\tif err != nil {\n\t\treturn\n\t}\n\th, _, err := net.SplitHostPort(conn.RemoteAddr().String())\n\tif err == nil {\n\t\tlocalhostIP = h\n\t}\n\tconn.Close()\n}\n\nfunc TestKrakenD_ginRouter(t *testing.T) {\n\tginlib.SetMode(ginlib.TestMode)\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\ttestKrakenD(t, func(logger logging.Logger, cfg *config.ServiceConfig) {\n\t\tif cfg.ExtraConfig == nil {\n\t\t\tcfg.ExtraConfig = map[string]interface{}{}\n\t\t}\n\t\tcfg.ExtraConfig[gin.Namespace] = map[string]interface{}{\n\t\t\t\"trusted_proxies\":        []interface{}{\"127.0.0.1/32\", \"::1\"},\n\t\t\t\"remote_ip_headers\":      []interface{}{\"x-forwarded-for\"},\n\t\t\t\"forwarded_by_client_ip\": true,\n\t\t\t\"return_error_msg\":       true,\n\t\t}\n\n\t\tignoredChan := make(chan string)\n\t\topts := gin.EngineOptions{\n\t\t\tLogger: logger,\n\t\t\tWriter: io.Discard,\n\t\t\tHealth: (<-chan string)(ignoredChan),\n\t\t}\n\n\t\tgin.NewFactory(\n\t\t\tgin.Config{\n\t\t\t\tEngine:         gin.NewEngine(*cfg, opts),\n\t\t\t\tMiddlewares:    []ginlib.HandlerFunc{},\n\t\t\t\tHandlerFactory: gin.EndpointHandler,\n\t\t\t\tProxyFactory:   proxy.DefaultFactory(logger),\n\t\t\t\tLogger:         logger,\n\t\t\t\tRunServer:      server.RunServer,\n\t\t\t},\n\t\t).NewWithContext(ctx).Run(*cfg)\n\t})\n}\n\nfunc TestKrakenD_gorillaRouter(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tconfig.RoutingPattern = config.BracketsRouterPatternBuilder\n\ttestKrakenD(t, func(logger logging.Logger, cfg *config.ServiceConfig) {\n\t\tgorilla.DefaultFactory(proxy.DefaultFactory(logger), logger).NewWithContext(ctx).Run(*cfg)\n\t})\n\tconfig.RoutingPattern = config.ColonRouterPatternBuilder\n}\n\nfunc TestKrakenD_negroniRouter(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tconfig.RoutingPattern = config.BracketsRouterPatternBuilder\n\ttestKrakenD(t, func(logger logging.Logger, cfg *config.ServiceConfig) {\n\t\tfactory := luranegroni.DefaultFactory(proxy.DefaultFactory(logger), logger, []negroni.Handler{})\n\t\tfactory.NewWithContext(ctx).Run(*cfg)\n\t})\n\tconfig.RoutingPattern = config.ColonRouterPatternBuilder\n}\n\nfunc TestKrakenD_httptreemuxRouter(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\ttestKrakenD(t, func(logger logging.Logger, cfg *config.ServiceConfig) {\n\t\thttptreemux.DefaultFactory(proxy.DefaultFactory(logger), logger).NewWithContext(ctx).Run(*cfg)\n\t})\n}\n\nfunc TestKrakenD_chiRouter(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tconfig.RoutingPattern = config.BracketsRouterPatternBuilder\n\ttestKrakenD(t, func(logger logging.Logger, cfg *config.ServiceConfig) {\n\t\tchi.DefaultFactory(proxy.DefaultFactory(logger), logger).NewWithContext(ctx).Run(*cfg)\n\t})\n\tconfig.RoutingPattern = config.ColonRouterPatternBuilder\n}\n\nfunc testKrakenD(t *testing.T, runRouter func(logging.Logger, *config.ServiceConfig)) {\n\tcfg, err := setupBackend(t)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\tlogger := logging.NoOp\n\tgo runRouter(logger, cfg)\n\n\t<-time.After(300 * time.Millisecond)\n\n\tdefaultHeaders := map[string]string{\n\t\t\"Content-Type\":        \"application/json\",\n\t\t\"X-KrakenD-Completed\": \"true\",\n\t\t\"X-Krakend\":           \"Version undefined\",\n\t}\n\n\tincompleteHeader := map[string]string{\n\t\t\"Content-Type\":        \"application/json\",\n\t\t\"X-KrakenD-Completed\": \"false\",\n\t\t\"X-Krakend\":           \"Version undefined\",\n\t}\n\n\tfor _, tc := range []struct {\n\t\tname          string\n\t\turl           string\n\t\tmethod        string\n\t\theaders       map[string]string\n\t\tbody          string\n\t\texpBody       string\n\t\texpHeaders    map[string]string\n\t\texpStatusCode int\n\t}{\n\t\t{\n\t\t\tname:       \"static\",\n\t\t\turl:        \"/static\",\n\t\t\theaders:    map[string]string{},\n\t\t\texpHeaders: incompleteHeader,\n\t\t\texpBody:    `{\"bar\":\"foobar\",\"foo\":42}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"param_forwarding\",\n\t\t\turl:    \"/param_forwarding/foo/constant/bar\",\n\t\t\tmethod: \"POST\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"Content-Type\":  \"application/json\",\n\t\t\t\t\"Authorization\": \"bearer AuthorizationToken\",\n\t\t\t\t\"X-Y-Z\":         \"x-y-z\",\n\t\t\t},\n\t\t\tbody:       `{\"foo\":\"bar\"}`,\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    `{\"path\":\"/foo/bar\"}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"param_forwarding_2\",\n\t\t\turl:    \"/param_forwarding/foo/constant/foobar\",\n\t\t\tmethod: \"POST\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"Content-Type\":  \"application/json\",\n\t\t\t\t\"Authorization\": \"bearer AuthorizationToken\",\n\t\t\t\t\"X-Y-Z\":         \"x-y-z\",\n\t\t\t},\n\t\t\tbody:       `{\"foo\":\"bar\"}`,\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    `{\"path\":\"/foo/foobar\"}`,\n\t\t},\n\t\t{\n\t\t\tname:       \"timeout\",\n\t\t\turl:        \"/timeout\",\n\t\t\theaders:    map[string]string{},\n\t\t\texpHeaders: incompleteHeader,\n\t\t\texpBody:    `{\"email\":\"some@email.com\",\"name\":\"a\"}`,\n\t\t},\n\t\t{\n\t\t\tname:       \"partial_with_static\",\n\t\t\turl:        \"/partial/static\",\n\t\t\theaders:    map[string]string{},\n\t\t\texpHeaders: incompleteHeader,\n\t\t\texpBody:    `{\"bar\":\"foobar\",\"email\":\"some@email.com\",\"foo\":42,\"name\":\"a\"}`,\n\t\t},\n\t\t{\n\t\t\tname:       \"partial\",\n\t\t\turl:        \"/partial\",\n\t\t\theaders:    map[string]string{},\n\t\t\texpHeaders: incompleteHeader,\n\t\t\texpBody:    `{\"email\":\"some@email.com\",\"name\":\"a\"}`,\n\t\t},\n\t\t{\n\t\t\tname:       \"combination\",\n\t\t\turl:        \"/combination\",\n\t\t\theaders:    map[string]string{},\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    `{\"name\":\"a\",\"personal_email\":\"some@email.com\",\"posts\":[{\"body\":\"some content\",\"date\":\"123456789\"},{\"body\":\"some other content\",\"date\":\"123496789\"}]}`,\n\t\t},\n\t\t{\n\t\t\tname:       \"detail_error\",\n\t\t\turl:        \"/detail_error\",\n\t\t\theaders:    map[string]string{},\n\t\t\texpHeaders: incompleteHeader,\n\t\t\texpBody:    `{\"email\":\"some@email.com\",\"error_backend_a\":{\"http_status_code\":429,\"http_body\":\"sad panda\\n\",\"http_body_encoding\":\"text/plain; charset=utf-8\"},\"name\":\"a\"}`,\n\t\t},\n\t\t{\n\t\t\tname:       \"querystring-params-no-params\",\n\t\t\turl:        \"/querystring-params-test/no-params?a=1&b=2&c=3\",\n\t\t\theaders:    map[string]string{},\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    fmt.Sprintf(`{\"headers\":{\"Accept-Encoding\":[\"gzip\"],\"User-Agent\":[\"KrakenD Version undefined\"],\"X-Forwarded-Host\":[\"localhost:%d\"]},\"path\":\"/no-params\",\"query\":{}}`, cfg.Port),\n\t\t},\n\t\t{\n\t\t\tname:       \"querystring-params-optional-query-params\",\n\t\t\turl:        \"/querystring-params-test/query-params?a=1&b=2&c=3\",\n\t\t\theaders:    map[string]string{},\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    fmt.Sprintf(`{\"headers\":{\"Accept-Encoding\":[\"gzip\"],\"User-Agent\":[\"KrakenD Version undefined\"],\"X-Forwarded-Host\":[\"localhost:%d\"]},\"path\":\"/query-params\",\"query\":{\"a\":[\"1\"],\"b\":[\"2\"]}}`, cfg.Port),\n\t\t},\n\t\t{\n\t\t\tname:       \"querystring-params-mandatory-query-params\",\n\t\t\turl:        \"/querystring-params-test/url-params/some?a=1&b=2&c=3\",\n\t\t\theaders:    map[string]string{},\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    fmt.Sprintf(`{\"headers\":{\"Accept-Encoding\":[\"gzip\"],\"User-Agent\":[\"KrakenD Version undefined\"],\"X-Forwarded-Host\":[\"localhost:%d\"]},\"path\":\"/url-params\",\"query\":{\"p\":[\"some\"]}}`, cfg.Port),\n\t\t},\n\t\t{\n\t\t\tname:       \"querystring-params-all\",\n\t\t\turl:        \"/querystring-params-test/all-params?a=1&b=2&c=3\",\n\t\t\theaders:    map[string]string{},\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    fmt.Sprintf(`{\"headers\":{\"Accept-Encoding\":[\"gzip\"],\"User-Agent\":[\"KrakenD Version undefined\"],\"X-Forwarded-Host\":[\"localhost:%d\"]},\"path\":\"/all-params\",\"query\":{\"a\":[\"1\"],\"b\":[\"2\"],\"c\":[\"3\"]}}`, cfg.Port),\n\t\t},\n\t\t{\n\t\t\tname: \"header-params-none\",\n\t\t\turl:  \"/header-params-test/no-params\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"x-Test-1\": \"some\",\n\t\t\t\t\"X-TEST-2\": \"none\",\n\t\t\t},\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    fmt.Sprintf(`{\"headers\":{\"Accept-Encoding\":[\"gzip\"],\"User-Agent\":[\"KrakenD Version undefined\"],\"X-Forwarded-Host\":[\"localhost:%d\"]},\"path\":\"/no-params\",\"query\":{}}`, cfg.Port),\n\t\t},\n\t\t{\n\t\t\tname: \"header-params-filter\",\n\t\t\turl:  \"/header-params-test/filter-params\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"x-tESt-1\": \"some\",\n\t\t\t\t\"X-TEST-2\": \"none\",\n\t\t\t},\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    fmt.Sprintf(`{\"headers\":{\"Accept-Encoding\":[\"gzip\"],\"User-Agent\":[\"KrakenD Version undefined\"],\"X-Forwarded-Host\":[\"localhost:%d\"],\"X-Test-1\":[\"some\"]},\"path\":\"/filter-params\",\"query\":{}}`, cfg.Port),\n\t\t},\n\t\t{\n\t\t\tname: \"header-params-all\",\n\t\t\turl:  \"/header-params-test/all-params\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"x-Test-1\":   \"some\",\n\t\t\t\t\"X-TEST-2\":   \"none\",\n\t\t\t\t\"User-Agent\": \"KrakenD Test\",\n\t\t\t},\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    fmt.Sprintf(`{\"headers\":{\"Accept-Encoding\":[\"gzip\"],\"User-Agent\":[\"KrakenD Test\"],\"X-Forwarded-Host\":[\"localhost:%d\"],\"X-Forwarded-Via\":[\"KrakenD Version undefined\"],\"X-Test-1\":[\"some\"],\"X-Test-2\":[\"none\"]},\"path\":\"/all-params\",\"query\":{}}`, cfg.Port),\n\t\t},\n\t\t{\n\t\t\tname:       \"sequential ok\",\n\t\t\turl:        \"/sequential/ok/foo\",\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    `{\"first\":{\"path\":\"/provider/foo\",\"random\":42},\"second\":{\"path\":\"/recipient/42\",\"random\":42}}`,\n\t\t},\n\t\t{\n\t\t\tname: \"sequential ko first\",\n\t\t\turl:  \"/sequential/ko/first/foo\",\n\t\t\texpHeaders: map[string]string{\n\t\t\t\t\"X-KrakenD-Completed\": \"false\",\n\t\t\t\t\"X-Krakend\":           \"Version undefined\",\n\t\t\t},\n\t\t\texpStatusCode: 500,\n\t\t},\n\t\t{\n\t\t\tname:       \"sequential ko last\",\n\t\t\turl:        \"/sequential/ko/last/foo\",\n\t\t\texpHeaders: incompleteHeader,\n\t\t\texpBody:    `{\"random\":42}`,\n\t\t},\n\t\t{\n\t\t\tname:       \"redirect\",\n\t\t\turl:        \"/redirect\",\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    `{\"path\":\"/\",\"random\":42}`,\n\t\t},\n\t\t{\n\t\t\tname:       \"found\",\n\t\t\turl:        \"/found\",\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    `{\"path\":\"/\",\"random\":42}`,\n\t\t},\n\t\t{\n\t\t\tname:       \"flatmap del\",\n\t\t\turl:        \"/flatmap/delete\",\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    `{\"collection\":[{\"body\":\"some content\"},{\"body\":\"some other content\"}]}`,\n\t\t},\n\t\t{\n\t\t\tname:       \"flatmap rename\",\n\t\t\turl:        \"/flatmap/rename\",\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    `{\"collection\":[{\"body\":\"some content\",\"created_at\":\"123456789\"},{\"body\":\"some other content\",\"created_at\":\"123496789\"}]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"x-forwarded-for\",\n\t\t\turl:  \"/x-forwarded-for\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"x-forwarded-for\": \"123.45.67.89\",\n\t\t\t},\n\t\t\texpHeaders: defaultHeaders,\n\t\t\texpBody:    fmt.Sprintf(`{\"headers\":{\"Accept-Encoding\":[\"gzip\"],\"User-Agent\":[\"KrakenD Version undefined\"],\"X-Forwarded-For\":[\"123.45.67.89\"],\"X-Forwarded-Host\":[\"localhost:%d\"]}}`, cfg.Port),\n\t\t},\n\t\t{\n\t\t\tmethod:     \"PUT\",\n\t\t\tname:       \"sequence-accept\",\n\t\t\turl:        \"/sequence-accept\",\n\t\t\texpHeaders: defaultHeaders,\n\t\t},\n\t\t{\n\t\t\tmethod:        \"GET\",\n\t\t\tname:          \"error-status-code-1\",\n\t\t\turl:           \"/error-status-code/1\",\n\t\t\texpStatusCode: 200,\n\t\t},\n\t\t{\n\t\t\tmethod:        \"GET\",\n\t\t\tname:          \"error-status-code-2\",\n\t\t\turl:           \"/error-status-code/2\",\n\t\t\texpStatusCode: 429,\n\t\t},\n\t\t{\n\t\t\tmethod:        \"GET\",\n\t\t\tname:          \"error-status-code-3\",\n\t\t\turl:           \"/error-status-code/3\",\n\t\t\texpStatusCode: 200,\n\t\t},\n\t\t{\n\t\t\tmethod:        \"POST\",\n\t\t\tname:          \"multipost_parallel\",\n\t\t\turl:           \"/multipost/parallel/foo\",\n\t\t\tbody:          `{\"foo\":\"bar\"}`,\n\t\t\texpStatusCode: 200,\n\t\t\texpHeaders:    defaultHeaders,\n\t\t\texpBody:       fmt.Sprintf(`{\"first\":{\"body\":\"{\\\"foo\\\":\\\"bar\\\"}\",\"headers\":{\"Accept-Encoding\":[\"gzip\"],\"User-Agent\":[\"KrakenD Version undefined\"],\"X-Forwarded-For\":[\"`+localhostIP+`\"],\"X-Forwarded-Host\":[\"localhost:%d\"]},\"method\":\"POST\",\"url\":\"/provider/foo\"},\"second\":{\"body\":\"{\\\"foo\\\":\\\"bar\\\"}\",\"headers\":{\"Accept-Encoding\":[\"gzip\"],\"User-Agent\":[\"KrakenD Version undefined\"],\"X-Forwarded-For\":[\"`+localhostIP+`\"],\"X-Forwarded-Host\":[\"localhost:%d\"]},\"method\":\"POST\",\"url\":\"/recipient/foo\"}}`, cfg.Port, cfg.Port),\n\t\t},\n\t\t{\n\t\t\tmethod:        \"POST\",\n\t\t\tname:          \"multipost_sequential\",\n\t\t\turl:           \"/multipost/sequential/foo\",\n\t\t\tbody:          `{\"foo\":\"bar\"}`,\n\t\t\texpStatusCode: 200,\n\t\t\texpHeaders:    defaultHeaders,\n\t\t\texpBody:       fmt.Sprintf(`{\"first\":{\"path\":\"/provider/foo\",\"random\":42},\"second\":{\"body\":\"{\\\"foo\\\":\\\"bar\\\"}\",\"headers\":{\"Accept-Encoding\":[\"gzip\"],\"User-Agent\":[\"KrakenD Version undefined\"],\"X-Forwarded-For\":[\"`+localhostIP+`\"],\"X-Forwarded-Host\":[\"localhost:%d\"]},\"method\":\"POST\",\"url\":\"/recipient/42\"},\"third\":{\"body\":\"{\\\"foo\\\":\\\"bar\\\"}\",\"headers\":{\"Accept-Encoding\":[\"gzip\"],\"User-Agent\":[\"KrakenD Version undefined\"],\"X-Forwarded-For\":[\"`+localhostIP+`\"],\"X-Forwarded-Host\":[\"localhost:%d\"]},\"method\":\"POST\",\"url\":\"/recipient/42\"}}`, cfg.Port, cfg.Port),\n\t\t},\n\t} {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif tc.method == \"\" {\n\t\t\t\ttc.method = \"GET\"\n\t\t\t}\n\n\t\t\tvar body io.Reader\n\t\t\tif tc.body != \"\" {\n\t\t\t\tbody = bytes.NewBufferString(tc.body)\n\t\t\t}\n\n\t\t\turl := fmt.Sprintf(\"http://localhost:%d%s\", cfg.Port, tc.url)\n\n\t\t\tr, _ := http.NewRequest(tc.method, url, body)\n\t\t\tfor k, v := range tc.headers {\n\t\t\t\tr.Header.Add(k, v)\n\t\t\t}\n\n\t\t\tresp, err := http.DefaultClient.Do(r)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif resp == nil {\n\t\t\t\tt.Errorf(\"%s: nil response\", resp.Request.URL.Path)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\texpectedStatusCode := http.StatusOK\n\t\t\tif tc.expStatusCode != 0 {\n\t\t\t\texpectedStatusCode = tc.expStatusCode\n\t\t\t}\n\t\t\tif resp.StatusCode != expectedStatusCode {\n\t\t\t\tt.Errorf(\"%s: unexpected status code. have: %d, want: %d\", resp.Request.URL.Path, resp.StatusCode, expectedStatusCode)\n\t\t\t}\n\n\t\t\tfor k, v := range tc.expHeaders {\n\t\t\t\tif c := resp.Header.Get(k); !strings.Contains(c, v) {\n\t\t\t\t\tt.Errorf(\"%s: unexpected header %s: %s\", resp.Request.URL.Path, k, c)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif tc.expBody == \"\" {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tb, _ := io.ReadAll(resp.Body)\n\t\t\tresp.Body.Close()\n\t\t\tif tc.expBody != string(b) {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"%s: unexpected body: %s\\n\\t%s was expecting: %s\",\n\t\t\t\t\tresp.Request.URL.Path,\n\t\t\t\t\tstring(b),\n\t\t\t\t\tresp.Request.URL.Path,\n\t\t\t\t\ttc.expBody,\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc setupBackend(t *testing.T) (*config.ServiceConfig, error) {\n\tdata := map[string]interface{}{\"port\": rand.Intn(2000) + 8080}\n\n\t// param forwarding validation backend\n\tb1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\tif c := r.Header.Get(\"Content-Type\"); c != \"application/json\" {\n\t\t\tt.Errorf(\"unexpected header content-type: %s\", c)\n\t\t\thttp.Error(rw, \"bad content-type\", 400)\n\t\t\treturn\n\t\t}\n\t\tif c := r.Header.Get(\"Authorization\"); c != \"bearer AuthorizationToken\" {\n\t\t\tt.Errorf(\"unexpected header Authorization: %s\", c)\n\t\t\thttp.Error(rw, \"bad Authorization\", 400)\n\t\t\treturn\n\t\t}\n\t\tif c := r.Header.Get(\"X-Y-Z\"); c != \"x-y-z\" {\n\t\t\tt.Errorf(\"unexpected header X-Y-Z: %s\", c)\n\t\t\thttp.Error(rw, \"bad X-Y-Z\", 400)\n\t\t\treturn\n\t\t}\n\t\tbody, err := io.ReadAll(r.Body)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t\tif string(body) != `{\"foo\":\"bar\"}` {\n\t\t\tt.Errorf(\"unexpected request body: %s\", string(body))\n\t\t\treturn\n\t\t}\n\t\trw.Header().Add(\"Content-Type\", \"application/json\")\n\t\tjson.NewEncoder(rw).Encode(map[string]interface{}{\"path\": r.URL.Path})\n\t}))\n\tdata[\"b1\"] = b1.URL\n\n\t// collection generator backend\n\tb2 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\trw.Header().Add(\"Content-Type\", \"application/json\")\n\t\tjson.NewEncoder(rw).Encode([]interface{}{\n\t\t\tmap[string]interface{}{\"body\": \"some content\", \"date\": \"123456789\"},\n\t\t\tmap[string]interface{}{\"body\": \"some other content\", \"date\": \"123496789\"},\n\t\t})\n\t}))\n\tdata[\"b2\"] = b2.URL\n\n\t// regular struct generator backend\n\tb3 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\trw.Header().Add(\"Content-Type\", \"application/json\")\n\t\tjson.NewEncoder(rw).Encode(map[string]interface{}{\"email\": \"some@email.com\", \"name\": \"a\"})\n\t}))\n\tdata[\"b3\"] = b3.URL\n\n\t// crasher backend\n\tb4 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\thttp.Error(rw, \"sad panda\", http.StatusTooManyRequests)\n\t}))\n\tdata[\"b4\"] = b4.URL\n\n\t// slow backend\n\tb5 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\t<-time.After(time.Second)\n\t\trw.Header().Add(\"Content-Type\", \"application/json\")\n\t\tjson.NewEncoder(rw).Encode(map[string]interface{}{\"email\": \"some@email.com\", \"name\": \"a\"})\n\t}))\n\tdata[\"b5\"] = b5.URL\n\n\t// querystring-forwarding backend\n\tb6 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\trw.Header().Add(\"Content-Type\", \"application/json\")\n\t\tif ip := net.ParseIP(r.Header.Get(\"X-Forwarded-For\")); ip == nil || !ip.IsLoopback() {\n\t\t\thttp.Error(rw, \"invalid X-Forwarded-For\", 400)\n\t\t\treturn\n\t\t}\n\t\tr.Header.Del(\"X-Forwarded-For\")\n\t\tjson.NewEncoder(rw).Encode(map[string]interface{}{\n\t\t\t\"path\":    r.URL.Path,\n\t\t\t\"query\":   r.URL.Query(),\n\t\t\t\"headers\": r.Header,\n\t\t})\n\t}))\n\tdata[\"b6\"] = b6.URL\n\n\t// path validator\n\tb7 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\trw.Header().Add(\"Content-Type\", \"application/json\")\n\t\tjson.NewEncoder(rw).Encode(map[string]interface{}{\"path\": r.URL.Path, \"random\": 42})\n\t}))\n\tdata[\"b7\"] = b7.URL\n\n\t// redirect\n\tb8 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\thttp.Redirect(rw, r, b7.URL, http.StatusMovedPermanently)\n\t}))\n\tdata[\"b8\"] = b8.URL\n\n\t// found\n\tb9 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\thttp.Redirect(rw, r, b7.URL, http.StatusFound)\n\t}))\n\tdata[\"b9\"] = b9.URL\n\n\t// X-Forwarded-For backend\n\tb11 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\trw.Header().Add(\"Content-Type\", \"application/json\")\n\t\tjson.NewEncoder(rw).Encode(map[string]interface{}{\n\t\t\t\"headers\": r.Header,\n\t\t})\n\t}))\n\tdata[\"b11\"] = b11.URL\n\n\t// Echo backend\n\tb12 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\trw.Header().Add(\"Content-Type\", \"application/json\")\n\t\tb, _ := io.ReadAll(r.Body)\n\t\tr.Body.Close()\n\t\tjson.NewEncoder(rw).Encode(map[string]interface{}{\n\t\t\t\"headers\": r.Header,\n\t\t\t\"body\":    string(b),\n\t\t\t\"url\":     r.URL.String(),\n\t\t\t\"method\":  r.Method,\n\t\t})\n\t}))\n\tdata[\"b12\"] = b12.URL\n\n\tc, err := loadConfig(data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn c, nil\n}\n\nfunc loadConfig(data map[string]interface{}) (*config.ServiceConfig, error) {\n\tcontent, _ := os.ReadFile(\"lura.json\")\n\ttmpl, err := template.New(\"test\").Parse(string(content))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbuf := new(bytes.Buffer)\n\tif err = tmpl.Execute(buf, data); err != nil {\n\t\treturn nil, err\n\t}\n\n\tc, err := config.NewParserWithFileReader(func(s string) ([]byte, error) {\n\t\treturn []byte(s), nil\n\t}).Parse(buf.String())\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &c, nil\n}\n"
  },
  {
    "path": "transport/http/client/executor.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\n\tPackage client provides some http helpers to create http clients and executors\n*/\npackage client\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\n// HTTPRequestExecutor defines the interface of the request executor for the HTTP transport protocol\ntype HTTPRequestExecutor func(ctx context.Context, req *http.Request) (*http.Response, error)\n\n// DefaultHTTPRequestExecutor creates a HTTPRequestExecutor with the received HTTPClientFactory\nfunc DefaultHTTPRequestExecutor(clientFactory HTTPClientFactory) HTTPRequestExecutor {\n\treturn func(ctx context.Context, req *http.Request) (*http.Response, error) {\n\t\treturn clientFactory(ctx).Do(req.WithContext(ctx))\n\t}\n}\n\n// HTTPClientFactory creates http clients based with the received context\ntype HTTPClientFactory func(ctx context.Context) *http.Client\n\n// NewHTTPClient just returns the http default client\nfunc NewHTTPClient(_ context.Context) *http.Client { return defaultHTTPClient }\n\nvar defaultHTTPClient = &http.Client{}\n"
  },
  {
    "path": "transport/http/client/executor_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestDefaultHTTPRequestExecutor(t *testing.T) {\n\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprintln(w, \"Hello, client\")\n\t}))\n\tdefer ts.Close()\n\n\tre := DefaultHTTPRequestExecutor(NewHTTPClient)\n\n\treq, _ := http.NewRequest(\"GET\", ts.URL, io.NopCloser(&bytes.Buffer{}))\n\n\tresp, err := re(context.Background(), req)\n\n\tif err != nil {\n\t\tt.Error(\"unexpected error:\", err.Error())\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Error(\"unexpected status code:\", resp.StatusCode)\n\t}\n}\n"
  },
  {
    "path": "transport/http/client/graphql/graphql.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\nPackage graphql offers a param extractor and basic types for building GraphQL requests\n*/\npackage graphql\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n)\n\n// Namespace is the key for the backend's extra config\nconst Namespace = \"github.com/devopsfaith/krakend/transport/http/client/graphql\"\n\n// OperationType contains all the operations allowed by graphql\ntype OperationType string\n\n// OperationMethod details the method to be used with the request\ntype OperationMethod string\n\nconst (\n\t// OperationMutation marks an operation as a mutation\n\tOperationMutation OperationType = \"mutation\"\n\t// OperationQuery marks an operation as a query\n\tOperationQuery OperationType = \"query\"\n\n\tMethodPost OperationMethod = http.MethodPost\n\tMethodGet  OperationMethod = http.MethodGet\n)\n\n// GraphQLRequest represents the graphql request body\ntype GraphQLRequest struct {\n\tQuery         string                 `json:\"query\"`\n\tOperationName string                 `json:\"operationName,omitempty\"`\n\tVariables     map[string]interface{} `json:\"variables,omitempty\"`\n}\n\n// Options defines a GraphQLRequest with a type, so the middlewares know what to do\ntype Options struct {\n\tGraphQLRequest\n\tQueryPath string          `json:\"query_path,omitempty\"`\n\tType      OperationType   `json:\"type\"`\n\tMethod    OperationMethod `json:\"method\"`\n}\n\nvar ErrNoConfigFound = errors.New(\"grapghql: no configuration found\")\n\n// GetOptions extracts the Options config from the backend's extra config\nfunc GetOptions(cfg config.ExtraConfig) (*Options, error) {\n\ttmp, ok := cfg[Namespace]\n\tif !ok {\n\t\treturn nil, ErrNoConfigFound\n\t}\n\n\tb, err := json.Marshal(tmp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar opt Options\n\tif err := json.Unmarshal(b, &opt); err != nil {\n\t\treturn nil, err\n\t}\n\n\topt.Method = OperationMethod(strings.ToUpper(string(opt.Method)))\n\topt.Type = OperationType(strings.ToLower(string(opt.Type)))\n\n\tif opt.Method != MethodGet && opt.Method != MethodPost {\n\t\topt.Method = MethodPost\n\t}\n\n\tif opt.QueryPath != \"\" {\n\t\tq, err := os.ReadFile(opt.QueryPath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\topt.Query = string(q)\n\t}\n\n\treturn &opt, nil\n}\n\n// New resturns a new Extractor, ready to be use on a middleware\nfunc New(opt Options) *Extractor {\n\tvar replacements [][2]string\n\n\ttitle := cases.Title(language.Und)\n\tfor k, v := range opt.Variables {\n\t\tval, ok := v.(string)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tif val[0] == '{' && val[len(val)-1] == '}' {\n\t\t\treplacements = append(replacements, [2]string{k, title.String(val[1:2]) + val[2:len(val)-1]})\n\t\t}\n\t}\n\n\tif len(replacements) == 0 {\n\t\tb, _ := json.Marshal(opt.GraphQLRequest)\n\n\t\treturn &Extractor{\n\t\t\tcfg: opt,\n\t\t\tparamExtractor: func(map[string]string) (*GraphQLRequest, error) {\n\t\t\t\treturn &opt.GraphQLRequest, nil\n\t\t\t},\n\t\t\tnewBody: func(_ map[string]string) ([]byte, error) {\n\t\t\t\treturn b, nil\n\t\t\t},\n\t\t}\n\t}\n\n\tparamExtractor := func(params map[string]string) (*GraphQLRequest, error) {\n\t\tval := GraphQLRequest{\n\t\t\tQuery:         opt.Query,\n\t\t\tOperationName: opt.OperationName,\n\t\t\tVariables:     map[string]interface{}{},\n\t\t}\n\t\tfor k, v := range opt.Variables {\n\t\t\tval.Variables[k] = v\n\t\t}\n\t\tfor _, vs := range replacements {\n\t\t\tval.Variables[vs[0]] = params[vs[1]]\n\t\t}\n\t\treturn &val, nil\n\t}\n\n\treturn &Extractor{\n\t\tcfg:            opt,\n\t\tparamExtractor: paramExtractor,\n\t\tnewBody: func(params map[string]string) ([]byte, error) {\n\t\t\tval, err := paramExtractor(params)\n\t\t\tif err != nil {\n\t\t\t\treturn []byte{}, err\n\t\t\t}\n\t\t\treturn json.Marshal(val)\n\t\t},\n\t}\n}\n\n// Extractor exposes two extractor factories: one for the params (query) and one\n// for the request body (mutator)\ntype Extractor struct {\n\tcfg            Options\n\tparamExtractor func(map[string]string) (*GraphQLRequest, error)\n\tnewBody        func(map[string]string) ([]byte, error)\n}\n\n// QueryFromBody returns a url.Values containing the graphql request with the given query and the default variables\n// overiden by the request body\nfunc (e *Extractor) QueryFromBody(r io.Reader) (url.Values, error) {\n\tgr, err := e.fromBody(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvars := url.Values{}\n\n\tvars.Add(\"query\", gr.Query)\n\tif gr.OperationName != \"\" {\n\t\tvars.Add(\"operationName\", gr.OperationName)\n\t}\n\tif len(gr.Variables) != 0 {\n\t\tencodedVars, _ := json.Marshal(gr.Variables)\n\t\tvars.Add(\"variables\", string(encodedVars))\n\t}\n\n\treturn vars, nil\n}\n\n// BodyFromBody returns a request body containing the graphql request with the given query and the default variables\n// overiden by the request body\nfunc (e *Extractor) BodyFromBody(r io.Reader) ([]byte, error) {\n\tv, err := e.fromBody(r)\n\tif err != nil {\n\t\treturn []byte{}, err\n\t}\n\treturn json.Marshal(v)\n}\n\nfunc (e *Extractor) fromBody(r io.Reader) (*GraphQLRequest, error) {\n\tb, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvars := map[string]interface{}{}\n\n\tif err := json.Unmarshal(b, &vars); err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor k, v := range e.cfg.Variables {\n\t\tif _, ok := vars[k]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tvars[k] = v\n\t}\n\n\treturn &GraphQLRequest{\n\t\tQuery:         e.cfg.Query,\n\t\tOperationName: e.cfg.OperationName,\n\t\tVariables:     vars,\n\t}, nil\n}\n\n// QueryFromParams returns a url.Values containing the grapql request generated for the given query and the default\n// variables overiden by the request params\nfunc (e *Extractor) QueryFromParams(params map[string]string) (url.Values, error) {\n\tgr, err := e.paramExtractor(params)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvars := url.Values{}\n\n\tvars.Add(\"query\", gr.Query)\n\tif gr.OperationName != \"\" {\n\t\tvars.Add(\"operationName\", gr.OperationName)\n\t}\n\tif len(gr.Variables) != 0 {\n\t\tencodedVars, _ := json.Marshal(gr.Variables)\n\t\tvars.Add(\"variables\", string(encodedVars))\n\t}\n\n\treturn vars, nil\n}\n\n// BodyFromParams returns a request body containing the grapql request generated for the given query and the default\n// variables overiden by the request params\nfunc (e *Extractor) BodyFromParams(params map[string]string) ([]byte, error) {\n\treturn e.newBody(params)\n}\n"
  },
  {
    "path": "transport/http/client/graphql/graphql_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage graphql\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n)\n\nfunc ExampleExtractor() {\n\tcfg, err := GetOptions(config.ExtraConfig{\n\t\tNamespace: map[string]interface{}{\n\t\t\t\"type\":  OperationQuery,\n\t\t\t\"query\": \"{\\n  find_follower(func: uid(\\\"0x3\\\")) {\\n    name \\n    }\\n  }\\n\",\n\t\t\t\"variables\": map[string]interface{}{\n\t\t\t\t\"foo\": \"{foo}\",\n\t\t\t\t\"bar\": \"1234abc\",\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\treturn\n\t}\n\textractor := New(*cfg)\n\n\t{\n\t\tfmt.Println(\"BodyFromParams\")\n\t\tbody, err := extractor.BodyFromParams(map[string]string{\n\t\t\t\"Foo\": \"foobar\",\n\t\t})\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Println(string(body))\n\t}\n\n\t{\n\t\tfmt.Println(\"QueryFromParams\")\n\t\tquery, err := extractor.QueryFromParams(map[string]string{\n\t\t\t\"Foo\": \"foobar\",\n\t\t})\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Println(query.Encode())\n\t}\n\n\t{\n\t\tfmt.Println(\"BodyFromBody\")\n\t\tbody, err := extractor.BodyFromBody(strings.NewReader(`{\n\t\t\"foo\": \"foobar\",\n\t\t\"foo1\": \"foobar\"\n\t}`))\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Println(string(body))\n\t}\n\n\t{\n\t\tfmt.Println(\"QueryFromBody\")\n\t\tquery, err := extractor.QueryFromBody(strings.NewReader(`{\n\t\t\"foo\": \"foobar\",\n\t\t\"foo1\": \"foobar\"\n\t}`))\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Println(query.Encode())\n\t}\n\n\t// output:\n\t// BodyFromParams\n\t// {\"query\":\"{\\n  find_follower(func: uid(\\\"0x3\\\")) {\\n    name \\n    }\\n  }\\n\",\"variables\":{\"bar\":\"1234abc\",\"foo\":\"foobar\"}}\n\t// QueryFromParams\n\t// query=%7B%0A++find_follower%28func%3A+uid%28%220x3%22%29%29+%7B%0A++++name+%0A++++%7D%0A++%7D%0A&variables=%7B%22bar%22%3A%221234abc%22%2C%22foo%22%3A%22foobar%22%7D\n\t// BodyFromBody\n\t// {\"query\":\"{\\n  find_follower(func: uid(\\\"0x3\\\")) {\\n    name \\n    }\\n  }\\n\",\"variables\":{\"bar\":\"1234abc\",\"foo\":\"foobar\",\"foo1\":\"foobar\"}}\n\t// QueryFromBody\n\t// query=%7B%0A++find_follower%28func%3A+uid%28%220x3%22%29%29+%7B%0A++++name+%0A++++%7D%0A++%7D%0A&variables=%7B%22bar%22%3A%221234abc%22%2C%22foo%22%3A%22foobar%22%2C%22foo1%22%3A%22foobar%22%7D\n\n}\n\nfunc ExampleExtractor_fromFile() {\n\tos.WriteFile(\".graphql_query.txt\", []byte(\"{\\n  find_follower(func: uid(\\\"0x3\\\")) {\\n    name \\n    }\\n  }\\n\"), 0664)\n\tdefer os.Remove(\".graphql_query.txt\")\n\n\tcfg, err := GetOptions(config.ExtraConfig{\n\t\tNamespace: map[string]interface{}{\n\t\t\t\"type\":       OperationQuery,\n\t\t\t\"query_path\": \".graphql_query.txt\",\n\t\t\t\"variables\": map[string]interface{}{\n\t\t\t\t\"foo\": \"{foo}\",\n\t\t\t\t\"bar\": \"1234abc\",\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\treturn\n\t}\n\textractor := New(*cfg)\n\n\t{\n\t\tfmt.Println(\"BodyFromParams\")\n\t\tbody, err := extractor.BodyFromParams(map[string]string{\n\t\t\t\"Foo\": \"foobar\",\n\t\t})\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Println(string(body))\n\t}\n\n\t{\n\t\tfmt.Println(\"QueryFromParams\")\n\t\tquery, err := extractor.QueryFromParams(map[string]string{\n\t\t\t\"Foo\": \"foobar\",\n\t\t})\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Println(query.Encode())\n\t}\n\n\t{\n\t\tfmt.Println(\"BodyFromBody\")\n\t\tbody, err := extractor.BodyFromBody(strings.NewReader(`{\n\t\t\"foo\": \"foobar\",\n\t\t\"foo1\": \"foobar\"\n\t}`))\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Println(string(body))\n\t}\n\n\t{\n\t\tfmt.Println(\"QueryFromBody\")\n\t\tquery, err := extractor.QueryFromBody(strings.NewReader(`{\n\t\t\"foo\": \"foobar\",\n\t\t\"foo1\": \"foobar\"\n\t}`))\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Println(query.Encode())\n\t}\n\n\t// output:\n\t// BodyFromParams\n\t// {\"query\":\"{\\n  find_follower(func: uid(\\\"0x3\\\")) {\\n    name \\n    }\\n  }\\n\",\"variables\":{\"bar\":\"1234abc\",\"foo\":\"foobar\"}}\n\t// QueryFromParams\n\t// query=%7B%0A++find_follower%28func%3A+uid%28%220x3%22%29%29+%7B%0A++++name+%0A++++%7D%0A++%7D%0A&variables=%7B%22bar%22%3A%221234abc%22%2C%22foo%22%3A%22foobar%22%7D\n\t// BodyFromBody\n\t// {\"query\":\"{\\n  find_follower(func: uid(\\\"0x3\\\")) {\\n    name \\n    }\\n  }\\n\",\"variables\":{\"bar\":\"1234abc\",\"foo\":\"foobar\",\"foo1\":\"foobar\"}}\n\t// QueryFromBody\n\t// query=%7B%0A++find_follower%28func%3A+uid%28%220x3%22%29%29+%7B%0A++++name+%0A++++%7D%0A++%7D%0A&variables=%7B%22bar%22%3A%221234abc%22%2C%22foo%22%3A%22foobar%22%2C%22foo1%22%3A%22foobar%22%7D\n\n}\n\nfunc ExampleExtractor_noReplacement() {\n\tcfg, err := GetOptions(config.ExtraConfig{\n\t\tNamespace: map[string]interface{}{\n\t\t\t\"type\":  OperationQuery,\n\t\t\t\"query\": \"{\\n  find_follower(func: uid(\\\"0x3\\\")) {\\n    name \\n    }\\n  }\\n\",\n\t\t\t\"variables\": map[string]interface{}{\n\t\t\t\t\"bar\": \"1234abc\",\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\treturn\n\t}\n\textractor := New(*cfg)\n\n\t{\n\t\tfmt.Println(\"BodyFromParams\")\n\t\tbody, err := extractor.BodyFromParams(map[string]string{\n\t\t\t\"Foo\": \"foobar\",\n\t\t})\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Println(string(body))\n\t}\n\n\t{\n\t\tfmt.Println(\"QueryFromParams\")\n\t\tquery, err := extractor.QueryFromParams(map[string]string{\n\t\t\t\"Foo\": \"foobar\",\n\t\t})\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Println(query.Encode())\n\t}\n\n\t{\n\t\tfmt.Println(\"BodyFromBody\")\n\t\tbody, err := extractor.BodyFromBody(strings.NewReader(`{\n\t\t\"foo\": \"foobar\",\n\t\t\"foo1\": \"foobar\"\n\t}`))\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Println(string(body))\n\t}\n\n\t{\n\t\tfmt.Println(\"QueryFromBody\")\n\t\tquery, err := extractor.QueryFromBody(strings.NewReader(`{\n\t\t\"foo\": \"foobar\",\n\t\t\"foo1\": \"foobar\"\n\t}`))\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Println(query.Encode())\n\t}\n\n\t// output:\n\t// BodyFromParams\n\t// {\"query\":\"{\\n  find_follower(func: uid(\\\"0x3\\\")) {\\n    name \\n    }\\n  }\\n\",\"variables\":{\"bar\":\"1234abc\"}}\n\t// QueryFromParams\n\t// query=%7B%0A++find_follower%28func%3A+uid%28%220x3%22%29%29+%7B%0A++++name+%0A++++%7D%0A++%7D%0A&variables=%7B%22bar%22%3A%221234abc%22%7D\n\t// BodyFromBody\n\t// {\"query\":\"{\\n  find_follower(func: uid(\\\"0x3\\\")) {\\n    name \\n    }\\n  }\\n\",\"variables\":{\"bar\":\"1234abc\",\"foo\":\"foobar\",\"foo1\":\"foobar\"}}\n\t// QueryFromBody\n\t// query=%7B%0A++find_follower%28func%3A+uid%28%220x3%22%29%29+%7B%0A++++name+%0A++++%7D%0A++%7D%0A&variables=%7B%22bar%22%3A%221234abc%22%2C%22foo%22%3A%22foobar%22%2C%22foo1%22%3A%22foobar%22%7D\n\n}\n"
  },
  {
    "path": "transport/http/client/plugin/doc.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n//Package plugin provides plugin register interfaces for building http client plugins.\n//\n// Usage example:\n//\n// \tpackage main\n//\n// \timport (\n// \t\t\"context\"\n// \t\t\"errors\"\n// \t\t\"fmt\"\n// \t\t\"html\"\n// \t\t\"net/http\"\n// \t)\n//\n// \t// ClientRegisterer is the symbol the plugin loader will try to load. It must implement the RegisterClient interface\n// \tvar ClientRegisterer = registerer(\"lura-example\")\n//\n// \ttype registerer string\n//\n// \tfunc (r registerer) RegisterClients(f func(\n// \t\tname string,\n// \t\thandler func(context.Context, map[string]interface{}) (http.Handler, error),\n// \t)) {\n// \t\tf(string(r), r.registerClients)\n// \t}\n//\n// \tfunc (r registerer) registerClients(ctx context.Context, extra map[string]interface{}) (http.Handler, error) {\n//\t\t// check the passed configuration and initialize the plugin\n// \t\tname, ok := extra[\"name\"].(string)\n// \t\tif !ok {\n// \t\t\treturn nil, errors.New(\"wrong config\")\n// \t\t}\n// \t\tif name != string(r) {\n// \t\t\treturn nil, fmt.Errorf(\"unknown register %s\", name)\n// \t\t}\n//\t\t// return the actual handler wrapping or your custom logic so it can be used as a replacement for the default http client\n// \t\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n// \t\t\tfmt.Fprintf(w, \"Hello, %q\", html.EscapeString(req.URL.Path))\n// \t\t}), nil\n// \t}\n//\n// \tfunc init() {\n// \t\tfmt.Println(\"lura-example client plugin loaded!!!\")\n// \t}\n//\n// \tfunc main() {}\npackage plugin\n"
  },
  {
    "path": "transport/http/client/plugin/executor.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage plugin\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/transport/http/client\"\n)\n\nconst Namespace = \"github.com/devopsfaith/krakend/transport/http/client/executor\"\n\nfunc HTTPRequestExecutor(\n\tlogger logging.Logger,\n\tnext func(*config.Backend) client.HTTPRequestExecutor,\n) func(*config.Backend) client.HTTPRequestExecutor {\n\treturn HTTPRequestExecutorWithContext(context.Background(), logger, next)\n}\n\nfunc HTTPRequestExecutorWithContext(\n\tctx context.Context,\n\tlogger logging.Logger,\n\tnext func(*config.Backend) client.HTTPRequestExecutor,\n) func(*config.Backend) client.HTTPRequestExecutor {\n\treturn func(cfg *config.Backend) client.HTTPRequestExecutor {\n\t\tlogPrefix := fmt.Sprintf(\"[BACKEND: %s %s -> %s]\", cfg.ParentEndpointMethod, cfg.ParentEndpoint, cfg.URLPattern)\n\t\tv, ok := cfg.ExtraConfig[Namespace]\n\t\tif !ok {\n\t\t\treturn next(cfg)\n\t\t}\n\t\textra, ok := v.(map[string]interface{})\n\t\tif !ok {\n\t\t\tlogger.Debug(logPrefix, \"[\"+Namespace+\"]\", \"Wrong extra config type for backend\")\n\t\t\treturn next(cfg)\n\t\t}\n\n\t\t// load plugin\n\t\tr, ok := clientRegister.Get(Namespace)\n\t\tif !ok {\n\t\t\tlogger.Debug(logPrefix, \"No plugins registered for the module\")\n\t\t\treturn next(cfg)\n\t\t}\n\n\t\tname, ok := extra[\"name\"].(string)\n\t\tif !ok {\n\t\t\tlogger.Debug(logPrefix, \"No name defined in the extra config for\", cfg.URLPattern)\n\t\t\treturn next(cfg)\n\t\t}\n\n\t\trawHf, ok := r.Get(name)\n\t\tif !ok {\n\t\t\tlogger.Debug(logPrefix, \"No plugin registered as\", name)\n\t\t\treturn next(cfg)\n\t\t}\n\n\t\thf, ok := rawHf.(func(context.Context, map[string]interface{}) (http.Handler, error))\n\t\tif !ok {\n\t\t\tlogger.Warning(logPrefix, \"Wrong plugin handler type:\", name)\n\t\t\treturn next(cfg)\n\t\t}\n\n\t\thandler, err := hf(ctx, extra)\n\t\tif err != nil {\n\t\t\tlogger.Warning(logPrefix, \"Error getting the plugin handler:\", err.Error())\n\t\t\treturn next(cfg)\n\t\t}\n\n\t\tlogger.Debug(logPrefix, \"Injecting plugin\", name)\n\t\treturn func(ctx context.Context, req *http.Request) (*http.Response, error) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\thandler.ServeHTTP(w, req.WithContext(ctx))\n\t\t\treturn w.Result(), nil\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "transport/http/client/plugin/plugin.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage plugin\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"plugin\"\n\t\"strings\"\n\n\t\"github.com/luraproject/lura/v2/logging\"\n\tluraplugin \"github.com/luraproject/lura/v2/plugin\"\n\t\"github.com/luraproject/lura/v2/register\"\n)\n\nvar clientRegister = register.New()\n\nfunc RegisterClient(\n\tname string,\n\thandler func(context.Context, map[string]interface{}) (http.Handler, error),\n) {\n\tclientRegister.Register(Namespace, name, handler)\n}\n\ntype Registerer interface {\n\tRegisterClients(func(\n\t\tname string,\n\t\thandler func(context.Context, map[string]interface{}) (http.Handler, error),\n\t))\n}\n\ntype LoggerRegisterer interface {\n\tRegisterLogger(interface{})\n}\n\ntype RegisterClientFunc func(\n\tname string,\n\thandler func(context.Context, map[string]interface{}) (http.Handler, error),\n)\n\nfunc Load(path, pattern string, rcf RegisterClientFunc) (int, error) {\n\treturn LoadWithLogger(path, pattern, rcf, nil)\n}\n\nfunc LoadWithLogger(path, pattern string, rcf RegisterClientFunc, logger logging.Logger) (int, error) {\n\tplugins, err := luraplugin.Scan(path, pattern)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn load(plugins, rcf, logger)\n}\n\nfunc load(plugins []string, rcf RegisterClientFunc, logger logging.Logger) (int, error) {\n\tvar errors []error\n\n\tloadedPlugins := 0\n\tfor k, pluginName := range plugins {\n\t\tif err := open(pluginName, rcf, logger); err != nil {\n\t\t\terrors = append(errors, fmt.Errorf(\"plugin #%d (%s): %s\", k, pluginName, err.Error()))\n\t\t\tcontinue\n\t\t}\n\t\tloadedPlugins++\n\t}\n\n\tif len(errors) > 0 {\n\t\treturn loadedPlugins, loaderError{errors: errors}\n\t}\n\treturn loadedPlugins, nil\n}\n\nfunc open(pluginName string, rcf RegisterClientFunc, logger logging.Logger) (err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tvar ok bool\n\t\t\terr, ok = r.(error)\n\t\t\tif !ok {\n\t\t\t\terr = fmt.Errorf(\"%v\", r)\n\t\t\t}\n\t\t}\n\t}()\n\n\tvar p Plugin\n\tp, err = pluginOpener(pluginName)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar r interface{}\n\tr, err = p.Lookup(\"ClientRegisterer\")\n\tif err != nil {\n\t\treturn\n\t}\n\tregisterer, ok := r.(Registerer)\n\tif !ok {\n\t\treturn fmt.Errorf(\"http-request-executor plugin loader: unknown type\")\n\t}\n\n\tif logger != nil {\n\t\tif lr, ok := r.(LoggerRegisterer); ok {\n\t\t\tlr.RegisterLogger(logger)\n\t\t}\n\t}\n\n\tRegisterExtraComponents(r)\n\n\tregisterer.RegisterClients(rcf)\n\treturn\n}\n\nvar RegisterExtraComponents = func(interface{}) {}\n\n// Plugin is the interface of the loaded plugins\ntype Plugin interface {\n\tLookup(name string) (plugin.Symbol, error)\n}\n\n// pluginOpener keeps the plugin open function in a var for easy testing\nvar pluginOpener = defaultPluginOpener\n\nfunc defaultPluginOpener(name string) (Plugin, error) {\n\treturn plugin.Open(name)\n}\n\ntype loaderError struct {\n\terrors []error\n}\n\n// Error implements the error interface\nfunc (l loaderError) Error() string {\n\tmsgs := make([]string, len(l.errors))\n\tfor i, err := range l.errors {\n\t\tmsgs[i] = err.Error()\n\t}\n\treturn fmt.Sprintf(\"plugin loader found %d error(s): \\n%s\", len(msgs), strings.Join(msgs, \"\\n\"))\n}\n\nfunc (l loaderError) Len() int {\n\treturn len(l.errors)\n}\n\nfunc (l loaderError) Errs() []error {\n\treturn l.errors\n}\n"
  },
  {
    "path": "transport/http/client/plugin/plugin_test.go",
    "content": "//go:build integration || !race\n// +build integration !race\n\n// SPDX-License-Identifier: Apache-2.0\n\npackage plugin\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"github.com/luraproject/lura/v2/transport/http/client\"\n)\n\nfunc TestLoadWithLogger(t *testing.T) {\n\tbuff := new(bytes.Buffer)\n\tl, _ := logging.NewLogger(\"DEBUG\", buff, \"\")\n\ttotal, err := LoadWithLogger(\"./tests\", \".so\", RegisterClient, l)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\tt.Fail()\n\t}\n\tif total != 1 {\n\t\tt.Errorf(\"unexpected number of loaded plugins!. have %d, want 1\", total)\n\t}\n\n\thre := HTTPRequestExecutor(l, func(_ *config.Backend) client.HTTPRequestExecutor {\n\t\tt.Error(\"this factory should not been called\")\n\t\tt.Fail()\n\t\treturn nil\n\t})\n\n\th := hre(&config.Backend{\n\t\tExtraConfig: map[string]interface{}{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\t\"name\": \"krakend-client-example\",\n\t\t\t},\n\t\t},\n\t})\n\n\treq, _ := http.NewRequest(\"GET\", \"http://some.example.tld/path\", http.NoBody)\n\tresp, err := h(context.Background(), req)\n\tif err != nil {\n\t\tt.Errorf(\"unexpected error: %s\", err.Error())\n\t\treturn\n\t}\n\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tresp.Body.Close()\n\n\tif string(b) != \"Hello, \\\"/path\\\"\" {\n\t\tt.Errorf(\"unexpected response body: %s\", string(b))\n\t}\n\n\tfmt.Println(buff.String())\n}\n"
  },
  {
    "path": "transport/http/client/plugin/tests/main.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html\"\n\t\"net/http\"\n)\n\n// ClientRegisterer is the symbol the plugin loader will try to load. It must implement the RegisterClient interface\nvar ClientRegisterer = registerer(\"krakend-client-example\")\n\ntype registerer string\n\nvar logger Logger = nil\n\nfunc (registerer) RegisterLogger(v interface{}) {\n\tl, ok := v.(Logger)\n\tif !ok {\n\t\treturn\n\t}\n\tlogger = l\n\tlogger.Debug(fmt.Sprintf(\"[PLUGIN: %s] Logger loaded\", ClientRegisterer))\n}\n\nfunc (r registerer) RegisterClients(f func(\n\tname string,\n\thandler func(context.Context, map[string]interface{}) (http.Handler, error),\n)) {\n\tf(string(r), r.registerClients)\n}\n\nfunc (r registerer) registerClients(_ context.Context, extra map[string]interface{}) (http.Handler, error) {\n\t// check the passed configuration and initialize the plugin\n\tname, ok := extra[\"name\"].(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"wrong config\")\n\t}\n\tif name != string(r) {\n\t\treturn nil, fmt.Errorf(\"unknown register %s\", name)\n\t}\n\n\tif logger == nil {\n\t\t// return the actual handler wrapping or your custom logic so it can be used as a replacement for the default http client\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\tfmt.Fprintf(w, \"Hello, %q\", html.EscapeString(req.URL.Path))\n\t\t}), nil\n\t}\n\n\t// return the actual handler wrapping or your custom logic so it can be used as a replacement for the default http client\n\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\tfmt.Fprintf(w, \"Hello, %q\", html.EscapeString(req.URL.Path))\n\t\tlogger.Debug(\"request:\", html.EscapeString(req.URL.Path))\n\t}), nil\n}\n\nfunc main() {}\n\ntype Logger interface {\n\tDebug(v ...interface{})\n\tInfo(v ...interface{})\n\tWarning(v ...interface{})\n\tError(v ...interface{})\n\tCritical(v ...interface{})\n\tFatal(v ...interface{})\n}\n"
  },
  {
    "path": "transport/http/client/status.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n)\n\n// Namespace to be used in extra config\nconst Namespace = \"github.com/devopsfaith/krakend/http\"\n\n// ErrInvalidStatusCode is the error returned by the http proxy when the received status code\n// is not a 200 nor a 201\nvar ErrInvalidStatusCode = errors.New(\"invalid status code\")\n\ntype ErrInvalidStatus struct {\n\tstatusCode int\n\terrPrefix  string\n\tpath       string\n}\n\nfunc (e *ErrInvalidStatus) Error() string {\n\treturn fmt.Sprintf(\"invalid status code %d %s %s\", e.statusCode, e.errPrefix, e.path)\n}\n\nfunc NewErrInvalidStatusCode(resp *http.Response, errPrefix string) *ErrInvalidStatus {\n\tvar p string\n\tif resp.Request != nil && resp.Request.URL != nil {\n\t\tp = resp.Request.URL.String()\n\t}\n\n\treturn &ErrInvalidStatus{\n\t\tstatusCode: resp.StatusCode,\n\t\terrPrefix:  errPrefix,\n\t\tpath:       p,\n\t}\n}\n\n// HTTPStatusHandler defines how we tread the http response code\ntype HTTPStatusHandler func(context.Context, *http.Response) (*http.Response, error)\n\n// GetHTTPStatusHandler returns a status handler. If the 'return_error_details' key is defined\n// at the extra config, it returns a DetailedHTTPStatusHandler. Otherwise, it returns a\n// DefaultHTTPStatusHandler\nfunc GetHTTPStatusHandler(remote *config.Backend) HTTPStatusHandler {\n\terrPrefix := fmt.Sprintf(\"[%s %s]:\", remote.Method, remote.URLPattern)\n\tif e, ok := remote.ExtraConfig[Namespace]; ok {\n\t\tif m, ok := e.(map[string]interface{}); ok {\n\t\t\tif v, ok := m[\"return_error_details\"]; ok {\n\t\t\t\tif b, ok := v.(string); ok && b != \"\" {\n\t\t\t\t\treturn DetailedHTTPStatusHandlerWithErrPrefix(b, errPrefix)\n\t\t\t\t}\n\t\t\t} else if v, ok := m[\"return_error_code\"].(bool); ok && v {\n\t\t\t\treturn ErrorHTTPStatusHandlerWithErrPrefix(errPrefix)\n\t\t\t}\n\t\t}\n\t}\n\treturn DefaultHTTPStatusHandlerWithErrPrefix(errPrefix)\n}\n\n// DefaultHTTPStatusHandler is the default implementation of HTTPStatusHandler\nfunc DefaultHTTPStatusHandler(_ context.Context, resp *http.Response) (*http.Response, error) {\n\tif resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {\n\t\treturn nil, ErrInvalidStatusCode\n\t}\n\n\treturn resp, nil\n}\n\n// DefaultHTTPStatusHandlerWithErrPrefix is the default implementation of HTTPStatusHandler\n// with information about the failing status code, and the failed request\nfunc DefaultHTTPStatusHandlerWithErrPrefix(errPrefix string) HTTPStatusHandler {\n\treturn func(_ context.Context, resp *http.Response) (*http.Response, error) {\n\t\tif resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {\n\t\t\treturn nil, NewErrInvalidStatusCode(resp, errPrefix)\n\t\t}\n\t\treturn resp, nil\n\t}\n}\n\n// ErrorHTTPStatusHandler is a HTTPStatusHandler that returns the status code as part of the error details\nfunc ErrorHTTPStatusHandler(ctx context.Context, resp *http.Response) (*http.Response, error) {\n\tif _, err := DefaultHTTPStatusHandler(ctx, resp); err == nil {\n\t\treturn resp, nil\n\t}\n\treturn resp, newHTTPResponseError(resp)\n}\n\n// ErrorHTTPStatusHandlerWithErrPrefix is a HTTPStatusHandler that returns the status code as part of the error details\nfunc ErrorHTTPStatusHandlerWithErrPrefix(errPrefix string) HTTPStatusHandler {\n\tdefaultH := DefaultHTTPStatusHandlerWithErrPrefix(errPrefix)\n\treturn func(ctx context.Context, resp *http.Response) (*http.Response, error) {\n\t\tif _, err := defaultH(ctx, resp); err == nil {\n\t\t\treturn resp, nil\n\t\t}\n\t\treturn resp, newHTTPResponseError(resp)\n\t}\n}\n\n// NoOpHTTPStatusHandler is a NO-OP implementation of HTTPStatusHandler\nfunc NoOpHTTPStatusHandler(_ context.Context, resp *http.Response) (*http.Response, error) {\n\treturn resp, nil\n}\n\n// DetailedHTTPStatusHandler is a HTTPStatusHandler implementation\nfunc DetailedHTTPStatusHandler(name string) HTTPStatusHandler {\n\treturn func(ctx context.Context, resp *http.Response) (*http.Response, error) {\n\t\tif _, err := DefaultHTTPStatusHandler(ctx, resp); err == nil {\n\t\t\treturn resp, nil\n\t\t}\n\n\t\treturn resp, NamedHTTPResponseError{\n\t\t\tHTTPResponseError: newHTTPResponseError(resp),\n\t\t\tname:              name,\n\t\t}\n\t}\n}\n\n// DetailedHTTPStatusHandlerWithErrPrefix is a HTTPStatusHandlers that\n// can receive an error prefix to be added when an error happens to help\n// identify the endpoint using this handler.\nfunc DetailedHTTPStatusHandlerWithErrPrefix(name, errPrefix string) HTTPStatusHandler {\n\tdefaultH := DefaultHTTPStatusHandlerWithErrPrefix(errPrefix)\n\treturn func(ctx context.Context, resp *http.Response) (*http.Response, error) {\n\t\tif _, err := defaultH(ctx, resp); err == nil {\n\t\t\treturn resp, nil\n\t\t}\n\n\t\treturn resp, NamedHTTPResponseError{\n\t\t\tHTTPResponseError: newHTTPResponseError(resp),\n\t\t\tname:              name,\n\t\t}\n\t}\n}\n\nfunc newHTTPResponseError(resp *http.Response) HTTPResponseError {\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tbody = []byte{}\n\t}\n\tresp.Body.Close()\n\tresp.Body = io.NopCloser(bytes.NewBuffer(body))\n\n\treturn HTTPResponseError{\n\t\tCode: resp.StatusCode,\n\t\tMsg:  string(body),\n\t\tEnc:  resp.Header.Get(\"Content-Type\"),\n\t}\n}\n\n// HTTPResponseError is the error to be returned by the ErrorHTTPStatusHandler\ntype HTTPResponseError struct {\n\tCode int    `json:\"http_status_code\"`\n\tMsg  string `json:\"http_body,omitempty\"`\n\tEnc  string `json:\"http_body_encoding,omitempty\"`\n}\n\n// Error returns the error message\nfunc (r HTTPResponseError) Error() string {\n\treturn r.Msg\n}\n\n// StatusCode returns the status code returned by the backend\nfunc (r HTTPResponseError) StatusCode() int {\n\treturn r.Code\n}\n\n// Encoding returns the content type returned by the backend\nfunc (r HTTPResponseError) Encoding() string {\n\treturn r.Enc\n}\n\n// NamedHTTPResponseError is the error to be returned by the DetailedHTTPStatusHandler\ntype NamedHTTPResponseError struct {\n\tHTTPResponseError\n\tname string\n}\n\n// Name returns the name of the backend where the error happened\nfunc (r NamedHTTPResponseError) Name() string {\n\treturn r.name\n}\n"
  },
  {
    "path": "transport/http/client/status_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n)\n\nfunc TestDetailedHTTPStatusHandler(t *testing.T) {\n\texpectedErrName := \"some\"\n\texpectedEncoding := \"application/json; charset=utf-8\"\n\tcfg := &config.Backend{\n\t\tExtraConfig: config.ExtraConfig{\n\t\t\tNamespace: map[string]interface{}{\n\t\t\t\t\"return_error_details\": expectedErrName,\n\t\t\t},\n\t\t},\n\t}\n\tsh := GetHTTPStatusHandler(cfg)\n\n\tfor _, code := range []int{http.StatusOK, http.StatusCreated} {\n\t\tresp := &http.Response{\n\t\t\tStatusCode: code,\n\t\t\tBody:       io.NopCloser(bytes.NewBufferString(`{\"foo\":\"bar\"}`)),\n\t\t}\n\n\t\tr, err := sh(context.Background(), resp)\n\n\t\tif r != resp {\n\t\t\tt.Errorf(\"#%d unexpected response: %v\", code, r)\n\t\t\treturn\n\t\t}\n\n\t\tif err != nil {\n\t\t\tt.Errorf(\"#%d unexpected error: %s\", code, err.Error())\n\t\t\treturn\n\t\t}\n\t}\n\n\tfor i, code := range statusCodes {\n\t\tmsg := http.StatusText(code)\n\n\t\tresp := &http.Response{\n\t\t\tStatusCode: code,\n\t\t\tBody:       io.NopCloser(bytes.NewBufferString(fmt.Sprintf(`{\"msg\":%q}`, msg))),\n\t\t\tHeader:     http.Header{\"Content-Type\": []string{expectedEncoding}},\n\t\t}\n\n\t\tr, err := sh(context.Background(), resp)\n\n\t\tif r != resp {\n\t\t\tt.Errorf(\"#%d unexpected response: %v\", i, r)\n\t\t\treturn\n\t\t}\n\n\t\te, ok := err.(NamedHTTPResponseError)\n\t\tif !ok {\n\t\t\tt.Errorf(\"#%d unexpected error type %T: %s\", i, err, err.Error())\n\t\t\treturn\n\t\t}\n\n\t\tif e.StatusCode() != code {\n\t\t\tt.Errorf(\"#%d unexpected status code: %d\", i, e.Code)\n\t\t\treturn\n\t\t}\n\n\t\tif e.Error() != fmt.Sprintf(`{\"msg\":%q}`, msg) {\n\t\t\tt.Errorf(\"#%d unexpected message: %s\", i, e.Msg)\n\t\t\treturn\n\t\t}\n\n\t\tif e.Name() != expectedErrName {\n\t\t\tt.Errorf(\"#%d unexpected error name: %s\", i, e.name)\n\t\t\treturn\n\t\t}\n\n\t\tif e.Encoding() != expectedEncoding {\n\t\t\tt.Errorf(\"#%d unexpected encoding: %s\", i, e.Enc)\n\t\t}\n\t}\n}\n\nfunc TestDefaultHTTPStatusHandler(t *testing.T) {\n\tsh := GetHTTPStatusHandler(&config.Backend{})\n\n\tfor _, code := range []int{http.StatusOK, http.StatusCreated} {\n\t\tresp := &http.Response{\n\t\t\tStatusCode: code,\n\t\t\tBody:       io.NopCloser(bytes.NewBufferString(`{\"foo\":\"bar\"}`)),\n\t\t}\n\n\t\tr, err := sh(context.Background(), resp)\n\n\t\tif r != resp {\n\t\t\tt.Errorf(\"#%d unexpected response: %v\", code, r)\n\t\t\treturn\n\t\t}\n\n\t\tif err != nil {\n\t\t\tt.Errorf(\"#%d unexpected error: %s\", code, err.Error())\n\t\t\treturn\n\t\t}\n\t}\n\n\tfor _, code := range statusCodes {\n\t\tmsg := http.StatusText(code)\n\n\t\tresp := &http.Response{\n\t\t\tStatusCode: code,\n\t\t\tBody:       io.NopCloser(bytes.NewBufferString(msg)),\n\t\t}\n\n\t\tr, err := sh(context.Background(), resp)\n\n\t\tif r != nil {\n\t\t\tt.Errorf(\"#%d unexpected response: %v\", code, r)\n\t\t\treturn\n\t\t}\n\n\t\tif !strings.HasPrefix(err.Error(), \"invalid status code\") {\n\t\t\tt.Errorf(\"#%d unexpected error: %v\", code, err)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nvar statusCodes = []int{\n\thttp.StatusBadRequest,\n\thttp.StatusUnauthorized,\n\thttp.StatusPaymentRequired,\n\thttp.StatusForbidden,\n\thttp.StatusNotFound,\n\thttp.StatusMethodNotAllowed,\n\thttp.StatusNotAcceptable,\n\thttp.StatusProxyAuthRequired,\n\thttp.StatusRequestTimeout,\n\thttp.StatusConflict,\n\thttp.StatusGone,\n\thttp.StatusLengthRequired,\n\thttp.StatusPreconditionFailed,\n\thttp.StatusRequestEntityTooLarge,\n\thttp.StatusRequestURITooLong,\n\thttp.StatusUnsupportedMediaType,\n\thttp.StatusRequestedRangeNotSatisfiable,\n\thttp.StatusExpectationFailed,\n\thttp.StatusTeapot,\n\t// http.StatusMisdirectedRequest,\n\thttp.StatusUnprocessableEntity,\n\thttp.StatusLocked,\n\thttp.StatusFailedDependency,\n\thttp.StatusUpgradeRequired,\n\thttp.StatusPreconditionRequired,\n\thttp.StatusTooManyRequests,\n\thttp.StatusRequestHeaderFieldsTooLarge,\n\thttp.StatusUnavailableForLegalReasons,\n\n\thttp.StatusInternalServerError,\n\thttp.StatusNotImplemented,\n\thttp.StatusBadGateway,\n\thttp.StatusServiceUnavailable,\n\thttp.StatusGatewayTimeout,\n\thttp.StatusHTTPVersionNotSupported,\n\thttp.StatusVariantAlsoNegotiates,\n\thttp.StatusInsufficientStorage,\n\thttp.StatusLoopDetected,\n\thttp.StatusNotExtended,\n\thttp.StatusNetworkAuthenticationRequired,\n}\n"
  },
  {
    "path": "transport/http/server/plugin/doc.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n//Package plugin provides plugin register interfaces for building http handler plugins.\n//\n// Usage example:\n//\n// \tpackage main\n//\n// \timport (\n// \t\t\"context\"\n// \t\t\"errors\"\n// \t\t\"fmt\"\n// \t\t\"html\"\n// \t\t\"net/http\"\n// \t)\n//\n// \t// HandlerRegisterer is the symbol the plugin loader will try to load. It must implement the Registerer interface\n// \tvar HandlerRegisterer = registerer(\"lura-example\")\n//\n// \ttype registerer string\n//\n// \tfunc (r registerer) RegisterHandlers(f func(\n// \t\tname string,\n// \t\thandler func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error),\n// \t)) {\n// \t\tf(string(r), r.registerHandlers)\n// \t}\n//\n// \tfunc (r registerer) registerHandlers(ctx context.Context, extra map[string]interface{}, _ http.Handler) (http.Handler, error) {\n//\t\t// check the passed configuration and initialize the plugin\n// \t\tname, ok := extra[\"name\"].(string)\n// \t\tif !ok {\n// \t\t\treturn nil, errors.New(\"wrong config\")\n// \t\t}\n// \t\tif name != string(r) {\n// \t\t\treturn nil, fmt.Errorf(\"unknown register %s\", name)\n// \t\t}\n//\t\t// return the actual handler wrapping or your custom logic so it can be used as a replacement for the default http handler\n// \t\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n// \t\t\tfmt.Fprintf(w, \"Hello, %q\", html.EscapeString(req.URL.Path))\n// \t\t}), nil\n// \t}\n//\n// \tfunc init() {\n// \t\tfmt.Println(\"lura-example handler plugin loaded!!!\")\n// \t}\n//\n// \tfunc main() {}\npackage plugin\n"
  },
  {
    "path": "transport/http/server/plugin/plugin.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage plugin\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"plugin\"\n\t\"strings\"\n\n\t\"github.com/luraproject/lura/v2/logging\"\n\tluraplugin \"github.com/luraproject/lura/v2/plugin\"\n\t\"github.com/luraproject/lura/v2/register\"\n)\n\nvar serverRegister = register.New()\n\nfunc RegisterHandler(\n\tname string,\n\thandler func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error),\n) {\n\tserverRegister.Register(Namespace, name, handler)\n}\n\ntype Registerer interface {\n\tRegisterHandlers(func(\n\t\tname string,\n\t\thandler func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error),\n\t))\n}\n\ntype LoggerRegisterer interface {\n\tRegisterLogger(interface{})\n}\n\ntype RegisterHandlerFunc func(\n\tname string,\n\thandler func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error),\n)\n\nfunc Load(path, pattern string, rcf RegisterHandlerFunc) (int, error) {\n\treturn LoadWithLogger(path, pattern, rcf, nil)\n}\n\nfunc LoadWithLogger(path, pattern string, rcf RegisterHandlerFunc, logger logging.Logger) (int, error) {\n\tplugins, err := luraplugin.Scan(path, pattern)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn load(plugins, rcf, logger)\n}\n\nfunc load(plugins []string, rcf RegisterHandlerFunc, logger logging.Logger) (int, error) {\n\tvar errors []error\n\n\tloadedPlugins := 0\n\tfor k, pluginName := range plugins {\n\t\tif err := open(pluginName, rcf, logger); err != nil {\n\t\t\terrors = append(errors, fmt.Errorf(\"plugin #%d (%s): %s\", k, pluginName, err.Error()))\n\t\t\tcontinue\n\t\t}\n\t\tloadedPlugins++\n\t}\n\n\tif len(errors) > 0 {\n\t\treturn loadedPlugins, loaderError{errors: errors}\n\t}\n\treturn loadedPlugins, nil\n}\n\nfunc open(pluginName string, rcf RegisterHandlerFunc, logger logging.Logger) (err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tvar ok bool\n\t\t\terr, ok = r.(error)\n\t\t\tif !ok {\n\t\t\t\terr = fmt.Errorf(\"%v\", r)\n\t\t\t}\n\t\t}\n\t}()\n\n\tvar p Plugin\n\tp, err = pluginOpener(pluginName)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar r interface{}\n\tr, err = p.Lookup(\"HandlerRegisterer\")\n\tif err != nil {\n\t\treturn\n\t}\n\tregisterer, ok := r.(Registerer)\n\tif !ok {\n\t\treturn fmt.Errorf(\"http-server-handler plugin loader: unknown type\")\n\t}\n\n\tif logger != nil {\n\t\tif lr, ok := r.(LoggerRegisterer); ok {\n\t\t\tlr.RegisterLogger(logger)\n\t\t}\n\t}\n\n\tRegisterExtraComponents(r)\n\n\tregisterer.RegisterHandlers(rcf)\n\treturn\n}\n\nvar RegisterExtraComponents = func(interface{}) {}\n\n// Plugin is the interface of the loaded plugins\ntype Plugin interface {\n\tLookup(name string) (plugin.Symbol, error)\n}\n\n// pluginOpener keeps the plugin open function in a var for easy testing\nvar pluginOpener = defaultPluginOpener\n\nfunc defaultPluginOpener(name string) (Plugin, error) {\n\treturn plugin.Open(name)\n}\n\ntype loaderError struct {\n\terrors []error\n}\n\n// Error implements the error interface\nfunc (l loaderError) Error() string {\n\tmsgs := make([]string, len(l.errors))\n\tfor i, err := range l.errors {\n\t\tmsgs[i] = err.Error()\n\t}\n\treturn fmt.Sprintf(\"plugin loader found %d error(s): \\n%s\", len(msgs), strings.Join(msgs, \"\\n\"))\n}\n\nfunc (l loaderError) Len() int {\n\treturn len(l.errors)\n}\n\nfunc (l loaderError) Errs() []error {\n\treturn l.errors\n}\n"
  },
  {
    "path": "transport/http/server/plugin/plugin_test.go",
    "content": "//go:build integration || !race\n// +build integration !race\n\n// SPDX-License-Identifier: Apache-2.0\n\npackage plugin\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nfunc TestLoadWithLogger(t *testing.T) {\n\tbuff := new(bytes.Buffer)\n\tl, _ := logging.NewLogger(\"DEBUG\", buff, \"\")\n\ttotal, err := LoadWithLogger(\"./tests\", \".so\", RegisterHandler, l)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\tt.Fail()\n\t}\n\tif total != 1 {\n\t\tt.Errorf(\"unexpected number of loaded plugins!. have %d, want 1\", total)\n\t}\n\n\tvar handler http.Handler\n\n\thre := New(l, func(_ context.Context, _ config.ServiceConfig, h http.Handler) error {\n\t\thandler = h\n\t\treturn nil\n\t})\n\n\tif err := hre(\n\t\tcontext.Background(),\n\t\tconfig.ServiceConfig{\n\t\t\tExtraConfig: map[string]interface{}{\n\t\t\t\tNamespace: map[string]interface{}{\n\t\t\t\t\t\"name\": \"krakend-server-example\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tt.Error(\"this handler should not been called\")\n\t\t}),\n\t); err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\treq, _ := http.NewRequest(\"GET\", \"http://some.example.tld/path\", http.NoBody)\n\tw := httptest.NewRecorder()\n\thandler.ServeHTTP(w, req)\n\n\tresp := w.Result()\n\n\tif resp.StatusCode != 200 {\n\t\tt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t\treturn\n\t}\n\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tresp.Body.Close()\n\n\tif string(b) != \"Hello, \\\"/path\\\"\" {\n\t\tt.Errorf(\"unexpected response body: %s\", string(b))\n\t}\n\n\tfmt.Println(buff.String())\n}\n"
  },
  {
    "path": "transport/http/server/plugin/server.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage plugin\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n)\n\nconst Namespace = \"github_com/devopsfaith/krakend/transport/http/server/handler\"\nconst logPrefix = \"[PLUGIN: Server]\"\n\ntype RunServer func(context.Context, config.ServiceConfig, http.Handler) error\n\nfunc New(logger logging.Logger, next RunServer) RunServer {\n\treturn func(ctx context.Context, cfg config.ServiceConfig, handler http.Handler) error {\n\t\tv, ok := cfg.ExtraConfig[Namespace]\n\n\t\tif !ok {\n\t\t\treturn next(ctx, cfg, handler)\n\t\t}\n\t\textra, ok := v.(map[string]interface{})\n\t\tif !ok {\n\t\t\tlogger.Debug(logPrefix, \"Wrong extra_config type\")\n\t\t\treturn next(ctx, cfg, handler)\n\t\t}\n\n\t\t// load plugin(s)\n\t\tr, ok := serverRegister.Get(Namespace)\n\t\tif !ok {\n\t\t\tlogger.Debug(logPrefix, \"No plugins registered for the module\")\n\t\t\treturn next(ctx, cfg, handler)\n\t\t}\n\n\t\tname, nameOk := extra[\"name\"].(string)\n\t\tfifoRaw, fifoOk := extra[\"name\"].([]interface{})\n\t\tif !nameOk && !fifoOk {\n\t\t\tlogger.Debug(logPrefix, \"No plugins required in the extra config\")\n\t\t\treturn next(ctx, cfg, handler)\n\t\t}\n\t\tvar fifo []string\n\n\t\tif !fifoOk {\n\t\t\tfifo = []string{name}\n\t\t} else {\n\t\t\tfor _, x := range fifoRaw {\n\t\t\t\tif v, ok := x.(string); ok {\n\t\t\t\t\tfifo = append(fifo, v)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor _, name := range fifo {\n\t\t\trawHf, ok := r.Get(name)\n\t\t\tif !ok {\n\t\t\t\tlogger.Error(logPrefix, \"No plugin registered as\", name)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\thf, ok := rawHf.(func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error))\n\t\t\tif !ok {\n\t\t\t\tlogger.Error(logPrefix, \"Wrong plugin handler type:\", name)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\thandlerWrapper, err := hf(ctx, extra, handler)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Error(logPrefix, \"Error getting the plugin handler:\", err.Error())\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlogger.Info(logPrefix, \"Injecting plugin\", name)\n\t\t\thandler = handlerWrapper\n\t\t}\n\t\treturn next(ctx, cfg, handler)\n\t}\n}\n"
  },
  {
    "path": "transport/http/server/plugin/tests/main.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"html\"\n\t\"net/http\"\n)\n\n// HandlerRegisterer is the symbol the plugin loader will try to load. It must implement the Registerer interface\nvar HandlerRegisterer = registerer(\"krakend-server-example\")\n\ntype registerer string\n\nvar logger Logger = nil\n\nfunc (registerer) RegisterLogger(v interface{}) {\n\tl, ok := v.(Logger)\n\tif !ok {\n\t\treturn\n\t}\n\tlogger = l\n\tlogger.Debug(fmt.Sprintf(\"[PLUGIN: %s] Logger loaded\", HandlerRegisterer))\n}\n\nfunc (r registerer) RegisterHandlers(f func(\n\tname string,\n\thandler func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error),\n)) {\n\tf(string(r), r.registerHandlers)\n}\n\nfunc (registerer) registerHandlers(_ context.Context, _ map[string]interface{}, _ http.Handler) (http.Handler, error) {\n\t// check the passed configuration and initialize the plugin\n\t// possible config example:\n\t/*\n\t   \"extra_config\":{\n\t       \"plugin/http-server\":{\n\t           \"name\":[\"krakend-server-example\"],\n\t           \"krakend-server-example\":{\n\t               \"A\":\"foo\",\n\t               \"B\":42\n\t           }\n\t       }\n\t   }\n\t*/\n\n\tif logger == nil {\n\t\t// return the actual handler wrapping or your custom logic so it can be used as a replacement for the default http handler\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\tfmt.Fprintf(w, \"Hello, %q\", html.EscapeString(req.URL.Path))\n\t\t}), nil\n\t}\n\n\t// return the actual handler wrapping or your custom logic so it can be used as a replacement for the default http handler\n\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\tfmt.Fprintf(w, \"Hello, %q\", html.EscapeString(req.URL.Path))\n\t\tlogger.Debug(\"request:\", html.EscapeString(req.URL.Path))\n\t}), nil\n}\n\nfunc main() {}\n\ntype Logger interface {\n\tDebug(v ...interface{})\n\tInfo(v ...interface{})\n\tWarning(v ...interface{})\n\tError(v ...interface{})\n\tCritical(v ...interface{})\n\tFatal(v ...interface{})\n}\n"
  },
  {
    "path": "transport/http/server/server.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\n/*\nPackage server provides tools to create http servers and handlers wrapping the lura router\n*/\npackage server\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/core\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"golang.org/x/net/http2\"\n\t\"golang.org/x/net/http2/h2c\"\n)\n\n// ToHTTPError translates an error into a HTTP status code\ntype ToHTTPError func(error) int\n\n// DefaultToHTTPError is a ToHTTPError transalator that always returns an\n// internal server error\nfunc DefaultToHTTPError(_ error) int {\n\treturn http.StatusInternalServerError\n}\n\nconst (\n\t// HeaderCompleteResponseValue is the value of the CompleteResponseHeader\n\t// if the response is complete\n\tHeaderCompleteResponseValue = \"true\"\n\t// HeaderIncompleteResponseValue is the value of the CompleteResponseHeader\n\t// if the response is not complete\n\tHeaderIncompleteResponseValue = \"false\"\n)\n\nvar (\n\t// CompleteResponseHeaderName is the header to flag incomplete responses to the client\n\tCompleteResponseHeaderName = \"X-Krakend-Completed\"\n\t// HeadersToSend are the headers to pass from the router request to the proxy\n\tHeadersToSend = []string{\"Content-Type\"}\n\t// UserAgentHeaderValue is the value of the User-Agent header to add to the proxy request\n\tUserAgentHeaderValue = []string{core.KrakendUserAgent}\n\n\t// ErrInternalError is the error returned by the router when something went wrong\n\tErrInternalError = errors.New(\"internal server error\")\n\t// ErrPrivateKey is the error returned by the router when the private key is not defined\n\tErrPrivateKey = errors.New(\"private key not defined\")\n\t// ErrPublicKey is the error returned by the router when the public key is not defined\n\tErrPublicKey = errors.New(\"public key not defined\")\n\tloggerPrefix = \"[SERVICE: HTTP Server]\"\n)\n\n// InitHTTPDefaultTransport ensures the default HTTP transport is configured just once per execution\nfunc InitHTTPDefaultTransport(cfg config.ServiceConfig) {\n\tInitHTTPDefaultTransportWithLogger(cfg, nil)\n}\n\nfunc InitHTTPDefaultTransportWithLogger(cfg config.ServiceConfig, logger logging.Logger) {\n\tif logger == nil {\n\t\tlogger = logging.NoOp\n\t}\n\tif cfg.AllowInsecureConnections {\n\t\tif cfg.ClientTLS == nil {\n\t\t\tcfg.ClientTLS = &config.ClientTLS{}\n\t\t}\n\t\tcfg.ClientTLS.AllowInsecureConnections = true\n\t}\n\tonceTransportConfig.Do(func() {\n\t\thttp.DefaultTransport = NewTransport(cfg, logger)\n\t})\n}\n\nfunc NewTransport(cfg config.ServiceConfig, logger logging.Logger) *http.Transport {\n\treturn &http.Transport{\n\t\tProxy: http.ProxyFromEnvironment,\n\t\tDialContext: (&net.Dialer{\n\t\t\tTimeout:       cfg.DialerTimeout,\n\t\t\tKeepAlive:     cfg.DialerKeepAlive,\n\t\t\tFallbackDelay: cfg.DialerFallbackDelay,\n\t\t\tDualStack:     true,\n\t\t}).DialContext,\n\t\tDisableCompression:    cfg.DisableCompression,\n\t\tDisableKeepAlives:     cfg.DisableKeepAlives,\n\t\tMaxIdleConns:          cfg.MaxIdleConns,\n\t\tMaxIdleConnsPerHost:   cfg.MaxIdleConnsPerHost,\n\t\tIdleConnTimeout:       cfg.IdleConnTimeout,\n\t\tResponseHeaderTimeout: cfg.ResponseHeaderTimeout,\n\t\tExpectContinueTimeout: cfg.ExpectContinueTimeout,\n\t\tTLSHandshakeTimeout:   10 * time.Second,\n\t\tTLSClientConfig:       ParseClientTLSConfigWithLogger(cfg.ClientTLS, logger),\n\t}\n}\n\n// RunServer runs a http.Server with the given handler and configuration.\n// It configures the TLS layer if required by the received configuration.\nfunc RunServer(ctx context.Context, cfg config.ServiceConfig, handler http.Handler) error {\n\treturn RunServerWithLoggerFactory(nil)(ctx, cfg, handler)\n}\n\nfunc RunServerWithLoggerFactory(l logging.Logger) func(context.Context, config.ServiceConfig, http.Handler) error {\n\treturn func(ctx context.Context, cfg config.ServiceConfig, handler http.Handler) error {\n\t\tdone := make(chan error)\n\t\ts := NewServerWithLogger(cfg, handler, l)\n\n\t\tif s.TLSConfig == nil {\n\t\t\tgo func() {\n\t\t\t\tdone <- s.ListenAndServe()\n\t\t\t}()\n\t\t} else {\n\t\t\tif cfg.TLS.PublicKey != \"\" || cfg.TLS.PrivateKey != \"\" {\n\t\t\t\tcfg.TLS.Keys = append(cfg.TLS.Keys, config.TLSKeyPair{\n\t\t\t\t\tPublicKey:  cfg.TLS.PublicKey,\n\t\t\t\t\tPrivateKey: cfg.TLS.PrivateKey,\n\t\t\t\t})\n\t\t\t}\n\t\t\tif len(cfg.TLS.Keys) == 0 {\n\t\t\t\treturn ErrPublicKey\n\t\t\t}\n\t\t\tfor _, k := range cfg.TLS.Keys {\n\t\t\t\tif k.PublicKey == \"\" {\n\t\t\t\t\treturn ErrPublicKey\n\t\t\t\t}\n\t\t\t\tif k.PrivateKey == \"\" {\n\t\t\t\t\treturn ErrPrivateKey\n\t\t\t\t}\n\t\t\t\tcert, err := tls.LoadX509KeyPair(k.PublicKey, k.PrivateKey)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\ts.TLSConfig.Certificates = append(s.TLSConfig.Certificates, cert)\n\t\t\t}\n\n\t\t\tgo func() {\n\t\t\t\t// since we already use the list of certificates in the config\n\t\t\t\t// we do not need to specify the files for public and private key here\n\t\t\t\tdone <- s.ListenAndServeTLS(\"\", \"\")\n\t\t\t}()\n\t\t}\n\n\t\tselect {\n\t\tcase err := <-done:\n\t\t\treturn err\n\t\tcase <-ctx.Done():\n\t\t\tif cfg.MaxShutdownDuration <= 0 {\n\t\t\t\treturn s.Shutdown(context.Background())\n\t\t\t}\n\t\t\twithTimeout, cancel := context.WithTimeout(context.Background(), cfg.MaxShutdownDuration)\n\t\t\tdefer cancel()\n\t\t\treturn s.Shutdown(withTimeout)\n\t\t}\n\t}\n}\n\n// NewServer returns a http.Server ready to serve the injected handler\nfunc NewServer(cfg config.ServiceConfig, handler http.Handler) *http.Server {\n\treturn NewServerWithLogger(cfg, handler, nil)\n}\n\nfunc NewServerWithLogger(cfg config.ServiceConfig, handler http.Handler, logger logging.Logger) *http.Server {\n\tif cfg.UseH2C {\n\t\thandler = h2c.NewHandler(handler, &http2.Server{})\n\t}\n\n\treturn &http.Server{\n\t\tAddr:              net.JoinHostPort(cfg.Address, fmt.Sprintf(\"%d\", cfg.Port)),\n\t\tHandler:           handler,\n\t\tReadTimeout:       cfg.ReadTimeout,\n\t\tWriteTimeout:      cfg.WriteTimeout,\n\t\tReadHeaderTimeout: cfg.ReadHeaderTimeout,\n\t\tIdleTimeout:       cfg.IdleTimeout,\n\t\tTLSConfig:         ParseTLSConfigWithLogger(cfg.TLS, logger),\n\t\tMaxHeaderBytes:    cfg.MaxHeaderBytes,\n\t}\n}\n\n// ParseTLSConfig creates a tls.Config from the TLS section of the service configuration\nfunc ParseTLSConfig(cfg *config.TLS) *tls.Config {\n\treturn ParseTLSConfigWithLogger(cfg, nil)\n}\n\nfunc ParseTLSConfigWithLogger(cfg *config.TLS, logger logging.Logger) *tls.Config {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.IsDisabled {\n\t\treturn nil\n\t}\n\n\tif logger == nil {\n\t\tlogger = logging.NoOp\n\t}\n\n\ttlsConfig := &tls.Config{\n\t\tMinVersion:       parseTLSVersion(cfg.MinVersion),\n\t\tMaxVersion:       parseTLSVersion(cfg.MaxVersion),\n\t\tCurvePreferences: parseCurveIDs(cfg.CurvePreferences),\n\t\tCipherSuites:     parseCipherSuites(cfg.CipherSuites),\n\t}\n\tif !cfg.EnableMTLS {\n\t\treturn tlsConfig\n\t}\n\n\tcertPool := loadCertPool(cfg.DisableSystemCaPool, cfg.CaCerts, logger)\n\n\tfor _, cert := range cfg.Keys {\n\t\tcaCert, err := os.ReadFile(cert.PublicKey)\n\t\tif err != nil {\n\t\t\tlogger.Error(fmt.Sprintf(\"%s Cannot load public key %s: %s\", loggerPrefix, cert.PublicKey, err.Error()))\n\t\t\tcontinue\n\t\t}\n\t\tcertPool.AppendCertsFromPEM(caCert)\n\t}\n\n\ttlsConfig.ClientCAs = certPool\n\ttlsConfig.ClientAuth = tls.RequireAndVerifyClientCert\n\n\treturn tlsConfig\n}\n\nfunc ParseClientTLSConfigWithLogger(cfg *config.ClientTLS, logger logging.Logger) *tls.Config {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\treturn &tls.Config{\n\t\tInsecureSkipVerify: cfg.AllowInsecureConnections,\n\t\tRootCAs:            loadCertPool(cfg.DisableSystemCaPool, cfg.CaCerts, logger),\n\t\tMinVersion:         parseTLSVersion(cfg.MinVersion),\n\t\tMaxVersion:         parseTLSVersion(cfg.MaxVersion),\n\t\tCurvePreferences:   parseCurveIDs(cfg.CurvePreferences),\n\t\tCipherSuites:       parseCipherSuites(cfg.CipherSuites),\n\t\tCertificates:       loadClientCerts(cfg.ClientCerts, logger),\n\t}\n}\n\nfunc loadCertPool(disableSystemCaPool bool, caCerts []string, logger logging.Logger) *x509.CertPool {\n\tcertPool := x509.NewCertPool()\n\tif !disableSystemCaPool {\n\t\tif systemCertPool, err := x509.SystemCertPool(); err == nil {\n\t\t\tcertPool = systemCertPool\n\t\t} else {\n\t\t\tlogger.Error(fmt.Sprintf(\"%s Cannot load system CA pool: %s\", loggerPrefix, err.Error()))\n\t\t}\n\t}\n\n\tfor _, path := range caCerts {\n\t\tif ca, err := os.ReadFile(path); err == nil {\n\t\t\tcertPool.AppendCertsFromPEM(ca)\n\t\t} else {\n\t\t\tlogger.Error(fmt.Sprintf(\"%s Cannot load certificate CA %s: %s\", loggerPrefix, path, err.Error()))\n\t\t}\n\t}\n\treturn certPool\n}\n\nfunc loadClientCerts(certFiles []config.ClientTLSCert, logger logging.Logger) []tls.Certificate {\n\tcerts := make([]tls.Certificate, 0, len(certFiles))\n\tfor _, certAndKey := range certFiles {\n\t\tcert, err := tls.LoadX509KeyPair(certAndKey.Certificate, certAndKey.PrivateKey)\n\t\tif err != nil {\n\t\t\tlogger.Error(fmt.Sprintf(\"%s Cannot load client certificate %s, %s: %s\",\n\t\t\t\tloggerPrefix, certAndKey.Certificate, certAndKey.PrivateKey, err.Error()))\n\t\t\tcontinue\n\t\t}\n\t\tcerts = append(certs, cert)\n\t}\n\n\treturn certs\n}\n\nfunc parseTLSVersion(key string) uint16 {\n\tif v, ok := versions[key]; ok {\n\t\treturn v\n\t}\n\treturn tls.VersionTLS13\n}\n\nfunc parseCurveIDs(curvePreferences []uint16) []tls.CurveID {\n\tl := len(curvePreferences)\n\tif l == 0 {\n\t\treturn defaultCurves\n\t}\n\n\tcurves := make([]tls.CurveID, len(curvePreferences))\n\tfor i := range curves {\n\t\tcurves[i] = tls.CurveID(curvePreferences[i])\n\t}\n\treturn curves\n}\n\nfunc parseCipherSuites(cipherSuites []uint16) []uint16 {\n\tl := len(cipherSuites)\n\tif l == 0 {\n\t\treturn defaultCipherSuites\n\t}\n\n\tcs := make([]uint16, l)\n\tfor i := range cs {\n\t\tcs[i] = uint16(cipherSuites[i])\n\t}\n\treturn cs\n}\n\nvar (\n\tonceTransportConfig sync.Once\n\tdefaultCurves       = []tls.CurveID{\n\t\ttls.CurveP521,\n\t\ttls.CurveP384,\n\t\ttls.CurveP256,\n\t}\n\tdefaultCipherSuites = []uint16{\n\t\ttls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,\n\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,\n\t\ttls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,\n\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,\n\t\ttls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,\n\t\ttls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,\n\t}\n\tversions = map[string]uint16{\n\t\t\"SSL3.0\": tls.VersionSSL30,\n\t\t\"TLS10\":  tls.VersionTLS10,\n\t\t\"TLS11\":  tls.VersionTLS11,\n\t\t\"TLS12\":  tls.VersionTLS12,\n\t\t\"TLS13\":  tls.VersionTLS13,\n\t}\n)\n"
  },
  {
    "path": "transport/http/server/server_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage server\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html\"\n\t\"log\"\n\t\"math/rand\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/luraproject/lura/v2/config\"\n\t\"github.com/luraproject/lura/v2/logging\"\n\t\"golang.org/x/net/http2\"\n)\n\nfunc init() {\n\trand.Seed(time.Now().Unix())\n}\n\nfunc TestRunServer_TLS(t *testing.T) {\n\ttestKeysAreAvailable(t)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tport := newPort()\n\n\tdone := make(chan error)\n\tgo func() {\n\t\tdone <- RunServer(\n\t\t\tctx,\n\t\t\tconfig.ServiceConfig{\n\t\t\t\tPort: port,\n\t\t\t\tTLS: &config.TLS{\n\t\t\t\t\tPublicKey:  \"cert.pem\",\n\t\t\t\t\tPrivateKey: \"key.pem\",\n\t\t\t\t\tCaCerts:    []string{\"ca.pem\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\thttp.HandlerFunc(dummyHandler),\n\t\t)\n\t}()\n\n\tclient, err := httpsClient(\"cert.pem\")\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\t<-time.After(100 * time.Millisecond)\n\n\tresp, err := client.Get(fmt.Sprintf(\"https://localhost:%d\", port))\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif resp.StatusCode != 200 {\n\t\tt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t\treturn\n\t}\n\n\t// now lets initialize the global default transport and use a regular\n\t// client to connect to the server\n\tInitHTTPDefaultTransport(config.ServiceConfig{\n\t\tClientTLS: &config.ClientTLS{\n\t\t\tCaCerts:             []string{\"ca.pem\"},\n\t\t\tDisableSystemCaPool: true,\n\t\t},\n\t})\n\trawClient := http.Client{}\n\tresp, err = rawClient.Get(fmt.Sprintf(\"https://localhost:%d\", port))\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif resp.StatusCode != 200 {\n\t\tt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t\treturn\n\t}\n\n\tcancel()\n\n\tif err = <-done; err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestRunServer_MTLS(t *testing.T) {\n\ttestKeysAreAvailable(t)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tport := 36517\n\tdone := make(chan error)\n\n\tcfg := config.ServiceConfig{\n\t\tPort: port,\n\t\tTLS: &config.TLS{\n\t\t\tKeys: []config.TLSKeyPair{\n\t\t\t\t{\n\t\t\t\t\tPublicKey:  \"cert.pem\",\n\t\t\t\t\tPrivateKey: \"key.pem\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tCaCerts:    []string{\"ca.pem\"},\n\t\t\tEnableMTLS: true,\n\t\t},\n\t\tClientTLS: &config.ClientTLS{\n\t\t\tAllowInsecureConnections: false, // we do not check the server cert\n\t\t\tCaCerts:                  []string{\"ca.pem\"},\n\t\t\tClientCerts: []config.ClientTLSCert{\n\t\t\t\t{\n\t\t\t\t\tCertificate: \"cert.pem\",\n\t\t\t\t\tPrivateKey:  \"key.pem\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tgo func() {\n\t\tdone <- RunServer(ctx, cfg, http.HandlerFunc(dummyHandler))\n\t}()\n\n\tclient, err := mtlsClient(\"cert.pem\", \"key.pem\")\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\t<-time.After(1000 * time.Millisecond)\n\n\tresp, err := client.Get(fmt.Sprintf(\"https://localhost:%d\", port))\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif resp.StatusCode != 200 {\n\t\tt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t\treturn\n\t}\n\n\tlogger := logging.NoOp\n\t// since test are run in a suite, and `InitHTTPDefaultTransportWithLogger` is\n\t// used to setup the `http.DefaultTransport` global variable once, we need to\n\t// create a client here like if it was using the default created with the\n\t// clientTLS config.\n\t// This is a copy of the code we can find inside\n\t// InitHTTPDefaultTransportWithLogger(serviceConfig, nil):\n\ttransport := NewTransport(cfg, logger)\n\n\tdefClient := http.Client{\n\t\tTransport: transport,\n\t}\n\tresp, err = defClient.Get(fmt.Sprintf(\"https://localhost:%d\", port))\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif resp.StatusCode != 200 {\n\t\tt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t\treturn\n\t}\n\n\tcancel()\n\n\tif err = <-done; err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestRunServer_MTLSOldConfigFormat(t *testing.T) {\n\ttestKeysAreAvailable(t)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tport := 36517\n\tdone := make(chan error)\n\n\tcfg := config.ServiceConfig{\n\t\tPort: port,\n\t\tTLS: &config.TLS{\n\t\t\tPublicKey:  \"cert.pem\",\n\t\t\tPrivateKey: \"key.pem\",\n\t\t\tCaCerts:    []string{\"ca.pem\"},\n\t\t\tEnableMTLS: true,\n\t\t},\n\t\tClientTLS: &config.ClientTLS{\n\t\t\tAllowInsecureConnections: false, // we do not check the server cert\n\t\t\tCaCerts:                  []string{\"ca.pem\"},\n\t\t\tClientCerts: []config.ClientTLSCert{\n\t\t\t\t{\n\t\t\t\t\tCertificate: \"cert.pem\",\n\t\t\t\t\tPrivateKey:  \"key.pem\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tgo func() {\n\t\tdone <- RunServer(ctx, cfg, http.HandlerFunc(dummyHandler))\n\t}()\n\n\tclient, err := mtlsClient(\"cert.pem\", \"key.pem\")\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\t<-time.After(1000 * time.Millisecond)\n\n\tresp, err := client.Get(fmt.Sprintf(\"https://localhost:%d\", port))\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif resp.StatusCode != 200 {\n\t\tt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t\treturn\n\t}\n\n\tlogger := logging.NoOp\n\t// since test are run in a suite, and `InitHTTPDefaultTransportWithLogger` is\n\t// used to setup the `http.DefaultTransport` global variable once, we need to\n\t// create a client here like if it was using the default created with the\n\t// clientTLS config.\n\t// This is a copy of the code we can find inside\n\t// InitHTTPDefaultTransportWithLogger(serviceConfig, nil):\n\ttransport := NewTransport(cfg, logger)\n\n\tdefClient := http.Client{\n\t\tTransport: transport,\n\t}\n\tresp, err = defClient.Get(fmt.Sprintf(\"https://localhost:%d\", port))\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif resp.StatusCode != 200 {\n\t\tt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t\treturn\n\t}\n\n\tcancel()\n\n\tif err = <-done; err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestRunServer_plain(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tport := newPort()\n\n\tdone := make(chan error)\n\tgo func() {\n\t\tdone <- RunServer(\n\t\t\tctx,\n\t\t\tconfig.ServiceConfig{Port: port},\n\t\t\thttp.HandlerFunc(dummyHandler),\n\t\t)\n\t}()\n\n\t<-time.After(100 * time.Millisecond)\n\n\tresp, err := http.Get(fmt.Sprintf(\"http://localhost:%d\", port))\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif resp.StatusCode != 200 {\n\t\tt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t\treturn\n\t}\n\tcancel()\n\n\tif err = <-done; err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestRunServer_h2c(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tport := newPort()\n\n\tdone := make(chan error)\n\tgo func() {\n\t\tdone <- RunServer(\n\t\t\tctx,\n\t\t\tconfig.ServiceConfig{\n\t\t\t\tPort:   port,\n\t\t\t\tUseH2C: true,\n\t\t\t},\n\t\t\thttp.HandlerFunc(dummyHandler),\n\t\t)\n\t}()\n\n\t<-time.After(100 * time.Millisecond)\n\n\tclient := h2cClient()\n\tresp, err := client.Get(fmt.Sprintf(\"http://localhost:%d\", port))\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif resp.StatusCode != 200 {\n\t\tt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t\treturn\n\t}\n\tcancel()\n\n\tif err = <-done; err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestRunServer_disabledTLS(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tdone := make(chan error)\n\n\tport := newPort()\n\n\tgo func() {\n\t\tdone <- RunServer(\n\t\t\tctx,\n\t\t\tconfig.ServiceConfig{\n\t\t\t\tPort: port,\n\t\t\t\tTLS: &config.TLS{\n\t\t\t\t\tIsDisabled: true,\n\t\t\t\t}},\n\t\t\thttp.HandlerFunc(dummyHandler),\n\t\t)\n\t}()\n\n\t<-time.After(100 * time.Millisecond)\n\n\tresp, err := http.Get(fmt.Sprintf(\"http://localhost:%d\", port))\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif resp.StatusCode != 200 {\n\t\tt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t\treturn\n\t}\n\tcancel()\n\n\tif err = <-done; err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestRunServer_err(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tdone := make(chan error)\n\tfor _, tc := range []struct {\n\t\tcfg *config.TLS\n\t\terr error\n\t}{\n\t\t{\n\t\t\tcfg: &config.TLS{},\n\t\t\terr: ErrPublicKey,\n\t\t},\n\t\t{\n\t\t\tcfg: &config.TLS{\n\t\t\t\tPublicKey: \"unknown\",\n\t\t\t},\n\t\t\terr: ErrPrivateKey,\n\t\t},\n\t} {\n\t\tgo func() {\n\t\t\tdone <- RunServer(\n\t\t\t\tctx,\n\t\t\t\tconfig.ServiceConfig{TLS: tc.cfg},\n\t\t\t\thttp.HandlerFunc(dummyHandler),\n\t\t\t)\n\t\t}()\n\t\tif err := <-done; err != tc.err {\n\t\t\tt.Error(err)\n\t\t}\n\t}\n}\n\nfunc TestRunServer_errBadKeys(t *testing.T) {\n\tdone := make(chan error)\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tgo func() {\n\t\tdone <- RunServer(\n\t\t\tctx,\n\t\t\tconfig.ServiceConfig{TLS: &config.TLS{\n\t\t\t\tPublicKey:  \"unknown\",\n\t\t\t\tPrivateKey: \"unknown\",\n\t\t\t}},\n\t\t\thttp.HandlerFunc(dummyHandler),\n\t\t)\n\t}()\n\tif err := <-done; err == nil || err.Error() != \"open unknown: no such file or directory\" {\n\t\tt.Error(err)\n\t}\n}\n\nfunc Test_parseTLSVersion(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tin  string\n\t\tout uint16\n\t}{\n\t\t{in: \"SSL3.0\", out: tls.VersionSSL30},\n\t\t{in: \"TLS10\", out: tls.VersionTLS10},\n\t\t{in: \"TLS11\", out: tls.VersionTLS11},\n\t\t{in: \"TLS12\", out: tls.VersionTLS12},\n\t\t{in: \"TLS13\", out: tls.VersionTLS13},\n\t\t{in: \"Unknown\", out: tls.VersionTLS13},\n\t} {\n\t\tif res := parseTLSVersion(tc.in); res != tc.out {\n\t\t\tt.Errorf(\"input %s generated output %d. expected: %d\", tc.in, res, tc.out)\n\t\t}\n\t}\n}\n\nfunc Test_parseCurveIDs(t *testing.T) {\n\toriginal := []uint16{1, 2, 3}\n\tcs := parseCurveIDs(original)\n\tfor k, v := range cs {\n\t\tif original[k] != uint16(v) {\n\t\t\tt.Errorf(\"unexpected curves %v. expected: %v\", cs, original)\n\t\t}\n\t}\n}\n\nfunc Test_parseCipherSuites(t *testing.T) {\n\toriginal := []uint16{1, 2, 3}\n\tcs := parseCipherSuites(original)\n\tfor k, v := range cs {\n\t\tif original[k] != uint16(v) {\n\t\t\tt.Errorf(\"unexpected ciphersuites %v. expected: %v\", cs, original)\n\t\t}\n\t}\n}\n\nfunc dummyHandler(rw http.ResponseWriter, req *http.Request) {\n\tfmt.Fprintf(rw, \"Hello, %q\", html.EscapeString(req.URL.Path))\n}\n\nfunc testKeysAreAvailable(t *testing.T) {\n\tfiles, err := os.ReadDir(\".\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tfor _, k := range []string{\"cert.pem\", \"key.pem\"} {\n\t\tvar exists bool\n\t\tfor _, file := range files {\n\t\t\tif file.Name() == k {\n\t\t\t\texists = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !exists {\n\t\t\tt.Errorf(\"file %s not present\", k)\n\t\t}\n\t}\n}\n\nfunc httpsClient(cert string) (*http.Client, error) {\n\tcer, err := os.ReadFile(cert)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\troots := x509.NewCertPool()\n\tok := roots.AppendCertsFromPEM(cer)\n\tif !ok {\n\t\treturn nil, errors.New(\"failed to parse root certificate\")\n\t}\n\ttlsConf := &tls.Config{\n\t\tMinVersion:               tls.VersionTLS12,\n\t\tCurvePreferences:         []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},\n\t\tPreferServerCipherSuites: true,\n\t\tCipherSuites: []uint16{\n\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,\n\t\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,\n\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,\n\t\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,\n\t\t\ttls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,\n\t\t\ttls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,\n\t\t},\n\t\tRootCAs: roots,\n\t}\n\treturn &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConf}}, nil\n}\n\nfunc mtlsClient(certPath, keyPath string) (*http.Client, error) {\n\tcert, err := tls.LoadX509KeyPair(certPath, keyPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcacer, err := os.ReadFile(certPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\troots := x509.NewCertPool()\n\tok := roots.AppendCertsFromPEM(cacer)\n\tif !ok {\n\t\treturn nil, errors.New(\"failed to parse root certificate\")\n\t}\n\ttlsConf := &tls.Config{\n\t\tMinVersion:               tls.VersionTLS12,\n\t\tCurvePreferences:         []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},\n\t\tPreferServerCipherSuites: true,\n\t\tCipherSuites: []uint16{\n\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,\n\t\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,\n\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,\n\t\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,\n\t\t\ttls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,\n\t\t\ttls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,\n\t\t},\n\t\tRootCAs:      roots,\n\t\tCertificates: []tls.Certificate{cert},\n\t}\n\treturn &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConf}}, nil\n}\n\n// h2cClient initializes client which executes cleartext http2 requests\nfunc h2cClient() *http.Client {\n\treturn &http.Client{\n\t\tTransport: &http2.Transport{\n\t\t\tDialTLSContext: func(_ context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {\n\t\t\t\treturn net.Dial(network, addr)\n\t\t\t},\n\t\t\tAllowHTTP: true,\n\t\t},\n\t}\n}\n\n// newPort returns random port numbers to avoid port collisions during the tests\nfunc newPort() int {\n\treturn 16666 + rand.Intn(40000) // skipcq: GSC-G404\n}\n\nfunc TestRunServer_MultipleTLS(t *testing.T) {\n\ttestKeysAreAvailable(t)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tport := newPort()\n\n\tdone := make(chan error)\n\tgo func() {\n\t\tdone <- RunServer(\n\t\t\tctx,\n\t\t\tconfig.ServiceConfig{\n\t\t\t\tPort: port,\n\t\t\t\tTLS: &config.TLS{\n\t\t\t\t\tCaCerts: []string{\"ca.pem\", \"exampleca.pem\"},\n\t\t\t\t\tKeys: []config.TLSKeyPair{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPublicKey:  \"cert.pem\",\n\t\t\t\t\t\t\tPrivateKey: \"key.pem\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPublicKey:  \"examplecert.pem\",\n\t\t\t\t\t\t\tPrivateKey: \"examplekey.pem\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\thttp.HandlerFunc(dummyHandler),\n\t\t)\n\t}()\n\n\tclient, err := httpsClient(\"cert.pem\")\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\t<-time.After(100 * time.Millisecond)\n\n\tresp, err := client.Get(fmt.Sprintf(\"https://localhost:%d\", port))\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif resp.StatusCode != 200 {\n\t\tt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t\treturn\n\t}\n\n\tclient, err = httpsClient(\"examplecert.pem\")\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\t_, err = client.Get(fmt.Sprintf(\"https://127.0.0.1:%d\", port))\n\t// should fail, because it will be served with cert.pem\n\tif err == nil || strings.Contains(err.Error(), \"bad certificate\") {\n\t\tt.Error(\"expected to have 'bad certificate' error\")\n\t\treturn\n\t}\n\n\treq, _ := http.NewRequest(\"GET\", fmt.Sprintf(\"https://example.com:%d\", port), http.NoBody)\n\toverrideHostTransport(client)\n\tresp, err = client.Do(req)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif resp.StatusCode != 200 {\n\t\tt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t\treturn\n\t}\n\n\tcancel()\n\tif err = <-done; err != nil {\n\t\tt.Error(err)\n\t}\n}\n\n// overrideHostTransport subtitutes the actual address that the request will\n// connecto (overriding the dns resolution).\nfunc overrideHostTransport(client *http.Client) {\n\tt := http.DefaultTransport.(*http.Transport).Clone()\n\tif client.Transport != nil {\n\t\tif tt, ok := client.Transport.(*http.Transport); ok {\n\t\t\tt = tt\n\t\t}\n\t}\n\tmyDialer := &net.Dialer{\n\t\tTimeout:   30 * time.Second,\n\t\tKeepAlive: 30 * time.Second,\n\t\tDualStack: true,\n\t}\n\tt.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {\n\t\t_, port, err := net.SplitHostPort(address)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\toverrideAddress := net.JoinHostPort(\"127.0.0.1\", port)\n\t\treturn myDialer.DialContext(ctx, network, overrideAddress)\n\t}\n\tclient.Transport = t\n}\n"
  },
  {
    "path": "transport/http/server/tls_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n\npackage server\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"log\"\n\t\"math/big\"\n\t\"net\"\n\t\"os\"\n\t\"time\"\n)\n\ntype certDef struct {\n\tPrefix      string\n\tIPAddresses []string\n\tDNSNames    []string\n}\n\nfunc (c certDef) Org() string {\n\tif c.Prefix == \"\" {\n\t\treturn \"Acme Co\"\n\t}\n\treturn c.Prefix + \" \" + \"Acme Co\"\n}\n\nfunc init() {\n\tcerts := []certDef{\n\t\tcertDef{\n\t\t\tPrefix:      \"\",\n\t\t\tIPAddresses: []string{\"127.0.0.1\", \"::1\"},\n\t\t\tDNSNames:    []string{\"localhost\"},\n\t\t},\n\t\tcertDef{\n\t\t\tPrefix:      \"example\",\n\t\t\tIPAddresses: []string{\"127.0.0.1\"},\n\t\t\tDNSNames:    []string{\"example.com\"},\n\t\t},\n\t}\n\n\tfor _, cd := range certs {\n\t\tif err := generateNamedCert(cd); err != nil {\n\t\t\tlog.Fatal(err.Error())\n\t\t}\n\t}\n}\n\nfunc generateNamedCert(hostCert certDef) error {\n\tpriv, err := rsa.GenerateKey(rand.Reader, 2048)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to generate private key: %v\", err)\n\t}\n\n\tkeyUsage := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment\n\n\tnotBefore := time.Now()\n\tnotAfter := notBefore.Add(1000000 * time.Hour)\n\n\tserialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)\n\tserialNumber, err := rand.Int(rand.Reader, serialNumberLimit)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to generate serial number: %v\", err)\n\t}\n\n\ttemplate := x509.Certificate{\n\t\tSerialNumber: serialNumber,\n\t\tSubject: pkix.Name{\n\t\t\tOrganization: []string{hostCert.Org()},\n\t\t},\n\t\tNotBefore:             notBefore,\n\t\tNotAfter:              notAfter,\n\t\tKeyUsage:              keyUsage,\n\t\tExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},\n\t\tBasicConstraintsValid: true,\n\t}\n\n\tfor _, strIP := range hostCert.IPAddresses {\n\t\tif ip := net.ParseIP(strIP); ip != nil {\n\t\t\ttemplate.IPAddresses = append(template.IPAddresses, ip)\n\t\t}\n\t}\n\ttemplate.DNSNames = append(template.DNSNames, hostCert.DNSNames...)\n\n\ttemplate.IsCA = true\n\ttemplate.KeyUsage |= x509.KeyUsageCertSign\n\n\tderBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to create certificate: %v\", err)\n\t}\n\n\tcaBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to create ca: %v\", err)\n\t}\n\n\tserverCert := hostCert.Prefix + \"cert.pem\"\n\tserverKey := hostCert.Prefix + \"key.pem\"\n\tcaCert := hostCert.Prefix + \"ca.pem\"\n\n\tcertOut, err := os.Create(serverCert)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to open %s for writing: %v\", serverCert, err)\n\t}\n\tif err := pem.Encode(certOut, &pem.Block{Type: \"CERTIFICATE\", Bytes: derBytes}); err != nil {\n\t\treturn fmt.Errorf(\"Failed to write data to %s: %v\", serverCert, err)\n\t}\n\tif err := certOut.Close(); err != nil {\n\t\treturn fmt.Errorf(\"Error closing %s: %v\", serverCert, err)\n\t}\n\tlog.Printf(\"wrote %s\\n\", serverCert)\n\n\tkeyOut, err := os.OpenFile(serverKey, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to open %s for writing: %v\", serverKey, err)\n\t}\n\tprivBytes, err := x509.MarshalPKCS8PrivateKey(priv)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Unable to marshal private key: %v\", err)\n\t}\n\tif err := pem.Encode(keyOut, &pem.Block{Type: \"PRIVATE KEY\", Bytes: privBytes}); err != nil {\n\t\treturn fmt.Errorf(\"Failed to write data to %s: %v\", serverKey, err)\n\t}\n\tif err := keyOut.Close(); err != nil {\n\t\treturn fmt.Errorf(\"Error closing %s: %v\", serverKey, err)\n\t}\n\tlog.Printf(\"wrote %s\\n\", serverKey)\n\n\tcaOut, err := os.Create(caCert)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to open %s for writing: %v\", caCert, err)\n\t}\n\tif err := pem.Encode(caOut, &pem.Block{Type: \"CERTIFICATE\", Bytes: caBytes}); err != nil {\n\t\treturn fmt.Errorf(\"Failed to write data to %s: %v\", caCert, err)\n\t}\n\tif err := caOut.Close(); err != nil {\n\t\treturn fmt.Errorf(\"Error closing %s: %v\", caCert, err)\n\t}\n\tlog.Printf(\"wrote %s\\n\", caCert)\n\treturn nil\n}\n"
  }
]