├── .deepsource.toml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── report-vulnerability.md ├── label-commenter-config.yml └── workflows │ ├── go.yml │ ├── labels.yml │ └── lock-threads.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── async ├── asyncagent.go └── asyncagent_test.go ├── backoff ├── backoff.go └── backoff_test.go ├── config ├── config.go ├── config_test.go ├── parser.go ├── parser_test.go ├── uri.go └── uri_test.go ├── core └── version.go ├── docs ├── BENCHMARKS.md ├── CONFIG.md ├── OVERVIEW.md └── README.md ├── encoding ├── encoding.go ├── encoding_test.go ├── json_benchmark_test.go ├── json_test.go └── register.go ├── go.mod ├── go.sum ├── logging ├── log.go └── log_test.go ├── plugin ├── plugin.go └── plugin_test.go ├── proxy ├── balancing.go ├── balancing_benchmark_test.go ├── balancing_test.go ├── concurrent.go ├── concurrent_benchmark_test.go ├── concurrent_test.go ├── factory.go ├── factory_test.go ├── formatter.go ├── formatter_benchmark_test.go ├── formatter_test.go ├── graphql.go ├── graphql_test.go ├── headers_filter.go ├── headers_filter_test.go ├── http.go ├── http_benchmark_test.go ├── http_response.go ├── http_response_test.go ├── http_test.go ├── logging.go ├── logging_test.go ├── merging.go ├── merging_benchmark_test.go ├── merging_test.go ├── plugin.go ├── plugin │ ├── modifier.go │ ├── modifier_test.go │ └── tests │ │ ├── error │ │ └── main.go │ │ └── logger │ │ └── main.go ├── plugin_test.go ├── proxy.go ├── proxy_test.go ├── query_strings_filter.go ├── query_strings_filter_test.go ├── register.go ├── register_test.go ├── request.go ├── request_benchmark_test.go ├── request_test.go ├── shadow.go ├── shadow_test.go ├── stack_benchmark_test.go ├── stack_test.go ├── static.go └── static_test.go ├── register ├── register.go └── register_test.go ├── router ├── chi │ ├── endpoint.go │ ├── endpoint_benchmark_test.go │ ├── endpoint_test.go │ ├── router.go │ └── router_test.go ├── gin │ ├── debug.go │ ├── debug_test.go │ ├── echo.go │ ├── echo_test.go │ ├── endpoint.go │ ├── endpoint_benchmark_test.go │ ├── endpoint_test.go │ ├── engine.go │ ├── engine_test.go │ ├── render.go │ ├── render_test.go │ ├── router.go │ └── router_test.go ├── gorilla │ ├── router.go │ └── router_test.go ├── helper.go ├── helper_test.go ├── httptreemux │ ├── router.go │ └── router_test.go ├── mux │ ├── debug.go │ ├── debug_test.go │ ├── echo.go │ ├── echo_test.go │ ├── endpoint.go │ ├── endpoint_benchmark_test.go │ ├── endpoint_test.go │ ├── engine.go │ ├── engine_test.go │ ├── render.go │ ├── render_test.go │ ├── router.go │ └── router_test.go ├── negroni │ ├── router.go │ └── router_test.go └── router.go ├── sd ├── dnssrv │ ├── subscriber.go │ └── subscriber_test.go ├── loadbalancing.go ├── loadbalancing_benchmark_test.go ├── loadbalancing_test.go ├── register.go ├── register_test.go └── subscriber.go ├── test ├── doc.go ├── integration_test.go └── lura.json └── transport └── http ├── client ├── executor.go ├── executor_test.go ├── graphql │ ├── graphql.go │ └── graphql_test.go ├── plugin │ ├── doc.go │ ├── executor.go │ ├── plugin.go │ ├── plugin_test.go │ └── tests │ │ └── main.go ├── status.go └── status_test.go └── server ├── plugin ├── doc.go ├── plugin.go ├── plugin_test.go ├── server.go └── tests │ └── main.go ├── server.go ├── server_test.go └── tls_test.go /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "shell" 5 | enabled = true 6 | 7 | [[analyzers]] 8 | name = "go" 9 | enabled = true 10 | 11 | [analyzers.meta] 12 | import_root = "github.com/luraproject/lura" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Configuration used 16 | 2. Steps to run the software 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Logs** 22 | If applicable, any logs and debugging information 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/report-vulnerability.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report vulnerability 3 | about: Report a vulnerability 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | For **private vulnerabilities** write to support@devops.faith instead 11 | 12 | **Vulnerabilty description** 13 | Explain the vulnerability in detail and how to reproduce when possible 14 | 15 | **Reference** 16 | E.g: https://nvd.nist.gov/vuln/detail/CVE-2019-6486 17 | 18 | **Additional information** 19 | Impact, Known Affected Software Configurations, etc. 20 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: "1.23" 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -cover -race ./... 26 | 27 | - name: Integration Test 28 | run: go test -tags integration ./test -------------------------------------------------------------------------------- /.github/workflows/labels.yml: -------------------------------------------------------------------------------- 1 | name: Label commenter 2 | on: 3 | issues: 4 | types: [labeled, unlabeled] 5 | pull_request_target: 6 | types: [labeled, unlabeled] 7 | jobs: 8 | stale: 9 | uses: luraproject/.github/.github/workflows/label-commenter.yml@main 10 | -------------------------------------------------------------------------------- /.github/workflows/lock-threads.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock Threads' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | concurrency: 13 | group: lock 14 | 15 | jobs: 16 | action: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: dessant/lock-threads@v3 20 | with: 21 | pr-inactive-days: '90' 22 | issue-inactive-days: '90' 23 | add-issue-labels: 'locked' 24 | issue-comment: > 25 | This issue was marked as resolved a long time ago and now has been 26 | automatically locked as there has not been any recent activity after it. 27 | You can still open a new issue and reference this link. 28 | pr-comment: > 29 | This pull request was marked as resolved a long time ago and now has been 30 | automatically locked as there has not been any recent activity after it. 31 | You can still open a new issue and reference this link. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | server.rsa.crt 3 | server.rsa.key 4 | *.pem 5 | *.json 6 | *.toml 7 | *.dot 8 | *.out 9 | *.so 10 | bench_res 11 | .cover 12 | .idea 13 | 14 | *DS_Store 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In 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. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project 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. 28 | 29 | Project 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. 30 | 31 | ## Scope 32 | 33 | This 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. 34 | 35 | ## Enforcement 36 | 37 | Instances 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. 38 | 39 | Project 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. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to Lura, there are several ways 4 | you can contribute and make this project more awesome, please see below: 5 | 6 | ## Reporting an Issue 7 | 8 | If 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 9 | filing the issue please describe the problem with the maximum level of detail 10 | and the steps to reproduce the problem, including information about your 11 | environment. 12 | 13 | You can also open an issue requesting for help or doing a question and it's 14 | also a good way of contributing since other users might be in a similar 15 | position. 16 | 17 | Please note we have a code of conduct, please follow it in all your interactions with the project. 18 | 19 | ## Code Contributions 20 | 21 | When contributing to this repository, it is generally a good idea to discuss 22 | the change with the owners before investing a lot of time coding. The process 23 | could be: 24 | 25 | 1. Open an issue explaining the improvment or fix you want to add 26 | 2. [Fork the project](https://github.com/luraproject/lura/fork) 27 | 3. Code it in your fork 28 | 4. Submit a [pull request](https://help.github.com/articles/creating-a-pull-request) referencing the issue 29 | 30 | 31 | Your work will then be reviewed as soon as possible (suggestions about some 32 | changes, improvements or alternatives may be given). 33 | 34 | **Don't forget to add tests**, make sure that they all pass! 35 | 36 | # Help with Git 37 | 38 | Once the repository is forked, you should track the upstream (original) one 39 | using the following command: 40 | 41 | git remote add upstream https://github.com/luraproject/lura.git 42 | 43 | Then you should create your own branch: 44 | 45 | git checkout -b /- 46 | 47 | Once your changes are done (`git commit -am ''`), get the 48 | upstream changes: 49 | 50 | git checkout master 51 | git pull --rebase origin master 52 | git pull --rebase upstream master 53 | git checkout 54 | git rebase master 55 | 56 | Finally, publish your changes: 57 | 58 | git push -f origin 59 | 60 | You should be now ready to make a pull request. 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2021 Lura Project a Series of LF Projects, LLC 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test build benchmark 2 | 3 | OS := $(shell uname | tr '[:upper:]' '[:lower:]') 4 | GIT_COMMIT := $(shell git rev-parse --short=7 HEAD) 5 | 6 | all: test build 7 | 8 | generate: 9 | go generate ./... 10 | go build -buildmode=plugin -o ./transport/http/client/plugin/tests/lura-client-example.so ./transport/http/client/plugin/tests 11 | go build -buildmode=plugin -o ./transport/http/server/plugin/tests/lura-server-example.so ./transport/http/server/plugin/tests 12 | go build -buildmode=plugin -o ./proxy/plugin/tests/lura-request-modifier-example.so ./proxy/plugin/tests/logger 13 | go build -buildmode=plugin -o ./proxy/plugin/tests/lura-error-example.so ./proxy/plugin/tests/error 14 | 15 | test: generate 16 | go test -cover -race ./... 17 | go test -tags integration ./test/... 18 | go test -tags integration ./transport/... 19 | go test -tags integration ./proxy/... 20 | 21 | benchmark: 22 | @mkdir -p bench_res 23 | @touch bench_res/${GIT_COMMIT}.out 24 | @go test -run none -bench . -benchmem ./... >> bench_res/${GIT_COMMIT}.out 25 | 26 | build: 27 | go build ./... 28 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | Lura only fixes the latest version of the software, and does not patch prior versions. 3 | 4 | ## Reporting a Vulnerability 5 | 6 | Please 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. 7 | 8 | We 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. 9 | 10 | The Lura Project DOES NOT provide cash awards for discovered vulnerabilities at this time. 11 | 12 | Thank you 13 | 14 | 15 | -------------------------------------------------------------------------------- /async/asyncagent.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | */ 5 | package async 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "fmt" 11 | "math" 12 | 13 | "github.com/luraproject/lura/v2/backoff" 14 | "github.com/luraproject/lura/v2/config" 15 | "github.com/luraproject/lura/v2/logging" 16 | "github.com/luraproject/lura/v2/proxy" 17 | 18 | "golang.org/x/sync/errgroup" 19 | ) 20 | 21 | // Options contains the configuration to pass to the async agent factory 22 | type Options struct { 23 | // Agent keeps the configuration for the async agent 24 | Agent *config.AsyncAgent 25 | // Endpoint encapsulates the configuration for the associated pipe 26 | Endpoint *config.EndpointConfig 27 | // Proxy is the pipe associated with the async agent 28 | Proxy proxy.Proxy 29 | // AgentPing is the channel for the agent to send ping messages 30 | AgentPing chan<- string 31 | // G is the error group responsible for managing the agents and the router itself 32 | G *errgroup.Group 33 | // ShouldContinue is a function signaling when to stop the connection retries 34 | ShouldContinue func(int) bool 35 | // BackoffF is a function encapsulating the backoff strategy 36 | BackoffF backoff.TimeToWaitBeforeRetry 37 | Logger logging.Logger 38 | } 39 | 40 | // Factory is a function able to start an async agent 41 | type Factory func(context.Context, Options) bool 42 | 43 | // AgentStarter groups a set of factories to be used 44 | type AgentStarter []Factory 45 | 46 | // Start executes all the factories for each async agent configuration 47 | func (a AgentStarter) Start( 48 | ctx context.Context, 49 | agents []*config.AsyncAgent, 50 | logger logging.Logger, 51 | agentPing chan<- string, 52 | pf proxy.Factory, 53 | ) func() error { 54 | if len(a) == 0 { 55 | return func() error { return ErrNoAgents } 56 | } 57 | 58 | g, ctx := errgroup.WithContext(ctx) 59 | 60 | for i, agent := range agents { 61 | i, agent := i, agent 62 | if agent.Name == "" { 63 | agent.Name = fmt.Sprintf("AsyncAgent-%02d", i) 64 | } 65 | 66 | logger.Debug(fmt.Sprintf("[SERVICE: AsyncAgent][%s] Starting the async agent", agent.Name)) 67 | 68 | for i := range agent.Backend { 69 | agent.Backend[i].Timeout = agent.Consumer.Timeout 70 | } 71 | 72 | endpoint := &config.EndpointConfig{ 73 | Endpoint: agent.Name, 74 | Timeout: agent.Consumer.Timeout, 75 | Backend: agent.Backend, 76 | ExtraConfig: agent.ExtraConfig, 77 | } 78 | p, err := pf.New(endpoint) 79 | if err != nil { 80 | logger.Error(fmt.Sprintf("[SERVICE: AsyncAgent][%s] building the proxy pipe:", agent.Name), err) 81 | continue 82 | } 83 | 84 | if agent.Connection.MaxRetries <= 0 { 85 | agent.Connection.MaxRetries = math.MaxInt64 86 | } 87 | 88 | opts := Options{ 89 | Agent: agent, 90 | Endpoint: endpoint, 91 | Proxy: p, 92 | AgentPing: agentPing, 93 | G: g, 94 | ShouldContinue: func(i int) bool { return i <= agent.Connection.MaxRetries }, 95 | BackoffF: backoff.GetByName(agent.Connection.BackoffStrategy), 96 | Logger: logger, 97 | } 98 | 99 | for _, f := range a { 100 | if f(ctx, opts) { 101 | break 102 | } 103 | } 104 | 105 | } 106 | 107 | return g.Wait 108 | } 109 | 110 | var ErrNoAgents = errors.New("no agent factories defined") 111 | -------------------------------------------------------------------------------- /async/asyncagent_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package async 4 | 5 | import ( 6 | "context" 7 | "testing" 8 | 9 | "github.com/luraproject/lura/v2/config" 10 | "github.com/luraproject/lura/v2/logging" 11 | "github.com/luraproject/lura/v2/proxy" 12 | ) 13 | 14 | func TestAgentStarter_Start_last(t *testing.T) { 15 | var firstAgentCalled, secondAgentCalled bool 16 | firstAgent := func(_ context.Context, opts Options) bool { 17 | // TODO: check opts 18 | firstAgentCalled = true 19 | return false 20 | } 21 | secondAgent := func(_ context.Context, opts Options) bool { 22 | // TODO: check opts 23 | secondAgentCalled = true 24 | return true 25 | } 26 | 27 | ctx, cancel := context.WithCancel(context.Background()) 28 | defer cancel() 29 | 30 | ch := make(chan string) 31 | as := AgentStarter([]Factory{firstAgent, secondAgent}) 32 | agents := []*config.AsyncAgent{ 33 | {}, 34 | } 35 | wait := as.Start(ctx, agents, logging.NoOp, (chan<- string)(ch), noopProxyFactory) 36 | 37 | if err := wait(); err != nil { 38 | t.Error(err) 39 | } 40 | 41 | if !firstAgentCalled { 42 | t.Error("first agent not called") 43 | } 44 | 45 | if !secondAgentCalled { 46 | t.Error("second agent not called") 47 | } 48 | } 49 | 50 | func TestAgentStarter_Start_first(t *testing.T) { 51 | var firstAgentCalled, secondAgentCalled bool 52 | firstAgent := func(_ context.Context, opts Options) bool { 53 | firstAgentCalled = true 54 | return true 55 | } 56 | secondAgent := func(_ context.Context, opts Options) bool { 57 | secondAgentCalled = true 58 | return false 59 | } 60 | 61 | ctx, cancel := context.WithCancel(context.Background()) 62 | defer cancel() 63 | 64 | ch := make(chan string) 65 | as := AgentStarter([]Factory{firstAgent, secondAgent}) 66 | agents := []*config.AsyncAgent{ 67 | {}, 68 | } 69 | wait := as.Start(ctx, agents, logging.NoOp, (chan<- string)(ch), noopProxyFactory) 70 | 71 | if err := wait(); err != nil { 72 | t.Error(err) 73 | } 74 | 75 | if !firstAgentCalled { 76 | t.Error("first agent not called") 77 | } 78 | 79 | if secondAgentCalled { 80 | t.Error("second agent called") 81 | } 82 | } 83 | 84 | var noopProxyFactory = proxy.FactoryFunc(func(*config.EndpointConfig) (proxy.Proxy, error) { 85 | return proxy.NoopProxy, nil 86 | }) 87 | -------------------------------------------------------------------------------- /backoff/backoff.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | Package backoff contains some basic implementations and a selector by strategy name 5 | */ 6 | package backoff 7 | 8 | import ( 9 | "math/rand" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // GetByName returns the WaitBeforeRetry function implementing the strategy 15 | func GetByName(strategy string) TimeToWaitBeforeRetry { 16 | switch strings.ToLower(strategy) { 17 | case "linear": 18 | return LinearBackoff 19 | case "linear-jitter": 20 | return LinearJitterBackoff 21 | case "exponential": 22 | return ExponentialBackoff 23 | case "exponential-jitter": 24 | return ExponentialJitterBackoff 25 | } 26 | return DefaultBackoff 27 | } 28 | 29 | // TimeToWaitBeforeRetry returns the duration to wait before retrying for the 30 | // given time 31 | type TimeToWaitBeforeRetry func(int) time.Duration 32 | 33 | // DefaultBackoffDuration is the duration returned by the DefaultBackoff 34 | var DefaultBackoffDuration = time.Second 35 | 36 | // DefaultBackoff always returns DefaultBackoffDuration 37 | func DefaultBackoff(_ int) time.Duration { 38 | return DefaultBackoffDuration 39 | } 40 | 41 | // ExponentialBackoff returns ever increasing backoffs by a power of 2 42 | func ExponentialBackoff(i int) time.Duration { 43 | return time.Duration(1< 2 | 3 | # The Lura Project 4 | 5 | ## How to use it 6 | 7 | Visit the [framework overview](/docs/OVERVIEW.md) for details about the components of the Lura project. 8 | 9 | A good example about how to use it can be found in the [KrakenD CE](https://github.com/krakend/krakend-ce) 10 | API Gateway project. 11 | 12 | ## Configuration file 13 | 14 | [Lura config file](/docs/CONFIG.md). 15 | 16 | ## Benchmarks 17 | 18 | Check out the [benchmark results](/docs/BENCHMARKS.md) of several Lura components. 19 | 20 | ## Contributing 21 | 22 | Read the guidelines about [contributing](../CONTRIBUTING.md). 23 | 24 | -------------------------------------------------------------------------------- /encoding/encoding.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | Package encoding provides basic decoding implementations. 5 | 6 | Decode decodes HTTP responses: 7 | 8 | resp, _ := http.Get("http://api.example.com/") 9 | ... 10 | var data map[string]interface{} 11 | err := JSONDecoder(resp.Body, &data) 12 | */ 13 | package encoding 14 | 15 | import ( 16 | "encoding/json" 17 | "io" 18 | ) 19 | 20 | // Decoder is a function that reads from the reader and decodes it 21 | // into an map of interfaces 22 | type Decoder func(io.Reader, *map[string]interface{}) error 23 | 24 | // DecoderFactory is a function that returns CollectionDecoder or an EntityDecoder 25 | type DecoderFactory func(bool) func(io.Reader, *map[string]interface{}) error 26 | 27 | // NOOP is the key for the NoOp encoding 28 | const NOOP = "no-op" 29 | 30 | // NoOpDecoder is a decoder that does nothing 31 | func NoOpDecoder(_ io.Reader, _ *map[string]interface{}) error { return nil } 32 | 33 | func noOpDecoderFactory(_ bool) func(io.Reader, *map[string]interface{}) error { return NoOpDecoder } 34 | 35 | // JSON is the key for the json encoding 36 | const JSON = "json" 37 | 38 | // NewJSONDecoder returns the right JSON decoder 39 | func NewJSONDecoder(isCollection bool) func(io.Reader, *map[string]interface{}) error { 40 | if isCollection { 41 | return JSONCollectionDecoder 42 | } 43 | return JSONDecoder 44 | } 45 | 46 | // JSONDecoder decodes a json message into a map 47 | func JSONDecoder(r io.Reader, v *map[string]interface{}) error { 48 | d := json.NewDecoder(r) 49 | d.UseNumber() 50 | return d.Decode(v) 51 | } 52 | 53 | // JSONCollectionDecoder decodes a json collection and returns a map with the array at the 'collection' key 54 | func JSONCollectionDecoder(r io.Reader, v *map[string]interface{}) error { 55 | var collection []interface{} 56 | d := json.NewDecoder(r) 57 | d.UseNumber() 58 | if err := d.Decode(&collection); err != nil { 59 | return err 60 | } 61 | *(v) = map[string]interface{}{"collection": collection} 62 | return nil 63 | } 64 | 65 | // SAFE_JSON is the key for the json encoding 66 | const SAFE_JSON = "safejson" 67 | 68 | // NewSafeJSONDecoder returns the universal json decoder 69 | func NewSafeJSONDecoder(_ bool) func(io.Reader, *map[string]interface{}) error { 70 | return SafeJSONDecoder 71 | } 72 | 73 | // SafeJSONDecoder decodes both json objects and collections 74 | func SafeJSONDecoder(r io.Reader, v *map[string]interface{}) error { 75 | d := json.NewDecoder(r) 76 | d.UseNumber() 77 | var t interface{} 78 | if err := d.Decode(&t); err != nil { 79 | return err 80 | } 81 | switch tt := t.(type) { 82 | case map[string]interface{}: 83 | *v = tt 84 | case []interface{}: 85 | *v = map[string]interface{}{"collection": tt} 86 | default: 87 | *v = map[string]interface{}{"content": tt} 88 | } 89 | return nil 90 | } 91 | 92 | // STRING is the key for the string encoding 93 | const STRING = "string" 94 | 95 | // NewStringDecoder returns a String decoder 96 | func NewStringDecoder(_ bool) func(io.Reader, *map[string]interface{}) error { 97 | return StringDecoder 98 | } 99 | 100 | // StringDecoder returns a map with the content of the reader under the key 'content' 101 | func StringDecoder(r io.Reader, v *map[string]interface{}) error { 102 | data, err := io.ReadAll(r) 103 | if err != nil { 104 | return err 105 | } 106 | *(v) = map[string]interface{}{"content": string(data)} 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /encoding/json_benchmark_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package encoding 4 | 5 | import ( 6 | "io" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func BenchmarkDecoder(b *testing.B) { 12 | for _, dec := range []struct { 13 | name string 14 | decoder func(io.Reader, *map[string]interface{}) error 15 | }{ 16 | { 17 | name: "json-collection", 18 | decoder: NewJSONDecoder(true), 19 | }, 20 | { 21 | name: "json-map", 22 | decoder: NewJSONDecoder(false), 23 | }, 24 | { 25 | name: "safe-json-collection", 26 | decoder: NewSafeJSONDecoder(true), 27 | }, 28 | { 29 | name: "safe-json-map", 30 | decoder: NewSafeJSONDecoder(true), 31 | }, 32 | } { 33 | for _, tc := range []struct { 34 | name string 35 | input string 36 | }{ 37 | { 38 | name: "collection", 39 | input: `["a","b","c"]`, 40 | }, 41 | { 42 | name: "map", 43 | input: `{"foo": "bar", "supu": false, "tupu": 4.20}`, 44 | }, 45 | } { 46 | b.Run(dec.name+"/"+tc.name, func(b *testing.B) { 47 | var result map[string]interface{} 48 | for i := 0; i < b.N; i++ { 49 | _ = dec.decoder(strings.NewReader(tc.input), &result) 50 | } 51 | }) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /encoding/register.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package encoding 4 | 5 | import ( 6 | "io" 7 | 8 | "github.com/luraproject/lura/v2/register" 9 | ) 10 | 11 | // GetRegister returns the package register 12 | func GetRegister() *DecoderRegister { 13 | return decoders 14 | } 15 | 16 | type untypedRegister interface { 17 | Register(name string, v interface{}) 18 | Get(name string) (interface{}, bool) 19 | Clone() map[string]interface{} 20 | } 21 | 22 | // DecoderRegister is the struct responsible of registering the decoder factories 23 | type DecoderRegister struct { 24 | data untypedRegister 25 | } 26 | 27 | // Register adds a decoder factory to the register 28 | func (r *DecoderRegister) Register(name string, dec func(bool) func(io.Reader, *map[string]interface{}) error) error { 29 | r.data.Register(name, dec) 30 | return nil 31 | } 32 | 33 | // Get returns a decoder factory from the register by name. If no factory is found, it returns a JSON decoder factory 34 | func (r *DecoderRegister) Get(name string) func(bool) func(io.Reader, *map[string]interface{}) error { 35 | for _, n := range []string{name, JSON} { 36 | if v, ok := r.data.Get(n); ok { 37 | if dec, ok := v.(func(bool) func(io.Reader, *map[string]interface{}) error); ok { 38 | return dec 39 | } 40 | } 41 | } 42 | return NewJSONDecoder 43 | } 44 | 45 | var ( 46 | decoders = initDecoderRegister() 47 | defaultDecoders = map[string]func(bool) func(io.Reader, *map[string]interface{}) error{ 48 | JSON: NewJSONDecoder, 49 | SAFE_JSON: NewSafeJSONDecoder, 50 | STRING: NewStringDecoder, 51 | NOOP: noOpDecoderFactory, 52 | } 53 | ) 54 | 55 | func initDecoderRegister() *DecoderRegister { 56 | r := &DecoderRegister{data: register.NewUntyped()} 57 | for k, v := range defaultDecoders { 58 | r.Register(k, v) 59 | } 60 | return r 61 | } 62 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/luraproject/lura/v2 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.7 6 | 7 | require ( 8 | github.com/dimfeld/httptreemux/v5 v5.5.0 9 | github.com/gin-contrib/sse v0.1.0 // indirect 10 | github.com/gin-gonic/gin v1.9.1 11 | github.com/go-chi/chi/v5 v5.1.0 12 | github.com/gorilla/mux v1.8.1 13 | github.com/krakendio/flatmap v1.1.1 14 | github.com/mattn/go-isatty v0.0.20 // indirect 15 | github.com/urfave/negroni/v2 v2.0.2 16 | github.com/valyala/fastrand v1.1.0 17 | ) 18 | 19 | require ( 20 | golang.org/x/net v0.38.0 21 | golang.org/x/sync v0.12.0 22 | golang.org/x/text v0.23.0 23 | ) 24 | 25 | require ( 26 | github.com/bytedance/sonic v1.12.5 // indirect 27 | github.com/bytedance/sonic/loader v0.2.0 // indirect 28 | github.com/cloudwego/base64x v0.1.4 // indirect 29 | github.com/cloudwego/iasm v0.2.0 // indirect 30 | github.com/gabriel-vasile/mimetype v1.4.7 // indirect 31 | github.com/go-playground/locales v0.14.1 // indirect 32 | github.com/go-playground/universal-translator v0.18.1 // indirect 33 | github.com/go-playground/validator/v10 v10.23.0 // indirect 34 | github.com/goccy/go-json v0.10.4 // indirect 35 | github.com/google/go-cmp v0.6.0 // indirect 36 | github.com/json-iterator/go v1.1.12 // indirect 37 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 38 | github.com/leodido/go-urn v1.4.0 // indirect 39 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 40 | github.com/modern-go/reflect2 v1.0.2 // indirect 41 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 42 | github.com/stretchr/testify v1.10.0 // indirect 43 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 44 | github.com/ugorji/go/codec v1.2.12 // indirect 45 | golang.org/x/arch v0.12.0 // indirect 46 | golang.org/x/crypto v0.36.0 // indirect 47 | golang.org/x/sys v0.31.0 // indirect 48 | google.golang.org/protobuf v1.35.2 // indirect 49 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 50 | gopkg.in/yaml.v3 v3.0.1 // indirect 51 | ) 52 | -------------------------------------------------------------------------------- /logging/log.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | Package logging provides a simple logger interface and implementations 5 | */ 6 | package logging 7 | 8 | import ( 9 | "errors" 10 | "io" 11 | "log" 12 | "os" 13 | "strings" 14 | ) 15 | 16 | // Logger collects logging information at several levels 17 | type Logger interface { 18 | Debug(v ...interface{}) 19 | Info(v ...interface{}) 20 | Warning(v ...interface{}) 21 | Error(v ...interface{}) 22 | Critical(v ...interface{}) 23 | Fatal(v ...interface{}) 24 | } 25 | 26 | const ( 27 | // LEVEL_DEBUG = 0 28 | LEVEL_DEBUG = iota 29 | // LEVEL_INFO = 1 30 | LEVEL_INFO 31 | // LEVEL_WARNING = 2 32 | LEVEL_WARNING 33 | // LEVEL_ERROR = 3 34 | LEVEL_ERROR 35 | // LEVEL_CRITICAL = 4 36 | LEVEL_CRITICAL 37 | ) 38 | 39 | var ( 40 | // ErrInvalidLogLevel is used when an invalid log level has been used. 41 | ErrInvalidLogLevel = errors.New("invalid log level") 42 | defaultLogger = BasicLogger{Level: LEVEL_CRITICAL, Logger: log.New(os.Stderr, "", log.LstdFlags)} 43 | logLevels = map[string]int{ 44 | "DEBUG": LEVEL_DEBUG, 45 | "INFO": LEVEL_INFO, 46 | "WARNING": LEVEL_WARNING, 47 | "ERROR": LEVEL_ERROR, 48 | "CRITICAL": LEVEL_CRITICAL, 49 | } 50 | // NoOp is the NO-OP logger 51 | NoOp, _ = NewLogger("CRITICAL", io.Discard, "") 52 | ) 53 | 54 | // NewLogger creates and returns a Logger object 55 | func NewLogger(level string, out io.Writer, prefix string) (BasicLogger, error) { 56 | l, ok := logLevels[strings.ToUpper(level)] 57 | if !ok { 58 | return defaultLogger, ErrInvalidLogLevel 59 | } 60 | return BasicLogger{Level: l, Prefix: prefix, Logger: log.New(out, "", log.LstdFlags)}, nil 61 | } 62 | 63 | type BasicLogger struct { 64 | Level int 65 | Prefix string 66 | Logger *log.Logger 67 | } 68 | 69 | // Debug logs a message using DEBUG as log level. 70 | func (l BasicLogger) Debug(v ...interface{}) { 71 | if l.Level > LEVEL_DEBUG { 72 | return 73 | } 74 | l.prependLog("DEBUG:", v...) 75 | } 76 | 77 | // Info logs a message using INFO as log level. 78 | func (l BasicLogger) Info(v ...interface{}) { 79 | if l.Level > LEVEL_INFO { 80 | return 81 | } 82 | l.prependLog("INFO:", v...) 83 | } 84 | 85 | // Warning logs a message using WARNING as log level. 86 | func (l BasicLogger) Warning(v ...interface{}) { 87 | if l.Level > LEVEL_WARNING { 88 | return 89 | } 90 | l.prependLog("WARNING:", v...) 91 | } 92 | 93 | // Error logs a message using ERROR as log level. 94 | func (l BasicLogger) Error(v ...interface{}) { 95 | if l.Level > LEVEL_ERROR { 96 | return 97 | } 98 | l.prependLog("ERROR:", v...) 99 | } 100 | 101 | // Critical logs a message using CRITICAL as log level. 102 | func (l BasicLogger) Critical(v ...interface{}) { 103 | l.prependLog("CRITICAL:", v...) 104 | } 105 | 106 | // Fatal is equivalent to l.Critical(fmt.Sprint()) followed by a call to os.Exit(1). 107 | func (l BasicLogger) Fatal(v ...interface{}) { 108 | l.prependLog("FATAL:", v...) 109 | os.Exit(1) 110 | } 111 | 112 | func (l BasicLogger) prependLog(level string, v ...interface{}) { 113 | msg := make([]interface{}, len(v)+2) 114 | msg[0] = l.Prefix 115 | msg[1] = level 116 | copy(msg[2:], v) 117 | l.Logger.Println(msg...) 118 | } 119 | -------------------------------------------------------------------------------- /logging/log_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package logging 4 | 5 | import ( 6 | "bytes" 7 | "os" 8 | "os/exec" 9 | "regexp" 10 | "testing" 11 | ) 12 | 13 | const ( 14 | debugMsg = "Debug msg" 15 | infoMsg = "Info msg" 16 | warningMsg = "Warning msg" 17 | errorMsg = "Error msg" 18 | criticalMsg = "Critical msg" 19 | fatalMsg = "Fatal msg" 20 | ) 21 | 22 | func TestNewLogger(t *testing.T) { 23 | levels := []string{"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} 24 | regexps := []*regexp.Regexp{ 25 | regexp.MustCompile(debugMsg), 26 | regexp.MustCompile(infoMsg), 27 | regexp.MustCompile(warningMsg), 28 | regexp.MustCompile(errorMsg), 29 | regexp.MustCompile(criticalMsg), 30 | } 31 | 32 | for i, level := range levels { 33 | output := logSomeStuff(level) 34 | for j := i; j < len(regexps); j++ { 35 | if !regexps[j].MatchString(output) { 36 | t.Errorf("The output doesn't contain the expected msg for the level: %s. [%s]", level, output) 37 | } 38 | } 39 | } 40 | } 41 | 42 | func TestNewLogger_unknownLevel(t *testing.T) { 43 | _, err := NewLogger("UNKNOWN", bytes.NewBuffer(make([]byte, 1024)), "pref") 44 | if err == nil { 45 | t.Error("The factory didn't return the expected error") 46 | return 47 | } 48 | if err != ErrInvalidLogLevel { 49 | t.Errorf("The factory didn't return the expected error. Got: %s", err.Error()) 50 | } 51 | } 52 | 53 | func TestNewLogger_fatal(t *testing.T) { 54 | if os.Getenv("BE_CRASHER") == "1" { 55 | l, err := NewLogger("Critical", bytes.NewBuffer(make([]byte, 1024)), "pref") 56 | if err != nil { 57 | t.Error("The factory returned an expected error:", err.Error()) 58 | return 59 | } 60 | l.Fatal("crash!!!") 61 | return 62 | } 63 | cmd := exec.Command(os.Args[0], "-test.run=TestNewLogger_fatal") 64 | cmd.Env = append(os.Environ(), "BE_CRASHER=1") 65 | err := cmd.Run() 66 | if e, ok := err.(*exec.ExitError); ok && !e.Success() { 67 | return 68 | } 69 | t.Fatalf("process ran with err %v, want exit status 1", err) 70 | } 71 | 72 | func logSomeStuff(level string) string { 73 | buff := bytes.NewBuffer(make([]byte, 1024)) 74 | logger, _ := NewLogger(level, buff, "pref") 75 | 76 | logger.Debug(debugMsg) 77 | logger.Info(infoMsg) 78 | logger.Warning(warningMsg) 79 | logger.Error(errorMsg) 80 | logger.Critical(criticalMsg) 81 | 82 | return buff.String() 83 | } 84 | -------------------------------------------------------------------------------- /plugin/plugin.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | Package plugin provides tools for loading and registering plugins 5 | */ 6 | package plugin 7 | 8 | import ( 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | // Scan returns all the files contained in the received folder with a filename matching the given pattern 15 | func Scan(folder, pattern string) ([]string, error) { 16 | files, err := os.ReadDir(folder) 17 | if err != nil { 18 | return []string{}, err 19 | } 20 | 21 | var plugins []string 22 | 23 | for _, file := range files { 24 | if !file.IsDir() && strings.Contains(file.Name(), pattern) { 25 | plugins = append(plugins, filepath.Join(folder, file.Name())) 26 | } 27 | } 28 | 29 | return plugins, nil 30 | } 31 | -------------------------------------------------------------------------------- /plugin/plugin_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package plugin 4 | 5 | import ( 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestScan_ok(t *testing.T) { 11 | tmpDir, err := os.MkdirTemp(".", "test") 12 | if err != nil { 13 | t.Error("unexpected error:", err.Error()) 14 | return 15 | } 16 | defer os.RemoveAll(tmpDir) 17 | f, err := os.CreateTemp(tmpDir, "test.so") 18 | if err != nil { 19 | t.Error("unexpected error:", err.Error()) 20 | return 21 | } 22 | f.Close() 23 | defer os.RemoveAll(tmpDir) 24 | 25 | tot, err := Scan(tmpDir, ".so") 26 | if len(tot) != 1 { 27 | t.Error("unexpected number of plugins found:", tot) 28 | } 29 | if err != nil { 30 | t.Error("unexpected error:", err.Error()) 31 | } 32 | } 33 | 34 | func TestScan_noFolder(t *testing.T) { 35 | expectedErr := "open unknown: no such file or directory" 36 | tot, err := Scan("unknown", "") 37 | if len(tot) != 0 { 38 | t.Error("unexpected number of plugins loaded:", tot) 39 | } 40 | if err == nil { 41 | t.Error("expecting error!") 42 | return 43 | } 44 | if err.Error() != expectedErr { 45 | t.Error("unexpected error:", err.Error()) 46 | } 47 | } 48 | 49 | func TestScan_emptyFolder(t *testing.T) { 50 | name, err := os.MkdirTemp(".", "test") 51 | if err != nil { 52 | t.Error("unexpected error:", err.Error()) 53 | return 54 | } 55 | tot, err := Scan(name, "") 56 | if len(tot) != 0 { 57 | t.Error("unexpected number of plugins loaded:", tot) 58 | } 59 | if err != nil { 60 | t.Error("unexpected error:", err.Error()) 61 | } 62 | os.RemoveAll(name) 63 | } 64 | 65 | func TestScan_noMatches(t *testing.T) { 66 | tmpDir, err := os.MkdirTemp(".", "test") 67 | if err != nil { 68 | t.Error("unexpected error:", err.Error()) 69 | return 70 | } 71 | defer os.RemoveAll(tmpDir) 72 | f, err := os.CreateTemp(tmpDir, "test") 73 | if err != nil { 74 | t.Error("unexpected error:", err.Error()) 75 | return 76 | } 77 | f.Close() 78 | defer os.RemoveAll(tmpDir) 79 | tot, err := Scan(tmpDir, ".so") 80 | if len(tot) != 0 { 81 | t.Error("unexpected number of plugins loaded:", tot) 82 | } 83 | if err != nil { 84 | t.Error("unexpected error:", err.Error()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /proxy/balancing_benchmark_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "context" 7 | "strconv" 8 | "testing" 9 | 10 | "github.com/luraproject/lura/v2/logging" 11 | ) 12 | 13 | const veryLargeString = "abcdefghijklmopqrstuvwxyzabcdefghijklmopqrstuvwxyzabcdefghijklmopqrstuvwxyzabcdefghijklmopqrstuvwxyz" 14 | 15 | func BenchmarkNewLoadBalancedMiddleware(b *testing.B) { 16 | for _, tc := range []int{3, 5, 9, 13, 17, 21, 25, 50, 100} { 17 | b.Run(strconv.Itoa(tc), func(b *testing.B) { 18 | proxy := newLoadBalancedMiddleware(logging.NoOp, dummyBalancer(veryLargeString[:tc]))(dummyProxy(&Response{})) 19 | b.ResetTimer() 20 | b.ReportAllocs() 21 | for i := 0; i < b.N; i++ { 22 | proxy(context.Background(), &Request{ 23 | Path: veryLargeString[:tc], 24 | }) 25 | } 26 | }) 27 | } 28 | } 29 | 30 | func BenchmarkNewLoadBalancedMiddleware_parallel3(b *testing.B) { 31 | benchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:3]) 32 | } 33 | 34 | func BenchmarkNewLoadBalancedMiddleware_parallel5(b *testing.B) { 35 | benchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:5]) 36 | } 37 | 38 | func BenchmarkNewLoadBalancedMiddleware_parallel9(b *testing.B) { 39 | benchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:9]) 40 | } 41 | 42 | func BenchmarkNewLoadBalancedMiddleware_parallel13(b *testing.B) { 43 | benchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:13]) 44 | } 45 | 46 | func BenchmarkNewLoadBalancedMiddleware_parallel17(b *testing.B) { 47 | benchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:17]) 48 | } 49 | 50 | func BenchmarkNewLoadBalancedMiddleware_parallel21(b *testing.B) { 51 | benchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:21]) 52 | } 53 | 54 | func BenchmarkNewLoadBalancedMiddleware_parallel25(b *testing.B) { 55 | benchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:25]) 56 | } 57 | 58 | func BenchmarkNewLoadBalancedMiddleware_parallel50(b *testing.B) { 59 | benchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:50]) 60 | } 61 | 62 | func BenchmarkNewLoadBalancedMiddleware_parallel100(b *testing.B) { 63 | benchmarkNewLoadBalancedMiddleware_parallel(b, veryLargeString[:100]) 64 | } 65 | 66 | func benchmarkNewLoadBalancedMiddleware_parallel(b *testing.B, subject string) { 67 | b.RunParallel(func(pb *testing.PB) { 68 | proxy := newLoadBalancedMiddleware(logging.NoOp, dummyBalancer(subject))(dummyProxy(&Response{})) 69 | for pb.Next() { 70 | proxy(context.Background(), &Request{ 71 | Path: subject, 72 | }) 73 | } 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /proxy/balancing_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "net" 9 | "net/url" 10 | "testing" 11 | 12 | "github.com/luraproject/lura/v2/config" 13 | "github.com/luraproject/lura/v2/logging" 14 | "github.com/luraproject/lura/v2/sd/dnssrv" 15 | ) 16 | 17 | func TestNewLoadBalancedMiddleware_ok(t *testing.T) { 18 | want := "supu:8080/tupu" 19 | lb := newLoadBalancedMiddleware(logging.NoOp, dummyBalancer("supu:8080")) 20 | assertion := func(ctx context.Context, request *Request) (*Response, error) { 21 | if request.URL.String() != want { 22 | t.Errorf("The middleware did not update the request URL! want [%s], have [%s]\n", want, request.URL) 23 | } 24 | return nil, nil 25 | } 26 | if _, err := lb(assertion)(context.Background(), &Request{ 27 | Path: "/tupu", 28 | }); err != nil { 29 | t.Errorf("The middleware propagated an unexpected error: %s\n", err.Error()) 30 | } 31 | } 32 | 33 | func TestNewLoadBalancedMiddleware_explosiveBalancer(t *testing.T) { 34 | expected := errors.New("supu") 35 | lb := newLoadBalancedMiddleware(logging.NoOp, explosiveBalancer{expected}) 36 | if _, err := lb(explosiveProxy(t))(context.Background(), &Request{}); err != expected { 37 | t.Errorf("The middleware did not propagate the lb error\n") 38 | } 39 | } 40 | 41 | func TestNewRoundRobinLoadBalancedMiddleware(t *testing.T) { 42 | testLoadBalancedMw(t, NewRoundRobinLoadBalancedMiddleware(&config.Backend{ 43 | Host: []string{"http://127.0.0.1:8080"}, 44 | })) 45 | } 46 | 47 | func TestNewRandomLoadBalancedMiddleware(t *testing.T) { 48 | testLoadBalancedMw(t, NewRandomLoadBalancedMiddleware(&config.Backend{ 49 | Host: []string{"http://127.0.0.1:8080"}, 50 | })) 51 | } 52 | 53 | func testLoadBalancedMw(t *testing.T, lb Middleware) { 54 | for _, tc := range []struct { 55 | path string 56 | query url.Values 57 | expected string 58 | }{ 59 | { 60 | path: "/tupu", 61 | expected: "http://127.0.0.1:8080/tupu", 62 | }, 63 | { 64 | path: "/tupu?extra=true", 65 | expected: "http://127.0.0.1:8080/tupu?extra=true", 66 | }, 67 | { 68 | path: "/tupu?extra=true", 69 | query: url.Values{"some": []string{"none"}}, 70 | expected: "http://127.0.0.1:8080/tupu?extra=true&some=none", 71 | }, 72 | { 73 | path: "/tupu", 74 | query: url.Values{"some": []string{"none"}}, 75 | expected: "http://127.0.0.1:8080/tupu?some=none", 76 | }, 77 | } { 78 | assertion := func(ctx context.Context, request *Request) (*Response, error) { 79 | if request.URL.String() != tc.expected { 80 | t.Errorf("The middleware did not update the request URL! want [%s], have [%s]\n", tc.expected, request.URL) 81 | } 82 | return nil, nil 83 | } 84 | if _, err := lb(assertion)(context.Background(), &Request{ 85 | Path: tc.path, 86 | Query: tc.query, 87 | }); err != nil { 88 | t.Errorf("The middleware propagated an unexpected error: %s\n", err.Error()) 89 | } 90 | } 91 | } 92 | 93 | func TestNewLoadBalancedMiddleware_parsingError(t *testing.T) { 94 | lb := NewRandomLoadBalancedMiddleware(&config.Backend{ 95 | Host: []string{"127.0.0.1:8080"}, 96 | }) 97 | assertion := func(ctx context.Context, request *Request) (*Response, error) { 98 | t.Error("The middleware didn't block the request!") 99 | return nil, nil 100 | } 101 | if _, err := lb(assertion)(context.Background(), &Request{ 102 | Path: "/tupu", 103 | }); err == nil { 104 | t.Error("The middleware didn't propagate the expected error") 105 | } 106 | } 107 | 108 | func TestNewRoundRobinLoadBalancedMiddleware_DNSSRV(t *testing.T) { 109 | defaultLookup := dnssrv.DefaultLookup 110 | 111 | dnssrv.DefaultLookup = func(service, proto, name string) (cname string, addrs []*net.SRV, err error) { 112 | return "cname", []*net.SRV{ 113 | { 114 | Port: 8080, 115 | Target: "127.0.0.1", 116 | Weight: 1, 117 | }, 118 | }, nil 119 | } 120 | testLoadBalancedMw(t, NewRoundRobinLoadBalancedMiddlewareWithSubscriber(dnssrv.New("some.service.example.tld"))) 121 | 122 | dnssrv.DefaultLookup = defaultLookup 123 | } 124 | 125 | type dummyBalancer string 126 | 127 | func (d dummyBalancer) Host() (string, error) { return string(d), nil } 128 | 129 | type explosiveBalancer struct { 130 | Error error 131 | } 132 | 133 | func (e explosiveBalancer) Host() (string, error) { return "", e.Error } 134 | -------------------------------------------------------------------------------- /proxy/concurrent.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "time" 10 | 11 | "github.com/luraproject/lura/v2/config" 12 | "github.com/luraproject/lura/v2/logging" 13 | ) 14 | 15 | // NewConcurrentMiddlewareWithLogger creates a proxy middleware that enables sending several requests concurrently 16 | func NewConcurrentMiddlewareWithLogger(logger logging.Logger, remote *config.Backend) Middleware { 17 | if remote.ConcurrentCalls == 1 { 18 | logger.Fatal(fmt.Sprintf("too few concurrent calls for %s %s -> %s: NewConcurrentMiddleware expects more than 1 concurrent call, got %d", 19 | remote.ParentEndpointMethod, remote.ParentEndpoint, remote.URLPattern, remote.ConcurrentCalls)) 20 | return nil 21 | } 22 | serviceTimeout := time.Duration(75*remote.Timeout.Nanoseconds()/100) * time.Nanosecond 23 | 24 | return func(next ...Proxy) Proxy { 25 | if len(next) > 1 { 26 | logger.Fatal(fmt.Sprintf("too many proxies for this %s %s -> %s proxy middleware: NewConcurrentMiddleware only accepts 1 proxy, got %d", 27 | remote.ParentEndpointMethod, remote.ParentEndpoint, remote.URLPattern, len(next))) 28 | return nil 29 | } 30 | 31 | return func(ctx context.Context, request *Request) (*Response, error) { 32 | localCtx, cancel := context.WithTimeout(ctx, serviceTimeout) 33 | 34 | results := make(chan *Response, remote.ConcurrentCalls) 35 | failed := make(chan error, remote.ConcurrentCalls) 36 | 37 | for i := 0; i < remote.ConcurrentCalls; i++ { 38 | if i < remote.ConcurrentCalls-1 { 39 | go processConcurrentCall(localCtx, next[0], CloneRequest(request), results, failed) 40 | } else { 41 | go processConcurrentCall(localCtx, next[0], request, results, failed) 42 | } 43 | } 44 | 45 | var response *Response 46 | var err error 47 | 48 | for i := 0; i < remote.ConcurrentCalls; i++ { 49 | select { 50 | case response = <-results: 51 | if response != nil && response.IsComplete { 52 | cancel() 53 | return response, nil 54 | } 55 | case err = <-failed: 56 | case <-ctx.Done(): 57 | } 58 | } 59 | cancel() 60 | return response, err 61 | } 62 | } 63 | } 64 | 65 | // NewConcurrentMiddlewareWithLogger creates a proxy middleware that enables sending several requests concurrently. 66 | // Is recommended to use the version with a logger param. 67 | func NewConcurrentMiddleware(remote *config.Backend) Middleware { 68 | return NewConcurrentMiddlewareWithLogger(logging.NoOp, remote) 69 | } 70 | 71 | var errNullResult = errors.New("invalid response") 72 | 73 | func processConcurrentCall(ctx context.Context, next Proxy, request *Request, out chan<- *Response, failed chan<- error) { 74 | localCtx, cancel := context.WithCancel(ctx) 75 | 76 | result, err := next(localCtx, request) 77 | if err != nil { 78 | failed <- err 79 | cancel() 80 | return 81 | } 82 | if result == nil { 83 | failed <- errNullResult 84 | cancel() 85 | return 86 | } 87 | select { 88 | case out <- result: 89 | case <-ctx.Done(): 90 | failed <- ctx.Err() 91 | } 92 | cancel() 93 | } 94 | -------------------------------------------------------------------------------- /proxy/concurrent_benchmark_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "context" 7 | "testing" 8 | "time" 9 | 10 | "github.com/luraproject/lura/v2/config" 11 | ) 12 | 13 | func BenchmarkNewConcurrentMiddleware_singleNext(b *testing.B) { 14 | backend := config.Backend{ 15 | ConcurrentCalls: 3, 16 | Timeout: time.Duration(100) * time.Millisecond, 17 | } 18 | proxy := NewConcurrentMiddleware(&backend)(dummyProxy(&Response{})) 19 | 20 | b.ResetTimer() 21 | b.ReportAllocs() 22 | for i := 0; i < b.N; i++ { 23 | proxy(context.Background(), &Request{}) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /proxy/concurrent_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "context" 7 | "sync/atomic" 8 | "testing" 9 | "time" 10 | 11 | "github.com/luraproject/lura/v2/config" 12 | ) 13 | 14 | func TestNewConcurrentMiddleware_ok(t *testing.T) { 15 | timeout := 700 16 | totalCalls := 3 17 | backend := config.Backend{ 18 | ConcurrentCalls: totalCalls, 19 | Timeout: time.Duration(timeout) * time.Millisecond, 20 | } 21 | expected := Response{ 22 | Data: map[string]interface{}{"supu": 42, "tupu": true, "foo": "bar"}, 23 | IsComplete: true, 24 | } 25 | mw := NewConcurrentMiddleware(&backend) 26 | mustEnd := time.After(time.Duration(timeout) * time.Millisecond) 27 | result, err := mw(dummyProxy(&expected))(context.Background(), &Request{}) 28 | if err != nil { 29 | t.Errorf("The middleware propagated an unexpected error: %s\n", err.Error()) 30 | } 31 | select { 32 | case <-mustEnd: 33 | t.Errorf("We were expecting a response but we got none\n") 34 | default: 35 | } 36 | if result == nil { 37 | t.Errorf("The proxy returned a null result\n") 38 | return 39 | } 40 | if !result.IsComplete { 41 | t.Errorf("The proxy returned an incomplete result: %v\n", result) 42 | } 43 | if v, ok := result.Data["supu"]; !ok || v.(int) != 42 { 44 | t.Errorf("The proxy returned an unexpected result: %v\n", result) 45 | } 46 | if v, ok := result.Data["tupu"]; !ok || !v.(bool) { 47 | t.Errorf("The proxy returned an unexpected result: %v\n", result) 48 | } 49 | if v, ok := result.Data["foo"]; !ok || v.(string) != "bar" { 50 | t.Errorf("The proxy returned an unexpected result: %v\n", result) 51 | } 52 | } 53 | 54 | func TestNewConcurrentMiddleware_okAfterKo(t *testing.T) { 55 | timeout := 700 56 | totalCalls := 3 57 | backend := config.Backend{ 58 | ConcurrentCalls: totalCalls, 59 | Timeout: time.Duration(timeout) * time.Millisecond, 60 | } 61 | expected := Response{ 62 | Data: map[string]interface{}{"supu": 42, "tupu": true, "foo": "bar"}, 63 | IsComplete: true, 64 | } 65 | mw := NewConcurrentMiddleware(&backend) 66 | 67 | calls := uint64(0) 68 | mock := func(_ context.Context, _ *Request) (*Response, error) { 69 | total := atomic.AddUint64(&calls, 1) 70 | if total%2 == 0 { 71 | return &expected, nil 72 | } 73 | return nil, nil 74 | } 75 | mustEnd := time.After(time.Duration(timeout) * time.Millisecond) 76 | result, err := mw(mock)(context.Background(), &Request{}) 77 | 78 | if result == nil { 79 | t.Errorf("The proxy returned a null result\n") 80 | return 81 | } 82 | if err != nil { 83 | t.Errorf("The middleware propagated an unexpected error: %s\n", err.Error()) 84 | } 85 | select { 86 | case <-mustEnd: 87 | t.Errorf("We were expecting a response but we got none\n") 88 | default: 89 | } 90 | if !result.IsComplete { 91 | t.Errorf("The proxy returned an incomplete result: %v\n", result) 92 | } 93 | if v, ok := result.Data["supu"]; !ok || v.(int) != 42 { 94 | t.Errorf("The proxy returned an unexpected result: %v\n", result) 95 | } 96 | if v, ok := result.Data["tupu"]; !ok || !v.(bool) { 97 | t.Errorf("The proxy returned an unexpected result: %v\n", result) 98 | } 99 | if v, ok := result.Data["foo"]; !ok || v.(string) != "bar" { 100 | t.Errorf("The proxy returned an unexpected result: %v\n", result) 101 | } 102 | } 103 | 104 | func TestNewConcurrentMiddleware_timeout(t *testing.T) { 105 | timeout := 100 106 | totalCalls := 3 107 | backend := config.Backend{ 108 | ConcurrentCalls: totalCalls, 109 | Timeout: time.Duration(timeout) * time.Millisecond, 110 | } 111 | mw := NewConcurrentMiddleware(&backend) 112 | mustEnd := time.After(time.Duration(2*timeout) * time.Millisecond) 113 | 114 | response, err := mw(delayedProxy(t, time.Duration(5*timeout)*time.Millisecond, &Response{}))(context.Background(), &Request{}) 115 | if err == nil || err.Error() != "context deadline exceeded" { 116 | t.Errorf("The middleware didn't propagate a timeout error: %s\n", err) 117 | } 118 | if response != nil { 119 | t.Errorf("We weren't expecting a response but we got one: %v\n", response) 120 | return 121 | } 122 | select { 123 | case <-mustEnd: 124 | t.Errorf("We were expecting a response at this point in time!\n") 125 | return 126 | default: 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /proxy/factory.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "github.com/luraproject/lura/v2/config" 7 | "github.com/luraproject/lura/v2/logging" 8 | "github.com/luraproject/lura/v2/sd" 9 | ) 10 | 11 | // Factory creates proxies based on the received endpoint configuration. 12 | // 13 | // Both, factories and backend factories, create proxies but factories are designed as a stack makers 14 | // because they are intended to generate the complete proxy stack for a given frontend endpoint 15 | // the app would expose and they could wrap several proxies provided by a backend factory 16 | type Factory interface { 17 | New(cfg *config.EndpointConfig) (Proxy, error) 18 | } 19 | 20 | // FactoryFunc type is an adapter to allow the use of ordinary functions as proxy factories. 21 | // If f is a function with the appropriate signature, FactoryFunc(f) is a Factory that calls f. 22 | type FactoryFunc func(*config.EndpointConfig) (Proxy, error) 23 | 24 | // New implements the Factory interface 25 | func (f FactoryFunc) New(cfg *config.EndpointConfig) (Proxy, error) { return f(cfg) } 26 | 27 | // DefaultFactory returns a default http proxy factory with the injected logger 28 | func DefaultFactory(logger logging.Logger) Factory { 29 | return NewDefaultFactory(httpProxy, logger) 30 | } 31 | 32 | // DefaultFactoryWithSubscriber returns a default proxy factory with the injected logger and subscriber factory 33 | func DefaultFactoryWithSubscriber(logger logging.Logger, sF sd.SubscriberFactory) Factory { 34 | return NewDefaultFactoryWithSubscriber(httpProxy, logger, sF) 35 | } 36 | 37 | // NewDefaultFactory returns a default proxy factory with the injected proxy builder and logger 38 | func NewDefaultFactory(backendFactory BackendFactory, logger logging.Logger) Factory { 39 | sf := func(remote *config.Backend) sd.Subscriber { 40 | return sd.GetRegister().Get(remote.SD)(remote) 41 | } 42 | return NewDefaultFactoryWithSubscriber(backendFactory, logger, sf) 43 | } 44 | 45 | // NewDefaultFactoryWithSubscriber returns a default proxy factory with the injected proxy builder, 46 | // logger and subscriber factory 47 | func NewDefaultFactoryWithSubscriber(backendFactory BackendFactory, logger logging.Logger, sF sd.SubscriberFactory) Factory { 48 | return defaultFactory{backendFactory, logger, sF} 49 | } 50 | 51 | type defaultFactory struct { 52 | backendFactory BackendFactory 53 | logger logging.Logger 54 | subscriberFactory sd.SubscriberFactory 55 | } 56 | 57 | // New implements the Factory interface 58 | func (pf defaultFactory) New(cfg *config.EndpointConfig) (p Proxy, err error) { 59 | switch len(cfg.Backend) { 60 | case 0: 61 | err = ErrNoBackends 62 | case 1: 63 | p, err = pf.newSingle(cfg) 64 | default: 65 | p, err = pf.newMulti(cfg) 66 | } 67 | if err != nil { 68 | return 69 | } 70 | 71 | p = NewPluginMiddleware(pf.logger, cfg)(p) 72 | p = NewStaticMiddleware(pf.logger, cfg)(p) 73 | return 74 | } 75 | 76 | func (pf defaultFactory) newMulti(cfg *config.EndpointConfig) (p Proxy, err error) { 77 | backendProxy := make([]Proxy, len(cfg.Backend)) 78 | for i, backend := range cfg.Backend { 79 | backendProxy[i] = pf.newStack(backend) 80 | } 81 | p = NewMergeDataMiddleware(pf.logger, cfg)(backendProxy...) 82 | p = NewFlatmapMiddleware(pf.logger, cfg)(p) 83 | return 84 | } 85 | 86 | func (pf defaultFactory) newSingle(cfg *config.EndpointConfig) (Proxy, error) { 87 | return pf.newStack(cfg.Backend[0]), nil 88 | } 89 | 90 | func (pf defaultFactory) newStack(backend *config.Backend) (p Proxy) { 91 | p = pf.backendFactory(backend) 92 | p = NewBackendPluginMiddleware(pf.logger, backend)(p) 93 | p = NewGraphQLMiddleware(pf.logger, backend)(p) 94 | p = NewFilterHeadersMiddleware(pf.logger, backend)(p) 95 | p = NewFilterQueryStringsMiddleware(pf.logger, backend)(p) 96 | p = NewLoadBalancedMiddlewareWithSubscriberAndLogger(pf.logger, pf.subscriberFactory(backend))(p) 97 | if backend.ConcurrentCalls > 1 { 98 | p = NewConcurrentMiddlewareWithLogger(pf.logger, backend)(p) 99 | } 100 | p = NewRequestBuilderMiddlewareWithLogger(pf.logger, backend)(p) 101 | return 102 | } 103 | -------------------------------------------------------------------------------- /proxy/graphql.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "io" 10 | "net/url" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/luraproject/lura/v2/config" 15 | "github.com/luraproject/lura/v2/logging" 16 | "github.com/luraproject/lura/v2/transport/http/client/graphql" 17 | ) 18 | 19 | // NewGraphQLMiddleware returns a middleware with or without the GraphQL 20 | // proxy wrapping the next element (depending on the configuration). 21 | // It supports both queries and mutations. 22 | // For queries, it completes the variables object using the request params. 23 | // For mutations, it overides the defined variables with the request body. 24 | // The resulting request will have a proper graphql body with the query and the 25 | // variables 26 | func NewGraphQLMiddleware(logger logging.Logger, remote *config.Backend) Middleware { 27 | opt, err := graphql.GetOptions(remote.ExtraConfig) 28 | if err != nil { 29 | if err != graphql.ErrNoConfigFound { 30 | logger.Warning( 31 | fmt.Sprintf("[BACKEND: %s %s -> %s][GraphQL] %s", remote.ParentEndpoint, remote.ParentEndpoint, remote.URLPattern, err.Error())) 32 | } 33 | return emptyMiddlewareFallback(logger) 34 | } 35 | 36 | extractor := graphql.New(*opt) 37 | var generateBodyFn func(*Request) ([]byte, error) 38 | var generateQueryFn func(*Request) (url.Values, error) 39 | 40 | switch opt.Type { 41 | case graphql.OperationMutation: 42 | generateBodyFn = func(req *Request) ([]byte, error) { 43 | if req.Body == nil { 44 | return extractor.BodyFromBody(strings.NewReader("")) 45 | } 46 | defer req.Body.Close() 47 | return extractor.BodyFromBody(req.Body) 48 | } 49 | generateQueryFn = func(req *Request) (url.Values, error) { 50 | if req.Body == nil { 51 | return extractor.QueryFromBody(strings.NewReader("")) 52 | } 53 | defer req.Body.Close() 54 | return extractor.QueryFromBody(req.Body) 55 | } 56 | 57 | case graphql.OperationQuery: 58 | generateBodyFn = func(req *Request) ([]byte, error) { 59 | return extractor.BodyFromParams(req.Params) 60 | } 61 | generateQueryFn = func(req *Request) (url.Values, error) { 62 | return extractor.QueryFromParams(req.Params) 63 | } 64 | 65 | default: 66 | return emptyMiddlewareFallback(logger) 67 | } 68 | 69 | return func(next ...Proxy) Proxy { 70 | if len(next) > 1 { 71 | logger.Fatal("too many proxies for this %s %s -> %s proxy middleware: NewGraphQLMiddleware only accepts 1 proxy, got %d", 72 | remote.ParentEndpointMethod, remote.ParentEndpoint, remote.URLPattern, len(next)) 73 | return nil 74 | } 75 | 76 | logger.Debug( 77 | fmt.Sprintf( 78 | "[BACKEND: %s %s -> %s][GraphQL] Operation: %s, Method: %s", 79 | remote.ParentEndpointMethod, 80 | remote.ParentEndpoint, 81 | remote.URLPattern, 82 | opt.Type, 83 | opt.Method, 84 | ), 85 | ) 86 | 87 | if opt.Method == graphql.MethodGet { 88 | return func(ctx context.Context, req *Request) (*Response, error) { 89 | q, err := generateQueryFn(req) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | req.Body = io.NopCloser(bytes.NewReader([]byte{})) 95 | req.Method = string(opt.Method) 96 | req.Headers["Content-Length"] = []string{"0"} 97 | // even when there is no content, we just set the content-type 98 | // header to be safe if the server side checks it: 99 | req.Headers["Content-Type"] = []string{"application/json"} 100 | if req.Query != nil { 101 | for k, vs := range q { 102 | for _, v := range vs { 103 | req.Query.Add(k, v) 104 | } 105 | } 106 | } else { 107 | req.Query = q 108 | } 109 | 110 | return next[0](ctx, req) 111 | } 112 | } 113 | 114 | return func(ctx context.Context, req *Request) (*Response, error) { 115 | b, err := generateBodyFn(req) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | req.Body = io.NopCloser(bytes.NewReader(b)) 121 | req.Method = string(opt.Method) 122 | req.Headers["Content-Length"] = []string{strconv.Itoa(len(b))} 123 | req.Headers["Content-Type"] = []string{"application/json"} 124 | 125 | return next[0](ctx, req) 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /proxy/graphql_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "io" 9 | "reflect" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/luraproject/lura/v2/config" 14 | "github.com/luraproject/lura/v2/logging" 15 | "github.com/luraproject/lura/v2/transport/http/client/graphql" 16 | ) 17 | 18 | func TestNewGraphQLMiddleware_mutation(t *testing.T) { 19 | query := "mutation addAuthor($author: [AddAuthorInput!]!) {\n addAuthor(input: $author) {\n author {\n id\n name\n }\n }\n}\n" 20 | mw := NewGraphQLMiddleware( 21 | logging.NoOp, 22 | &config.Backend{ 23 | ExtraConfig: config.ExtraConfig{ 24 | graphql.Namespace: map[string]interface{}{ 25 | "type": "mutation", 26 | "query": query, 27 | "variables": map[string]interface{}{ 28 | "author": map[string]interface{}{ 29 | "name": "A.N. Author", 30 | "dob": "2000-01-01", 31 | "posts": []interface{}{}, 32 | }, 33 | }, 34 | }, 35 | }, 36 | }, 37 | ) 38 | 39 | expectedResponse := &Response{ 40 | Data: map[string]interface{}{"foo": "bar"}, 41 | } 42 | prxy := mw(func(ctx context.Context, req *Request) (*Response, error) { 43 | b, err := io.ReadAll(req.Body) 44 | req.Body.Close() 45 | if err != nil { 46 | return nil, err 47 | } 48 | var request graphql.GraphQLRequest 49 | if err := json.Unmarshal(b, &request); err != nil { 50 | return nil, err 51 | } 52 | return expectedResponse, nil 53 | }) 54 | 55 | resp, err := prxy(context.Background(), &Request{ 56 | Body: io.NopCloser(strings.NewReader(`{ 57 | "name": "foo", 58 | "dob": "bar" 59 | }`)), 60 | Params: map[string]string{}, 61 | Headers: map[string][]string{}, 62 | }) 63 | 64 | if err != nil { 65 | t.Error(err) 66 | return 67 | } 68 | 69 | if !reflect.DeepEqual(resp, expectedResponse) { 70 | t.Errorf("unexpected response: %v", resp) 71 | } 72 | } 73 | 74 | func TestNewGraphQLMiddleware_query(t *testing.T) { 75 | query := "{ q(func: uid(1)) { uid } }" 76 | mw := NewGraphQLMiddleware( 77 | logging.NoOp, 78 | &config.Backend{ 79 | ExtraConfig: config.ExtraConfig{ 80 | graphql.Namespace: map[string]interface{}{ 81 | "method": "get", 82 | "type": "query", 83 | "query": query, 84 | "variables": map[string]interface{}{ 85 | "name": "{foo}", 86 | "dob": "{bar}", 87 | "posts": []interface{}{}, 88 | }, 89 | }, 90 | }, 91 | }, 92 | ) 93 | 94 | expectedResponse := &Response{Data: map[string]interface{}{"foo": "bar"}} 95 | 96 | prxy := mw(func(ctx context.Context, req *Request) (*Response, error) { 97 | request := graphql.GraphQLRequest{ 98 | Query: req.Query.Get("query"), 99 | OperationName: req.Query.Get("operationName"), 100 | Variables: map[string]interface{}{}, 101 | } 102 | json.Unmarshal([]byte(req.Query.Get("variables")), &request.Variables) 103 | 104 | if request.Query != query { 105 | t.Errorf("unexpected query: %s", request.Query) 106 | } 107 | if len(request.Variables) != 3 { 108 | t.Errorf("unexpected variables: %v", request.Variables) 109 | } 110 | if v, ok := request.Variables["name"].(string); !ok || v != "foo" { 111 | t.Errorf("unexpected var name: %v", request.Variables["name"]) 112 | } 113 | if v, ok := request.Variables["dob"].(string); !ok || v != "bar" { 114 | t.Errorf("unexpected var dob: %v", request.Variables["dob"]) 115 | } 116 | 117 | return expectedResponse, nil 118 | }) 119 | 120 | resp, err := prxy(context.Background(), &Request{ 121 | Params: map[string]string{ 122 | "Foo": "foo", 123 | "Bar": "bar", 124 | }, 125 | Body: io.NopCloser(strings.NewReader("")), 126 | Headers: map[string][]string{}, 127 | }) 128 | 129 | if err != nil { 130 | t.Error(err) 131 | return 132 | } 133 | 134 | if !reflect.DeepEqual(resp, expectedResponse) { 135 | t.Errorf("unexpected response: %v", resp) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /proxy/headers_filter.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/luraproject/lura/v2/config" 9 | "github.com/luraproject/lura/v2/logging" 10 | ) 11 | 12 | // NewFilterHeadersMiddleware returns a middleware with or without a header filtering 13 | // proxy wrapping the next element (depending on the configuration). 14 | func NewFilterHeadersMiddleware(logger logging.Logger, remote *config.Backend) Middleware { 15 | if len(remote.HeadersToPass) == 0 { 16 | return emptyMiddlewareFallback(logger) 17 | } 18 | 19 | return func(next ...Proxy) Proxy { 20 | if len(next) > 1 { 21 | logger.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)) 22 | return nil 23 | } 24 | nextProxy := next[0] 25 | return func(ctx context.Context, request *Request) (*Response, error) { 26 | if len(request.Headers) == 0 { 27 | return nextProxy(ctx, request) 28 | } 29 | numHeadersToPass := 0 30 | for _, v := range remote.HeadersToPass { 31 | if _, ok := request.Headers[v]; ok { 32 | numHeadersToPass++ 33 | } 34 | } 35 | if numHeadersToPass == len(request.Headers) { 36 | // all the headers should pass, no need to clone the headers 37 | return nextProxy(ctx, request) 38 | } 39 | // ATTENTION: this is not a clone of headers! 40 | // this just filters the headers we do not want to send: 41 | // issues and race conditions could happen the same way as when we 42 | // do not filter the headers. This is a design decission, and if we 43 | // want to clone the header values (because of write modifications), 44 | // that should be done at an upper level (so the approach is the same 45 | // for non filtered parallel requests). 46 | newHeaders := make(map[string][]string, numHeadersToPass) 47 | for _, v := range remote.HeadersToPass { 48 | if values, ok := request.Headers[v]; ok { 49 | newHeaders[v] = values 50 | } 51 | } 52 | return nextProxy(ctx, &Request{ 53 | Method: request.Method, 54 | URL: request.URL, 55 | Query: request.Query, 56 | Path: request.Path, 57 | Body: request.Body, 58 | Params: request.Params, 59 | Headers: newHeaders, 60 | }) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /proxy/headers_filter_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "context" 7 | "testing" 8 | 9 | "github.com/luraproject/lura/v2/config" 10 | "github.com/luraproject/lura/v2/logging" 11 | ) 12 | 13 | func TestNewFilterHeadersMiddleware(t *testing.T) { 14 | mw := NewFilterHeadersMiddleware( 15 | logging.NoOp, 16 | &config.Backend{ 17 | HeadersToPass: []string{ 18 | "X-This-Shall-Pass", 19 | "X-Gandalf-Will-Pass", 20 | }, 21 | }, 22 | ) 23 | 24 | var receivedReq *Request 25 | prxy := mw(func(ctx context.Context, req *Request) (*Response, error) { 26 | receivedReq = req 27 | return nil, nil 28 | }) 29 | 30 | sentReq := &Request{ 31 | Body: nil, 32 | Params: map[string]string{}, 33 | Headers: map[string][]string{ 34 | "X-This-Shall-Pass": []string{"tupu", "supu"}, 35 | "X-You-Shall-Not-Pass": []string{"Balrog"}, 36 | "X-Gandalf-Will-Pass": []string{"White", "Grey"}, 37 | "X-Drop-Tables": []string{"foo"}, 38 | }, 39 | } 40 | 41 | prxy(context.Background(), sentReq) 42 | 43 | if receivedReq == sentReq { 44 | t.Errorf("request should be different") 45 | return 46 | } 47 | 48 | if _, ok := receivedReq.Headers["X-This-Shall-Pass"]; !ok { 49 | t.Errorf("missing X-This-Shall-Pass") 50 | return 51 | } 52 | 53 | if _, ok := receivedReq.Headers["X-Gandalf-Will-Pass"]; !ok { 54 | t.Errorf("missing X-Gandalf-Will-Pass") 55 | return 56 | } 57 | 58 | if _, ok := receivedReq.Headers["X-Drop-Tables"]; ok { 59 | t.Errorf("should not be there X-Drop-Tables") 60 | return 61 | } 62 | 63 | if _, ok := receivedReq.Headers["X-You-Shall-Not-Pass"]; ok { 64 | t.Errorf("should not be there X-You-Shall-Not-Pass") 65 | return 66 | } 67 | 68 | // check that when headers are the expected, no need to copy 69 | sentReq = &Request{ 70 | Body: nil, 71 | Params: map[string]string{}, 72 | Headers: map[string][]string{ 73 | "X-This-Shall-Pass": []string{"tupu", "supu"}, 74 | }, 75 | } 76 | 77 | prxy(context.Background(), sentReq) 78 | 79 | if receivedReq != sentReq { 80 | t.Errorf("request should be the same, no modification of headers expected") 81 | return 82 | } 83 | } 84 | 85 | func TestNewFilterHeadersMiddlewareBlockAll(t *testing.T) { 86 | mw := NewFilterHeadersMiddleware( 87 | logging.NoOp, 88 | &config.Backend{ 89 | HeadersToPass: []string{""}, 90 | }, 91 | ) 92 | 93 | var receivedReq *Request 94 | prxy := mw(func(ctx context.Context, req *Request) (*Response, error) { 95 | receivedReq = req 96 | return nil, nil 97 | }) 98 | 99 | sentReq := &Request{ 100 | Body: nil, 101 | Params: map[string]string{}, 102 | Headers: map[string][]string{ 103 | "X-This-Shall-Pass": []string{"tupu", "supu"}, 104 | "X-You-Shall-Not-Pass": []string{"Balrog"}, 105 | }, 106 | } 107 | 108 | prxy(context.Background(), sentReq) 109 | 110 | if receivedReq == sentReq { 111 | t.Errorf("request should be different") 112 | return 113 | } 114 | 115 | if len(receivedReq.Headers) != 0 { 116 | t.Errorf("should have blocked all headers") 117 | return 118 | } 119 | } 120 | 121 | func TestNewFilterHeadersMiddlewareAllowAll(t *testing.T) { 122 | mw := NewFilterHeadersMiddleware( 123 | logging.NoOp, 124 | &config.Backend{ 125 | HeadersToPass: []string{}, 126 | }, 127 | ) 128 | 129 | var receivedReq *Request 130 | prxy := mw(func(ctx context.Context, req *Request) (*Response, error) { 131 | receivedReq = req 132 | return nil, nil 133 | }) 134 | 135 | sentReq := &Request{ 136 | Body: nil, 137 | Params: map[string]string{}, 138 | Headers: map[string][]string{ 139 | "X-This-Shall-Pass": []string{"tupu", "supu"}, 140 | "X-You-Shall-Not-Pass": []string{"Balrog"}, 141 | }, 142 | } 143 | 144 | prxy(context.Background(), sentReq) 145 | 146 | if len(receivedReq.Headers) != 2 { 147 | t.Errorf("should have let pass all headers") 148 | return 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /proxy/http_benchmark_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "context" 7 | "testing" 8 | 9 | "github.com/luraproject/lura/v2/config" 10 | ) 11 | 12 | func BenchmarkNewRequestBuilderMiddleware(b *testing.B) { 13 | backend := config.Backend{ 14 | URLPattern: "/supu", 15 | Method: "GET", 16 | } 17 | proxy := NewRequestBuilderMiddleware(&backend)(dummyProxy(&Response{})) 18 | 19 | b.ResetTimer() 20 | b.ReportAllocs() 21 | for i := 0; i < b.N; i++ { 22 | proxy(context.Background(), &Request{}) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /proxy/http_response.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "compress/gzip" 7 | "context" 8 | "io" 9 | "net/http" 10 | 11 | "github.com/luraproject/lura/v2/encoding" 12 | ) 13 | 14 | // HTTPResponseParser defines how the response is parsed from http.Response to Response object 15 | type HTTPResponseParser func(context.Context, *http.Response) (*Response, error) 16 | 17 | // DefaultHTTPResponseParserConfig defines a default HTTPResponseParserConfig 18 | var DefaultHTTPResponseParserConfig = HTTPResponseParserConfig{ 19 | func(_ io.Reader, _ *map[string]interface{}) error { return nil }, 20 | EntityFormatterFunc(func(r Response) Response { return r }), 21 | } 22 | 23 | // HTTPResponseParserConfig contains the config for a given HttpResponseParser 24 | type HTTPResponseParserConfig struct { 25 | Decoder encoding.Decoder 26 | EntityFormatter EntityFormatter 27 | } 28 | 29 | // HTTPResponseParserFactory creates HTTPResponseParser from a given HTTPResponseParserConfig 30 | type HTTPResponseParserFactory func(HTTPResponseParserConfig) HTTPResponseParser 31 | 32 | // DefaultHTTPResponseParserFactory is the default implementation of HTTPResponseParserFactory 33 | func DefaultHTTPResponseParserFactory(cfg HTTPResponseParserConfig) HTTPResponseParser { 34 | return func(_ context.Context, resp *http.Response) (*Response, error) { 35 | defer resp.Body.Close() 36 | 37 | var reader io.ReadCloser 38 | switch resp.Header.Get("Content-Encoding") { 39 | case "gzip": 40 | gzipReader, err := gzip.NewReader(resp.Body) 41 | if err != nil { 42 | return nil, err 43 | } 44 | reader = gzipReader 45 | defer reader.Close() 46 | default: 47 | reader = resp.Body 48 | } 49 | 50 | var data map[string]interface{} 51 | if err := cfg.Decoder(reader, &data); err != nil { 52 | return nil, err 53 | } 54 | 55 | newResponse := Response{Data: data, IsComplete: true} 56 | newResponse = cfg.EntityFormatter.Format(newResponse) 57 | return &newResponse, nil 58 | } 59 | } 60 | 61 | // NoOpHTTPResponseParser is a HTTPResponseParser implementation that just copies the 62 | // http response body into the proxy response IO 63 | func NoOpHTTPResponseParser(ctx context.Context, resp *http.Response) (*Response, error) { 64 | return &Response{ 65 | Data: map[string]interface{}{}, 66 | IsComplete: true, 67 | Io: NewReadCloserWrapper(ctx, resp.Body), 68 | Metadata: Metadata{ 69 | StatusCode: resp.StatusCode, 70 | Headers: resp.Header, 71 | }, 72 | }, nil 73 | } 74 | -------------------------------------------------------------------------------- /proxy/logging.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "context" 7 | "strings" 8 | "time" 9 | 10 | "github.com/luraproject/lura/v2/logging" 11 | ) 12 | 13 | // NewLoggingMiddleware creates proxy middleware for logging requests and responses 14 | func NewLoggingMiddleware(logger logging.Logger, name string) Middleware { 15 | logPrefix := "[" + strings.ToUpper(name) + "]" 16 | return func(next ...Proxy) Proxy { 17 | if len(next) > 1 { 18 | logger.Fatal("too many proxies for this proxy middleware: NewLoggingMiddleware only accepts 1 proxy, got %d", len(next)) 19 | return nil 20 | } 21 | return func(ctx context.Context, request *Request) (*Response, error) { 22 | begin := time.Now() 23 | logger.Info(logPrefix, "Calling backend") 24 | logger.Debug(logPrefix, "Request", request) 25 | 26 | result, err := next[0](ctx, request) 27 | 28 | logger.Info(logPrefix, "Call to backend took", time.Since(begin).String()) 29 | if err != nil { 30 | logger.Warning(logPrefix, "Call to backend failed:", err.Error()) 31 | return result, err 32 | } 33 | if result == nil { 34 | logger.Warning(logPrefix, "Call to backend returned a null response") 35 | } 36 | 37 | return result, err 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /proxy/logging_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/luraproject/lura/v2/logging" 13 | ) 14 | 15 | func TestNewLoggingMiddleware_ok(t *testing.T) { 16 | buff := bytes.NewBuffer(make([]byte, 1024)) 17 | logger, _ := logging.NewLogger("DEBUG", buff, "pref") 18 | resp := &Response{IsComplete: true} 19 | mw := NewLoggingMiddleware(logger, "supu") 20 | p := mw(dummyProxy(resp)) 21 | r, err := p(context.Background(), &Request{}) 22 | if r != resp { 23 | t.Error("The proxy didn't return the expected response") 24 | return 25 | } 26 | if err != nil { 27 | t.Errorf("The proxy returned an unexpected error: %s", err.Error()) 28 | return 29 | } 30 | logMsg := buff.String() 31 | if strings.Count(logMsg, "pref") != 3 { 32 | t.Error("The logs don't have the injected prefix") 33 | } 34 | if strings.Count(logMsg, "INFO") != 2 { 35 | t.Error("The logs don't have the expected INFO messages") 36 | } 37 | if strings.Count(logMsg, "DEBU") != 1 { 38 | t.Error("The logs don't have the expected DEBUG messages") 39 | } 40 | if !strings.Contains(logMsg, "[SUPU] Calling backend") { 41 | t.Error("The logs didn't mark the start of the execution") 42 | } 43 | if !strings.Contains(logMsg, "[SUPU] Call to backend took") { 44 | t.Error("The logs didn't mark the end of the execution") 45 | } 46 | } 47 | 48 | func TestNewLoggingMiddleware_erroredResponse(t *testing.T) { 49 | buff := bytes.NewBuffer(make([]byte, 1024)) 50 | logger, _ := logging.NewLogger("DEBUG", buff, "pref") 51 | resp := &Response{IsComplete: true} 52 | mw := NewLoggingMiddleware(logger, "supu") 53 | expextedError := fmt.Errorf("NO-body expects the %s Inquisition!", "Spanish") 54 | p := mw(func(_ context.Context, _ *Request) (*Response, error) { 55 | return resp, expextedError 56 | }) 57 | r, err := p(context.Background(), &Request{}) 58 | if r != resp { 59 | t.Error("The proxy didn't return the expected response") 60 | return 61 | } 62 | if err != expextedError { 63 | t.Errorf("The proxy didn't return the expected error: %s", err.Error()) 64 | return 65 | } 66 | logMsg := buff.String() 67 | if strings.Count(logMsg, "pref") != 4 { 68 | t.Error("The logs don't have the injected prefix") 69 | } 70 | if strings.Count(logMsg, "INFO") != 2 { 71 | t.Error("The logs don't have the expected INFO messages") 72 | } 73 | if strings.Count(logMsg, "DEBU") != 1 { 74 | t.Error("The logs don't have the expected DEBUG messages") 75 | } 76 | if strings.Count(logMsg, "WARN") != 1 { 77 | t.Error("The logs don't have the expected DEBUG messages") 78 | } 79 | if !strings.Contains(logMsg, "[SUPU] Call to backend failed: NO-body expects the Spanish Inquisition!") { 80 | t.Error("The logs didn't mark the fail of the execution") 81 | } 82 | if !strings.Contains(logMsg, "[SUPU] Calling backend") { 83 | t.Error("The logs didn't mark the start of the execution") 84 | } 85 | if !strings.Contains(logMsg, "[SUPU] Call to backend took") { 86 | t.Error("The logs didn't mark the end of the execution") 87 | } 88 | } 89 | 90 | func TestNewLoggingMiddleware_nullResponse(t *testing.T) { 91 | buff := bytes.NewBuffer(make([]byte, 1024)) 92 | logger, _ := logging.NewLogger("DEBUG", buff, "pref") 93 | mw := NewLoggingMiddleware(logger, "supu") 94 | p := mw(dummyProxy(nil)) 95 | r, err := p(context.Background(), &Request{}) 96 | if r != nil { 97 | t.Error("The proxy didn't return the expected response") 98 | return 99 | } 100 | if err != nil { 101 | t.Errorf("The proxy returned an unexpected error: %s", err.Error()) 102 | return 103 | } 104 | logMsg := buff.String() 105 | if strings.Count(logMsg, "pref") != 4 { 106 | t.Error("The logs don't have the injected prefix") 107 | } 108 | if strings.Count(logMsg, "INFO") != 2 { 109 | t.Error("The logs don't have the expected INFO messages") 110 | } 111 | if strings.Count(logMsg, "DEBU") != 1 { 112 | t.Error("The logs don't have the expected DEBUG messages") 113 | } 114 | if strings.Count(logMsg, "WARN") != 1 { 115 | t.Error("The logs don't have the expected DEBUG messages") 116 | } 117 | if !strings.Contains(logMsg, "[SUPU] Call to backend returned a null response") { 118 | t.Error("The logs didn't mark the fail of the execution") 119 | } 120 | if !strings.Contains(logMsg, "[SUPU] Calling backend") { 121 | t.Error("The logs didn't mark the start of the execution") 122 | } 123 | if !strings.Contains(logMsg, "[SUPU] Call to backend took") { 124 | t.Error("The logs didn't mark the end of the execution") 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /proxy/plugin/tests/error/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func main() {} 12 | 13 | var ModifierRegisterer = registerer("lura-error-example") 14 | 15 | var logger Logger = nil 16 | 17 | type registerer string 18 | 19 | func (r registerer) RegisterModifiers(f func( 20 | name string, 21 | modifierFactory func(map[string]interface{}) func(interface{}) (interface{}, error), 22 | appliesToRequest bool, 23 | appliesToResponse bool, 24 | )) { 25 | f(string(r)+"-request", r.requestModifierFactory, true, false) 26 | f(string(r)+"-response", r.reqsponseModifierFactory, false, true) 27 | } 28 | 29 | func (registerer) RegisterLogger(in interface{}) { 30 | l, ok := in.(Logger) 31 | if !ok { 32 | return 33 | } 34 | logger = l 35 | logger.Debug(fmt.Sprintf("[PLUGIN: %s] Logger loaded", ModifierRegisterer)) 36 | } 37 | 38 | func (registerer) requestModifierFactory(_ map[string]interface{}) func(interface{}) (interface{}, error) { 39 | logger.Debug(fmt.Sprintf("[PLUGIN: %s] Request modifier injected", ModifierRegisterer)) 40 | return func(_ interface{}) (interface{}, error) { 41 | logger.Debug(fmt.Sprintf("[PLUGIN: %s] Rejecting request", ModifierRegisterer)) 42 | return nil, requestErr 43 | } 44 | } 45 | 46 | func (registerer) reqsponseModifierFactory(_ map[string]interface{}) func(interface{}) (interface{}, error) { 47 | logger.Debug(fmt.Sprintf("[PLUGIN: %s] Response modifier injected", ModifierRegisterer)) 48 | return func(_ interface{}) (interface{}, error) { 49 | logger.Debug(fmt.Sprintf("[PLUGIN: %s] Replacing response", ModifierRegisterer)) 50 | return nil, responseErr 51 | } 52 | } 53 | 54 | type customError struct { 55 | error 56 | statusCode int 57 | } 58 | 59 | func (r customError) StatusCode() int { return r.statusCode } 60 | 61 | var ( 62 | requestErr = customError{ 63 | error: errors.New("request rejected just because"), 64 | statusCode: http.StatusTeapot, 65 | } 66 | responseErr = customError{ 67 | error: errors.New("response replaced because reasons"), 68 | statusCode: http.StatusTeapot, 69 | } 70 | ) 71 | 72 | type Logger interface { 73 | Debug(v ...interface{}) 74 | Info(v ...interface{}) 75 | Warning(v ...interface{}) 76 | Error(v ...interface{}) 77 | Critical(v ...interface{}) 78 | Fatal(v ...interface{}) 79 | } 80 | -------------------------------------------------------------------------------- /proxy/proxy.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | Package proxy provides proxy and proxy middleware interfaces and implementations. 5 | */ 6 | package proxy 7 | 8 | import ( 9 | "context" 10 | "errors" 11 | "io" 12 | 13 | "github.com/luraproject/lura/v2/config" 14 | "github.com/luraproject/lura/v2/logging" 15 | ) 16 | 17 | // Namespace to be used in extra config 18 | const Namespace = "github.com/devopsfaith/krakend/proxy" 19 | 20 | // Metadata is the Metadata of the Response which contains Headers and StatusCode 21 | type Metadata struct { 22 | Headers map[string][]string 23 | StatusCode int 24 | } 25 | 26 | // Response is the entity returned by the proxy 27 | type Response struct { 28 | Data map[string]interface{} 29 | IsComplete bool 30 | Metadata Metadata 31 | Io io.Reader 32 | } 33 | 34 | // readCloserWrapper is Io.Reader which is closed when the Context is closed or canceled 35 | type readCloserWrapper struct { 36 | ctx context.Context 37 | rc io.ReadCloser 38 | } 39 | 40 | // NewReadCloserWrapper Creates a new closeable io.Read 41 | func NewReadCloserWrapper(ctx context.Context, in io.ReadCloser) io.Reader { 42 | wrapper := readCloserWrapper{ctx, in} 43 | go wrapper.closeOnCancel() 44 | return wrapper 45 | } 46 | 47 | func (w readCloserWrapper) Read(b []byte) (int, error) { 48 | return w.rc.Read(b) 49 | } 50 | 51 | // closeOnCancel closes the io.Reader when the context is Done 52 | func (w readCloserWrapper) closeOnCancel() { 53 | <-w.ctx.Done() 54 | w.rc.Close() 55 | } 56 | 57 | var ( 58 | // ErrNoBackends is the error returned when an endpoint has no backends defined 59 | ErrNoBackends = errors.New("all endpoints must have at least one backend") 60 | // ErrTooManyBackends is the error returned when an endpoint has too many backends defined 61 | ErrTooManyBackends = errors.New("too many backends for this proxy") 62 | // ErrTooManyProxies is the error returned when a middleware has too many proxies defined 63 | ErrTooManyProxies = errors.New("too many proxies for this proxy middleware") 64 | // ErrNotEnoughProxies is the error returned when an endpoint has not enough proxies defined 65 | ErrNotEnoughProxies = errors.New("not enough proxies for this endpoint") 66 | ) 67 | 68 | // Proxy processes a request in a given context and returns a response and an error 69 | type Proxy func(ctx context.Context, request *Request) (*Response, error) 70 | 71 | // BackendFactory creates a proxy based on the received backend configuration 72 | type BackendFactory func(remote *config.Backend) Proxy 73 | 74 | // Middleware adds a middleware, decorator or wrapper over a collection of proxies, 75 | // exposing a proxy interface. 76 | // 77 | // Proxy middlewares can be stacked: 78 | // 79 | // var p Proxy 80 | // p := EmptyMiddleware(NoopProxy) 81 | // response, err := p(ctx, r) 82 | type Middleware func(next ...Proxy) Proxy 83 | 84 | // EmptyMiddlewareWithLoggger is a dummy middleware, useful for testing and fallback 85 | func EmptyMiddlewareWithLogger(logger logging.Logger, next ...Proxy) Proxy { 86 | if len(next) > 1 { 87 | logger.Fatal("too many proxies for this proxy middleware: EmptyMiddleware only accepts 1 proxy, got %d", len(next)) 88 | return nil 89 | } 90 | return next[0] 91 | } 92 | 93 | func EmptyMiddleware(next ...Proxy) Proxy { 94 | return EmptyMiddlewareWithLogger(logging.NoOp, next...) 95 | } 96 | 97 | func emptyMiddlewareFallback(logger logging.Logger) Middleware { 98 | return func(next ...Proxy) Proxy { 99 | return EmptyMiddlewareWithLogger(logger, next...) 100 | } 101 | } 102 | 103 | // NoopProxy is a do nothing proxy, useful for testing 104 | func NoopProxy(_ context.Context, _ *Request) (*Response, error) { return nil, nil } 105 | -------------------------------------------------------------------------------- /proxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "io" 10 | "strings" 11 | "sync" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestEmptyMiddleware_ok(t *testing.T) { 17 | expected := Response{} 18 | result, err := EmptyMiddleware(dummyProxy(&expected))(context.Background(), &Request{}) 19 | if err != nil { 20 | t.Errorf("The middleware propagated an unexpected error: %s\n", err.Error()) 21 | } 22 | if result != &expected { 23 | t.Errorf("The middleware returned an unexpected result: %v\n", result) 24 | } 25 | } 26 | 27 | func explosiveProxy(t *testing.T) Proxy { 28 | return func(ctx context.Context, _ *Request) (*Response, error) { 29 | t.Error("This proxy shouldn't been executed!") 30 | return &Response{}, nil 31 | } 32 | } 33 | 34 | func dummyProxy(r *Response) Proxy { 35 | return func(_ context.Context, _ *Request) (*Response, error) { 36 | return r, nil 37 | } 38 | } 39 | 40 | func delayedProxy(_ *testing.T, timeout time.Duration, r *Response) Proxy { 41 | return func(ctx context.Context, _ *Request) (*Response, error) { 42 | select { 43 | case <-ctx.Done(): 44 | return nil, ctx.Err() 45 | case <-time.After(timeout): 46 | return r, nil 47 | } 48 | } 49 | } 50 | 51 | func newDummyReadCloser(content string) io.ReadCloser { 52 | return dummyReadCloser{strings.NewReader(content)} 53 | } 54 | 55 | type dummyReadCloser struct { 56 | reader io.Reader 57 | } 58 | 59 | func (d dummyReadCloser) Read(p []byte) (int, error) { 60 | return d.reader.Read(p) 61 | } 62 | 63 | func (dummyReadCloser) Close() error { 64 | return nil 65 | } 66 | 67 | func TestWrapper(t *testing.T) { 68 | expected := "supu" 69 | ctx, cancel := context.WithCancel(context.Background()) 70 | defer cancel() 71 | 72 | readCloser := &dummyRC{ 73 | r: bytes.NewBufferString(expected), 74 | mu: &sync.Mutex{}, 75 | } 76 | 77 | r := NewReadCloserWrapper(ctx, readCloser) 78 | var out bytes.Buffer 79 | tot, err := out.ReadFrom(r) 80 | if err != nil { 81 | t.Errorf("Total bits read: %d. Err: %s", tot, err.Error()) 82 | return 83 | } 84 | if readCloser.IsClosed() { 85 | t.Error("The subject shouldn't be closed yet") 86 | return 87 | } 88 | if tot != 4 { 89 | t.Errorf("Unexpected number of bits read: %d", tot) 90 | return 91 | } 92 | if v := out.String(); v != expected { 93 | t.Errorf("Unexpected content: %s", v) 94 | return 95 | } 96 | 97 | cancel() 98 | <-time.After(100 * time.Millisecond) 99 | if !readCloser.IsClosed() { 100 | t.Error("The subject should be already closed") 101 | return 102 | } 103 | } 104 | 105 | type dummyRC struct { 106 | r io.Reader 107 | closed bool 108 | mu *sync.Mutex 109 | } 110 | 111 | func (d *dummyRC) Read(b []byte) (int, error) { 112 | d.mu.Lock() 113 | defer d.mu.Unlock() 114 | 115 | if d.closed { 116 | return -1, fmt.Errorf("Reading from a closed source") 117 | } 118 | return d.r.Read(b) 119 | } 120 | 121 | func (d *dummyRC) Close() error { 122 | d.mu.Lock() 123 | defer d.mu.Unlock() 124 | 125 | d.closed = true 126 | return nil 127 | } 128 | 129 | func (d *dummyRC) IsClosed() bool { 130 | d.mu.Lock() 131 | defer d.mu.Unlock() 132 | 133 | res := d.closed 134 | return res 135 | } 136 | -------------------------------------------------------------------------------- /proxy/query_strings_filter.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "context" 7 | "net/url" 8 | 9 | "github.com/luraproject/lura/v2/config" 10 | "github.com/luraproject/lura/v2/logging" 11 | ) 12 | 13 | // NewFilterQueryStringsMiddleware returns a middleware with or without a header filtering 14 | // proxy wrapping the next element (depending on the configuration). 15 | func NewFilterQueryStringsMiddleware(logger logging.Logger, remote *config.Backend) Middleware { 16 | if len(remote.QueryStringsToPass) == 0 { 17 | return emptyMiddlewareFallback(logger) 18 | } 19 | 20 | return func(next ...Proxy) Proxy { 21 | if len(next) > 1 { 22 | logger.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)) 23 | return nil 24 | } 25 | nextProxy := next[0] 26 | return func(ctx context.Context, request *Request) (*Response, error) { 27 | if len(request.Query) == 0 { 28 | return nextProxy(ctx, request) 29 | } 30 | numQueryStringsToPass := 0 31 | for _, v := range remote.QueryStringsToPass { 32 | if _, ok := request.Query[v]; ok { 33 | numQueryStringsToPass++ 34 | } 35 | } 36 | if numQueryStringsToPass == len(request.Query) { 37 | // all the query strings should pass, no need to clone the headers 38 | return nextProxy(ctx, request) 39 | } 40 | // ATTENTION: this is not a clone of query strings! 41 | // this just filters the query strings we do not want to send: 42 | // issues and race conditions could happen the same way as when we 43 | // do not filter the headers. This is a design decission, and if we 44 | // want to clone the query string values (because of write modifications), 45 | // that should be done at an upper level (so the approach is the same 46 | // for non filtered parallel requests). 47 | newQueryStrings := make(url.Values, numQueryStringsToPass) 48 | for _, v := range remote.QueryStringsToPass { 49 | if values, ok := request.Query[v]; ok { 50 | newQueryStrings[v] = values 51 | } 52 | } 53 | return nextProxy(ctx, &Request{ 54 | Method: request.Method, 55 | URL: request.URL, 56 | Query: newQueryStrings, 57 | Path: request.Path, 58 | Body: request.Body, 59 | Params: request.Params, 60 | Headers: request.Headers, 61 | }) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /proxy/query_strings_filter_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "context" 7 | "testing" 8 | 9 | "github.com/luraproject/lura/v2/config" 10 | "github.com/luraproject/lura/v2/logging" 11 | ) 12 | 13 | func TestNewFilterQueryStringsMiddleware(t *testing.T) { 14 | mw := NewFilterQueryStringsMiddleware( 15 | logging.NoOp, 16 | &config.Backend{ 17 | QueryStringsToPass: []string{ 18 | "oak", 19 | "cedar", 20 | }, 21 | }, 22 | ) 23 | 24 | var receivedReq *Request 25 | prxy := mw(func(ctx context.Context, req *Request) (*Response, error) { 26 | receivedReq = req 27 | return nil, nil 28 | }) 29 | 30 | sentReq := &Request{ 31 | Body: nil, 32 | Params: map[string]string{}, 33 | Query: map[string][]string{ 34 | "oak": []string{"acorn", "evergreen"}, 35 | "maple": []string{"tree", "shrub"}, 36 | "cedar": []string{"mediterranean", "himalayas"}, 37 | "willow": []string{"350"}, 38 | }, 39 | } 40 | 41 | prxy(context.Background(), sentReq) 42 | 43 | if receivedReq == sentReq { 44 | t.Errorf("request should be different") 45 | return 46 | } 47 | 48 | oak, ok := receivedReq.Query["oak"] 49 | if !ok { 50 | t.Errorf("missing 'oak'") 51 | return 52 | } 53 | if len(oak) != len(sentReq.Query["oak"]) { 54 | t.Errorf("want len(oak): %d, got %d", 55 | len(sentReq.Query["oak"]), len(oak)) 56 | return 57 | } 58 | 59 | for idx, expected := range sentReq.Query["oak"] { 60 | if expected != oak[idx] { 61 | t.Errorf("want oak[%d] = %s, got %s", 62 | idx, expected, oak[idx]) 63 | return 64 | } 65 | } 66 | 67 | if _, ok := receivedReq.Query["cedar"]; !ok { 68 | t.Errorf("missing 'cedar'") 69 | return 70 | } 71 | 72 | if _, ok := receivedReq.Query["mapple"]; ok { 73 | t.Errorf("should not be there: 'mapple'") 74 | return 75 | } 76 | 77 | if _, ok := receivedReq.Query["willow"]; ok { 78 | t.Errorf("should not be there: 'willow'") 79 | return 80 | } 81 | 82 | // check that when query strings are all the expected, no need to copy 83 | sentReq = &Request{ 84 | Body: nil, 85 | Params: map[string]string{}, 86 | Query: map[string][]string{ 87 | "oak": []string{"acorn", "evergreen"}, 88 | "cedar": []string{"mediterranean", "himalayas"}, 89 | }, 90 | } 91 | 92 | prxy(context.Background(), sentReq) 93 | 94 | if receivedReq != sentReq { 95 | t.Errorf("request should be the same, no modification of query string expected") 96 | return 97 | } 98 | } 99 | 100 | func TestFilterQueryStringsBlockAll(t *testing.T) { 101 | // In order to block all the query strings, we must only let pass 102 | // the 'empty' string "" 103 | mw := NewFilterQueryStringsMiddleware( 104 | logging.NoOp, 105 | &config.Backend{ 106 | QueryStringsToPass: []string{""}, 107 | }, 108 | ) 109 | 110 | var receivedReq *Request 111 | prxy := mw(func(ctx context.Context, req *Request) (*Response, error) { 112 | receivedReq = req 113 | return nil, nil 114 | }) 115 | 116 | sentReq := &Request{ 117 | Body: nil, 118 | Params: map[string]string{}, 119 | Query: map[string][]string{ 120 | "oak": []string{"acorn", "evergreen"}, 121 | "maple": []string{"tree", "shrub"}, 122 | }, 123 | } 124 | 125 | prxy(context.Background(), sentReq) 126 | 127 | if receivedReq == sentReq { 128 | t.Errorf("request should be different") 129 | return 130 | } 131 | 132 | if len(receivedReq.Query) != 0 { 133 | t.Errorf("should have blocked all query strings") 134 | return 135 | } 136 | } 137 | 138 | func TestFilterQueryStringsAllowAll(t *testing.T) { 139 | // Empty backend query strings to passa everything 140 | mw := NewFilterQueryStringsMiddleware( 141 | logging.NoOp, 142 | &config.Backend{ 143 | QueryStringsToPass: []string{}, 144 | }, 145 | ) 146 | 147 | var receivedReq *Request 148 | prxy := mw(func(ctx context.Context, req *Request) (*Response, error) { 149 | receivedReq = req 150 | return nil, nil 151 | }) 152 | 153 | sentReq := &Request{ 154 | Body: nil, 155 | Params: map[string]string{}, 156 | Query: map[string][]string{ 157 | "oak": []string{"acorn", "evergreen"}, 158 | "maple": []string{"tree", "shrub"}, 159 | }, 160 | } 161 | 162 | prxy(context.Background(), sentReq) 163 | 164 | if len(receivedReq.Query) != 2 { 165 | t.Errorf("should have passed all query strings") 166 | return 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /proxy/register.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "github.com/luraproject/lura/v2/register" 7 | ) 8 | 9 | func NewRegister() *Register { 10 | return &Register{ 11 | responseCombiners, 12 | } 13 | } 14 | 15 | type Register struct { 16 | *combinerRegister 17 | } 18 | 19 | type combinerRegister struct { 20 | data *register.Untyped 21 | fallback ResponseCombiner 22 | } 23 | 24 | func newCombinerRegister(data map[string]ResponseCombiner, fallback ResponseCombiner) *combinerRegister { 25 | r := register.NewUntyped() 26 | for k, v := range data { 27 | r.Register(k, v) 28 | } 29 | return &combinerRegister{r, fallback} 30 | } 31 | 32 | func (r *combinerRegister) GetResponseCombiner(name string) (ResponseCombiner, bool) { 33 | v, ok := r.data.Get(name) 34 | if !ok { 35 | return r.fallback, ok 36 | } 37 | if rc, ok := v.(ResponseCombiner); ok { 38 | return rc, ok 39 | } 40 | return r.fallback, ok 41 | } 42 | 43 | func (r *combinerRegister) SetResponseCombiner(name string, rc ResponseCombiner) { 44 | r.data.Register(name, rc) 45 | } 46 | -------------------------------------------------------------------------------- /proxy/register_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestNewRegister_responseCombiner_ok(t *testing.T) { 11 | r := NewRegister() 12 | r.SetResponseCombiner("name1", func(total int, parts []*Response) *Response { 13 | if total < 0 || total >= len(parts) { 14 | return nil 15 | } 16 | return parts[total] 17 | }) 18 | 19 | rc, ok := r.GetResponseCombiner("name1") 20 | if !ok { 21 | t.Error("expecting response combiner") 22 | return 23 | } 24 | 25 | result := rc(0, []*Response{{IsComplete: true, Data: map[string]interface{}{"a": 42}}}) 26 | 27 | if result == nil { 28 | t.Error("expecting result") 29 | return 30 | } 31 | 32 | if !result.IsComplete { 33 | t.Error("expecting a complete result") 34 | return 35 | } 36 | 37 | if len(result.Data) != 1 { 38 | t.Error("unexpected result size:", len(result.Data)) 39 | return 40 | } 41 | } 42 | 43 | func TestNewRegister_responseCombiner_fallbackIfErrored(t *testing.T) { 44 | r := NewRegister() 45 | 46 | r.data.Register("errored", true) 47 | 48 | rc, ok := r.GetResponseCombiner("errored") 49 | if !ok { 50 | t.Error("expecting response combiner") 51 | return 52 | } 53 | 54 | original := &Response{IsComplete: true, Data: map[string]interface{}{"a": 42}} 55 | 56 | result := rc(1, []*Response{{Data: original.Data, IsComplete: original.IsComplete}}) 57 | 58 | if !reflect.DeepEqual(original.Data, result.Data) { 59 | t.Errorf("unexpected data, want=%+v | have=%+v", original.Data, result.Data) 60 | return 61 | } 62 | if result.IsComplete != original.IsComplete { 63 | t.Errorf("unexpected complete flag, want=%+v | have=%+v", original.IsComplete, result.IsComplete) 64 | return 65 | } 66 | } 67 | 68 | func TestNewRegister_responseCombiner_fallbackIfUnknown(t *testing.T) { 69 | r := NewRegister() 70 | 71 | rc, ok := r.GetResponseCombiner("unknown") 72 | if ok { 73 | t.Error("the response combiner should not be found") 74 | return 75 | } 76 | 77 | original := &Response{IsComplete: true, Data: map[string]interface{}{"a": 42}} 78 | 79 | result := rc(1, []*Response{{Data: original.Data, IsComplete: original.IsComplete}}) 80 | 81 | if !reflect.DeepEqual(original.Data, result.Data) { 82 | t.Errorf("unexpected data, want=%+v | have=%+v", original.Data, result.Data) 83 | return 84 | } 85 | if result.IsComplete != original.IsComplete { 86 | t.Errorf("unexpected complete flag, want=%+v | have=%+v", original.IsComplete, result.IsComplete) 87 | return 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /proxy/request.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "bytes" 7 | "io" 8 | "net/url" 9 | ) 10 | 11 | // Request contains the data to send to the backend 12 | type Request struct { 13 | Method string 14 | URL *url.URL 15 | Query url.Values 16 | Path string 17 | Body io.ReadCloser 18 | Params map[string]string 19 | Headers map[string][]string 20 | } 21 | 22 | // GeneratePath takes a pattern and updates the path of the request 23 | func (r *Request) GeneratePath(URLPattern string) { 24 | if len(r.Params) == 0 { 25 | r.Path = URLPattern 26 | return 27 | } 28 | buff := []byte(URLPattern) 29 | for k, v := range r.Params { 30 | var key []byte 31 | 32 | key = append(key, "{{."...) 33 | key = append(key, k...) 34 | key = append(key, "}}"...) 35 | buff = bytes.ReplaceAll(buff, key, []byte(v)) 36 | } 37 | r.Path = string(buff) 38 | } 39 | 40 | // Clone clones itself into a new request. The returned cloned request is not 41 | // thread-safe, so changes on request.Params and request.Headers could generate 42 | // race-conditions depending on the part of the pipe they are being executed. 43 | // For thread-safe request headers and/or params manipulation, use the proxy.CloneRequest 44 | // function. 45 | func (r *Request) Clone() Request { 46 | var clonedURL *url.URL 47 | if r.URL != nil { 48 | clonedURL, _ = url.Parse(r.URL.String()) 49 | } 50 | return Request{ 51 | Method: r.Method, 52 | URL: clonedURL, 53 | Query: r.Query, 54 | Path: r.Path, 55 | Body: r.Body, 56 | Params: r.Params, 57 | Headers: r.Headers, 58 | } 59 | } 60 | 61 | // CloneRequest returns a deep copy of the received request, so the received and the 62 | // returned proxy.Request do not share a pointer 63 | func CloneRequest(r *Request) *Request { 64 | clone := r.Clone() 65 | clone.Headers = CloneRequestHeaders(r.Headers) 66 | clone.Params = CloneRequestParams(r.Params) 67 | if r.Body == nil { 68 | return &clone 69 | } 70 | buf := new(bytes.Buffer) 71 | buf.ReadFrom(r.Body) 72 | r.Body.Close() 73 | 74 | r.Body = io.NopCloser(bytes.NewReader(buf.Bytes())) 75 | clone.Body = io.NopCloser(buf) 76 | 77 | return &clone 78 | } 79 | 80 | // CloneRequestHeaders returns a copy of the received request headers 81 | func CloneRequestHeaders(headers map[string][]string) map[string][]string { 82 | m := make(map[string][]string, len(headers)) 83 | for k, vs := range headers { 84 | tmp := make([]string, len(vs)) 85 | copy(tmp, vs) 86 | m[k] = tmp 87 | } 88 | return m 89 | } 90 | 91 | // CloneRequestParams returns a copy of the received request params 92 | func CloneRequestParams(params map[string]string) map[string]string { 93 | m := make(map[string]string, len(params)) 94 | for k, v := range params { 95 | m[k] = v 96 | } 97 | return m 98 | } 99 | -------------------------------------------------------------------------------- /proxy/request_benchmark_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import "testing" 6 | 7 | func BenchmarkRequestGeneratePath(b *testing.B) { 8 | r := Request{ 9 | Method: "GET", 10 | Params: map[string]string{ 11 | "Supu": "42", 12 | "Tupu": "false", 13 | "Foo": "bar", 14 | }, 15 | } 16 | 17 | for _, testCase := range []string{ 18 | "/a", 19 | "/a/{{.Supu}}", 20 | "/a?b={{.Tupu}}", 21 | "/a/{{.Supu}}/foo/{{.Foo}}", 22 | "/a/{{.Supu}}/foo/{{.Foo}}/b?c={{.Tupu}}", 23 | } { 24 | b.Run(testCase, func(b *testing.B) { 25 | b.ReportAllocs() 26 | for i := 0; i < b.N; i++ { 27 | r.GeneratePath(testCase) 28 | } 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /proxy/request_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "bytes" 7 | "io" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestRequestGeneratePath(t *testing.T) { 13 | r := Request{ 14 | Method: "GET", 15 | Params: map[string]string{ 16 | "Supu": "42", 17 | "Tupu": "false", 18 | "Foo": "bar", 19 | }, 20 | } 21 | 22 | for i, testCase := range [][]string{ 23 | {"/a/{{.Supu}}", "/a/42"}, 24 | {"/a?b={{.Tupu}}", "/a?b=false"}, 25 | {"/a/{{.Supu}}/foo/{{.Foo}}", "/a/42/foo/bar"}, 26 | {"/a", "/a"}, 27 | } { 28 | r.GeneratePath(testCase[0]) 29 | if r.Path != testCase[1] { 30 | t.Errorf("%d: want %s, have %s", i, testCase[1], r.Path) 31 | } 32 | } 33 | } 34 | 35 | func TestRequest_Clone(t *testing.T) { 36 | r := Request{ 37 | Method: "GET", 38 | Params: map[string]string{ 39 | "Supu": "42", 40 | "Tupu": "false", 41 | "Foo": "bar", 42 | }, 43 | Headers: map[string][]string{ 44 | "Content-Type": {"application/json"}, 45 | }, 46 | } 47 | clone := r.Clone() 48 | 49 | if len(r.Params) != len(clone.Params) { 50 | t.Errorf("wrong num of params. have: %d, want: %d", len(clone.Params), len(r.Params)) 51 | return 52 | } 53 | for k, v := range r.Params { 54 | if res, ok := clone.Params[k]; !ok { 55 | t.Errorf("param %s not cloned", k) 56 | } else if res != v { 57 | t.Errorf("unexpected param %s. have: %s, want: %s", k, res, v) 58 | } 59 | } 60 | 61 | if len(r.Headers) != len(clone.Headers) { 62 | t.Errorf("wrong num of headers. have: %d, want: %d", len(clone.Headers), len(r.Headers)) 63 | return 64 | } 65 | 66 | for k, vs := range r.Headers { 67 | if res, ok := clone.Headers[k]; !ok { 68 | t.Errorf("header %s not cloned", k) 69 | } else if len(res) != len(vs) { 70 | t.Errorf("unexpected header %s. have: %v, want: %v", k, res, vs) 71 | } 72 | } 73 | 74 | r.Headers["extra"] = []string{"supu"} 75 | 76 | if len(r.Headers) != len(clone.Headers) { 77 | t.Errorf("wrong num of headers. have: %d, want: %d", len(clone.Headers), len(r.Headers)) 78 | return 79 | } 80 | 81 | for k, vs := range r.Headers { 82 | if res, ok := clone.Headers[k]; !ok { 83 | t.Errorf("header %s not cloned", k) 84 | } else if len(res) != len(vs) { 85 | t.Errorf("unexpected header %s. have: %v, want: %v", k, res, vs) 86 | } 87 | } 88 | } 89 | 90 | func TestCloneRequest(t *testing.T) { 91 | body := `{"a":1,"b":2}` 92 | r := Request{ 93 | Method: "POST", 94 | Params: map[string]string{ 95 | "Supu": "42", 96 | "Tupu": "false", 97 | "Foo": "bar", 98 | }, 99 | Headers: map[string][]string{ 100 | "Content-Type": {"application/json"}, 101 | }, 102 | Body: io.NopCloser(strings.NewReader(body)), 103 | } 104 | clone := CloneRequest(&r) 105 | 106 | if len(r.Params) != len(clone.Params) { 107 | t.Errorf("wrong num of params. have: %d, want: %d", len(clone.Params), len(r.Params)) 108 | return 109 | } 110 | for k, v := range r.Params { 111 | if res, ok := clone.Params[k]; !ok { 112 | t.Errorf("param %s not cloned", k) 113 | } else if res != v { 114 | t.Errorf("unexpected param %s. have: %s, want: %s", k, res, v) 115 | } 116 | } 117 | 118 | if len(r.Headers) != len(clone.Headers) { 119 | t.Errorf("wrong num of headers. have: %d, want: %d", len(clone.Headers), len(r.Headers)) 120 | return 121 | } 122 | 123 | for k, vs := range r.Headers { 124 | if res, ok := clone.Headers[k]; !ok { 125 | t.Errorf("header %s not cloned", k) 126 | } else if len(res) != len(vs) { 127 | t.Errorf("unexpected header %s. have: %v, want: %v", k, res, vs) 128 | } 129 | } 130 | 131 | r.Headers["extra"] = []string{"supu"} 132 | 133 | if _, ok := clone.Headers["extra"]; ok { 134 | t.Error("the cloned instance shares its headers with the original one") 135 | } 136 | 137 | delete(r.Params, "Supu") 138 | 139 | if _, ok := clone.Params["Supu"]; !ok { 140 | t.Error("the cloned instance shares its params with the original one") 141 | } 142 | 143 | rb, _ := io.ReadAll(r.Body) 144 | cb, _ := io.ReadAll(clone.Body) 145 | 146 | if !bytes.Equal(cb, rb) || body != string(rb) { 147 | t.Errorf("unexpected bodies. original: %s, returned: %s", string(rb), string(cb)) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /proxy/stack_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration || !race 2 | // +build integration !race 3 | 4 | // SPDX-License-Identifier: Apache-2.0 5 | 6 | package proxy 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | "io" 12 | "net/http" 13 | "net/http/httptest" 14 | "net/url" 15 | "os" 16 | "strings" 17 | "sync" 18 | "testing" 19 | 20 | "github.com/luraproject/lura/v2/config" 21 | "github.com/luraproject/lura/v2/logging" 22 | ) 23 | 24 | func TestProxyStack_multi(t *testing.T) { 25 | results := map[string]int{} 26 | m := new(sync.Mutex) 27 | total := 100000 28 | cfgPath := ".config.json" 29 | 30 | s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | m.Lock() 32 | results[r.URL.String()]++ 33 | m.Unlock() 34 | w.Write([]byte("{\"foo\":42}")) 35 | })) 36 | defer s.Close() 37 | 38 | { 39 | cfgContent := `{ 40 | "version":3, 41 | "endpoints":[{ 42 | "endpoint":"/{foo}", 43 | "backend":[ 44 | { 45 | "host": ["%s"], 46 | "url_pattern": "/first/{foo}", 47 | "group": "1" 48 | }, 49 | { 50 | "host": ["%s"], 51 | "url_pattern": "/second/{foo}", 52 | "group": "2" 53 | }, 54 | { 55 | "host": ["%s"], 56 | "url_pattern": "/third/{foo}", 57 | "group": "3" 58 | } 59 | ] 60 | }] 61 | }` 62 | if err := os.WriteFile(cfgPath, []byte(fmt.Sprintf(cfgContent, s.URL, s.URL, s.URL)), 0666); err != nil { 63 | t.Error(err) 64 | return 65 | } 66 | defer os.Remove(cfgPath) 67 | } 68 | 69 | cfg, err := config.NewParser().Parse(cfgPath) 70 | if err != nil { 71 | t.Error(err) 72 | return 73 | } 74 | cfg.Normalize() 75 | 76 | factory := NewDefaultFactory(httpProxy, logging.NoOp) 77 | p, err := factory.New(cfg.Endpoints[0]) 78 | if err != nil { 79 | t.Error(err) 80 | return 81 | } 82 | 83 | for i := 0; i < total; i++ { 84 | p(context.Background(), &Request{ 85 | Method: "GET", 86 | Params: map[string]string{"Foo": "42"}, 87 | Headers: map[string][]string{}, 88 | Path: "/", 89 | Query: url.Values{}, 90 | Body: io.NopCloser(strings.NewReader("")), 91 | URL: new(url.URL), 92 | }) 93 | } 94 | 95 | for k, v := range results { 96 | if v != total { 97 | t.Errorf("the url %s was consumed %d times", k, v) 98 | } 99 | } 100 | 101 | if len(results) != 3 { 102 | t.Errorf("unexpected number of consumed urls. have %d, want 3", len(results)) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /proxy/static.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package proxy 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | 10 | "github.com/luraproject/lura/v2/config" 11 | "github.com/luraproject/lura/v2/logging" 12 | ) 13 | 14 | // NewStaticMiddleware creates proxy middleware for adding static values to the processed responses 15 | func NewStaticMiddleware(logger logging.Logger, endpointConfig *config.EndpointConfig) Middleware { 16 | cfg, ok := getStaticMiddlewareCfg(endpointConfig.ExtraConfig) 17 | if !ok { 18 | return emptyMiddlewareFallback(logger) 19 | } 20 | 21 | b, _ := json.Marshal(cfg.Data) 22 | 23 | logger.Debug( 24 | fmt.Sprintf( 25 | "[ENDPOINT: %s][Static] Adding a static response using '%s' strategy. Data: %s", 26 | endpointConfig.Endpoint, 27 | cfg.Strategy, 28 | string(b), 29 | ), 30 | ) 31 | 32 | return func(next ...Proxy) Proxy { 33 | if len(next) > 1 { 34 | logger.Fatal("too many proxies for this proxy middleware: NewStaticMiddleware only accepts 1 proxy, got %d", len(next)) 35 | return nil 36 | } 37 | return func(ctx context.Context, request *Request) (*Response, error) { 38 | result, err := next[0](ctx, request) 39 | if !cfg.Match(result, err) { 40 | return result, err 41 | } 42 | 43 | if result == nil { 44 | result = &Response{Data: map[string]interface{}{}} 45 | } else if result.Data == nil { 46 | result.Data = map[string]interface{}{} 47 | } 48 | 49 | for k, v := range cfg.Data { 50 | result.Data[k] = v 51 | } 52 | 53 | return result, err 54 | } 55 | } 56 | } 57 | 58 | const ( 59 | staticKey = "static" 60 | 61 | staticAlwaysStrategy = "always" 62 | staticIfSuccessStrategy = "success" 63 | staticIfErroredStrategy = "errored" 64 | staticIfCompleteStrategy = "complete" 65 | staticIfIncompleteStrategy = "incomplete" 66 | ) 67 | 68 | type staticConfig struct { 69 | Data map[string]interface{} 70 | Strategy string 71 | Match func(*Response, error) bool 72 | } 73 | 74 | func getStaticMiddlewareCfg(extra config.ExtraConfig) (staticConfig, bool) { 75 | v, ok := extra[Namespace] 76 | if !ok { 77 | return staticConfig{}, ok 78 | } 79 | e, ok := v.(map[string]interface{}) 80 | if !ok { 81 | return staticConfig{}, ok 82 | } 83 | v, ok = e[staticKey] 84 | if !ok { 85 | return staticConfig{}, ok 86 | } 87 | tmp, ok := v.(map[string]interface{}) 88 | if !ok { 89 | return staticConfig{}, ok 90 | } 91 | data, ok := tmp["data"].(map[string]interface{}) 92 | if !ok { 93 | return staticConfig{}, ok 94 | } 95 | 96 | name, ok := tmp["strategy"].(string) 97 | if !ok { 98 | name = staticAlwaysStrategy 99 | } 100 | cfg := staticConfig{ 101 | Data: data, 102 | Strategy: name, 103 | Match: staticAlwaysMatch, 104 | } 105 | switch name { 106 | case staticAlwaysStrategy: 107 | cfg.Match = staticAlwaysMatch 108 | case staticIfSuccessStrategy: 109 | cfg.Match = staticIfSuccessMatch 110 | case staticIfErroredStrategy: 111 | cfg.Match = staticIfErroredMatch 112 | case staticIfCompleteStrategy: 113 | cfg.Match = staticIfCompleteMatch 114 | case staticIfIncompleteStrategy: 115 | cfg.Match = staticIfIncompleteMatch 116 | } 117 | return cfg, true 118 | } 119 | 120 | func staticAlwaysMatch(_ *Response, _ error) bool { return true } 121 | func staticIfSuccessMatch(_ *Response, err error) bool { return err == nil } 122 | func staticIfErroredMatch(_ *Response, err error) bool { return err != nil } 123 | func staticIfCompleteMatch(r *Response, err error) bool { 124 | return err == nil && r != nil && r.IsComplete 125 | } 126 | func staticIfIncompleteMatch(r *Response, _ error) bool { return r == nil || !r.IsComplete } 127 | -------------------------------------------------------------------------------- /register/register.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | Package register offers tools for creating and managing registers. 5 | */ 6 | package register 7 | 8 | import "sync" 9 | 10 | // New returns an initialized Namespaced register 11 | func New() *Namespaced { 12 | return &Namespaced{data: NewUntyped()} 13 | } 14 | 15 | // Namespaced is a register able to keep track of elements stored 16 | // under namespaces and keys 17 | type Namespaced struct { 18 | data *Untyped 19 | } 20 | 21 | // Get returns the Untyped register stored under the namespace 22 | func (n *Namespaced) Get(namespace string) (*Untyped, bool) { 23 | v, ok := n.data.Get(namespace) 24 | if !ok { 25 | return nil, ok 26 | } 27 | register, ok := v.(*Untyped) 28 | return register, ok 29 | } 30 | 31 | // Register stores v at the key name of the Untyped register named namespace 32 | func (n *Namespaced) Register(namespace, name string, v interface{}) { 33 | if register, ok := n.Get(namespace); ok { 34 | register.Register(name, v) 35 | return 36 | } 37 | 38 | register := NewUntyped() 39 | register.Register(name, v) 40 | n.data.Register(namespace, register) 41 | } 42 | 43 | // AddNamespace adds a new, empty Untyped register under the give namespace (if 44 | // it did not exist) 45 | func (n *Namespaced) AddNamespace(namespace string) { 46 | if _, ok := n.Get(namespace); ok { 47 | return 48 | } 49 | n.data.Register(namespace, NewUntyped()) 50 | } 51 | 52 | // NewUntyped returns an empty Untyped register 53 | func NewUntyped() *Untyped { 54 | return &Untyped{ 55 | data: map[string]interface{}{}, 56 | mutex: &sync.RWMutex{}, 57 | } 58 | } 59 | 60 | // Untyped is a simple register, safe for concurrent access 61 | type Untyped struct { 62 | data map[string]interface{} 63 | mutex *sync.RWMutex 64 | } 65 | 66 | // Register stores v under the key name 67 | func (u *Untyped) Register(name string, v interface{}) { 68 | u.mutex.Lock() 69 | u.data[name] = v 70 | u.mutex.Unlock() 71 | } 72 | 73 | // Get returns the value stored at the key name 74 | func (u *Untyped) Get(name string) (interface{}, bool) { 75 | u.mutex.RLock() 76 | v, ok := u.data[name] 77 | u.mutex.RUnlock() 78 | return v, ok 79 | } 80 | 81 | // Clone returns a snapshot of the register 82 | func (u *Untyped) Clone() map[string]interface{} { 83 | u.mutex.RLock() 84 | res := make(map[string]interface{}, len(u.data)) 85 | for k, v := range u.data { 86 | res[k] = v 87 | } 88 | u.mutex.RUnlock() 89 | return res 90 | } 91 | -------------------------------------------------------------------------------- /register/register_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package register 4 | 5 | import "testing" 6 | 7 | func TestNamespaced(t *testing.T) { 8 | r := New() 9 | r.Register("namespace1", "name1", 42) 10 | r.AddNamespace("namespace1") 11 | r.AddNamespace("namespace2") 12 | r.Register("namespace2", "name2", true) 13 | 14 | nr, ok := r.Get("namespace1") 15 | if !ok { 16 | t.Error("namespace1 not found") 17 | return 18 | } 19 | if _, ok := nr.Get("name2"); ok { 20 | t.Error("name2 found into namespace1") 21 | return 22 | } 23 | v1, ok := nr.Get("name1") 24 | if !ok { 25 | t.Error("name1 not found") 26 | return 27 | } 28 | if i, ok := v1.(int); !ok || i != 42 { 29 | t.Error("unexpected value:", v1) 30 | } 31 | 32 | nr, ok = r.Get("namespace2") 33 | if !ok { 34 | t.Error("namespace2 not found") 35 | return 36 | } 37 | if _, ok := nr.Get("name1"); ok { 38 | t.Error("name1 found into namespace2") 39 | return 40 | } 41 | v2, ok := nr.Get("name2") 42 | if !ok { 43 | t.Error("name2 not found") 44 | return 45 | } 46 | if b, ok := v2.(bool); !ok || !b { 47 | t.Error("unexpected value:", v2) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /router/chi/endpoint.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package chi 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/go-chi/chi/v5" 9 | "github.com/luraproject/lura/v2/config" 10 | "github.com/luraproject/lura/v2/proxy" 11 | "github.com/luraproject/lura/v2/router/mux" 12 | "golang.org/x/text/cases" 13 | "golang.org/x/text/language" 14 | ) 15 | 16 | // HandlerFactory creates a handler function that adapts the chi router with the injected proxy 17 | type HandlerFactory func(*config.EndpointConfig, proxy.Proxy) http.HandlerFunc 18 | 19 | // NewEndpointHandler implements the HandleFactory interface using the default ToHTTPError function 20 | func NewEndpointHandler(cfg *config.EndpointConfig, prxy proxy.Proxy) http.HandlerFunc { 21 | hf := mux.CustomEndpointHandler( 22 | mux.NewRequestBuilder(extractParamsFromEndpoint), 23 | ) 24 | return hf(cfg, prxy) 25 | } 26 | 27 | func extractParamsFromEndpoint(r *http.Request) map[string]string { 28 | ctx := r.Context() 29 | rctx := chi.RouteContext(ctx) 30 | 31 | params := map[string]string{} 32 | if len(rctx.URLParams.Keys) > 0 { 33 | title := cases.Title(language.Und) 34 | for _, param := range rctx.URLParams.Keys { 35 | params[title.String(param[:1])+param[1:]] = chi.URLParam(r, param) 36 | } 37 | } 38 | return params 39 | } 40 | -------------------------------------------------------------------------------- /router/chi/endpoint_benchmark_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package chi 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | "time" 14 | 15 | "github.com/go-chi/chi/v5" 16 | "github.com/luraproject/lura/v2/config" 17 | "github.com/luraproject/lura/v2/proxy" 18 | ) 19 | 20 | func BenchmarkEndpointHandler_ko(b *testing.B) { 21 | p := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 22 | return nil, fmt.Errorf("This is %s", "a dummy error") 23 | } 24 | endpoint := &config.EndpointConfig{ 25 | Timeout: time.Second, 26 | CacheTTL: 6 * time.Hour, 27 | QueryString: []string{"b"}, 28 | } 29 | 30 | router := chi.NewRouter() 31 | router.Handle("/_chi_endpoint/", NewEndpointHandler(endpoint, p)) 32 | 33 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8080/_chi_endpoint/a?b=1", http.NoBody) 34 | req.Header.Set("Content-Type", "application/json") 35 | 36 | b.ReportAllocs() 37 | for i := 0; i < b.N; i++ { 38 | w := httptest.NewRecorder() 39 | router.ServeHTTP(w, req) 40 | } 41 | } 42 | 43 | func BenchmarkEndpointHandler_ok(b *testing.B) { 44 | pResp := proxy.Response{ 45 | Data: map[string]interface{}{}, 46 | Io: io.NopCloser(&bytes.Buffer{}), 47 | IsComplete: true, 48 | Metadata: proxy.Metadata{}, 49 | } 50 | p := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 51 | return &pResp, nil 52 | } 53 | endpoint := &config.EndpointConfig{ 54 | Timeout: time.Second, 55 | CacheTTL: 6 * time.Hour, 56 | QueryString: []string{"b"}, 57 | } 58 | 59 | router := chi.NewRouter() 60 | router.Handle("/_chi_endpoint/", NewEndpointHandler(endpoint, p)) 61 | 62 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8080/_chi_endpoint/a?b=1", http.NoBody) 63 | req.Header.Set("Content-Type", "application/json") 64 | 65 | b.ReportAllocs() 66 | for i := 0; i < b.N; i++ { 67 | w := httptest.NewRecorder() 68 | router.ServeHTTP(w, req) 69 | } 70 | } 71 | 72 | func BenchmarkEndpointHandler_ko_Parallel(b *testing.B) { 73 | p := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 74 | return nil, fmt.Errorf("This is %s", "a dummy error") 75 | } 76 | endpoint := &config.EndpointConfig{ 77 | Timeout: time.Second, 78 | CacheTTL: 6 * time.Hour, 79 | QueryString: []string{"b"}, 80 | } 81 | 82 | router := chi.NewRouter() 83 | router.Handle("/_chi_endpoint/", NewEndpointHandler(endpoint, p)) 84 | 85 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8080/_chi_endpoint/a?b=1", http.NoBody) 86 | req.Header.Set("Content-Type", "application/json") 87 | 88 | b.ReportAllocs() 89 | b.RunParallel(func(pb *testing.PB) { 90 | for pb.Next() { 91 | w := httptest.NewRecorder() 92 | router.ServeHTTP(w, req) 93 | } 94 | }) 95 | } 96 | 97 | func BenchmarkEndpointHandler_ok_Parallel(b *testing.B) { 98 | pResp := proxy.Response{ 99 | Data: map[string]interface{}{}, 100 | Io: io.NopCloser(&bytes.Buffer{}), 101 | IsComplete: true, 102 | Metadata: proxy.Metadata{}, 103 | } 104 | p := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 105 | return &pResp, nil 106 | } 107 | endpoint := &config.EndpointConfig{ 108 | Timeout: time.Second, 109 | CacheTTL: 6 * time.Hour, 110 | QueryString: []string{"b"}, 111 | } 112 | 113 | router := chi.NewRouter() 114 | router.Handle("/_chi_endpoint/", NewEndpointHandler(endpoint, p)) 115 | 116 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8080/_chi_endpoint/a?b=1", http.NoBody) 117 | req.Header.Set("Content-Type", "application/json") 118 | 119 | b.ReportAllocs() 120 | b.RunParallel(func(pb *testing.PB) { 121 | for pb.Next() { 122 | w := httptest.NewRecorder() 123 | router.ServeHTTP(w, req) 124 | } 125 | }) 126 | } 127 | -------------------------------------------------------------------------------- /router/gin/debug.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package gin 4 | 5 | import ( 6 | "io" 7 | 8 | "github.com/gin-gonic/gin" 9 | 10 | "github.com/luraproject/lura/v2/logging" 11 | ) 12 | 13 | // DebugHandler creates a dummy handler function, useful for quick integration tests 14 | func DebugHandler(logger logging.Logger) gin.HandlerFunc { 15 | logPrefixSecondary := "[ENDPOINT: /__debug/*]" 16 | return func(c *gin.Context) { 17 | logger.Debug(logPrefixSecondary, "Method:", c.Request.Method) 18 | logger.Debug(logPrefixSecondary, "URL:", c.Request.RequestURI) 19 | logger.Debug(logPrefixSecondary, "Query:", c.Request.URL.Query()) 20 | logger.Debug(logPrefixSecondary, "Params:", c.Params) 21 | logger.Debug(logPrefixSecondary, "Headers:", c.Request.Header) 22 | body, _ := io.ReadAll(c.Request.Body) 23 | c.Request.Body.Close() 24 | logger.Debug(logPrefixSecondary, "Body:", string(body)) 25 | c.JSON(200, gin.H{ 26 | "message": "pong", 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /router/gin/debug_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package gin 4 | 5 | import ( 6 | "bytes" 7 | "io" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/gin-gonic/gin" 13 | 14 | "github.com/luraproject/lura/v2/logging" 15 | ) 16 | 17 | func TestDebugHandler(t *testing.T) { 18 | buff := bytes.NewBuffer(make([]byte, 1024)) 19 | logger, err := logging.NewLogger("ERROR", buff, "pref") 20 | if err != nil { 21 | t.Error("building the logger:", err.Error()) 22 | return 23 | } 24 | 25 | router := gin.New() 26 | router.GET("/_gin_endpoint/:param", DebugHandler(logger)) 27 | 28 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8088/_gin_endpoint/a?b=1", io.NopCloser(&bytes.Buffer{})) 29 | req.Header.Set("Content-Type", "application/json") 30 | 31 | w := httptest.NewRecorder() 32 | 33 | router.ServeHTTP(w, req) 34 | 35 | body, ioerr := io.ReadAll(w.Result().Body) 36 | if ioerr != nil { 37 | t.Error("reading a response:", ioerr.Error()) 38 | return 39 | } 40 | w.Result().Body.Close() 41 | 42 | expectedBody := "{\"message\":\"pong\"}" 43 | 44 | content := string(body) 45 | if w.Result().Header.Get("Cache-Control") != "" { 46 | t.Error("Cache-Control error:", w.Result().Header.Get("Cache-Control")) 47 | } 48 | if w.Result().Header.Get("Content-Type") != "application/json; charset=utf-8" { 49 | t.Error("Content-Type error:", w.Result().Header.Get("Content-Type")) 50 | } 51 | if w.Result().Header.Get("X-Krakend") != "" { 52 | t.Error("X-Krakend error:", w.Result().Header.Get("X-Krakend")) 53 | } 54 | if w.Result().StatusCode != http.StatusOK { 55 | t.Error("Unexpected status code:", w.Result().StatusCode) 56 | } 57 | if content != expectedBody { 58 | t.Error("Unexpected body:", content, "expected:", expectedBody) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /router/gin/echo.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package gin 4 | 5 | import ( 6 | "io" 7 | "net/http" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | type echoResponse struct { 13 | Uri string `json:"req_uri"` 14 | UriDetails map[string]string `json:"req_uri_details"` 15 | Method string `json:"req_method"` 16 | Querystring map[string][]string `json:"req_querystring"` 17 | Body string `json:"req_body"` 18 | Headers map[string][]string `json:"req_headers"` 19 | } 20 | 21 | // EchoHandler creates a dummy handler function, useful for quick integration tests 22 | func EchoHandler() gin.HandlerFunc { 23 | return func(c *gin.Context) { 24 | var body string 25 | if c.Request.Body != nil { 26 | tmp, _ := io.ReadAll(c.Request.Body) 27 | c.Request.Body.Close() 28 | body = string(tmp) 29 | } 30 | resp := echoResponse{ 31 | Uri: c.Request.RequestURI, 32 | UriDetails: map[string]string{ 33 | "user": c.Request.URL.User.String(), 34 | "host": c.Request.Host, 35 | "path": c.Request.URL.Path, 36 | "query": c.Request.URL.Query().Encode(), 37 | "fragment": c.Request.URL.Fragment, 38 | }, 39 | Method: c.Request.Method, 40 | Querystring: c.Request.URL.Query(), 41 | Body: body, 42 | Headers: c.Request.Header, 43 | } 44 | 45 | c.JSON(http.StatusOK, resp) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /router/gin/echo_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package gin 4 | 5 | import ( 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func TestEchoHandler(t *testing.T) { 16 | reqBody := `{"message":"some body to send"}` 17 | expectedRespBody := `{"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"]}}` 18 | expectedRespNoBody := `{"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"]}}` 19 | expectedRespString := `{"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"]}}` 20 | 21 | gin.SetMode(gin.TestMode) 22 | router := gin.New() 23 | router.GET("/_gin_endpoint/:param", EchoHandler()) 24 | 25 | for _, tc := range []struct { 26 | name string 27 | body io.Reader 28 | resp string 29 | }{ 30 | { 31 | name: "json body", 32 | body: strings.NewReader(reqBody), 33 | resp: expectedRespBody, 34 | }, 35 | { 36 | name: "no body", 37 | body: http.NoBody, 38 | resp: expectedRespNoBody, 39 | }, 40 | { 41 | name: "string body", 42 | body: strings.NewReader("Hello lura"), 43 | resp: expectedRespString, 44 | }, 45 | } { 46 | t.Run(tc.name, func(t *testing.T) { 47 | echoRunTestRequest(t, router, tc.body, tc.resp) 48 | }) 49 | } 50 | 51 | } 52 | 53 | func echoRunTestRequest(t *testing.T, e *gin.Engine, body io.Reader, expected string) { 54 | req := httptest.NewRequest("GET", "http://127.0.0.1:8088/_gin_endpoint/a?b=1", body) 55 | req.Header.Set("Content-Type", "application/json") 56 | 57 | w := httptest.NewRecorder() 58 | 59 | e.ServeHTTP(w, req) 60 | 61 | respBody, ioerr := io.ReadAll(w.Result().Body) 62 | if ioerr != nil { 63 | t.Error("reading a response:", ioerr.Error()) 64 | return 65 | } 66 | w.Result().Body.Close() 67 | 68 | content := string(respBody) 69 | if w.Result().Header.Get("Cache-Control") != "" { 70 | t.Error("Cache-Control error:", w.Result().Header.Get("Cache-Control")) 71 | } 72 | if w.Result().Header.Get("Content-Type") != "application/json; charset=utf-8" { 73 | t.Error("Content-Type error:", w.Result().Header.Get("Content-Type")) 74 | } 75 | if w.Result().Header.Get("X-Krakend") != "" { 76 | t.Error("X-Krakend error:", w.Result().Header.Get("X-Krakend")) 77 | } 78 | if w.Result().StatusCode != http.StatusOK { 79 | t.Error("Unexpected status code:", w.Result().StatusCode) 80 | } 81 | if content != expected { 82 | t.Error("Unexpected body:", content, "expected:", expected) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /router/gin/endpoint_benchmark_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package gin 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | "time" 14 | 15 | "github.com/gin-gonic/gin" 16 | "github.com/luraproject/lura/v2/config" 17 | "github.com/luraproject/lura/v2/proxy" 18 | ) 19 | 20 | func BenchmarkEndpointHandler_ko(b *testing.B) { 21 | p := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 22 | return nil, fmt.Errorf("This is %s", "a dummy error") 23 | } 24 | endpoint := &config.EndpointConfig{ 25 | Timeout: time.Second, 26 | CacheTTL: 6 * time.Hour, 27 | QueryString: []string{"b"}, 28 | } 29 | 30 | gin.SetMode(gin.TestMode) 31 | router := gin.New() 32 | router.GET("/_gin_endpoint/:param", EndpointHandler(endpoint, p)) 33 | 34 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8080/_gin_endpoint/a?b=1", http.NoBody) 35 | req.Header.Set("Content-Type", "application/json") 36 | 37 | b.ReportAllocs() 38 | for i := 0; i < b.N; i++ { 39 | w := httptest.NewRecorder() 40 | router.ServeHTTP(w, req) 41 | } 42 | } 43 | 44 | func BenchmarkEndpointHandler_ok(b *testing.B) { 45 | pResp := proxy.Response{ 46 | Data: map[string]interface{}{}, 47 | Io: io.NopCloser(&bytes.Buffer{}), 48 | IsComplete: true, 49 | Metadata: proxy.Metadata{}, 50 | } 51 | p := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 52 | return &pResp, nil 53 | } 54 | endpoint := &config.EndpointConfig{ 55 | Timeout: time.Second, 56 | CacheTTL: 6 * time.Hour, 57 | QueryString: []string{"b"}, 58 | } 59 | 60 | gin.SetMode(gin.TestMode) 61 | router := gin.New() 62 | router.GET("/_gin_endpoint/:param", EndpointHandler(endpoint, p)) 63 | 64 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8080/_gin_endpoint/a?b=1", http.NoBody) 65 | req.Header.Set("Content-Type", "application/json") 66 | 67 | b.ReportAllocs() 68 | for i := 0; i < b.N; i++ { 69 | w := httptest.NewRecorder() 70 | router.ServeHTTP(w, req) 71 | } 72 | } 73 | 74 | func BenchmarkEndpointHandler_ko_Parallel(b *testing.B) { 75 | p := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 76 | return nil, fmt.Errorf("This is %s", "a dummy error") 77 | } 78 | endpoint := &config.EndpointConfig{ 79 | Timeout: time.Second, 80 | CacheTTL: 6 * time.Hour, 81 | QueryString: []string{"b"}, 82 | } 83 | 84 | gin.SetMode(gin.TestMode) 85 | router := gin.New() 86 | router.GET("/_gin_endpoint/:param", EndpointHandler(endpoint, p)) 87 | 88 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8080/_gin_endpoint/a?b=1", http.NoBody) 89 | req.Header.Set("Content-Type", "application/json") 90 | 91 | b.ReportAllocs() 92 | b.RunParallel(func(pb *testing.PB) { 93 | for pb.Next() { 94 | w := httptest.NewRecorder() 95 | router.ServeHTTP(w, req) 96 | } 97 | }) 98 | } 99 | 100 | func BenchmarkEndpointHandler_ok_Parallel(b *testing.B) { 101 | pResp := proxy.Response{ 102 | Data: map[string]interface{}{}, 103 | Io: io.NopCloser(&bytes.Buffer{}), 104 | IsComplete: true, 105 | Metadata: proxy.Metadata{}, 106 | } 107 | p := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 108 | return &pResp, nil 109 | } 110 | endpoint := &config.EndpointConfig{ 111 | Timeout: time.Second, 112 | CacheTTL: 6 * time.Hour, 113 | QueryString: []string{"b"}, 114 | } 115 | 116 | gin.SetMode(gin.TestMode) 117 | router := gin.New() 118 | router.GET("/_gin_endpoint/:param", EndpointHandler(endpoint, p)) 119 | 120 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8080/_gin_endpoint/a?b=1", http.NoBody) 121 | req.Header.Set("Content-Type", "application/json") 122 | 123 | b.ReportAllocs() 124 | b.RunParallel(func(pb *testing.PB) { 125 | for pb.Next() { 126 | w := httptest.NewRecorder() 127 | router.ServeHTTP(w, req) 128 | } 129 | }) 130 | } 131 | -------------------------------------------------------------------------------- /router/gin/engine_test.go: -------------------------------------------------------------------------------- 1 | package gin 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/luraproject/lura/v2/config" 12 | ) 13 | 14 | func TestNewEngine_contextIsPropagated(t *testing.T) { 15 | engine := NewEngine( 16 | config.ServiceConfig{}, 17 | EngineOptions{}, 18 | ) 19 | 20 | type ctxKeyType string 21 | 22 | ctxKey := ctxKeyType("foo") 23 | ctxValue := "bar" 24 | 25 | engine.GET("/some/path", func(c *gin.Context) { 26 | c.String(http.StatusOK, "%v", c.Value(ctxKey)) 27 | }) 28 | 29 | req, _ := http.NewRequest("GET", "/some/path", http.NoBody) 30 | req = req.WithContext(context.WithValue(req.Context(), ctxKey, ctxValue)) 31 | 32 | w := httptest.NewRecorder() 33 | 34 | engine.ServeHTTP(w, req) 35 | 36 | resp := w.Result() 37 | 38 | if sc := resp.StatusCode; sc != http.StatusOK { 39 | t.Errorf("unexpected status code: %d", sc) 40 | return 41 | } 42 | 43 | b, err := io.ReadAll(resp.Body) 44 | if err != nil { 45 | t.Errorf("reading the response body: %s", err.Error()) 46 | return 47 | } 48 | 49 | if string(b) != ctxValue { 50 | t.Errorf("unexpected value: %s", string(b)) 51 | } 52 | } 53 | 54 | func TestNewEngine_paramsAreChecked(t *testing.T) { 55 | engine := NewEngine( 56 | config.ServiceConfig{}, 57 | EngineOptions{}, 58 | ) 59 | 60 | engine.GET("/user/:id/public", func(c *gin.Context) { 61 | c.String(http.StatusOK, "ok") 62 | }) 63 | 64 | assertResponse := func(path string, statusCode int, body string) { 65 | req, _ := http.NewRequest("GET", path, http.NoBody) 66 | w := httptest.NewRecorder() 67 | engine.ServeHTTP(w, req) 68 | resp := w.Result() 69 | 70 | if sc := resp.StatusCode; sc != statusCode { 71 | t.Errorf("unexpected status code: %d (expected %d)", sc, statusCode) 72 | return 73 | } 74 | 75 | b, err := io.ReadAll(resp.Body) 76 | if err != nil { 77 | t.Errorf("reading the response body: %s", err.Error()) 78 | return 79 | } 80 | 81 | if string(b) != body { 82 | t.Errorf("unexpected response body: '%s' (expected '%s')", string(b), body) 83 | } 84 | } 85 | 86 | assertResponse("/user/123/public", http.StatusOK, "ok") 87 | assertResponse("/user/123%3f/public", http.StatusBadRequest, "error: encoded url params") 88 | assertResponse("/user/123%23/public", http.StatusBadRequest, "error: encoded url params") 89 | } 90 | -------------------------------------------------------------------------------- /router/gin/render.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package gin 4 | 5 | import ( 6 | "io" 7 | "net/http" 8 | "sync" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/luraproject/lura/v2/config" 12 | "github.com/luraproject/lura/v2/encoding" 13 | "github.com/luraproject/lura/v2/proxy" 14 | ) 15 | 16 | // Render defines the signature of the functions to be use for the final response 17 | // encoding and rendering 18 | type Render func(*gin.Context, *proxy.Response) 19 | 20 | // NEGOTIATE defines the value of the OutputEncoding for the negotiated render 21 | const NEGOTIATE = "negotiate" 22 | const XML = "xml" 23 | const YAML = "yaml" 24 | 25 | var ( 26 | mutex = &sync.RWMutex{} 27 | renderRegister = map[string]Render{ 28 | encoding.STRING: stringRender, 29 | encoding.JSON: jsonRender, 30 | encoding.NOOP: noopRender, 31 | "json-collection": jsonCollectionRender, 32 | XML: xmlRender, 33 | YAML: yamlRender, 34 | } 35 | ) 36 | 37 | func init() { 38 | // the negotiated render must be registered at the init function in order 39 | // to avoid a cyclical dependency 40 | renderRegister[NEGOTIATE] = negotiatedRender 41 | } 42 | 43 | // RegisterRender allows clients to register their custom renders 44 | func RegisterRender(name string, r Render) { 45 | mutex.Lock() 46 | renderRegister[name] = r 47 | mutex.Unlock() 48 | } 49 | 50 | func getRender(cfg *config.EndpointConfig) Render { 51 | fallback := jsonRender 52 | if len(cfg.Backend) == 1 { 53 | fallback = getWithFallback(cfg.Backend[0].Encoding, fallback) 54 | } 55 | 56 | if cfg.OutputEncoding == "" { 57 | return fallback 58 | } 59 | 60 | return getWithFallback(cfg.OutputEncoding, fallback) 61 | } 62 | 63 | func getWithFallback(key string, fallback Render) Render { 64 | mutex.RLock() 65 | r, ok := renderRegister[key] 66 | mutex.RUnlock() 67 | if !ok { 68 | return fallback 69 | } 70 | return r 71 | } 72 | 73 | func negotiatedRender(c *gin.Context, response *proxy.Response) { 74 | switch c.NegotiateFormat(gin.MIMEJSON, gin.MIMEPlain, gin.MIMEXML, gin.MIMEYAML) { 75 | case gin.MIMEXML: 76 | getWithFallback(XML, jsonRender)(c, response) 77 | case gin.MIMEPlain, gin.MIMEYAML: 78 | getWithFallback(YAML, jsonRender)(c, response) 79 | default: 80 | getWithFallback(encoding.JSON, jsonRender)(c, response) 81 | } 82 | } 83 | 84 | func stringRender(c *gin.Context, response *proxy.Response) { 85 | status := c.Writer.Status() 86 | 87 | if response == nil { 88 | c.String(status, "") 89 | return 90 | } 91 | d, ok := response.Data["content"] 92 | if !ok { 93 | c.String(status, "") 94 | return 95 | } 96 | msg, ok := d.(string) 97 | if !ok { 98 | c.String(status, "") 99 | return 100 | } 101 | c.String(status, msg) 102 | } 103 | 104 | func jsonRender(c *gin.Context, response *proxy.Response) { 105 | status := c.Writer.Status() 106 | if response == nil { 107 | c.JSON(status, emptyResponse) 108 | return 109 | } 110 | c.JSON(status, response.Data) 111 | } 112 | 113 | func jsonCollectionRender(c *gin.Context, response *proxy.Response) { 114 | status := c.Writer.Status() 115 | if response == nil { 116 | c.JSON(status, []struct{}{}) 117 | return 118 | } 119 | col, ok := response.Data["collection"] 120 | if !ok { 121 | c.JSON(status, []struct{}{}) 122 | return 123 | } 124 | c.JSON(status, col) 125 | } 126 | 127 | func xmlRender(c *gin.Context, response *proxy.Response) { 128 | status := c.Writer.Status() 129 | if response == nil { 130 | c.XML(status, nil) 131 | return 132 | } 133 | d, ok := response.Data["content"] 134 | if !ok { 135 | c.XML(status, nil) 136 | return 137 | } 138 | c.XML(status, d) 139 | } 140 | 141 | func yamlRender(c *gin.Context, response *proxy.Response) { 142 | status := c.Writer.Status() 143 | if response == nil { 144 | c.YAML(status, emptyResponse) 145 | return 146 | } 147 | c.YAML(status, response.Data) 148 | } 149 | 150 | func noopRender(c *gin.Context, response *proxy.Response) { 151 | if response == nil { 152 | c.Status(http.StatusInternalServerError) 153 | return 154 | } 155 | for k, vs := range response.Metadata.Headers { 156 | for _, v := range vs { 157 | c.Writer.Header().Add(k, v) 158 | } 159 | } 160 | c.Status(response.Metadata.StatusCode) 161 | if response.Io == nil { 162 | return 163 | } 164 | io.Copy(c.Writer, response.Io) 165 | } 166 | 167 | var emptyResponse = gin.H{} 168 | -------------------------------------------------------------------------------- /router/gorilla/router.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | Package gorilla provides some basic implementations for building routers based on gorilla/mux 5 | */ 6 | package gorilla 7 | 8 | import ( 9 | "net/http" 10 | 11 | gorilla "github.com/gorilla/mux" 12 | 13 | "github.com/luraproject/lura/v2/logging" 14 | "github.com/luraproject/lura/v2/proxy" 15 | "github.com/luraproject/lura/v2/router" 16 | "github.com/luraproject/lura/v2/router/mux" 17 | "github.com/luraproject/lura/v2/transport/http/server" 18 | "golang.org/x/text/cases" 19 | "golang.org/x/text/language" 20 | ) 21 | 22 | // DefaultFactory returns a net/http mux router factory with the injected proxy factory and logger 23 | func DefaultFactory(pf proxy.Factory, logger logging.Logger) router.Factory { 24 | return mux.NewFactory(DefaultConfig(pf, logger)) 25 | } 26 | 27 | // DefaultConfig returns the struct that collects the parts the router should be builded from 28 | func DefaultConfig(pf proxy.Factory, logger logging.Logger) mux.Config { 29 | return mux.Config{ 30 | Engine: gorillaEngine{gorilla.NewRouter()}, 31 | Middlewares: []mux.HandlerMiddleware{}, 32 | HandlerFactory: mux.CustomEndpointHandler(mux.NewRequestBuilder(gorillaParamsExtractor)), 33 | ProxyFactory: pf, 34 | Logger: logger, 35 | DebugPattern: "/__debug/{params}", 36 | EchoPattern: "/__echo/{params}", 37 | RunServer: server.RunServer, 38 | } 39 | } 40 | 41 | func gorillaParamsExtractor(r *http.Request) map[string]string { 42 | params := map[string]string{} 43 | title := cases.Title(language.Und) 44 | for key, value := range gorilla.Vars(r) { 45 | params[title.String(key)] = value 46 | } 47 | return params 48 | } 49 | 50 | type gorillaEngine struct { 51 | r *gorilla.Router 52 | } 53 | 54 | // Handle implements the mux.Engine interface from the lura router package 55 | func (g gorillaEngine) Handle(pattern, method string, handler http.Handler) { 56 | g.r.Handle(pattern, handler).Methods(method) 57 | } 58 | 59 | // ServeHTTP implements the http:Handler interface from the stdlib 60 | func (g gorillaEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) { 61 | g.r.ServeHTTP(mux.NewHTTPErrorInterceptor(w), r) 62 | } 63 | -------------------------------------------------------------------------------- /router/helper.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package router 4 | 5 | import ( 6 | "github.com/luraproject/lura/v2/config" 7 | ) 8 | 9 | func IsValidSequentialEndpoint(_ *config.EndpointConfig) bool { 10 | // if endpoint.ExtraConfig[proxy.Namespace] == nil { 11 | // return false 12 | // } 13 | 14 | // proxyCfg := endpoint.ExtraConfig[proxy.Namespace].(map[string]interface{}) 15 | // if proxyCfg["sequential"] == false { 16 | // return false 17 | // } 18 | 19 | // for i, backend := range endpoint.Backend { 20 | // if backend.Method != http.MethodGet && (i+1) != len(endpoint.Backend) { 21 | // return false 22 | // } 23 | // } 24 | 25 | return true 26 | } 27 | -------------------------------------------------------------------------------- /router/helper_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package router 4 | 5 | // func TestIsValidSequentialEndpoint_ok(t *testing.T) { 6 | 7 | // endpoint := &config.EndpointConfig{ 8 | // Endpoint: "/correct", 9 | // Method: "PUT", 10 | // Backend: []*config.Backend{ 11 | // { 12 | // Method: "GET", 13 | // }, 14 | // { 15 | // Method: "PUT", 16 | // }, 17 | // }, 18 | // ExtraConfig: map[string]interface{}{ 19 | // proxy.Namespace: map[string]interface{}{ 20 | // "sequential": true, 21 | // }, 22 | // }, 23 | // } 24 | 25 | // success := IsValidSequentialEndpoint(endpoint) 26 | 27 | // if !success { 28 | // t.Error("Endpoint expected valid but receive invalid") 29 | // } 30 | // } 31 | 32 | // func TestIsValidSequentialEndpoint_wrong_config_not_given(t *testing.T) { 33 | 34 | // endpoint := &config.EndpointConfig{ 35 | // Endpoint: "/correct", 36 | // Method: "PUT", 37 | // Backend: []*config.Backend{ 38 | // { 39 | // Method: "GET", 40 | // }, 41 | // { 42 | // Method: "PUT", 43 | // }, 44 | // }, 45 | // ExtraConfig: map[string]interface{}{}, 46 | // } 47 | 48 | // success := IsValidSequentialEndpoint(endpoint) 49 | 50 | // if success { 51 | // t.Error("Endpoint expected invalid but receive valid") 52 | // } 53 | // } 54 | 55 | // func TestIsValidSequentialEndpoint_wrong_config_set_false(t *testing.T) { 56 | 57 | // endpoint := &config.EndpointConfig{ 58 | // Endpoint: "/correct", 59 | // Method: "PUT", 60 | // Backend: []*config.Backend{ 61 | // { 62 | // Method: "GET", 63 | // }, 64 | // { 65 | // Method: "PUT", 66 | // }, 67 | // }, 68 | // ExtraConfig: map[string]interface{}{ 69 | // proxy.Namespace: map[string]interface{}{ 70 | // "sequential": false, 71 | // }, 72 | // }} 73 | 74 | // success := IsValidSequentialEndpoint(endpoint) 75 | 76 | // if success { 77 | // t.Error("Endpoint expected invalid but receive valid") 78 | // } 79 | // } 80 | 81 | // func TestIsValidSequentialEndpoint_wrong_order(t *testing.T) { 82 | 83 | // endpoint := &config.EndpointConfig{ 84 | // Endpoint: "/correct", 85 | // Method: "PUT", 86 | // Backend: []*config.Backend{ 87 | // { 88 | // Method: "PUT", 89 | // }, 90 | // { 91 | // Method: "GET", 92 | // }, 93 | // }, 94 | // ExtraConfig: map[string]interface{}{ 95 | // proxy.Namespace: map[string]interface{}{ 96 | // "sequential": true, 97 | // }, 98 | // }, 99 | // } 100 | 101 | // success := IsValidSequentialEndpoint(endpoint) 102 | 103 | // if success { 104 | // t.Error("Endpoint expected invalid but receive valid") 105 | // } 106 | // } 107 | 108 | // func TestIsValidSequentialEndpoint_wrong_all_non_get(t *testing.T) { 109 | 110 | // endpoint := &config.EndpointConfig{ 111 | // Endpoint: "/correct", 112 | // Method: "PUT", 113 | // Backend: []*config.Backend{ 114 | // { 115 | // Method: "POST", 116 | // }, 117 | // { 118 | // Method: "PUT", 119 | // }, 120 | // }, 121 | // ExtraConfig: map[string]interface{}{ 122 | // proxy.Namespace: map[string]interface{}{ 123 | // "sequential": true, 124 | // }, 125 | // }, 126 | // } 127 | 128 | // success := IsValidSequentialEndpoint(endpoint) 129 | 130 | // if success { 131 | // t.Error("Endpoint expected invalid but receive valid") 132 | // } 133 | // } 134 | -------------------------------------------------------------------------------- /router/httptreemux/router.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | Package httptreemux provides some basic implementations for building routers based on dimfeld/httptreemux 5 | */ 6 | package httptreemux 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/dimfeld/httptreemux/v5" 12 | "github.com/luraproject/lura/v2/logging" 13 | "github.com/luraproject/lura/v2/proxy" 14 | "github.com/luraproject/lura/v2/router" 15 | "github.com/luraproject/lura/v2/router/mux" 16 | "github.com/luraproject/lura/v2/transport/http/server" 17 | "golang.org/x/text/cases" 18 | "golang.org/x/text/language" 19 | ) 20 | 21 | // DefaultFactory returns a net/http mux router factory with the injected proxy factory and logger 22 | func DefaultFactory(pf proxy.Factory, logger logging.Logger) router.Factory { 23 | return mux.NewFactory(DefaultConfig(pf, logger)) 24 | } 25 | 26 | // DefaultConfig returns the struct that collects the parts the router should be built from 27 | func DefaultConfig(pf proxy.Factory, logger logging.Logger) mux.Config { 28 | return mux.Config{ 29 | Engine: NewEngine(httptreemux.NewContextMux()), 30 | Middlewares: []mux.HandlerMiddleware{}, 31 | HandlerFactory: mux.CustomEndpointHandler(mux.NewRequestBuilder(ParamsExtractor)), 32 | ProxyFactory: pf, 33 | Logger: logger, 34 | DebugPattern: "/__debug/{params}", 35 | RunServer: server.RunServer, 36 | } 37 | } 38 | 39 | func ParamsExtractor(r *http.Request) map[string]string { 40 | params := map[string]string{} 41 | title := cases.Title(language.Und) 42 | for key, value := range httptreemux.ContextParams(r.Context()) { 43 | params[title.String(key)] = value 44 | } 45 | return params 46 | } 47 | 48 | func NewEngine(m *httptreemux.ContextMux) Engine { 49 | return Engine{m} 50 | } 51 | 52 | type Engine struct { 53 | r *httptreemux.ContextMux 54 | } 55 | 56 | // Handle implements the mux.Engine interface from the lura router package 57 | func (g Engine) Handle(pattern, method string, handler http.Handler) { 58 | g.r.Handle(method, pattern, handler.(http.HandlerFunc)) 59 | } 60 | 61 | // ServeHTTP implements the http:Handler interface from the stdlib 62 | func (g Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { 63 | g.r.ServeHTTP(mux.NewHTTPErrorInterceptor(w), r) 64 | } 65 | -------------------------------------------------------------------------------- /router/mux/debug.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package mux 4 | 5 | import ( 6 | "encoding/json" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/luraproject/lura/v2/logging" 11 | ) 12 | 13 | // DebugHandler creates a dummy handler function, useful for quick integration tests 14 | func DebugHandler(logger logging.Logger) http.HandlerFunc { 15 | logPrefixSecondary := "[ENDPOINT /__debug/*]" 16 | return func(w http.ResponseWriter, r *http.Request) { 17 | logger.Debug(logPrefixSecondary, "Method:", r.Method) 18 | logger.Debug(logPrefixSecondary, "URL:", r.RequestURI) 19 | logger.Debug(logPrefixSecondary, "Query:", r.URL.Query()) 20 | // logger.Debug(logPrefixSecondary, "Params:", c.Params) 21 | logger.Debug(logPrefixSecondary, "Headers:", r.Header) 22 | body, _ := io.ReadAll(r.Body) 23 | r.Body.Close() 24 | logger.Debug(logPrefixSecondary, "Body:", string(body)) 25 | 26 | js, _ := json.Marshal(map[string]string{"message": "pong"}) 27 | 28 | w.Header().Set("Content-Type", "application/json") 29 | w.Write(js) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /router/mux/debug_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package mux 4 | 5 | import ( 6 | "bytes" 7 | "io" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/luraproject/lura/v2/logging" 13 | ) 14 | 15 | func TestDebugHandler(t *testing.T) { 16 | buff := bytes.NewBuffer(make([]byte, 1024)) 17 | logger, err := logging.NewLogger("ERROR", buff, "pref") 18 | if err != nil { 19 | t.Error("building the logger:", err.Error()) 20 | return 21 | } 22 | 23 | handler := DebugHandler(logger) 24 | 25 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8089/_mux_debug?b=1", io.NopCloser(&bytes.Buffer{})) 26 | req.Header.Set("Content-Type", "application/json") 27 | w := httptest.NewRecorder() 28 | 29 | handler.ServeHTTP(w, req) 30 | 31 | body, ioerr := io.ReadAll(w.Result().Body) 32 | if ioerr != nil { 33 | t.Error("reading a response:", err.Error()) 34 | return 35 | } 36 | w.Result().Body.Close() 37 | 38 | expectedBody := "{\"message\":\"pong\"}" 39 | 40 | content := string(body) 41 | if w.Result().Header.Get("Cache-Control") != "" { 42 | t.Error("Cache-Control error:", w.Result().Header.Get("Cache-Control")) 43 | } 44 | if w.Result().Header.Get("Content-Type") != "application/json" { 45 | t.Error("Content-Type error:", w.Result().Header.Get("Content-Type")) 46 | } 47 | if w.Result().Header.Get("X-Krakend") != "" { 48 | t.Error("X-Krakend error:", w.Result().Header.Get("X-Krakend")) 49 | } 50 | if w.Result().StatusCode != http.StatusOK { 51 | t.Error("Unexpected status code:", w.Result().StatusCode) 52 | } 53 | if content != expectedBody { 54 | t.Error("Unexpected body:", content, "expected:", expectedBody) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /router/mux/echo.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package mux 4 | 5 | import ( 6 | "encoding/json" 7 | "io" 8 | "net/http" 9 | ) 10 | 11 | type echoResponse struct { 12 | Uri string `json:"req_uri"` 13 | UriDetails map[string]string `json:"req_uri_details"` 14 | Method string `json:"req_method"` 15 | Querystring map[string][]string `json:"req_querystring"` 16 | Body string `json:"req_body"` 17 | Headers map[string][]string `json:"req_headers"` 18 | } 19 | 20 | // EchoHandler creates a dummy handler function, useful for quick integration tests 21 | func EchoHandler() http.HandlerFunc { 22 | return func(w http.ResponseWriter, r *http.Request) { 23 | var body string 24 | if r.Body != nil { 25 | tmp, _ := io.ReadAll(r.Body) 26 | r.Body.Close() 27 | body = string(tmp) 28 | } 29 | resp, err := json.Marshal(echoResponse{ 30 | Uri: r.RequestURI, 31 | UriDetails: map[string]string{ 32 | "user": r.URL.User.String(), 33 | "host": r.Host, 34 | "path": r.URL.Path, 35 | "query": r.URL.Query().Encode(), 36 | "fragment": r.URL.Fragment, 37 | }, 38 | Method: r.Method, 39 | Querystring: r.URL.Query(), 40 | Body: body, 41 | Headers: r.Header, 42 | }) 43 | if err != nil { 44 | w.WriteHeader(http.StatusInternalServerError) 45 | return 46 | } 47 | 48 | w.Header().Set("Content-Type", "application/json") 49 | w.Write(resp) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /router/mux/echo_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package mux 4 | 5 | import ( 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestEchoHandlerNew(t *testing.T) { 14 | reqBody := `{"message":"some body to send"}` 15 | expectedRespBody := `{"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"]}}` 16 | expectedRespNoBody := `{"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"]}}` 17 | expectedRespString := `{"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"]}}` 18 | e := EchoHandler() 19 | 20 | for _, tc := range []struct { 21 | name string 22 | body io.Reader 23 | resp string 24 | }{ 25 | { 26 | name: "json body", 27 | body: strings.NewReader(reqBody), 28 | resp: expectedRespBody, 29 | }, 30 | { 31 | name: "no body", 32 | body: http.NoBody, 33 | resp: expectedRespNoBody, 34 | }, 35 | { 36 | name: "string body", 37 | body: strings.NewReader("Hello lura"), 38 | resp: expectedRespString, 39 | }, 40 | } { 41 | t.Run(tc.name, func(t *testing.T) { 42 | echoRunTestRequest(t, e, tc.body, tc.resp) 43 | }) 44 | } 45 | 46 | } 47 | 48 | func echoRunTestRequest(t *testing.T, e http.HandlerFunc, body io.Reader, expected string) { 49 | req := httptest.NewRequest("GET", "http://127.0.0.1:8088/_gin_endpoint/a?b=1", body) 50 | req.Header.Set("Content-Type", "application/json") 51 | 52 | w := httptest.NewRecorder() 53 | 54 | e.ServeHTTP(w, req) 55 | 56 | respBody, ioerr := io.ReadAll(w.Result().Body) 57 | if ioerr != nil { 58 | t.Error("reading a response:", ioerr.Error()) 59 | return 60 | } 61 | w.Result().Body.Close() 62 | 63 | content := string(respBody) 64 | if w.Result().Header.Get("Cache-Control") != "" { 65 | t.Error("Cache-Control error:", w.Result().Header.Get("Cache-Control")) 66 | } 67 | if w.Result().Header.Get("Content-Type") != "application/json" { 68 | t.Error("Content-Type error:", w.Result().Header.Get("Content-Type")) 69 | } 70 | if w.Result().Header.Get("X-Krakend") != "" { 71 | t.Error("X-Krakend error:", w.Result().Header.Get("X-Krakend")) 72 | } 73 | if w.Result().StatusCode != http.StatusOK { 74 | t.Error("Unexpected status code:", w.Result().StatusCode) 75 | } 76 | if content != expected { 77 | t.Error("Unexpected body:", content, "expected:", expected) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /router/mux/endpoint_benchmark_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package mux 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | "time" 14 | 15 | "github.com/luraproject/lura/v2/config" 16 | "github.com/luraproject/lura/v2/proxy" 17 | ) 18 | 19 | func BenchmarkEndpointHandler_ko(b *testing.B) { 20 | p := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 21 | return nil, fmt.Errorf("This is %s", "a dummy error") 22 | } 23 | endpoint := &config.EndpointConfig{ 24 | Timeout: time.Second, 25 | CacheTTL: 6 * time.Hour, 26 | QueryString: []string{"b"}, 27 | } 28 | 29 | router := http.NewServeMux() 30 | router.Handle("/_gin_endpoint/", EndpointHandler(endpoint, p)) 31 | 32 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8080/_gin_endpoint/a?b=1", http.NoBody) 33 | req.Header.Set("Content-Type", "application/json") 34 | 35 | b.ReportAllocs() 36 | for i := 0; i < b.N; i++ { 37 | w := httptest.NewRecorder() 38 | router.ServeHTTP(w, req) 39 | } 40 | } 41 | 42 | func BenchmarkEndpointHandler_ok(b *testing.B) { 43 | pResp := proxy.Response{ 44 | Data: map[string]interface{}{}, 45 | Io: io.NopCloser(&bytes.Buffer{}), 46 | IsComplete: true, 47 | Metadata: proxy.Metadata{}, 48 | } 49 | p := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 50 | return &pResp, nil 51 | } 52 | endpoint := &config.EndpointConfig{ 53 | Timeout: time.Second, 54 | CacheTTL: 6 * time.Hour, 55 | QueryString: []string{"b"}, 56 | } 57 | 58 | router := http.NewServeMux() 59 | router.Handle("/_gin_endpoint/", EndpointHandler(endpoint, p)) 60 | 61 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8080/_gin_endpoint/a?b=1", http.NoBody) 62 | req.Header.Set("Content-Type", "application/json") 63 | 64 | b.ReportAllocs() 65 | for i := 0; i < b.N; i++ { 66 | w := httptest.NewRecorder() 67 | router.ServeHTTP(w, req) 68 | } 69 | } 70 | 71 | func BenchmarkEndpointHandler_ko_Parallel(b *testing.B) { 72 | p := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 73 | return nil, fmt.Errorf("This is %s", "a dummy error") 74 | } 75 | endpoint := &config.EndpointConfig{ 76 | Timeout: time.Second, 77 | CacheTTL: 6 * time.Hour, 78 | QueryString: []string{"b"}, 79 | } 80 | 81 | router := http.NewServeMux() 82 | router.Handle("/_gin_endpoint/", EndpointHandler(endpoint, p)) 83 | 84 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8080/_gin_endpoint/a?b=1", http.NoBody) 85 | req.Header.Set("Content-Type", "application/json") 86 | 87 | b.ReportAllocs() 88 | b.RunParallel(func(pb *testing.PB) { 89 | for pb.Next() { 90 | w := httptest.NewRecorder() 91 | router.ServeHTTP(w, req) 92 | } 93 | }) 94 | } 95 | 96 | func BenchmarkEndpointHandler_ok_Parallel(b *testing.B) { 97 | pResp := proxy.Response{ 98 | Data: map[string]interface{}{}, 99 | Io: io.NopCloser(&bytes.Buffer{}), 100 | IsComplete: true, 101 | Metadata: proxy.Metadata{}, 102 | } 103 | p := func(_ context.Context, _ *proxy.Request) (*proxy.Response, error) { 104 | return &pResp, nil 105 | } 106 | endpoint := &config.EndpointConfig{ 107 | Timeout: time.Second, 108 | CacheTTL: 6 * time.Hour, 109 | QueryString: []string{"b"}, 110 | } 111 | 112 | router := http.NewServeMux() 113 | router.Handle("/_gin_endpoint/", EndpointHandler(endpoint, p)) 114 | 115 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8080/_gin_endpoint/a?b=1", http.NoBody) 116 | req.Header.Set("Content-Type", "application/json") 117 | 118 | b.ReportAllocs() 119 | b.RunParallel(func(pb *testing.PB) { 120 | for pb.Next() { 121 | w := httptest.NewRecorder() 122 | router.ServeHTTP(w, req) 123 | } 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /router/mux/engine.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package mux 4 | 5 | import ( 6 | "net/http" 7 | "sync" 8 | 9 | "github.com/luraproject/lura/v2/transport/http/server" 10 | ) 11 | 12 | // Engine defines the minimun required interface for the mux compatible engine 13 | type Engine interface { 14 | http.Handler 15 | Handle(pattern, method string, handler http.Handler) 16 | } 17 | 18 | // BasicEngine is a slightly customized http.ServeMux router 19 | type BasicEngine struct { 20 | handler *http.ServeMux 21 | dict map[string]map[string]http.HandlerFunc 22 | } 23 | 24 | // NewHTTPErrorInterceptor returns a HTTPErrorInterceptor over the injected response writer 25 | func NewHTTPErrorInterceptor(w http.ResponseWriter) *HTTPErrorInterceptor { 26 | return &HTTPErrorInterceptor{w, new(sync.Once)} 27 | } 28 | 29 | // HTTPErrorInterceptor is a reposnse writer that adds a header signaling incomplete response in case of 30 | // seeing a status code not equal to 200 31 | type HTTPErrorInterceptor struct { 32 | http.ResponseWriter 33 | once *sync.Once 34 | } 35 | 36 | // WriteHeader records the status code and adds a header signaling incomplete responses 37 | func (i *HTTPErrorInterceptor) WriteHeader(code int) { 38 | i.once.Do(func() { 39 | if code != http.StatusOK { 40 | i.ResponseWriter.Header().Set(server.CompleteResponseHeaderName, server.HeaderIncompleteResponseValue) 41 | } 42 | }) 43 | i.ResponseWriter.WriteHeader(code) 44 | } 45 | 46 | // DefaultEngine returns a new engine using BasicEngine 47 | func DefaultEngine() *BasicEngine { 48 | return &BasicEngine{ 49 | handler: http.NewServeMux(), 50 | dict: map[string]map[string]http.HandlerFunc{}, 51 | } 52 | } 53 | 54 | // Handle registers a handler at a given url pattern and http method 55 | func (e *BasicEngine) Handle(pattern, method string, handler http.Handler) { 56 | if _, ok := e.dict[pattern]; !ok { 57 | e.dict[pattern] = map[string]http.HandlerFunc{} 58 | e.handler.Handle(pattern, e.registrableHandler(pattern)) 59 | } 60 | e.dict[pattern][method] = handler.ServeHTTP 61 | } 62 | 63 | // ServeHTTP adds a error interceptor and delegates the request dispatching to the 64 | // internal request multiplexer. 65 | func (e *BasicEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) { 66 | e.handler.ServeHTTP(NewHTTPErrorInterceptor(w), r) 67 | } 68 | 69 | func (e *BasicEngine) registrableHandler(pattern string) http.Handler { 70 | return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 71 | if handler, ok := e.dict[pattern][req.Method]; ok { 72 | handler(rw, req) 73 | return 74 | } 75 | 76 | rw.Header().Set(server.CompleteResponseHeaderName, server.HeaderIncompleteResponseValue) 77 | http.Error(rw, "", http.StatusMethodNotAllowed) 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /router/mux/engine_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package mux 4 | 5 | import ( 6 | "bytes" 7 | "io" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | ) 12 | 13 | func TestEngine(t *testing.T) { 14 | e := DefaultEngine() 15 | 16 | for _, method := range []string{"PUT", "POST", "DELETE"} { 17 | e.Handle("/", method, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 18 | http.Error(rw, "hi there!", http.StatusTeapot) 19 | })) 20 | } 21 | 22 | for _, tc := range []struct { 23 | method string 24 | status int 25 | }{ 26 | {status: http.StatusTeapot, method: "PUT"}, 27 | {status: http.StatusTeapot, method: "POST"}, 28 | {status: http.StatusTeapot, method: "DELETE"}, 29 | {status: http.StatusMethodNotAllowed, method: "GET"}, 30 | } { 31 | req, _ := 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{})) 32 | 33 | w := httptest.NewRecorder() 34 | e.ServeHTTP(w, req) 35 | 36 | if sc := w.Result().StatusCode; tc.status != sc { 37 | t.Error("unexpected status code:", sc) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /router/mux/render.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package mux 4 | 5 | import ( 6 | "encoding/json" 7 | "io" 8 | "net/http" 9 | "sync" 10 | 11 | "github.com/luraproject/lura/v2/config" 12 | "github.com/luraproject/lura/v2/encoding" 13 | "github.com/luraproject/lura/v2/proxy" 14 | ) 15 | 16 | // Render defines the signature of the functions to be use for the final response 17 | // encoding and rendering 18 | type Render func(http.ResponseWriter, *proxy.Response) 19 | 20 | // NEGOTIATE defines the value of the OutputEncoding for the negotiated render 21 | const NEGOTIATE = "negotiate" 22 | 23 | var ( 24 | mutex = &sync.RWMutex{} 25 | renderRegister = map[string]Render{ 26 | encoding.STRING: stringRender, 27 | encoding.JSON: jsonRender, 28 | encoding.NOOP: noopRender, 29 | "json-collection": jsonCollectionRender, 30 | } 31 | ) 32 | 33 | // RegisterRender allows clients to register their custom renders 34 | func RegisterRender(name string, r Render) { 35 | mutex.Lock() 36 | renderRegister[name] = r 37 | mutex.Unlock() 38 | } 39 | 40 | func getRender(cfg *config.EndpointConfig) Render { 41 | fallback := jsonRender 42 | if len(cfg.Backend) == 1 { 43 | fallback = getWithFallback(cfg.Backend[0].Encoding, fallback) 44 | } 45 | 46 | if cfg.OutputEncoding == "" { 47 | return fallback 48 | } 49 | 50 | return getWithFallback(cfg.OutputEncoding, fallback) 51 | } 52 | 53 | func getWithFallback(key string, fallback Render) Render { 54 | mutex.RLock() 55 | r, ok := renderRegister[key] 56 | mutex.RUnlock() 57 | if !ok { 58 | return fallback 59 | } 60 | return r 61 | } 62 | 63 | var ( 64 | emptyResponse = []byte("{}") 65 | emptyCollection = []byte("[]") 66 | ) 67 | 68 | func jsonRender(w http.ResponseWriter, response *proxy.Response) { 69 | w.Header().Set("Content-Type", "application/json") 70 | if response == nil { 71 | w.Write(emptyResponse) 72 | return 73 | } 74 | 75 | js, err := json.Marshal(response.Data) 76 | if err != nil { 77 | http.Error(w, err.Error(), http.StatusInternalServerError) 78 | return 79 | } 80 | w.Write(js) 81 | } 82 | 83 | func jsonCollectionRender(w http.ResponseWriter, response *proxy.Response) { 84 | w.Header().Set("Content-Type", "application/json") 85 | if response == nil { 86 | w.Write(emptyCollection) 87 | return 88 | } 89 | col, ok := response.Data["collection"] 90 | if !ok { 91 | w.Write(emptyCollection) 92 | return 93 | } 94 | 95 | js, err := json.Marshal(col) 96 | if err != nil { 97 | http.Error(w, err.Error(), http.StatusInternalServerError) 98 | return 99 | } 100 | w.Write(js) 101 | } 102 | 103 | func stringRender(w http.ResponseWriter, response *proxy.Response) { 104 | w.Header().Set("Content-Type", "text/plain") 105 | if response == nil { 106 | w.Write([]byte{}) 107 | return 108 | } 109 | d, ok := response.Data["content"] 110 | if !ok { 111 | w.Write([]byte{}) 112 | return 113 | } 114 | msg, ok := d.(string) 115 | if !ok { 116 | w.Write([]byte{}) 117 | return 118 | } 119 | w.Write([]byte(msg)) 120 | } 121 | 122 | func noopRender(w http.ResponseWriter, response *proxy.Response) { 123 | if response == nil { 124 | http.Error(w, "", http.StatusInternalServerError) 125 | return 126 | } 127 | 128 | for k, vs := range response.Metadata.Headers { 129 | for _, v := range vs { 130 | w.Header().Add(k, v) 131 | } 132 | } 133 | if response.Metadata.StatusCode != 0 { 134 | w.WriteHeader(response.Metadata.StatusCode) 135 | } 136 | 137 | if response.Io == nil { 138 | return 139 | } 140 | io.Copy(w, response.Io) 141 | } 142 | -------------------------------------------------------------------------------- /router/negroni/router.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | Package negroni provides some basic implementations for building routers based on urfave/negroni 5 | */ 6 | package negroni 7 | 8 | import ( 9 | "net/http" 10 | 11 | gorilla "github.com/gorilla/mux" 12 | "github.com/urfave/negroni/v2" 13 | 14 | "github.com/luraproject/lura/v2/logging" 15 | "github.com/luraproject/lura/v2/proxy" 16 | "github.com/luraproject/lura/v2/router" 17 | luragorilla "github.com/luraproject/lura/v2/router/gorilla" 18 | "github.com/luraproject/lura/v2/router/mux" 19 | ) 20 | 21 | // DefaultFactory returns a net/http mux router factory with the injected proxy factory and logger 22 | func DefaultFactory(pf proxy.Factory, logger logging.Logger, middlewares []negroni.Handler) router.Factory { 23 | return mux.NewFactory(DefaultConfig(pf, logger, middlewares)) 24 | } 25 | 26 | // DefaultConfig returns the struct that collects the parts the router should be builded from 27 | func DefaultConfig(pf proxy.Factory, logger logging.Logger, middlewares []negroni.Handler) mux.Config { 28 | return DefaultConfigWithRouter(pf, logger, NewGorillaRouter(), middlewares) 29 | } 30 | 31 | // DefaultConfigWithRouter returns the struct that collects the parts the router should be builded from with the 32 | // injected gorilla mux router 33 | func DefaultConfigWithRouter(pf proxy.Factory, logger logging.Logger, muxEngine *gorilla.Router, middlewares []negroni.Handler) mux.Config { 34 | cfg := luragorilla.DefaultConfig(pf, logger) 35 | cfg.Engine = newNegroniEngine(muxEngine, middlewares...) 36 | return cfg 37 | } 38 | 39 | // NewGorillaRouter is a wrapper over the default gorilla router builder 40 | func NewGorillaRouter() *gorilla.Router { 41 | return gorilla.NewRouter() 42 | } 43 | 44 | func newNegroniEngine(muxEngine *gorilla.Router, middlewares ...negroni.Handler) negroniEngine { 45 | negroniRouter := negroni.Classic() 46 | for _, m := range middlewares { 47 | negroniRouter.Use(m) 48 | } 49 | 50 | negroniRouter.UseHandler(muxEngine) 51 | 52 | return negroniEngine{muxEngine, negroniRouter} 53 | } 54 | 55 | type negroniEngine struct { 56 | r *gorilla.Router 57 | n *negroni.Negroni 58 | } 59 | 60 | // Handle implements the mux.Engine interface from the lura router package 61 | func (e negroniEngine) Handle(pattern, method string, handler http.Handler) { 62 | e.r.Handle(pattern, handler).Methods(method) 63 | } 64 | 65 | // ServeHTTP implements the http:Handler interface from the stdlib 66 | func (e negroniEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) { 67 | e.n.ServeHTTP(mux.NewHTTPErrorInterceptor(w), r) 68 | } 69 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | Package router defines some interfaces and common helpers for router adapters 5 | */ 6 | package router 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/luraproject/lura/v2/config" 12 | ) 13 | 14 | // Router sets up the public layer exposed to the users 15 | type Router interface { 16 | Run(config.ServiceConfig) 17 | } 18 | 19 | // RouterFunc type is an adapter to allow the use of ordinary functions as routers. 20 | // If f is a function with the appropriate signature, RouterFunc(f) is a Router that calls f. 21 | type RouterFunc func(config.ServiceConfig) 22 | 23 | // Run implements the Router interface 24 | func (f RouterFunc) Run(cfg config.ServiceConfig) { f(cfg) } 25 | 26 | // Factory creates new routers 27 | type Factory interface { 28 | New() Router 29 | NewWithContext(context.Context) Router 30 | } 31 | -------------------------------------------------------------------------------- /sd/loadbalancing.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package sd 4 | 5 | import ( 6 | "errors" 7 | "runtime" 8 | "sync/atomic" 9 | 10 | "github.com/valyala/fastrand" 11 | ) 12 | 13 | // Balancer applies a balancing stategy in order to select the backend host to be used 14 | type Balancer interface { 15 | Host() (string, error) 16 | } 17 | 18 | // ErrNoHosts is the error the balancer must return when there are 0 hosts ready 19 | var ErrNoHosts = errors.New("no hosts available") 20 | 21 | // NewBalancer returns the best perfomant balancer depending on the number of available processors. 22 | // If GOMAXPROCS = 1, it returns a round robin LB due there is no contention over the atomic counter. 23 | // If GOMAXPROCS > 1, it returns a pseudo random LB optimized for scaling over the number of CPUs. 24 | func NewBalancer(subscriber Subscriber) Balancer { 25 | if p := runtime.GOMAXPROCS(-1); p == 1 { 26 | return NewRoundRobinLB(subscriber) 27 | } 28 | return NewRandomLB(subscriber) 29 | } 30 | 31 | // NewRoundRobinLB returns a new balancer using a round robin strategy and starting at a random 32 | // position in the set of hosts. 33 | func NewRoundRobinLB(subscriber Subscriber) Balancer { 34 | s, ok := subscriber.(FixedSubscriber) 35 | start := uint64(0) 36 | if ok { 37 | if l := len(s); l == 1 { 38 | return nopBalancer(s[0]) 39 | } else if l > 1 { 40 | start = uint64(fastrand.Uint32n(uint32(l))) 41 | } 42 | } 43 | return &roundRobinLB{ 44 | balancer: balancer{subscriber: subscriber}, 45 | counter: start, 46 | } 47 | } 48 | 49 | type roundRobinLB struct { 50 | balancer 51 | counter uint64 52 | } 53 | 54 | // Host implements the balancer interface 55 | func (r *roundRobinLB) Host() (string, error) { 56 | hosts, err := r.hosts() 57 | if err != nil { 58 | return "", err 59 | } 60 | offset := (atomic.AddUint64(&r.counter, 1) - 1) % uint64(len(hosts)) 61 | return hosts[offset], nil 62 | } 63 | 64 | // NewRandomLB returns a new balancer using a fastrand pseudorandom generator 65 | func NewRandomLB(subscriber Subscriber) Balancer { 66 | if s, ok := subscriber.(FixedSubscriber); ok && len(s) == 1 { 67 | return nopBalancer(s[0]) 68 | } 69 | return &randomLB{ 70 | balancer: balancer{subscriber: subscriber}, 71 | rand: fastrand.Uint32n, 72 | } 73 | } 74 | 75 | type randomLB struct { 76 | balancer 77 | rand func(uint32) uint32 78 | } 79 | 80 | // Host implements the balancer interface 81 | func (r *randomLB) Host() (string, error) { 82 | hosts, err := r.hosts() 83 | if err != nil { 84 | return "", err 85 | } 86 | return hosts[int(r.rand(uint32(len(hosts))))], nil 87 | } 88 | 89 | type balancer struct { 90 | subscriber Subscriber 91 | } 92 | 93 | func (b *balancer) hosts() ([]string, error) { 94 | hs, err := b.subscriber.Hosts() 95 | if err != nil { 96 | return hs, err 97 | } 98 | if len(hs) <= 0 { 99 | return hs, ErrNoHosts 100 | } 101 | return hs, nil 102 | } 103 | 104 | type nopBalancer string 105 | 106 | func (b nopBalancer) Host() (string, error) { return string(b), nil } 107 | -------------------------------------------------------------------------------- /sd/loadbalancing_benchmark_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package sd 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | ) 9 | 10 | var balancerTestsCases = [][]string{ 11 | {"a"}, 12 | {"a", "b", "c"}, 13 | {"a", "b", "c", "e", "f"}, 14 | } 15 | 16 | func BenchmarkLB(b *testing.B) { 17 | for _, tc := range []struct { 18 | name string 19 | f func([]string) Balancer 20 | }{ 21 | {name: "round_robin", f: func(hs []string) Balancer { return NewRoundRobinLB(FixedSubscriber(hs)) }}, 22 | {name: "random", f: func(hs []string) Balancer { return NewRandomLB(FixedSubscriber(hs)) }}, 23 | } { 24 | for _, testCase := range balancerTestsCases { 25 | b.Run(fmt.Sprintf("%s/%d", tc.name, len(testCase)), func(b *testing.B) { 26 | balancer := tc.f(testCase) 27 | b.ResetTimer() 28 | for i := 0; i < b.N; i++ { 29 | balancer.Host() 30 | } 31 | }) 32 | } 33 | } 34 | } 35 | 36 | func BenchmarkLB_parallel(b *testing.B) { 37 | for _, tc := range []struct { 38 | name string 39 | f func([]string) Balancer 40 | }{ 41 | {name: "round_robin", f: func(hs []string) Balancer { return NewRoundRobinLB(FixedSubscriber(hs)) }}, 42 | {name: "random", f: func(hs []string) Balancer { return NewRandomLB(FixedSubscriber(hs)) }}, 43 | } { 44 | for _, testCase := range balancerTestsCases { 45 | b.Run(fmt.Sprintf("%s/%d", tc.name, len(testCase)), func(b *testing.B) { 46 | balancer := tc.f(testCase) 47 | b.ResetTimer() 48 | b.RunParallel(func(pb *testing.PB) { 49 | for pb.Next() { 50 | balancer.Host() 51 | } 52 | }) 53 | }) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sd/register.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package sd 4 | 5 | import ( 6 | "github.com/luraproject/lura/v2/register" 7 | ) 8 | 9 | // GetRegister returns the package register 10 | func GetRegister() *Register { 11 | return subscriberFactories 12 | } 13 | 14 | type untypedRegister interface { 15 | Register(name string, v interface{}) 16 | Get(name string) (interface{}, bool) 17 | } 18 | 19 | // Register is a SD register, mapping different SD subscriber factories 20 | // to their respective name, so they can be accessed by name 21 | type Register struct { 22 | data untypedRegister 23 | } 24 | 25 | func initRegister() *Register { 26 | return &Register{register.NewUntyped()} 27 | } 28 | 29 | // Register adds the SubscriberFactory to the internal register under the given 30 | // name 31 | func (r *Register) Register(name string, sf SubscriberFactory) error { 32 | r.data.Register(name, sf) 33 | return nil 34 | } 35 | 36 | // Get returns the SubscriberFactory stored under the given name. It falls back to 37 | // a FixedSubscriberFactory if there is no factory with that name 38 | func (r *Register) Get(name string) SubscriberFactory { 39 | tmp, ok := r.data.Get(name) 40 | if !ok { 41 | return FixedSubscriberFactory 42 | } 43 | sf, ok := tmp.(SubscriberFactory) 44 | if !ok { 45 | return FixedSubscriberFactory 46 | } 47 | return sf 48 | } 49 | 50 | var subscriberFactories = initRegister() 51 | -------------------------------------------------------------------------------- /sd/register_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package sd 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/luraproject/lura/v2/config" 9 | ) 10 | 11 | func TestGetRegister_Register_ok(t *testing.T) { 12 | sf1 := func(*config.Backend) Subscriber { 13 | return SubscriberFunc(func() ([]string, error) { return []string{"one"}, nil }) 14 | } 15 | sf2 := func(*config.Backend) Subscriber { 16 | return SubscriberFunc(func() ([]string, error) { return []string{"two", "three"}, nil }) 17 | } 18 | if err := GetRegister().Register("name1", sf1); err != nil { 19 | t.Error(err) 20 | } 21 | if err := GetRegister().Register("name2", sf2); err != nil { 22 | t.Error(err) 23 | } 24 | 25 | if h, err := GetRegister().Get("name1")(&config.Backend{SD: "name1"}).Hosts(); err != nil || len(h) != 1 { 26 | t.Error("error using the sd name1") 27 | } 28 | 29 | if h, err := GetRegister().Get("name2")(&config.Backend{SD: "name2"}).Hosts(); err != nil || len(h) != 2 { 30 | t.Error("error using the sd name2") 31 | } 32 | 33 | if h, err := GetRegister().Get("name2")(&config.Backend{SD: "name2"}).Hosts(); err != nil || len(h) != 2 { 34 | t.Error("error using the sd name2") 35 | } 36 | 37 | subscriberFactories = initRegister() 38 | } 39 | 40 | func TestGetRegister_Get_unknown(t *testing.T) { 41 | if h, err := GetRegister().Get("name")(&config.Backend{Host: []string{"name"}}).Hosts(); err != nil || len(h) != 1 { 42 | t.Error("error using the default sd") 43 | } 44 | } 45 | 46 | func TestGetRegister_Get_errored(t *testing.T) { 47 | subscriberFactories.data.Register("errored", true) 48 | if h, err := GetRegister().Get("errored")(&config.Backend{SD: "errored", Host: []string{"name"}}).Hosts(); err != nil || len(h) != 1 { 49 | t.Error("error using the default sd") 50 | } 51 | subscriberFactories = initRegister() 52 | } 53 | -------------------------------------------------------------------------------- /sd/subscriber.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | Package sd defines some interfaces and implementations for service discovery 5 | */ 6 | package sd 7 | 8 | import ( 9 | "math/rand" 10 | 11 | "github.com/luraproject/lura/v2/config" 12 | ) 13 | 14 | // Subscriber keeps the set of backend hosts up to date 15 | type Subscriber interface { 16 | Hosts() ([]string, error) 17 | } 18 | 19 | // SubscriberFunc type is an adapter to allow the use of ordinary functions as subscribers. 20 | // If f is a function with the appropriate signature, SubscriberFunc(f) is a Subscriber that calls f. 21 | type SubscriberFunc func() ([]string, error) 22 | 23 | // Hosts implements the Subscriber interface by executing the wrapped function 24 | func (f SubscriberFunc) Hosts() ([]string, error) { return f() } 25 | 26 | // FixedSubscriber has a constant set of backend hosts and they never get updated 27 | type FixedSubscriber []string 28 | 29 | // Hosts implements the subscriber interface 30 | func (s FixedSubscriber) Hosts() ([]string, error) { return s, nil } 31 | 32 | // SubscriberFactory builds subscribers with the received config 33 | type SubscriberFactory func(*config.Backend) Subscriber 34 | 35 | // FixedSubscriberFactory builds a FixedSubscriber with the received config 36 | func FixedSubscriberFactory(cfg *config.Backend) Subscriber { 37 | return FixedSubscriber(cfg.Host) 38 | } 39 | 40 | // NewRandomFixedSubscriber randomizes a list of hosts and builds a FixedSubscriber with it 41 | func NewRandomFixedSubscriber(hosts []string) FixedSubscriber { 42 | res := make([]string, len(hosts)) 43 | j := 0 44 | for _, i := range rand.Perm(len(hosts)) { 45 | res[j] = hosts[i] 46 | j++ 47 | } 48 | return FixedSubscriber(res) 49 | } 50 | -------------------------------------------------------------------------------- /test/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | Package test contains the integration tests for the KrakenD framework 5 | */ 6 | package test 7 | -------------------------------------------------------------------------------- /transport/http/client/executor.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | /* 4 | Package client provides some http helpers to create http clients and executors 5 | */ 6 | package client 7 | 8 | import ( 9 | "context" 10 | "net/http" 11 | ) 12 | 13 | // HTTPRequestExecutor defines the interface of the request executor for the HTTP transport protocol 14 | type HTTPRequestExecutor func(ctx context.Context, req *http.Request) (*http.Response, error) 15 | 16 | // DefaultHTTPRequestExecutor creates a HTTPRequestExecutor with the received HTTPClientFactory 17 | func DefaultHTTPRequestExecutor(clientFactory HTTPClientFactory) HTTPRequestExecutor { 18 | return func(ctx context.Context, req *http.Request) (*http.Response, error) { 19 | return clientFactory(ctx).Do(req.WithContext(ctx)) 20 | } 21 | } 22 | 23 | // HTTPClientFactory creates http clients based with the received context 24 | type HTTPClientFactory func(ctx context.Context) *http.Client 25 | 26 | // NewHTTPClient just returns the http default client 27 | func NewHTTPClient(_ context.Context) *http.Client { return defaultHTTPClient } 28 | 29 | var defaultHTTPClient = &http.Client{} 30 | -------------------------------------------------------------------------------- /transport/http/client/executor_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package client 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | ) 14 | 15 | func TestDefaultHTTPRequestExecutor(t *testing.T) { 16 | 17 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | fmt.Fprintln(w, "Hello, client") 19 | })) 20 | defer ts.Close() 21 | 22 | re := DefaultHTTPRequestExecutor(NewHTTPClient) 23 | 24 | req, _ := http.NewRequest("GET", ts.URL, io.NopCloser(&bytes.Buffer{})) 25 | 26 | resp, err := re(context.Background(), req) 27 | 28 | if err != nil { 29 | t.Error("unexpected error:", err.Error()) 30 | } 31 | 32 | if resp.StatusCode != http.StatusOK { 33 | t.Error("unexpected status code:", resp.StatusCode) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /transport/http/client/plugin/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | //Package plugin provides plugin register interfaces for building http client plugins. 4 | // 5 | // Usage example: 6 | // 7 | // package main 8 | // 9 | // import ( 10 | // "context" 11 | // "errors" 12 | // "fmt" 13 | // "html" 14 | // "net/http" 15 | // ) 16 | // 17 | // // ClientRegisterer is the symbol the plugin loader will try to load. It must implement the RegisterClient interface 18 | // var ClientRegisterer = registerer("lura-example") 19 | // 20 | // type registerer string 21 | // 22 | // func (r registerer) RegisterClients(f func( 23 | // name string, 24 | // handler func(context.Context, map[string]interface{}) (http.Handler, error), 25 | // )) { 26 | // f(string(r), r.registerClients) 27 | // } 28 | // 29 | // func (r registerer) registerClients(ctx context.Context, extra map[string]interface{}) (http.Handler, error) { 30 | // // check the passed configuration and initialize the plugin 31 | // name, ok := extra["name"].(string) 32 | // if !ok { 33 | // return nil, errors.New("wrong config") 34 | // } 35 | // if name != string(r) { 36 | // return nil, fmt.Errorf("unknown register %s", name) 37 | // } 38 | // // return the actual handler wrapping or your custom logic so it can be used as a replacement for the default http client 39 | // return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 40 | // fmt.Fprintf(w, "Hello, %q", html.EscapeString(req.URL.Path)) 41 | // }), nil 42 | // } 43 | // 44 | // func init() { 45 | // fmt.Println("lura-example client plugin loaded!!!") 46 | // } 47 | // 48 | // func main() {} 49 | package plugin 50 | -------------------------------------------------------------------------------- /transport/http/client/plugin/executor.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package plugin 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | 11 | "github.com/luraproject/lura/v2/config" 12 | "github.com/luraproject/lura/v2/logging" 13 | "github.com/luraproject/lura/v2/transport/http/client" 14 | ) 15 | 16 | const Namespace = "github.com/devopsfaith/krakend/transport/http/client/executor" 17 | 18 | func HTTPRequestExecutor( 19 | logger logging.Logger, 20 | next func(*config.Backend) client.HTTPRequestExecutor, 21 | ) func(*config.Backend) client.HTTPRequestExecutor { 22 | return HTTPRequestExecutorWithContext(context.Background(), logger, next) 23 | } 24 | 25 | func HTTPRequestExecutorWithContext( 26 | ctx context.Context, 27 | logger logging.Logger, 28 | next func(*config.Backend) client.HTTPRequestExecutor, 29 | ) func(*config.Backend) client.HTTPRequestExecutor { 30 | return func(cfg *config.Backend) client.HTTPRequestExecutor { 31 | logPrefix := fmt.Sprintf("[BACKEND: %s %s -> %s]", cfg.ParentEndpointMethod, cfg.ParentEndpoint, cfg.URLPattern) 32 | v, ok := cfg.ExtraConfig[Namespace] 33 | if !ok { 34 | return next(cfg) 35 | } 36 | extra, ok := v.(map[string]interface{}) 37 | if !ok { 38 | logger.Debug(logPrefix, "["+Namespace+"]", "Wrong extra config type for backend") 39 | return next(cfg) 40 | } 41 | 42 | // load plugin 43 | r, ok := clientRegister.Get(Namespace) 44 | if !ok { 45 | logger.Debug(logPrefix, "No plugins registered for the module") 46 | return next(cfg) 47 | } 48 | 49 | name, ok := extra["name"].(string) 50 | if !ok { 51 | logger.Debug(logPrefix, "No name defined in the extra config for", cfg.URLPattern) 52 | return next(cfg) 53 | } 54 | 55 | rawHf, ok := r.Get(name) 56 | if !ok { 57 | logger.Debug(logPrefix, "No plugin registered as", name) 58 | return next(cfg) 59 | } 60 | 61 | hf, ok := rawHf.(func(context.Context, map[string]interface{}) (http.Handler, error)) 62 | if !ok { 63 | logger.Warning(logPrefix, "Wrong plugin handler type:", name) 64 | return next(cfg) 65 | } 66 | 67 | handler, err := hf(ctx, extra) 68 | if err != nil { 69 | logger.Warning(logPrefix, "Error getting the plugin handler:", err.Error()) 70 | return next(cfg) 71 | } 72 | 73 | logger.Debug(logPrefix, "Injecting plugin", name) 74 | return func(ctx context.Context, req *http.Request) (*http.Response, error) { 75 | w := httptest.NewRecorder() 76 | handler.ServeHTTP(w, req.WithContext(ctx)) 77 | return w.Result(), nil 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /transport/http/client/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package plugin 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "net/http" 9 | "plugin" 10 | "strings" 11 | 12 | "github.com/luraproject/lura/v2/logging" 13 | luraplugin "github.com/luraproject/lura/v2/plugin" 14 | "github.com/luraproject/lura/v2/register" 15 | ) 16 | 17 | var clientRegister = register.New() 18 | 19 | func RegisterClient( 20 | name string, 21 | handler func(context.Context, map[string]interface{}) (http.Handler, error), 22 | ) { 23 | clientRegister.Register(Namespace, name, handler) 24 | } 25 | 26 | type Registerer interface { 27 | RegisterClients(func( 28 | name string, 29 | handler func(context.Context, map[string]interface{}) (http.Handler, error), 30 | )) 31 | } 32 | 33 | type LoggerRegisterer interface { 34 | RegisterLogger(interface{}) 35 | } 36 | 37 | type RegisterClientFunc func( 38 | name string, 39 | handler func(context.Context, map[string]interface{}) (http.Handler, error), 40 | ) 41 | 42 | func Load(path, pattern string, rcf RegisterClientFunc) (int, error) { 43 | return LoadWithLogger(path, pattern, rcf, nil) 44 | } 45 | 46 | func LoadWithLogger(path, pattern string, rcf RegisterClientFunc, logger logging.Logger) (int, error) { 47 | plugins, err := luraplugin.Scan(path, pattern) 48 | if err != nil { 49 | return 0, err 50 | } 51 | return load(plugins, rcf, logger) 52 | } 53 | 54 | func load(plugins []string, rcf RegisterClientFunc, logger logging.Logger) (int, error) { 55 | var errors []error 56 | 57 | loadedPlugins := 0 58 | for k, pluginName := range plugins { 59 | if err := open(pluginName, rcf, logger); err != nil { 60 | errors = append(errors, fmt.Errorf("plugin #%d (%s): %s", k, pluginName, err.Error())) 61 | continue 62 | } 63 | loadedPlugins++ 64 | } 65 | 66 | if len(errors) > 0 { 67 | return loadedPlugins, loaderError{errors: errors} 68 | } 69 | return loadedPlugins, nil 70 | } 71 | 72 | func open(pluginName string, rcf RegisterClientFunc, logger logging.Logger) (err error) { 73 | defer func() { 74 | if r := recover(); r != nil { 75 | var ok bool 76 | err, ok = r.(error) 77 | if !ok { 78 | err = fmt.Errorf("%v", r) 79 | } 80 | } 81 | }() 82 | 83 | var p Plugin 84 | p, err = pluginOpener(pluginName) 85 | if err != nil { 86 | return 87 | } 88 | var r interface{} 89 | r, err = p.Lookup("ClientRegisterer") 90 | if err != nil { 91 | return 92 | } 93 | registerer, ok := r.(Registerer) 94 | if !ok { 95 | return fmt.Errorf("http-request-executor plugin loader: unknown type") 96 | } 97 | 98 | if logger != nil { 99 | if lr, ok := r.(LoggerRegisterer); ok { 100 | lr.RegisterLogger(logger) 101 | } 102 | } 103 | 104 | registerer.RegisterClients(rcf) 105 | return 106 | } 107 | 108 | // Plugin is the interface of the loaded plugins 109 | type Plugin interface { 110 | Lookup(name string) (plugin.Symbol, error) 111 | } 112 | 113 | // pluginOpener keeps the plugin open function in a var for easy testing 114 | var pluginOpener = defaultPluginOpener 115 | 116 | func defaultPluginOpener(name string) (Plugin, error) { 117 | return plugin.Open(name) 118 | } 119 | 120 | type loaderError struct { 121 | errors []error 122 | } 123 | 124 | // Error implements the error interface 125 | func (l loaderError) Error() string { 126 | msgs := make([]string, len(l.errors)) 127 | for i, err := range l.errors { 128 | msgs[i] = err.Error() 129 | } 130 | return fmt.Sprintf("plugin loader found %d error(s): \n%s", len(msgs), strings.Join(msgs, "\n")) 131 | } 132 | 133 | func (l loaderError) Len() int { 134 | return len(l.errors) 135 | } 136 | 137 | func (l loaderError) Errs() []error { 138 | return l.errors 139 | } 140 | -------------------------------------------------------------------------------- /transport/http/client/plugin/plugin_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration || !race 2 | // +build integration !race 3 | 4 | // SPDX-License-Identifier: Apache-2.0 5 | 6 | package plugin 7 | 8 | import ( 9 | "bytes" 10 | "context" 11 | "fmt" 12 | "io" 13 | "net/http" 14 | "testing" 15 | 16 | "github.com/luraproject/lura/v2/config" 17 | "github.com/luraproject/lura/v2/logging" 18 | "github.com/luraproject/lura/v2/transport/http/client" 19 | ) 20 | 21 | func TestLoadWithLogger(t *testing.T) { 22 | buff := new(bytes.Buffer) 23 | l, _ := logging.NewLogger("DEBUG", buff, "") 24 | total, err := LoadWithLogger("./tests", ".so", RegisterClient, l) 25 | if err != nil { 26 | t.Error(err.Error()) 27 | t.Fail() 28 | } 29 | if total != 1 { 30 | t.Errorf("unexpected number of loaded plugins!. have %d, want 1", total) 31 | } 32 | 33 | hre := HTTPRequestExecutor(l, func(_ *config.Backend) client.HTTPRequestExecutor { 34 | t.Error("this factory should not been called") 35 | t.Fail() 36 | return nil 37 | }) 38 | 39 | h := hre(&config.Backend{ 40 | ExtraConfig: map[string]interface{}{ 41 | Namespace: map[string]interface{}{ 42 | "name": "krakend-client-example", 43 | }, 44 | }, 45 | }) 46 | 47 | req, _ := http.NewRequest("GET", "http://some.example.tld/path", http.NoBody) 48 | resp, err := h(context.Background(), req) 49 | if err != nil { 50 | t.Errorf("unexpected error: %s", err.Error()) 51 | return 52 | } 53 | 54 | b, err := io.ReadAll(resp.Body) 55 | if err != nil { 56 | t.Error(err) 57 | return 58 | } 59 | resp.Body.Close() 60 | 61 | if string(b) != "Hello, \"/path\"" { 62 | t.Errorf("unexpected response body: %s", string(b)) 63 | } 64 | 65 | fmt.Println(buff.String()) 66 | } 67 | -------------------------------------------------------------------------------- /transport/http/client/plugin/tests/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "html" 10 | "net/http" 11 | ) 12 | 13 | // ClientRegisterer is the symbol the plugin loader will try to load. It must implement the RegisterClient interface 14 | var ClientRegisterer = registerer("krakend-client-example") 15 | 16 | type registerer string 17 | 18 | var logger Logger = nil 19 | 20 | func (registerer) RegisterLogger(v interface{}) { 21 | l, ok := v.(Logger) 22 | if !ok { 23 | return 24 | } 25 | logger = l 26 | logger.Debug(fmt.Sprintf("[PLUGIN: %s] Logger loaded", ClientRegisterer)) 27 | } 28 | 29 | func (r registerer) RegisterClients(f func( 30 | name string, 31 | handler func(context.Context, map[string]interface{}) (http.Handler, error), 32 | )) { 33 | f(string(r), r.registerClients) 34 | } 35 | 36 | func (r registerer) registerClients(_ context.Context, extra map[string]interface{}) (http.Handler, error) { 37 | // check the passed configuration and initialize the plugin 38 | name, ok := extra["name"].(string) 39 | if !ok { 40 | return nil, errors.New("wrong config") 41 | } 42 | if name != string(r) { 43 | return nil, fmt.Errorf("unknown register %s", name) 44 | } 45 | 46 | if logger == nil { 47 | // return the actual handler wrapping or your custom logic so it can be used as a replacement for the default http client 48 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 49 | fmt.Fprintf(w, "Hello, %q", html.EscapeString(req.URL.Path)) 50 | }), nil 51 | } 52 | 53 | // return the actual handler wrapping or your custom logic so it can be used as a replacement for the default http client 54 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 55 | fmt.Fprintf(w, "Hello, %q", html.EscapeString(req.URL.Path)) 56 | logger.Debug("request:", html.EscapeString(req.URL.Path)) 57 | }), nil 58 | } 59 | 60 | func main() {} 61 | 62 | type Logger interface { 63 | Debug(v ...interface{}) 64 | Info(v ...interface{}) 65 | Warning(v ...interface{}) 66 | Error(v ...interface{}) 67 | Critical(v ...interface{}) 68 | Fatal(v ...interface{}) 69 | } 70 | -------------------------------------------------------------------------------- /transport/http/server/plugin/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | //Package plugin provides plugin register interfaces for building http handler plugins. 4 | // 5 | // Usage example: 6 | // 7 | // package main 8 | // 9 | // import ( 10 | // "context" 11 | // "errors" 12 | // "fmt" 13 | // "html" 14 | // "net/http" 15 | // ) 16 | // 17 | // // HandlerRegisterer is the symbol the plugin loader will try to load. It must implement the Registerer interface 18 | // var HandlerRegisterer = registerer("lura-example") 19 | // 20 | // type registerer string 21 | // 22 | // func (r registerer) RegisterHandlers(f func( 23 | // name string, 24 | // handler func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error), 25 | // )) { 26 | // f(string(r), r.registerHandlers) 27 | // } 28 | // 29 | // func (r registerer) registerHandlers(ctx context.Context, extra map[string]interface{}, _ http.Handler) (http.Handler, error) { 30 | // // check the passed configuration and initialize the plugin 31 | // name, ok := extra["name"].(string) 32 | // if !ok { 33 | // return nil, errors.New("wrong config") 34 | // } 35 | // if name != string(r) { 36 | // return nil, fmt.Errorf("unknown register %s", name) 37 | // } 38 | // // return the actual handler wrapping or your custom logic so it can be used as a replacement for the default http handler 39 | // return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 40 | // fmt.Fprintf(w, "Hello, %q", html.EscapeString(req.URL.Path)) 41 | // }), nil 42 | // } 43 | // 44 | // func init() { 45 | // fmt.Println("lura-example handler plugin loaded!!!") 46 | // } 47 | // 48 | // func main() {} 49 | package plugin 50 | -------------------------------------------------------------------------------- /transport/http/server/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package plugin 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "net/http" 9 | "plugin" 10 | "strings" 11 | 12 | "github.com/luraproject/lura/v2/logging" 13 | luraplugin "github.com/luraproject/lura/v2/plugin" 14 | "github.com/luraproject/lura/v2/register" 15 | ) 16 | 17 | var serverRegister = register.New() 18 | 19 | func RegisterHandler( 20 | name string, 21 | handler func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error), 22 | ) { 23 | serverRegister.Register(Namespace, name, handler) 24 | } 25 | 26 | type Registerer interface { 27 | RegisterHandlers(func( 28 | name string, 29 | handler func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error), 30 | )) 31 | } 32 | 33 | type LoggerRegisterer interface { 34 | RegisterLogger(interface{}) 35 | } 36 | 37 | type RegisterHandlerFunc func( 38 | name string, 39 | handler func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error), 40 | ) 41 | 42 | func Load(path, pattern string, rcf RegisterHandlerFunc) (int, error) { 43 | return LoadWithLogger(path, pattern, rcf, nil) 44 | } 45 | 46 | func LoadWithLogger(path, pattern string, rcf RegisterHandlerFunc, logger logging.Logger) (int, error) { 47 | plugins, err := luraplugin.Scan(path, pattern) 48 | if err != nil { 49 | return 0, err 50 | } 51 | return load(plugins, rcf, logger) 52 | } 53 | 54 | func load(plugins []string, rcf RegisterHandlerFunc, logger logging.Logger) (int, error) { 55 | var errors []error 56 | 57 | loadedPlugins := 0 58 | for k, pluginName := range plugins { 59 | if err := open(pluginName, rcf, logger); err != nil { 60 | errors = append(errors, fmt.Errorf("plugin #%d (%s): %s", k, pluginName, err.Error())) 61 | continue 62 | } 63 | loadedPlugins++ 64 | } 65 | 66 | if len(errors) > 0 { 67 | return loadedPlugins, loaderError{errors: errors} 68 | } 69 | return loadedPlugins, nil 70 | } 71 | 72 | func open(pluginName string, rcf RegisterHandlerFunc, logger logging.Logger) (err error) { 73 | defer func() { 74 | if r := recover(); r != nil { 75 | var ok bool 76 | err, ok = r.(error) 77 | if !ok { 78 | err = fmt.Errorf("%v", r) 79 | } 80 | } 81 | }() 82 | 83 | var p Plugin 84 | p, err = pluginOpener(pluginName) 85 | if err != nil { 86 | return 87 | } 88 | var r interface{} 89 | r, err = p.Lookup("HandlerRegisterer") 90 | if err != nil { 91 | return 92 | } 93 | registerer, ok := r.(Registerer) 94 | if !ok { 95 | return fmt.Errorf("http-server-handler plugin loader: unknown type") 96 | } 97 | 98 | if logger != nil { 99 | if lr, ok := r.(LoggerRegisterer); ok { 100 | lr.RegisterLogger(logger) 101 | } 102 | } 103 | 104 | registerer.RegisterHandlers(rcf) 105 | return 106 | } 107 | 108 | // Plugin is the interface of the loaded plugins 109 | type Plugin interface { 110 | Lookup(name string) (plugin.Symbol, error) 111 | } 112 | 113 | // pluginOpener keeps the plugin open function in a var for easy testing 114 | var pluginOpener = defaultPluginOpener 115 | 116 | func defaultPluginOpener(name string) (Plugin, error) { 117 | return plugin.Open(name) 118 | } 119 | 120 | type loaderError struct { 121 | errors []error 122 | } 123 | 124 | // Error implements the error interface 125 | func (l loaderError) Error() string { 126 | msgs := make([]string, len(l.errors)) 127 | for i, err := range l.errors { 128 | msgs[i] = err.Error() 129 | } 130 | return fmt.Sprintf("plugin loader found %d error(s): \n%s", len(msgs), strings.Join(msgs, "\n")) 131 | } 132 | 133 | func (l loaderError) Len() int { 134 | return len(l.errors) 135 | } 136 | 137 | func (l loaderError) Errs() []error { 138 | return l.errors 139 | } 140 | -------------------------------------------------------------------------------- /transport/http/server/plugin/plugin_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration || !race 2 | // +build integration !race 3 | 4 | // SPDX-License-Identifier: Apache-2.0 5 | 6 | package plugin 7 | 8 | import ( 9 | "bytes" 10 | "context" 11 | "fmt" 12 | "io" 13 | "net/http" 14 | "net/http/httptest" 15 | "testing" 16 | 17 | "github.com/luraproject/lura/v2/config" 18 | "github.com/luraproject/lura/v2/logging" 19 | ) 20 | 21 | func TestLoadWithLogger(t *testing.T) { 22 | buff := new(bytes.Buffer) 23 | l, _ := logging.NewLogger("DEBUG", buff, "") 24 | total, err := LoadWithLogger("./tests", ".so", RegisterHandler, l) 25 | if err != nil { 26 | t.Error(err.Error()) 27 | t.Fail() 28 | } 29 | if total != 1 { 30 | t.Errorf("unexpected number of loaded plugins!. have %d, want 1", total) 31 | } 32 | 33 | var handler http.Handler 34 | 35 | hre := New(l, func(_ context.Context, _ config.ServiceConfig, h http.Handler) error { 36 | handler = h 37 | return nil 38 | }) 39 | 40 | if err := hre( 41 | context.Background(), 42 | config.ServiceConfig{ 43 | ExtraConfig: map[string]interface{}{ 44 | Namespace: map[string]interface{}{ 45 | "name": "krakend-server-example", 46 | }, 47 | }, 48 | }, 49 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 50 | t.Error("this handler should not been called") 51 | }), 52 | ); err != nil { 53 | t.Error(err) 54 | return 55 | } 56 | 57 | req, _ := http.NewRequest("GET", "http://some.example.tld/path", http.NoBody) 58 | w := httptest.NewRecorder() 59 | handler.ServeHTTP(w, req) 60 | 61 | resp := w.Result() 62 | 63 | if resp.StatusCode != 200 { 64 | t.Errorf("unexpected status code: %d", resp.StatusCode) 65 | return 66 | } 67 | 68 | b, err := io.ReadAll(resp.Body) 69 | if err != nil { 70 | t.Error(err) 71 | return 72 | } 73 | resp.Body.Close() 74 | 75 | if string(b) != "Hello, \"/path\"" { 76 | t.Errorf("unexpected response body: %s", string(b)) 77 | } 78 | 79 | fmt.Println(buff.String()) 80 | } 81 | -------------------------------------------------------------------------------- /transport/http/server/plugin/server.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package plugin 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | 9 | "github.com/luraproject/lura/v2/config" 10 | "github.com/luraproject/lura/v2/logging" 11 | ) 12 | 13 | const Namespace = "github_com/devopsfaith/krakend/transport/http/server/handler" 14 | const logPrefix = "[PLUGIN: Server]" 15 | 16 | type RunServer func(context.Context, config.ServiceConfig, http.Handler) error 17 | 18 | func New(logger logging.Logger, next RunServer) RunServer { 19 | return func(ctx context.Context, cfg config.ServiceConfig, handler http.Handler) error { 20 | v, ok := cfg.ExtraConfig[Namespace] 21 | 22 | if !ok { 23 | return next(ctx, cfg, handler) 24 | } 25 | extra, ok := v.(map[string]interface{}) 26 | if !ok { 27 | logger.Debug(logPrefix, "Wrong extra_config type") 28 | return next(ctx, cfg, handler) 29 | } 30 | 31 | // load plugin(s) 32 | r, ok := serverRegister.Get(Namespace) 33 | if !ok { 34 | logger.Debug(logPrefix, "No plugins registered for the module") 35 | return next(ctx, cfg, handler) 36 | } 37 | 38 | name, nameOk := extra["name"].(string) 39 | fifoRaw, fifoOk := extra["name"].([]interface{}) 40 | if !nameOk && !fifoOk { 41 | logger.Debug(logPrefix, "No plugins required in the extra config") 42 | return next(ctx, cfg, handler) 43 | } 44 | var fifo []string 45 | 46 | if !fifoOk { 47 | fifo = []string{name} 48 | } else { 49 | for _, x := range fifoRaw { 50 | if v, ok := x.(string); ok { 51 | fifo = append(fifo, v) 52 | } 53 | } 54 | } 55 | 56 | for _, name := range fifo { 57 | rawHf, ok := r.Get(name) 58 | if !ok { 59 | logger.Error(logPrefix, "No plugin registered as", name) 60 | continue 61 | } 62 | 63 | hf, ok := rawHf.(func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error)) 64 | if !ok { 65 | logger.Error(logPrefix, "Wrong plugin handler type:", name) 66 | continue 67 | } 68 | 69 | handlerWrapper, err := hf(ctx, extra, handler) 70 | if err != nil { 71 | logger.Error(logPrefix, "Error getting the plugin handler:", err.Error()) 72 | continue 73 | } 74 | 75 | logger.Info(logPrefix, "Injecting plugin", name) 76 | handler = handlerWrapper 77 | } 78 | return next(ctx, cfg, handler) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /transport/http/server/plugin/tests/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "html" 9 | "net/http" 10 | ) 11 | 12 | // HandlerRegisterer is the symbol the plugin loader will try to load. It must implement the Registerer interface 13 | var HandlerRegisterer = registerer("krakend-server-example") 14 | 15 | type registerer string 16 | 17 | var logger Logger = nil 18 | 19 | func (registerer) RegisterLogger(v interface{}) { 20 | l, ok := v.(Logger) 21 | if !ok { 22 | return 23 | } 24 | logger = l 25 | logger.Debug(fmt.Sprintf("[PLUGIN: %s] Logger loaded", HandlerRegisterer)) 26 | } 27 | 28 | func (r registerer) RegisterHandlers(f func( 29 | name string, 30 | handler func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error), 31 | )) { 32 | f(string(r), r.registerHandlers) 33 | } 34 | 35 | func (registerer) registerHandlers(_ context.Context, _ map[string]interface{}, _ http.Handler) (http.Handler, error) { 36 | // check the passed configuration and initialize the plugin 37 | // possible config example: 38 | /* 39 | "extra_config":{ 40 | "plugin/http-server":{ 41 | "name":["krakend-server-example"], 42 | "krakend-server-example":{ 43 | "A":"foo", 44 | "B":42 45 | } 46 | } 47 | } 48 | */ 49 | 50 | if logger == nil { 51 | // return the actual handler wrapping or your custom logic so it can be used as a replacement for the default http handler 52 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 53 | fmt.Fprintf(w, "Hello, %q", html.EscapeString(req.URL.Path)) 54 | }), nil 55 | } 56 | 57 | // return the actual handler wrapping or your custom logic so it can be used as a replacement for the default http handler 58 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 59 | fmt.Fprintf(w, "Hello, %q", html.EscapeString(req.URL.Path)) 60 | logger.Debug("request:", html.EscapeString(req.URL.Path)) 61 | }), nil 62 | } 63 | 64 | func main() {} 65 | 66 | type Logger interface { 67 | Debug(v ...interface{}) 68 | Info(v ...interface{}) 69 | Warning(v ...interface{}) 70 | Error(v ...interface{}) 71 | Critical(v ...interface{}) 72 | Fatal(v ...interface{}) 73 | } 74 | --------------------------------------------------------------------------------