├── .adr-dir ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── issue-bug.yml │ └── issue-enhance.yml ├── PULL_REQUEST_TEMPLATE.md └── TEMPLATE-README.md ├── .gitignore ├── .travis.yml ├── CODEOWNERS ├── LICENSE ├── NOTICE ├── README.md ├── accesslog ├── access_log_suite_test.go ├── dropsonde_logsender.go ├── dropsonde_logsender_test.go ├── fakes │ ├── accesslogger.go │ └── eventemitter.go ├── file_and_loggregator_access_logger.go ├── file_and_loggregator_access_logger_test.go ├── schema │ ├── access_log_record.go │ ├── access_log_record_test.go │ ├── fakes │ │ ├── access_log_record..go.backup │ │ └── access_log_record.go │ └── schema_suite_test.go └── syslog │ ├── syslog.go │ └── syslog_test.go ├── bin └── test.bash ├── common ├── common_suite_test.go ├── component.go ├── component_test.go ├── health │ ├── health.go │ ├── health_suite_test.go │ ├── health_test.go │ ├── varz.go │ └── varz_test.go ├── http │ ├── basic_auth.go │ ├── basic_auth_test.go │ ├── fakes │ │ └── fake_response_writer.go │ ├── headers.go │ ├── headers_test.go │ └── http_suite_test.go ├── schema │ ├── duration.go │ ├── duration_test.go │ ├── log_counter.go │ ├── log_counter_test.go │ └── schema_suite_test.go ├── secure │ ├── crypto.go │ ├── crypto_test.go │ ├── fakes │ │ └── fake_crypto.go │ └── secure_suite_test.go ├── spec │ └── component.go └── uuid │ ├── uuid.go │ ├── uuid_suite_test.go │ └── uuid_test.go ├── config ├── config.go ├── config_suite_test.go └── config_test.go ├── docs ├── 01-development-guide.md ├── 02-nats-configurations.md ├── 03-features.md ├── 04-observability.md └── images │ ├── architecture.svg │ ├── basic_request.svg │ ├── ifrit.svg │ ├── indepth_request.svg │ └── routeservice.svg ├── errorwriter ├── error_writer.go ├── error_writer_test.go └── errorwriter_suite_test.go ├── example_config └── example.yml ├── fakes ├── round_tripper.go └── route_services_server.go ├── handlers ├── access_log.go ├── access_log_test.go ├── clientcert.go ├── clientcert_test.go ├── handlers_suite_test.go ├── healthcheck.go ├── healthcheck_test.go ├── helpers.go ├── hop_by_hop.go ├── hop_by_hop_test.go ├── http_rewrite.go ├── http_rewrite_test.go ├── httplatencyprometheus.go ├── httplatencyprometheus_test.go ├── httpstartstop.go ├── httpstartstop_test.go ├── lookup.go ├── lookup_test.go ├── max_request_size.go ├── max_request_size_test.go ├── paniccheck.go ├── paniccheck_test.go ├── protocolcheck.go ├── protocolcheck_test.go ├── proxy_healthcheck.go ├── proxy_healthcheck_test.go ├── proxy_picker.go ├── proxy_picker_test.go ├── proxywriter.go ├── proxywriter_test.go ├── query_param.go ├── query_param_test.go ├── reporter.go ├── reporter_test.go ├── request_id.go ├── request_id_test.go ├── requestinfo.go ├── requestinfo_test.go ├── routeservice.go ├── routeservice_test.go ├── w3c.go ├── w3c_test.go ├── w3c_traceparent.go ├── w3c_traceparent_test.go ├── w3c_tracestate.go ├── w3c_tracestate_test.go ├── x_forwarded_proto.go ├── x_forwarded_proto_test.go ├── zipkin.go └── zipkin_test.go ├── integration ├── access_log_test.go ├── backend_keepalive_test.go ├── common_integration_test.go ├── error_writer_test.go ├── gdpr_test.go ├── header_test.go ├── init_test.go ├── large_request_test.go ├── large_upload_test.go ├── main_test.go ├── nats_test.go ├── redirect_test.go ├── retry_test.go ├── route_services_test.go ├── test_utils_test.go ├── tls_to_backends_test.go ├── w3c_tracing_test.go ├── web_socket_test.go ├── x_forwarded_proto_integration_test.go └── xfcc_integration_test.go ├── logger ├── lager_adapter.go ├── lager_adapter_test.go ├── logger.go ├── logger_suite_test.go ├── logger_test.go └── logger_test_init.go ├── main.go ├── mbus ├── client.go ├── fakes │ └── fake_client.go ├── mbus_suite_test.go ├── registry_message_test.go ├── subscriber.go └── subscriber_test.go ├── metrics ├── compositereporter.go ├── compositereporter_test.go ├── fakes │ ├── fake_metricreporter.go │ ├── fake_subscriber.go │ ├── fake_value_chainer.go │ ├── fake_varzreporter.go │ ├── metricbatcher.go │ └── metricsender.go ├── metrics_suite_test.go ├── metricsreporter.go ├── metricsreporter_test.go └── monitor │ ├── fd_monitor.go │ ├── fd_monitor_test.go │ ├── nats_monitor.go │ ├── nats_monitor_test.go │ ├── uptime_monitor.go │ ├── uptime_monitor_test.go │ └── uptime_suite_test.go ├── metrics_prometheus ├── metrics.go ├── metrics_suite_test.go └── metrics_test.go ├── proxy ├── backend_tls_test.go ├── buffer_pool.go ├── fails │ ├── basic_classifiers.go │ ├── basic_classifiers_test.go │ ├── classifier.go │ ├── classifier_group.go │ ├── classifier_group_test.go │ ├── fails_suite_test.go │ └── fakes │ │ └── fake_classifier.go ├── modifyresponse.go ├── modifyresponse_unit_test.go ├── proxy.go ├── proxy_suite_test.go ├── proxy_test.go ├── proxy_unit_test.go ├── round_tripper │ ├── dropsonde_round_tripper.go │ ├── error_handler.go │ ├── error_handler_test.go │ ├── fakes │ │ ├── fake_error_handler.go │ │ └── fake_proxy_round_tripper.go │ ├── proxy_round_tripper.go │ ├── proxy_round_tripper_test.go │ ├── round_tripper_suite_test.go │ └── trace.go ├── route_service_test.go ├── session_affinity_test.go ├── test_helpers │ └── helper.go └── utils │ ├── headerrewriter.go │ ├── headerrewriter_test.go │ ├── logging.go │ ├── logging_test.go │ ├── response_reader.go │ ├── response_reader_test.go │ ├── responsewriter.go │ ├── responsewriter_test.go │ ├── tls_config.go │ └── utils_suite_test.go ├── registry ├── container │ ├── container_suite_test.go │ ├── trie.go │ └── trie_test.go ├── fakes │ └── fake_registry.go ├── registry.go ├── registry_benchmark_test.go ├── registry_suite_test.go └── registry_test.go ├── route ├── endpoint_iterator_benchmark_test.go ├── fakes │ └── fake_endpoint_iterator.go ├── leastconnection.go ├── leastconnection_test.go ├── pool.go ├── pool_benchmark_test.go ├── pool_test.go ├── roundrobin.go ├── roundrobin_test.go ├── route_suite_test.go ├── uris.go └── uris_test.go ├── route_fetcher ├── route_fetcher.go ├── route_fetcher_suite_test.go └── route_fetcher_test.go ├── router ├── health_listener.go ├── health_listener_test.go ├── helper_test.go ├── route_service_server.go ├── route_service_server_test.go ├── router.go ├── router_drain_test.go ├── router_suite_test.go ├── router_test.go ├── routes_listener.go └── routes_listener_test.go ├── routeservice ├── routeservice_config.go ├── routeservice_config_test.go ├── routeservice_suite_test.go ├── signature.go └── signature_test.go ├── stats ├── active_apps.go ├── active_apps_test.go ├── container │ └── heap.go ├── stats_suite_test.go ├── top_apps.go └── top_apps_test.go ├── test ├── common │ ├── app.go │ ├── network.go │ ├── nginx_app.go │ └── tcp_app.go ├── greet_app.go ├── nginx-app │ ├── nginx.conf │ └── public │ │ └── index.html ├── sticky_app.go └── websocket_app.go ├── test_util ├── failure_reporter.go ├── fake_file.go ├── fake_metron.go ├── helpers.go ├── http_conn.go ├── localhost_dns.go ├── nats_client.go ├── nats_runner.go ├── ports.go ├── rss │ ├── README.md │ ├── commands │ │ ├── generate.go │ │ └── read.go │ ├── common │ │ └── utils.go │ ├── fixtures │ │ ├── invalidkey │ │ ├── key │ │ └── otherkey │ ├── main.go │ ├── main_suite_test.go │ └── main_test.go └── test_logger.go └── varz ├── varz.go ├── varz_suite_test.go └── varz_test.go /.adr-dir: -------------------------------------------------------------------------------- 1 | docs/decisions 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: CloudFoundry slack 4 | url: https://cloudfoundry.slack.com 5 | about: For help or questions about this component, you can reach the maintainers on Slack 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug 2 | description: Report a defect, such as a bug or regression. 3 | title: "Start the title with a verb (e.g. Change header styles). Use the imperative mood in the title (e.g. Fix, not Fixed or Fixes header styles)" 4 | labels: 5 | - bug 6 | body: 7 | - type: textarea 8 | id: current 9 | attributes: 10 | label: Current behavior 11 | validations: 12 | required: true 13 | - type: markdown 14 | id: current_md 15 | attributes: 16 | value: | 17 | - Explain, in detail, what the current state of the world is 18 | - Include code snippets, log output, and analysis as necessary to explain the whole problem 19 | - Include links to logs, GitHub issues, slack conversations, etc.. to tell us where the problem came from 20 | - Steps to reproduce 21 | - type: textarea 22 | id: desired 23 | attributes: 24 | label: Desired behavior 25 | validations: 26 | required: true 27 | - type: markdown 28 | id: desired_md 29 | attributes: 30 | value: | 31 | - Describe how the problem should be fixed 32 | - Does this require a new bosh release? 33 | - Does it require configuration changes in cf-deployment? 34 | - Do we need to have a special release note? 35 | - Do we need to update repo documentation? 36 | - type: input 37 | id: version 38 | attributes: 39 | label: Affected Version 40 | description: Please enter the version 41 | validations: 42 | required: true 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-enhance.yml: -------------------------------------------------------------------------------- 1 | name: Enhance 2 | description: Propose an enhancement or new feature. 3 | title: "Start the title with a verb (e.g. Change header styles). Use the imperative mood in the title (e.g. Fix, not Fixed or Fixes header styles)" 4 | labels: 5 | - enhancement 6 | body: 7 | - type: textarea 8 | id: change 9 | attributes: 10 | label: Proposed Change 11 | validations: 12 | required: true 13 | - type: markdown 14 | id: change_md 15 | attributes: 16 | value: | 17 | Briefly explain why this feature is necessary in the following format 18 | 19 | **As a** *developer/operator/whatever* 20 | **I want** *this ability to do X* 21 | **So that** *I can do Y* 22 | 23 | - Provide details of where this request is coming from including links, GitHub Issues, etc.. 24 | - Provide details of prior work (if applicable) including links to commits, github issues, etc... 25 | - type: textarea 26 | id: acceptance 27 | attributes: 28 | label: Acceptance criteria 29 | validations: 30 | required: true 31 | - type: markdown 32 | id: acceptance_md 33 | attributes: 34 | value: | 35 | Detail the exact work that is required to accept this story in the following format 36 | 37 | **Scenario:** *describe scenario* 38 | **Given** *I have some sort of configuration* 39 | **When** *I do X* 40 | **And** *do Y* 41 | **Then** *I see the desired behavior* 42 | 43 | - type: textarea 44 | id: related 45 | attributes: 46 | label: Related links 47 | description: Please list related links for this issue 48 | placeholder: | 49 | - [ ] code.cloudfoundry.org/bbs for links 50 | - [x] cloudfoundry/rep#123 for issues/prs 51 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] Read the [Contributing document](../blob/-/.github/CONTRIBUTING.md). 2 | 3 | Summary 4 | --------------- 5 | 10 | 11 | 12 | Backward Compatibility 13 | --------------- 14 | Breaking Change? **Yes/No** 15 | 22 | -------------------------------------------------------------------------------- /.github/TEMPLATE-README.md: -------------------------------------------------------------------------------- 1 | 2 | > [!IMPORTANT] 3 | > Content in this directory is managed by the CI task `sync-dot-github-dir`. 4 | 5 | Changing templates 6 | --------------- 7 | These templates are synced from [these shared templates](https://github.com/cloudfoundry/wg-app-platform-runtime-ci/tree/main/shared/github). 8 | Each pipeline will contain a `sync-dot-github-dir-*` job for updating the content of these files. 9 | If you would like to modify these, please change them in the shared group. 10 | It's also possible to override the templates on pipeline's parent directory by introducing a custom 11 | template in `$PARENT_TEMPLATE_DIR/github/FILENAME` or `$PARENT_TEMPLATE_DIR/github/REPO_NAME/FILENAME` in CI repo 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/*.sh 2 | !bin/test.bash 3 | !bin/env 4 | !bin/go 5 | /pkg 6 | *.test 7 | *.swp 8 | /tmp 9 | *.iml 10 | .idea/ 11 | tags 12 | .DS_Store 13 | *.coverprofile 14 | gorouter 15 | test_util/rss/rss 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go_import_path: code.cloudfoundry.org/gorouter 3 | go: 4 | - 1.7 5 | - tip 6 | 7 | matrix: 8 | allow_failures: 9 | - go: tip 10 | 11 | install: 12 | - git clone --branch=develop --recurse-submodules https://github.com/cloudfoundry-incubator/routing-release.git $HOME/routing-release 13 | - export PATH=$HOME/routing-release/bin:$PATH 14 | - export GOPATH=$GOPATH:$HOME/routing-release 15 | 16 | script: 17 | - ./bin/test 18 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @cloudfoundry/wg-app-runtime-platform-networking-approvers 2 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. 2 | 3 | This project contains software that is Copyright (c) 2012-2015 Pivotal Software, Inc. 4 | 5 | This project is licensed to you under the Apache License, Version 2.0 (the "License"). 6 | 7 | You may not use this project except in compliance with the License. 8 | 9 | This project may include a number of subcomponents with separate copyright notices 10 | and license terms. Your use of these subcomponents is subject to the terms and 11 | conditions of the subcomponent's license, as noted in the LICENSE file. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gorouter 2 | 3 | > [!CAUTION] 4 | > This repository has been in-lined (using git-subtree) into routing-release. Please make any 5 | > future contributions directly to routing-release. 6 | 7 | [![Go Report 8 | Card](https://goreportcard.com/badge/code.cloudfoundry.org/gorouter)](https://goreportcard.com/report/code.cloudfoundry.org/gorouter) 9 | [![Go 10 | Reference](https://pkg.go.dev/badge/code.cloudfoundry.org/gorouter.svg)](https://pkg.go.dev/code.cloudfoundry.org/gorouter) 11 | 12 | This repository contains the source code for the Cloud Foundry L7 HTTP 13 | router. Gorouter is deployed by default with Cloud Foundry 14 | ([cf-deployment](https://github.com/cloudfoundry/cf-deployment)) which 15 | includes 16 | [routing-release](https://github.com/cloudfoundry/routing-release) as 17 | submodule. 18 | 19 | > \[!NOTE\] 20 | > 21 | > This repository should be imported as 22 | > `code.cloudfoundry.org/gorouter`. 23 | 24 | # Docs 25 | 26 | - [Development Guide](./docs/01-development-guide.md) 27 | - [NATS Configuration](./docs/02-nats-configurations.md) 28 | - [Features](./docs/03-features.md) 29 | - [Observability](./docs/04-observability.md) 30 | 31 | # Contributing 32 | 33 | See the [Contributing.md](./.github/CONTRIBUTING.md) for more 34 | information on how to contribute. 35 | 36 | # Working Group Charter 37 | 38 | This repository is maintained by [App Runtime 39 | Platform](https://github.com/cloudfoundry/community/blob/main/toc/working-groups/app-runtime-platform.md) 40 | under `Networking` area. 41 | 42 | > \[!IMPORTANT\] 43 | > 44 | > Content in this file is managed by the [CI task 45 | > `sync-readme`](https://github.com/cloudfoundry/wg-app-platform-runtime-ci/blob/main/shared/tasks/sync-readme/metadata.yml) 46 | > and is generated by CI following a convention. 47 | -------------------------------------------------------------------------------- /accesslog/access_log_suite_test.go: -------------------------------------------------------------------------------- 1 | package accesslog_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestAccessLog(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "AccessLog Suite") 13 | } 14 | -------------------------------------------------------------------------------- /accesslog/dropsonde_logsender.go: -------------------------------------------------------------------------------- 1 | package accesslog 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/cloudfoundry/dropsonde" 10 | "github.com/cloudfoundry/dropsonde/emitter" 11 | "github.com/cloudfoundry/sonde-go/events" 12 | "google.golang.org/protobuf/proto" 13 | 14 | "code.cloudfoundry.org/gorouter/accesslog/schema" 15 | "code.cloudfoundry.org/gorouter/config" 16 | log "code.cloudfoundry.org/gorouter/logger" 17 | ) 18 | 19 | type DropsondeLogSender struct { 20 | eventEmitter dropsonde.EventEmitter 21 | sourceInstance string 22 | logger *slog.Logger 23 | } 24 | 25 | func (l *DropsondeLogSender) SendAppLog(appID, message string, tags map[string]string) { 26 | if l.sourceInstance == "" || appID == "" { 27 | l.logger.Debug("dropping-loggregator-access-log", 28 | log.ErrAttr(fmt.Errorf("either no appId or source instance present")), 29 | slog.String("appID", appID), 30 | slog.String("sourceInstance", l.sourceInstance), 31 | ) 32 | 33 | return 34 | } 35 | 36 | sourceType := "RTR" 37 | messageType := events.LogMessage_OUT 38 | logMessage := &events.LogMessage{ 39 | Message: []byte(message), 40 | AppId: proto.String(appID), 41 | MessageType: &messageType, 42 | SourceType: &sourceType, 43 | SourceInstance: &l.sourceInstance, 44 | Timestamp: proto.Int64(time.Now().UnixNano()), 45 | } 46 | 47 | envelope, err := emitter.Wrap(logMessage, l.eventEmitter.Origin()) 48 | if err != nil { 49 | l.logger.Error("error-wrapping-access-log-for-emitting", log.ErrAttr(err)) 50 | return 51 | } 52 | 53 | envelope.Tags = tags 54 | 55 | if err = l.eventEmitter.EmitEnvelope(envelope); err != nil { 56 | l.logger.Error("error-emitting-access-log-to-writers", log.ErrAttr(err)) 57 | } 58 | } 59 | 60 | func NewLogSender( 61 | c *config.Config, 62 | e dropsonde.EventEmitter, 63 | logger *slog.Logger, 64 | ) schema.LogSender { 65 | var dropsondeSourceInstance string 66 | 67 | if c.Logging.LoggregatorEnabled { 68 | dropsondeSourceInstance = strconv.FormatUint(uint64(c.Index), 10) 69 | } 70 | 71 | return &DropsondeLogSender{ 72 | e, dropsondeSourceInstance, logger, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /accesslog/dropsonde_logsender_test.go: -------------------------------------------------------------------------------- 1 | package accesslog_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | "go.uber.org/zap/zapcore" 7 | "google.golang.org/protobuf/proto" 8 | 9 | "code.cloudfoundry.org/gorouter/accesslog" 10 | "code.cloudfoundry.org/gorouter/accesslog/fakes" 11 | "code.cloudfoundry.org/gorouter/accesslog/schema" 12 | "code.cloudfoundry.org/gorouter/config" 13 | "code.cloudfoundry.org/gorouter/test_util" 14 | ) 15 | 16 | //go:generate counterfeiter -o fakes/eventemitter.go github.com/cloudfoundry/dropsonde.EventEmitter 17 | 18 | var _ = Describe("DropsondeLogSender", func() { 19 | Describe("SendAppLog", func() { 20 | var ( 21 | logSender schema.LogSender 22 | conf *config.Config 23 | eventEmitter *fakes.FakeEventEmitter 24 | logger *test_util.TestLogger 25 | ) 26 | 27 | BeforeEach(func() { 28 | var err error 29 | conf, err = config.DefaultConfig() 30 | Expect(err).ToNot(HaveOccurred()) 31 | conf.Logging.LoggregatorEnabled = true 32 | eventEmitter = &fakes.FakeEventEmitter{} 33 | logger = test_util.NewTestLogger("test") 34 | 35 | logSender = accesslog.NewLogSender(conf, eventEmitter, logger.Logger) 36 | 37 | eventEmitter.OriginReturns("someOrigin") 38 | }) 39 | 40 | It("emits an envelope", func() { 41 | logSender.SendAppLog("someID", "someMessage", nil) 42 | Expect(logger.Lines(zapcore.ErrorLevel)).To(HaveLen(0)) 43 | Expect(eventEmitter.EmitEnvelopeCallCount()).To(Equal(1)) 44 | logMessage := eventEmitter.EmitEnvelopeArgsForCall(0).LogMessage 45 | Expect(logMessage.AppId).To(Equal(proto.String("someID"))) 46 | Expect(logMessage.Message).To(Equal([]byte("someMessage"))) 47 | }) 48 | 49 | It("emits an envelope with tags", func() { 50 | tags := map[string]string{ 51 | "foo": "bar", 52 | "baz": "fuz", 53 | } 54 | logSender.SendAppLog("someID", "someMessage", tags) 55 | 56 | Expect(logger.Lines(zapcore.ErrorLevel)).To(HaveLen(0)) 57 | Expect(eventEmitter.EmitEnvelopeCallCount()).To(Equal(1)) 58 | envelope := eventEmitter.EmitEnvelopeArgsForCall(0) 59 | Expect(envelope.Tags).To(Equal(map[string]string{ 60 | "foo": "bar", 61 | "baz": "fuz", 62 | })) 63 | }) 64 | 65 | Context("when app id is empty", func() { 66 | It("does not emit an envelope", func() { 67 | logSender.SendAppLog("", "someMessage", nil) 68 | 69 | Expect(eventEmitter.EmitEnvelopeCallCount()).To(Equal(0)) 70 | }) 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /accesslog/schema/fakes/access_log_record..go.backup: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/gorouter/a602bb6a624308630305f7f61689fddd21347dcb/accesslog/schema/fakes/access_log_record..go.backup -------------------------------------------------------------------------------- /accesslog/schema/fakes/access_log_record.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package fakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "code.cloudfoundry.org/gorouter/accesslog/schema" 8 | ) 9 | 10 | type FakeLogSender struct { 11 | SendAppLogStub func(string, string, map[string]string) 12 | sendAppLogMutex sync.RWMutex 13 | sendAppLogArgsForCall []struct { 14 | arg1 string 15 | arg2 string 16 | arg3 map[string]string 17 | } 18 | invocations map[string][][]interface{} 19 | invocationsMutex sync.RWMutex 20 | } 21 | 22 | func (fake *FakeLogSender) SendAppLog(arg1 string, arg2 string, arg3 map[string]string) { 23 | fake.sendAppLogMutex.Lock() 24 | fake.sendAppLogArgsForCall = append(fake.sendAppLogArgsForCall, struct { 25 | arg1 string 26 | arg2 string 27 | arg3 map[string]string 28 | }{arg1, arg2, arg3}) 29 | stub := fake.SendAppLogStub 30 | fake.recordInvocation("SendAppLog", []interface{}{arg1, arg2, arg3}) 31 | fake.sendAppLogMutex.Unlock() 32 | if stub != nil { 33 | fake.SendAppLogStub(arg1, arg2, arg3) 34 | } 35 | } 36 | 37 | func (fake *FakeLogSender) SendAppLogCallCount() int { 38 | fake.sendAppLogMutex.RLock() 39 | defer fake.sendAppLogMutex.RUnlock() 40 | return len(fake.sendAppLogArgsForCall) 41 | } 42 | 43 | func (fake *FakeLogSender) SendAppLogCalls(stub func(string, string, map[string]string)) { 44 | fake.sendAppLogMutex.Lock() 45 | defer fake.sendAppLogMutex.Unlock() 46 | fake.SendAppLogStub = stub 47 | } 48 | 49 | func (fake *FakeLogSender) SendAppLogArgsForCall(i int) (string, string, map[string]string) { 50 | fake.sendAppLogMutex.RLock() 51 | defer fake.sendAppLogMutex.RUnlock() 52 | argsForCall := fake.sendAppLogArgsForCall[i] 53 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 54 | } 55 | 56 | func (fake *FakeLogSender) Invocations() map[string][][]interface{} { 57 | fake.invocationsMutex.RLock() 58 | defer fake.invocationsMutex.RUnlock() 59 | fake.sendAppLogMutex.RLock() 60 | defer fake.sendAppLogMutex.RUnlock() 61 | copiedInvocations := map[string][][]interface{}{} 62 | for key, value := range fake.invocations { 63 | copiedInvocations[key] = value 64 | } 65 | return copiedInvocations 66 | } 67 | 68 | func (fake *FakeLogSender) recordInvocation(key string, args []interface{}) { 69 | fake.invocationsMutex.Lock() 70 | defer fake.invocationsMutex.Unlock() 71 | if fake.invocations == nil { 72 | fake.invocations = map[string][][]interface{}{} 73 | } 74 | if fake.invocations[key] == nil { 75 | fake.invocations[key] = [][]interface{}{} 76 | } 77 | fake.invocations[key] = append(fake.invocations[key], args) 78 | } 79 | 80 | var _ schema.LogSender = new(FakeLogSender) 81 | -------------------------------------------------------------------------------- /accesslog/schema/schema_suite_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSchema(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Schema Suite") 13 | } 14 | -------------------------------------------------------------------------------- /accesslog/syslog/syslog_test.go: -------------------------------------------------------------------------------- 1 | package syslog_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "code.cloudfoundry.org/gorouter/accesslog/syslog" 10 | "code.cloudfoundry.org/gorouter/test/common" 11 | 12 | . "github.com/onsi/gomega" 13 | "github.com/onsi/gomega/format" 14 | ) 15 | 16 | func init() { 17 | format.TruncatedDiff = false 18 | } 19 | 20 | func TestLogger(t *testing.T) { 21 | tests := []struct { 22 | name string 23 | network string 24 | severity syslog.Priority 25 | facility syslog.Priority 26 | appName string 27 | message string 28 | // Since the syslog message contains dynamic parts there is a bit of magic around this 29 | // variable. It has two formatting directives: the first is the hostname as a string, the 30 | // second the pid as an int. The timestamp will be cut from both the returned and the 31 | // provided output to not make this test depend on time. 32 | want string 33 | }{{ 34 | "ensure UDP syslog works and the BOM is properly set", 35 | "udp", 36 | syslog.SeverityCrit, 37 | syslog.FacilityDaemon, 38 | "vcap.gorouter", 39 | "foobar", 40 | "<26>1 1970-01-01T00:00:00Z %s vcap.gorouter %d - \ufefffoobar", 41 | }, { 42 | "ensure UDP syslog does not mangle trailing newlines", 43 | "udp", 44 | syslog.SeverityCrit, 45 | syslog.FacilityFtp, 46 | "gorouter", 47 | "foobar\n", 48 | "<90>1 1970-01-01T00:00:00Z %s gorouter %d - \ufefffoobar\n", 49 | }, { 50 | "ensure TCP syslog appends a line feed at the end", 51 | "tcp", 52 | syslog.SeverityCrit, 53 | syslog.FacilityFtp, 54 | "gorouter", 55 | "foobar", 56 | "<90>1 1970-01-01T00:00:00Z %s gorouter %d - \ufefffoobar", // line feed is stripped, but if there is none at all the log will not be returned 57 | }, { 58 | "ensure TCP syslog does not append additional line feeds at the end", 59 | "tcp", 60 | syslog.SeverityCrit, 61 | syslog.FacilityFtp, 62 | "gorouter", 63 | "foobar\n", 64 | "<90>1 1970-01-01T00:00:00Z %s gorouter %d - \ufefffoobar", 65 | }} 66 | 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | RegisterTestingT(t) 70 | done := make(chan bool) 71 | defer close(done) 72 | 73 | var ( 74 | addr string 75 | logs <-chan string 76 | ) 77 | // we only support tcp and udp 78 | switch tt.network { 79 | case "tcp": 80 | addr, logs = common.TestTcp(done) 81 | case "udp": 82 | addr, logs = common.TestUdp(done) 83 | default: 84 | t.Fatalf("invalid network: %s", tt.network) 85 | } 86 | 87 | w, err := syslog.Dial(tt.network, addr, tt.severity, tt.facility, tt.appName) 88 | Expect(err).NotTo(HaveOccurred()) 89 | defer func() { _ = w.Close() }() 90 | 91 | err = w.Log(tt.message) 92 | Expect(err).NotTo(HaveOccurred()) 93 | 94 | want := fmt.Sprintf(tt.want, must(os.Hostname), os.Getpid()) 95 | Expect(cutTimestamp(<-logs)).To(Equal(cutTimestamp(want))) 96 | }) 97 | } 98 | } 99 | 100 | func cutTimestamp(in string) string { 101 | parts := strings.SplitN(in, " ", 3) 102 | if len(parts) < 3 { 103 | return "" 104 | } 105 | return parts[0] + " 1970-01-01T00:00:00Z " + parts[2] 106 | } 107 | 108 | func must[T any, F func() (T, error)](f F) T { 109 | t, err := f() 110 | if err != nil { 111 | panic(err.Error()) 112 | } 113 | 114 | return t 115 | } 116 | -------------------------------------------------------------------------------- /bin/test.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | configure_rsyslog 7 | configure_db "${DB}" 8 | # shellcheck disable=SC2068 9 | # Double-quoting array expansion here causes ginkgo to fail 10 | go run github.com/onsi/ginkgo/v2/ginkgo ${@} 11 | -------------------------------------------------------------------------------- /common/common_suite_test.go: -------------------------------------------------------------------------------- 1 | package common_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestCommon(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Common Suite") 13 | } 14 | -------------------------------------------------------------------------------- /common/health/health.go: -------------------------------------------------------------------------------- 1 | package health 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type Status uint64 8 | 9 | const ( 10 | Initializing Status = iota 11 | Healthy 12 | Degraded 13 | ) 14 | 15 | type onDegradeCallback func() 16 | 17 | type Health struct { 18 | mu sync.RWMutex // to lock health r/w 19 | health Status 20 | 21 | OnDegrade onDegradeCallback 22 | } 23 | 24 | func (h *Health) Health() Status { 25 | h.mu.RLock() 26 | defer h.mu.RUnlock() 27 | return h.health 28 | } 29 | 30 | func (h *Health) SetHealth(s Status) { 31 | h.mu.Lock() 32 | 33 | if h.health == Degraded { 34 | h.mu.Unlock() 35 | return 36 | } 37 | 38 | h.health = s 39 | h.mu.Unlock() 40 | 41 | if h.OnDegrade != nil && s == Degraded { 42 | h.OnDegrade() 43 | } 44 | } 45 | 46 | func (h *Health) String() string { 47 | switch h.Health() { 48 | case Initializing: 49 | return "Initializing" 50 | case Healthy: 51 | return "Healthy" 52 | case Degraded: 53 | return "Degraded" 54 | default: 55 | panic("health: unknown status") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /common/health/health_suite_test.go: -------------------------------------------------------------------------------- 1 | package health_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestHealth(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Health Suite") 13 | } 14 | -------------------------------------------------------------------------------- /common/health/health_test.go: -------------------------------------------------------------------------------- 1 | package health_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | . "code.cloudfoundry.org/gorouter/common/health" 8 | ) 9 | 10 | var _ = Describe("Health", func() { 11 | var ( 12 | h *Health 13 | ) 14 | 15 | BeforeEach(func() { 16 | h = &Health{} 17 | }) 18 | 19 | Context("when is healthy", func() { 20 | It("reports healhty", func() { 21 | h.SetHealth(Healthy) 22 | 23 | Expect(h.Health()).To(Equal(Healthy)) 24 | }) 25 | 26 | It("does not degrade", func() { 27 | called := false 28 | h.OnDegrade = func() { 29 | called = true 30 | } 31 | 32 | h.SetHealth(Healthy) 33 | Expect(called).To(BeFalse(), "OnDegrade was called") 34 | }) 35 | 36 | Context("set degraded", func() { 37 | BeforeEach(func() { 38 | h.SetHealth(Healthy) 39 | }) 40 | 41 | It("updates the status", func() { 42 | h.SetHealth(Degraded) 43 | 44 | Expect(h.Health()).To(Equal(Degraded)) 45 | }) 46 | 47 | It("calls h.onDegrade callback", func() { 48 | called := false 49 | h.OnDegrade = func() { 50 | called = true 51 | } 52 | 53 | h.SetHealth(Degraded) 54 | 55 | Expect(called).To(BeTrue(), "OnDegrade wasn't called") 56 | }) 57 | }) 58 | }) 59 | 60 | Context("when is degraded", func() { 61 | calledN := 0 62 | 63 | BeforeEach(func() { 64 | calledN = 0 65 | h.OnDegrade = func() { 66 | calledN++ 67 | } 68 | 69 | h.SetHealth(Degraded) 70 | }) 71 | 72 | Context("set degraded", func() { 73 | It("does not call h.onDegrade callback", func() { 74 | h.SetHealth(Degraded) 75 | 76 | Expect(calledN).To(Equal(1), "OnDegrade was called multiple times") 77 | }) 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /common/health/varz.go: -------------------------------------------------------------------------------- 1 | package health 2 | 3 | import ( 4 | "encoding/json" 5 | "sync" 6 | 7 | "code.cloudfoundry.org/gorouter/common/schema" 8 | ) 9 | 10 | type GenericVarz struct { 11 | // These fields are from individual components 12 | Type string `json:"type"` 13 | Index uint `json:"index"` 14 | Host string `json:"host"` 15 | Credentials []string `json:"credentials"` 16 | 17 | // These fields are automatically generated 18 | UUID string `json:"uuid"` 19 | StartTime schema.Time `json:"start"` 20 | 21 | // Static common metrics 22 | NumCores int `json:"num_cores"` 23 | 24 | // Dynamic common metrics 25 | MemStat int64 `json:"mem"` 26 | Cpu float64 `json:"cpu"` 27 | 28 | Uptime schema.Duration `json:"uptime"` 29 | LogCounts *schema.LogCounter `json:"log_counts"` 30 | } 31 | 32 | type Varz struct { 33 | sync.Mutex 34 | GenericVarz 35 | UniqueVarz interface{} // Every component's unique metrics 36 | } 37 | 38 | func transform(x interface{}, y *map[string]interface{}) error { 39 | var b []byte 40 | var err error 41 | 42 | b, err = json.Marshal(x) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | err = json.Unmarshal(b, y) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (v *Varz) MarshalJSON() ([]byte, error) { 56 | r := make(map[string]interface{}) 57 | 58 | var err error 59 | 60 | err = transform(v.UniqueVarz, &r) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | err = transform(v.GenericVarz, &r) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return json.Marshal(r) 71 | } 72 | -------------------------------------------------------------------------------- /common/health/varz_test.go: -------------------------------------------------------------------------------- 1 | package health_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | 8 | "code.cloudfoundry.org/lager/v3" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | 12 | "code.cloudfoundry.org/gorouter/common/health" 13 | "code.cloudfoundry.org/gorouter/common/schema" 14 | ) 15 | 16 | var _ = Describe("Varz", func() { 17 | 18 | It("contains expected keys", func() { 19 | varz := &health.Varz{} 20 | varz.LogCounts = schema.NewLogCounter() 21 | 22 | bytes, err := json.Marshal(varz) 23 | Expect(err).ToNot(HaveOccurred()) 24 | 25 | data := make(map[string]interface{}) 26 | err = json.Unmarshal(bytes, &data) 27 | Expect(err).ToNot(HaveOccurred()) 28 | 29 | members := []string{ 30 | "type", 31 | "index", 32 | "host", 33 | "credentials", 34 | "start", 35 | "uuid", 36 | "uptime", 37 | "num_cores", 38 | "mem", 39 | "cpu", 40 | "log_counts", 41 | } 42 | 43 | _, ok := data["config"] 44 | Expect(ok).To(BeFalse(), "config should be omitted from /varz") 45 | 46 | for _, key := range members { 47 | _, ok = data[key] 48 | Expect(ok).To(BeTrue(), fmt.Sprintf("member %s not found", key)) 49 | } 50 | }) 51 | 52 | It("contains Log counts", func() { 53 | varz := &health.Varz{} 54 | varz.LogCounts = schema.NewLogCounter() 55 | 56 | infoMsg := lager.LogFormat{ 57 | LogLevel: lager.INFO, 58 | Message: "info-message", 59 | } 60 | varz.LogCounts.Log(infoMsg) 61 | 62 | bytes, _ := json.Marshal(varz) 63 | data := make(map[string]interface{}) 64 | json.Unmarshal(bytes, &data) 65 | 66 | counts := data["log_counts"].(map[string]interface{}) 67 | count := counts[strconv.Itoa(int(lager.INFO))] 68 | 69 | Expect(count).To(Equal(float64(1))) 70 | }) 71 | 72 | Context("UniqueVarz", func() { 73 | It("marshals as a struct", func() { 74 | varz := &health.Varz{ 75 | UniqueVarz: struct { 76 | Type string `json:"my_type"` 77 | Index int `json:"my_index"` 78 | }{ 79 | Type: "Router", 80 | Index: 1, 81 | }, 82 | } 83 | 84 | bytes, _ := json.Marshal(varz) 85 | data := make(map[string]interface{}) 86 | json.Unmarshal(bytes, &data) 87 | 88 | Expect(data["my_type"]).To(Equal("Router")) 89 | Expect(data["my_index"]).To(Equal(float64(1))) 90 | }) 91 | 92 | It("marshals as a map", func() { 93 | varz := &health.Varz{ 94 | UniqueVarz: map[string]interface{}{"my_type": "Dea", "my_index": 1}, 95 | } 96 | bytes, _ := json.Marshal(varz) 97 | data := make(map[string]interface{}) 98 | json.Unmarshal(bytes, &data) 99 | 100 | Expect(data["my_type"]).To(Equal("Dea")) 101 | Expect(data["my_index"]).To(Equal(float64(1))) 102 | }) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /common/http/basic_auth.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | type Authenticator func(user, password string) bool 11 | 12 | type BasicAuth struct { 13 | http.Handler 14 | Authenticator 15 | } 16 | 17 | func extractCredentials(req *http.Request) []string { 18 | x := strings.Split(req.Header.Get("Authorization"), " ") 19 | if len(x) != 2 || x[0] != "Basic" { 20 | return nil 21 | } 22 | 23 | y, err := base64.StdEncoding.DecodeString(x[1]) 24 | if err != nil { 25 | return nil 26 | } 27 | 28 | z := strings.Split(string(y), ":") 29 | if len(z) != 2 { 30 | return nil 31 | } 32 | 33 | return z 34 | } 35 | 36 | func authenticatedEndpoint(path string) bool { 37 | return path != "/healthz" && path != "/health" 38 | 39 | } 40 | func (x *BasicAuth) ServeHTTP(w http.ResponseWriter, req *http.Request) { 41 | y := extractCredentials(req) 42 | // Beware of the hack 43 | if authenticatedEndpoint(req.URL.Path) && (y == nil || !x.Authenticator(y[0], y[1])) { 44 | w.Header().Set("WWW-Authenticate", "Basic") 45 | w.WriteHeader(http.StatusUnauthorized) 46 | // #nosec G104 - ignore errors when writing HTTP responses so we don't spam our logs during a DoS 47 | w.Write([]byte(fmt.Sprintf("%d Unauthorized\n", http.StatusUnauthorized))) 48 | } else { 49 | x.Handler.ServeHTTP(w, req) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /common/http/basic_auth_test.go: -------------------------------------------------------------------------------- 1 | package http_test 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | 10 | . "code.cloudfoundry.org/gorouter/common/http" 11 | ) 12 | 13 | var _ = Describe("http", func() { 14 | var listener net.Listener 15 | 16 | AfterEach(func() { 17 | if listener != nil { 18 | listener.Close() 19 | } 20 | }) 21 | 22 | bootstrap := func(x Authenticator) *http.Request { 23 | var err error 24 | 25 | h := func(w http.ResponseWriter, r *http.Request) { 26 | w.WriteHeader(http.StatusOK) 27 | } 28 | 29 | y := &BasicAuth{Handler: http.HandlerFunc(h), Authenticator: x} 30 | 31 | z := &http.Server{Handler: y} 32 | 33 | l, err := net.Listen("tcp", "127.0.0.1:0") 34 | Expect(err).ToNot(HaveOccurred()) 35 | 36 | go z.Serve(l) 37 | 38 | // Keep listener around such that test teardown can close it 39 | listener = l 40 | 41 | r, err := http.NewRequest("GET", "http://"+l.Addr().String(), nil) 42 | Expect(err).ToNot(HaveOccurred()) 43 | return r 44 | } 45 | 46 | Context("Unauthorized", func() { 47 | It("without credentials", func() { 48 | req := bootstrap(nil) 49 | 50 | resp, err := http.DefaultClient.Do(req) 51 | Expect(err).ToNot(HaveOccurred()) 52 | 53 | Expect(resp.StatusCode).To(Equal(http.StatusUnauthorized)) 54 | }) 55 | 56 | It("with invalid header", func() { 57 | req := bootstrap(nil) 58 | 59 | req.Header.Set("Authorization", "invalid") 60 | 61 | resp, err := http.DefaultClient.Do(req) 62 | Expect(err).ToNot(HaveOccurred()) 63 | Expect(resp.StatusCode).To(Equal(http.StatusUnauthorized)) 64 | }) 65 | 66 | It("with bad credentials", func() { 67 | f := func(u, p string) bool { 68 | Expect(u).To(Equal("user")) 69 | Expect(p).To(Equal("bad")) 70 | return false 71 | } 72 | 73 | req := bootstrap(f) 74 | 75 | req.SetBasicAuth("user", "bad") 76 | 77 | resp, err := http.DefaultClient.Do(req) 78 | Expect(err).ToNot(HaveOccurred()) 79 | Expect(resp.StatusCode).To(Equal(http.StatusUnauthorized)) 80 | }) 81 | }) 82 | It("succeeds with good credentials", func() { 83 | f := func(u, p string) bool { 84 | Expect(u).To(Equal("user")) 85 | Expect(p).To(Equal("good")) 86 | return true 87 | } 88 | 89 | req := bootstrap(f) 90 | 91 | req.SetBasicAuth("user", "good") 92 | 93 | resp, err := http.DefaultClient.Do(req) 94 | Expect(err).ToNot(HaveOccurred()) 95 | Expect(resp.StatusCode).To(Equal(http.StatusOK)) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /common/http/fakes/fake_response_writer.go: -------------------------------------------------------------------------------- 1 | package httpfakes 2 | 3 | import "net/http" 4 | 5 | type FakeResponseWriter struct { 6 | http.Response 7 | } 8 | 9 | func NewFakeResponseWriter() *FakeResponseWriter { 10 | return &FakeResponseWriter{http.Response{ 11 | Header: make(http.Header), 12 | }} 13 | } 14 | 15 | func (w *FakeResponseWriter) Write([]byte) (int, error) { 16 | return 0, nil 17 | } 18 | 19 | func (w *FakeResponseWriter) Header() http.Header { 20 | return w.Response.Header 21 | } 22 | func (w *FakeResponseWriter) WriteHeader(status int) { 23 | w.Response.StatusCode = status 24 | } 25 | -------------------------------------------------------------------------------- /common/http/headers.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import "net/http" 4 | 5 | const ( 6 | VcapBackendHeader = "X-Vcap-Backend" 7 | CfRouteEndpointHeader = "X-Cf-RouteEndpoint" 8 | VcapRouterHeader = "X-Vcap-Router" 9 | VcapTraceHeader = "X-Vcap-Trace" 10 | CfInstanceIdHeader = "X-CF-InstanceID" 11 | CfAppInstance = "X-CF-APP-INSTANCE" 12 | CfProcessInstance = "X-CF-PROCESS-INSTANCE" 13 | CfRouterError = "X-Cf-RouterError" 14 | ) 15 | 16 | func SetTraceHeaders(responseWriter http.ResponseWriter, routerIp, addr string) { 17 | responseWriter.Header().Set(VcapRouterHeader, routerIp) 18 | responseWriter.Header().Set(VcapBackendHeader, addr) 19 | responseWriter.Header().Set(CfRouteEndpointHeader, addr) 20 | } 21 | -------------------------------------------------------------------------------- /common/http/headers_test.go: -------------------------------------------------------------------------------- 1 | package http_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | 9 | commonhttp "code.cloudfoundry.org/gorouter/common/http" 10 | httpfakes "code.cloudfoundry.org/gorouter/common/http/fakes" 11 | ) 12 | 13 | var _ = Describe("Headers", func() { 14 | Describe("SetTraceHeaders", func() { 15 | var respWriter http.ResponseWriter 16 | 17 | BeforeEach(func() { 18 | respWriter = httpfakes.NewFakeResponseWriter() 19 | }) 20 | 21 | JustBeforeEach(func() { 22 | commonhttp.SetTraceHeaders(respWriter, "1.1.1.1", "example.com") 23 | }) 24 | 25 | It("sets the trace headers on the response", func() { 26 | Expect(respWriter.Header().Get(commonhttp.VcapRouterHeader)).To(Equal("1.1.1.1")) 27 | Expect(respWriter.Header().Get(commonhttp.VcapBackendHeader)).To(Equal("example.com")) 28 | Expect(respWriter.Header().Get(commonhttp.CfRouteEndpointHeader)).To(Equal("example.com")) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /common/http/http_suite_test.go: -------------------------------------------------------------------------------- 1 | package http_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestHttp(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Http Suite") 13 | } 14 | -------------------------------------------------------------------------------- /common/schema/duration.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type Duration time.Duration 11 | 12 | func (d Duration) MarshalJSON() ([]byte, error) { 13 | ds := formatDuration(time.Duration(d)) 14 | return []byte(fmt.Sprintf(`"%s"`, ds)), nil 15 | } 16 | 17 | func (d *Duration) UnmarshalJSON(b []byte) error { 18 | str := strings.Trim(string(b), "\"") 19 | u := strings.Split(str, ":") 20 | 21 | ds := u[0] 22 | di, err := strconv.ParseInt(ds[:len(ds)-1], 10, 64) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | hs := u[1] 28 | hi, err := strconv.ParseInt(hs[:len(hs)-1], 10, 64) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | hi += di * 24 34 | 35 | u[1] = fmt.Sprintf("%dh", hi) 36 | 37 | dur, err := time.ParseDuration(strings.Join(u[1:], "")) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | *d = Duration(dur) 43 | return nil 44 | } 45 | 46 | func formatDuration(d time.Duration) string { 47 | t := int64(d.Seconds()) 48 | day := t / (60 * 60 * 24) 49 | t = t % (60 * 60 * 24) 50 | hour := t / (60 * 60) 51 | t = t % (60 * 60) 52 | min := t / 60 53 | sec := t % 60 54 | 55 | ds := fmt.Sprintf("%dd:%dh:%dm:%ds", day, hour, min, sec) 56 | return ds 57 | } 58 | 59 | type Time time.Time 60 | 61 | func (t Time) MarshalJSON() ([]byte, error) { 62 | f := "2006-01-02 15:04:05 -0700" 63 | s := time.Time(t).Format(f) 64 | return []byte(fmt.Sprintf(`"%s"`, s)), nil 65 | } 66 | 67 | func (t *Time) UnmarshalJSON(b []byte) error { 68 | s := string(b) 69 | f := `"2006-01-02 15:04:05 -0700"` 70 | 71 | tt, err := time.Parse(f, s) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | *t = Time(tt) 77 | return nil 78 | } 79 | 80 | func (t Time) Elapsed() Duration { 81 | d := time.Since(time.Time(t)) 82 | return Duration(d) 83 | } 84 | 85 | func UnixToTime(unixTime string) (time.Time, error) { 86 | unixTimeInt, err := strconv.ParseInt(unixTime, 10, 64) 87 | return time.Unix(unixTimeInt, 0), err 88 | } 89 | -------------------------------------------------------------------------------- /common/schema/duration_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | 11 | "code.cloudfoundry.org/gorouter/common/schema" 12 | ) 13 | 14 | var _ = Describe("Durations", func() { 15 | Context("Duration", func() { 16 | It("supports JSON", func() { 17 | d := schema.Duration(123456) 18 | var i interface{} = &d 19 | 20 | _, ok := i.(json.Marshaler) 21 | Expect(ok).To(BeTrue()) 22 | 23 | _, ok = i.(json.Unmarshaler) 24 | Expect(ok).To(BeTrue()) 25 | }) 26 | 27 | It("marshals JSON", func() { 28 | d := schema.Duration(time.Hour*36 + time.Second*10) 29 | b, err := json.Marshal(d) 30 | Expect(err).ToNot(HaveOccurred()) 31 | Expect(string(b)).To(Equal(`"1d:12h:0m:10s"`)) 32 | }) 33 | 34 | It("unmarshals JSON", func() { 35 | d := schema.Duration(time.Hour*36 + time.Second*20) 36 | b, err := json.Marshal(d) 37 | Expect(err).ToNot(HaveOccurred()) 38 | 39 | var dd schema.Duration 40 | dd.UnmarshalJSON(b) 41 | Expect(dd).To(Equal(d)) 42 | }) 43 | }) 44 | 45 | Context("Time", func() { 46 | It("marshals JSON", func() { 47 | n := time.Now() 48 | f := "2006-01-02 15:04:05 -0700" 49 | 50 | t := schema.Time(n) 51 | b, e := json.Marshal(t) 52 | Expect(e).ToNot(HaveOccurred()) 53 | Expect(string(b)).To(Equal(fmt.Sprintf(`"%s"`, n.Format(f)))) 54 | }) 55 | 56 | It("unmarshals JSON", func() { 57 | t := schema.Time(time.Unix(time.Now().Unix(), 0)) // The precision of Time is 'second' 58 | b, err := json.Marshal(t) 59 | Expect(err).ToNot(HaveOccurred()) 60 | 61 | var tt schema.Time 62 | err = tt.UnmarshalJSON(b) 63 | Expect(err).ToNot(HaveOccurred()) 64 | Expect(tt).To(Equal(t)) 65 | }) 66 | 67 | }) 68 | 69 | Describe("Unix To Time", func() { 70 | Context("when the unix time is valid", func() { 71 | It("converts unix time stamp to time struct", func() { 72 | unixTime := "1437497865" 73 | 74 | tm, err := schema.UnixToTime(unixTime) 75 | Expect(err).ToNot(HaveOccurred()) 76 | expectedTime, err := time.Parse(time.UnixDate, "Tue Jul 21 16:57:45 UTC 2015") 77 | Expect(err).ToNot(HaveOccurred()) 78 | 79 | Expect(tm.Sub(expectedTime)).To(Equal(time.Duration(0))) 80 | }) 81 | }) 82 | 83 | Context("when the unix time is invalid", func() { 84 | It("returns an error", func() { 85 | unixTime := "invalid time string" 86 | 87 | _, err := schema.UnixToTime(unixTime) 88 | Expect(err).To(HaveOccurred()) 89 | }) 90 | }) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /common/schema/log_counter.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | "sync" 7 | 8 | "code.cloudfoundry.org/lager/v3" 9 | ) 10 | 11 | type LogCounter struct { 12 | sync.Mutex 13 | counts map[string]int 14 | } 15 | 16 | func NewLogCounter() *LogCounter { 17 | lc := &LogCounter{ 18 | counts: make(map[string]int), 19 | } 20 | return lc 21 | } 22 | 23 | func (lc *LogCounter) Log(log lager.LogFormat) { 24 | lc.Lock() 25 | lc.counts[strconv.Itoa(int(log.LogLevel))] += 1 26 | lc.Unlock() 27 | } 28 | 29 | func (lc *LogCounter) GetCount(key string) int { 30 | lc.Lock() 31 | defer lc.Unlock() 32 | return lc.counts[key] 33 | } 34 | 35 | func (lc *LogCounter) MarshalJSON() ([]byte, error) { 36 | lc.Lock() 37 | defer lc.Unlock() 38 | return json.Marshal(lc.counts) 39 | } 40 | -------------------------------------------------------------------------------- /common/schema/log_counter_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | 7 | "code.cloudfoundry.org/lager/v3" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | 11 | "code.cloudfoundry.org/gorouter/common/schema" 12 | ) 13 | 14 | var _ = Describe("LogCounter", func() { 15 | var ( 16 | infoMsg lager.LogFormat 17 | errMsg lager.LogFormat 18 | ) 19 | 20 | BeforeEach(func() { 21 | infoMsg = lager.LogFormat{ 22 | LogLevel: lager.INFO, 23 | Message: "info-message", 24 | } 25 | 26 | errMsg = lager.LogFormat{ 27 | LogLevel: lager.ERROR, 28 | Message: "error-message", 29 | } 30 | }) 31 | 32 | It("counts the number of records", func() { 33 | counter := schema.NewLogCounter() 34 | counter.Log(infoMsg) 35 | Expect(counter.GetCount(strconv.Itoa(int(lager.INFO)))).To(Equal(1)) 36 | 37 | counter.Log(infoMsg) 38 | Expect(counter.GetCount(strconv.Itoa(int(lager.INFO)))).To(Equal(2)) 39 | }) 40 | 41 | It("counts all log levels", func() { 42 | counter := schema.NewLogCounter() 43 | counter.Log(infoMsg) 44 | Expect(counter.GetCount(strconv.Itoa(int(lager.INFO)))).To(Equal(1)) 45 | 46 | counter.Log(errMsg) 47 | Expect(counter.GetCount(strconv.Itoa(int(lager.ERROR)))).To(Equal(1)) 48 | }) 49 | 50 | It("marshals the set of counts", func() { 51 | counter := schema.NewLogCounter() 52 | counter.Log(infoMsg) 53 | counter.Log(errMsg) 54 | 55 | b, e := counter.MarshalJSON() 56 | Expect(e).ToNot(HaveOccurred()) 57 | 58 | v := map[string]int{} 59 | e = json.Unmarshal(b, &v) 60 | Expect(e).ToNot(HaveOccurred()) 61 | Expect(v).To(HaveLen(2)) 62 | Expect(v[strconv.Itoa(int(lager.INFO))]).To(Equal(1)) 63 | Expect(v[strconv.Itoa(int(lager.ERROR))]).To(Equal(1)) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /common/schema/schema_suite_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSchema(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Schema Suite") 13 | } 14 | -------------------------------------------------------------------------------- /common/secure/crypto.go: -------------------------------------------------------------------------------- 1 | package secure 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "crypto/sha256" 8 | "errors" 9 | "fmt" 10 | 11 | "golang.org/x/crypto/pbkdf2" 12 | ) 13 | 14 | type Crypto interface { 15 | Encrypt(plainText []byte) (cipherText []byte, nonce []byte, err error) 16 | Decrypt(cipherText, nonce []byte) ([]byte, error) 17 | } 18 | 19 | type AesGCM struct { 20 | cipher.AEAD 21 | } 22 | 23 | func NewAesGCM(key []byte) (*AesGCM, error) { 24 | aes, err := aes.NewCipher(key) 25 | if err != nil { 26 | return &AesGCM{}, err 27 | } 28 | 29 | aead, err := cipher.NewGCM(aes) 30 | if err != nil { 31 | return &AesGCM{}, err 32 | } 33 | 34 | aesGCM := AesGCM{aead} 35 | return &aesGCM, nil 36 | } 37 | 38 | func (gcm *AesGCM) Encrypt(plainText []byte) (cipherText, nonce []byte, err error) { 39 | nonce, err = gcm.generateNonce() 40 | if err != nil { 41 | return nil, nil, err 42 | } 43 | 44 | cipherText = gcm.Seal(nil, nonce, plainText, []byte{}) 45 | 46 | return cipherText, nonce, nil 47 | } 48 | 49 | func (gcm *AesGCM) Decrypt(cipherText, nonce []byte) ([]byte, error) { 50 | if len(nonce) != gcm.NonceSize() { 51 | return nil, errors.New("incorrect nonce length") 52 | } 53 | 54 | plainText, err := gcm.Open(nil, nonce, cipherText, []byte{}) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return plainText, nil 60 | } 61 | 62 | func NewPbkdf2(input []byte, keyLen int) []byte { 63 | noSalt := []byte("") 64 | return pbkdf2.Key(input, noSalt, 100001, keyLen, sha256.New) 65 | } 66 | 67 | func (gcm *AesGCM) generateNonce() ([]byte, error) { 68 | return RandomBytes(gcm.NonceSize()) 69 | } 70 | 71 | func RandomBytes(size int) ([]byte, error) { 72 | if size < 0 { 73 | return nil, fmt.Errorf("cannot generate a negative number of random bytes") 74 | } 75 | b := make([]byte, size) 76 | _, err := rand.Read(b) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | return b, nil 82 | } 83 | -------------------------------------------------------------------------------- /common/secure/fakes/fake_crypto.go: -------------------------------------------------------------------------------- 1 | // This file was generated by counterfeiter 2 | package fakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "code.cloudfoundry.org/gorouter/common/secure" 8 | ) 9 | 10 | type FakeCrypto struct { 11 | EncryptStub func(plainText []byte) (cipherText []byte, nonce []byte, err error) 12 | encryptMutex sync.RWMutex 13 | encryptArgsForCall []struct { 14 | plainText []byte 15 | } 16 | encryptReturns struct { 17 | result1 []byte 18 | result2 []byte 19 | result3 error 20 | } 21 | DecryptStub func(cipherText, nonce []byte) ([]byte, error) 22 | decryptMutex sync.RWMutex 23 | decryptArgsForCall []struct { 24 | cipherText []byte 25 | nonce []byte 26 | } 27 | decryptReturns struct { 28 | result1 []byte 29 | result2 error 30 | } 31 | } 32 | 33 | func (fake *FakeCrypto) Encrypt(plainText []byte) (cipherText []byte, nonce []byte, err error) { 34 | fake.encryptMutex.Lock() 35 | fake.encryptArgsForCall = append(fake.encryptArgsForCall, struct { 36 | plainText []byte 37 | }{plainText}) 38 | fake.encryptMutex.Unlock() 39 | if fake.EncryptStub != nil { 40 | return fake.EncryptStub(plainText) 41 | } else { 42 | return fake.encryptReturns.result1, fake.encryptReturns.result2, fake.encryptReturns.result3 43 | } 44 | } 45 | 46 | func (fake *FakeCrypto) EncryptCallCount() int { 47 | fake.encryptMutex.RLock() 48 | defer fake.encryptMutex.RUnlock() 49 | return len(fake.encryptArgsForCall) 50 | } 51 | 52 | func (fake *FakeCrypto) EncryptArgsForCall(i int) []byte { 53 | fake.encryptMutex.RLock() 54 | defer fake.encryptMutex.RUnlock() 55 | return fake.encryptArgsForCall[i].plainText 56 | } 57 | 58 | func (fake *FakeCrypto) EncryptReturns(result1 []byte, result2 []byte, result3 error) { 59 | fake.EncryptStub = nil 60 | fake.encryptReturns = struct { 61 | result1 []byte 62 | result2 []byte 63 | result3 error 64 | }{result1, result2, result3} 65 | } 66 | 67 | func (fake *FakeCrypto) Decrypt(cipherText []byte, nonce []byte) ([]byte, error) { 68 | fake.decryptMutex.Lock() 69 | fake.decryptArgsForCall = append(fake.decryptArgsForCall, struct { 70 | cipherText []byte 71 | nonce []byte 72 | }{cipherText, nonce}) 73 | fake.decryptMutex.Unlock() 74 | if fake.DecryptStub != nil { 75 | return fake.DecryptStub(cipherText, nonce) 76 | } else { 77 | return fake.decryptReturns.result1, fake.decryptReturns.result2 78 | } 79 | } 80 | 81 | func (fake *FakeCrypto) DecryptCallCount() int { 82 | fake.decryptMutex.RLock() 83 | defer fake.decryptMutex.RUnlock() 84 | return len(fake.decryptArgsForCall) 85 | } 86 | 87 | func (fake *FakeCrypto) DecryptArgsForCall(i int) ([]byte, []byte) { 88 | fake.decryptMutex.RLock() 89 | defer fake.decryptMutex.RUnlock() 90 | return fake.decryptArgsForCall[i].cipherText, fake.decryptArgsForCall[i].nonce 91 | } 92 | 93 | func (fake *FakeCrypto) DecryptReturns(result1 []byte, result2 error) { 94 | fake.DecryptStub = nil 95 | fake.decryptReturns = struct { 96 | result1 []byte 97 | result2 error 98 | }{result1, result2} 99 | } 100 | 101 | var _ secure.Crypto = new(FakeCrypto) 102 | -------------------------------------------------------------------------------- /common/secure/secure_suite_test.go: -------------------------------------------------------------------------------- 1 | package secure_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestCrypto(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Crypto Suite") 13 | } 14 | -------------------------------------------------------------------------------- /common/spec/component.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | type Component interface { 4 | Start() error 5 | Stop() 6 | Running() bool 7 | } 8 | -------------------------------------------------------------------------------- /common/uuid/uuid.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import . "github.com/nu7hatch/gouuid" 4 | 5 | func GenerateUUID() (string, error) { 6 | guid, err := NewV4() 7 | if err != nil { 8 | return "", err 9 | } 10 | return guid.String(), nil 11 | } 12 | -------------------------------------------------------------------------------- /common/uuid/uuid_suite_test.go: -------------------------------------------------------------------------------- 1 | package uuid_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestUuid(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Uuid Suite") 13 | } 14 | -------------------------------------------------------------------------------- /common/uuid/uuid_test.go: -------------------------------------------------------------------------------- 1 | package uuid_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "code.cloudfoundry.org/gorouter/common/uuid" 8 | ) 9 | 10 | var _ = Describe("UUID", func() { 11 | It("creates a uuid", func() { 12 | uuid, err := uuid.GenerateUUID() 13 | Expect(err).ToNot(HaveOccurred()) 14 | Expect(uuid).To(HaveLen(36)) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /config/config_suite_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "code.cloudfoundry.org/lager/v3" 7 | "code.cloudfoundry.org/lager/v3/lagertest" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var ( 13 | logger lager.Logger 14 | ) 15 | 16 | func TestConfig(t *testing.T) { 17 | RegisterFailHandler(Fail) 18 | RunSpecs(t, "Config Suite") 19 | } 20 | 21 | var _ = BeforeEach(func() { 22 | logger = lagertest.NewTestLogger("test") 23 | }) 24 | -------------------------------------------------------------------------------- /errorwriter/errorwriter_suite_test.go: -------------------------------------------------------------------------------- 1 | package errorwriter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestErrorwriter(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "ErrorWriter Suite") 13 | } 14 | -------------------------------------------------------------------------------- /example_config/example.yml: -------------------------------------------------------------------------------- 1 | status: 2 | port: 8082 3 | user: 4 | pass: 5 | 6 | nats: 7 | hosts: 8 | - hostname: localhost 9 | port: 4222 10 | user: 11 | pass: 12 | 13 | logging: 14 | file: 15 | syslog: 16 | level: debug 17 | 18 | port: 8081 19 | index: 0 20 | 21 | go_max_procs: 8 22 | 23 | publish_start_message_interval: 60s 24 | prune_stale_droplets_interval: 30s 25 | droplet_stale_threshold: 120s 26 | publish_active_apps_interval: 0 # 0 means disabled 27 | secure_cookies: true 28 | route_service_timeout: 60s 29 | route_services_secret: "tWPE+sWJq+ZnGJpyKkIPYg==" 30 | 31 | extra_headers_to_log: 32 | - Span-Id 33 | - Trace-Id 34 | - Cache-Control 35 | -------------------------------------------------------------------------------- /handlers/clientcert.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/pem" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/urfave/negroni/v3" 12 | 13 | "code.cloudfoundry.org/gorouter/config" 14 | "code.cloudfoundry.org/gorouter/errorwriter" 15 | log "code.cloudfoundry.org/gorouter/logger" 16 | "code.cloudfoundry.org/gorouter/routeservice" 17 | ) 18 | 19 | const xfcc = "X-Forwarded-Client-Cert" 20 | 21 | type clientCert struct { 22 | skipSanitization func(req *http.Request) bool 23 | forceDeleteHeader func(req *http.Request) (bool, error) 24 | forwardingMode string 25 | logger *slog.Logger 26 | errorWriter errorwriter.ErrorWriter 27 | } 28 | 29 | func NewClientCert( 30 | skipSanitization func(req *http.Request) bool, 31 | forceDeleteHeader func(req *http.Request) (bool, error), 32 | forwardingMode string, 33 | logger *slog.Logger, 34 | ew errorwriter.ErrorWriter, 35 | ) negroni.Handler { 36 | return &clientCert{ 37 | skipSanitization: skipSanitization, 38 | forceDeleteHeader: forceDeleteHeader, 39 | forwardingMode: forwardingMode, 40 | logger: logger, 41 | errorWriter: ew, 42 | } 43 | } 44 | 45 | func (c *clientCert) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 46 | logger := LoggerWithTraceInfo(c.logger, r) 47 | skip := c.skipSanitization(r) 48 | if !skip { 49 | switch c.forwardingMode { 50 | case config.FORWARD: 51 | if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { 52 | r.Header.Del(xfcc) 53 | } 54 | case config.SANITIZE_SET: 55 | r.Header.Del(xfcc) 56 | if r.TLS != nil { 57 | replaceXFCCHeader(r) 58 | } 59 | } 60 | } 61 | 62 | delete, err := c.forceDeleteHeader(r) 63 | if err != nil { 64 | c.logger.Error("signature-validation-failed", log.ErrAttr(err)) 65 | if errors.Is(err, routeservice.ErrExpired) { 66 | c.errorWriter.WriteError( 67 | rw, 68 | http.StatusGatewayTimeout, 69 | fmt.Sprintf("Failed to validate Route Service Signature: %s", err.Error()), 70 | logger, 71 | ) 72 | } else { 73 | c.errorWriter.WriteError( 74 | rw, 75 | http.StatusBadGateway, 76 | fmt.Sprintf("Failed to validate Route Service Signature: %s", err.Error()), 77 | logger, 78 | ) 79 | } 80 | return 81 | } 82 | if delete { 83 | r.Header.Del(xfcc) 84 | } 85 | next(rw, r) 86 | } 87 | 88 | func replaceXFCCHeader(r *http.Request) { 89 | // we only care about the first cert at this moment 90 | if len(r.TLS.PeerCertificates) > 0 { 91 | cert := r.TLS.PeerCertificates[0] 92 | b := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw} 93 | certPEM := pem.EncodeToMemory(&b) 94 | r.Header.Add(xfcc, sanitize(certPEM)) 95 | } 96 | } 97 | 98 | func sanitize(cert []byte) string { 99 | s := string(cert) 100 | r := strings.NewReplacer("-----BEGIN CERTIFICATE-----", "", 101 | "-----END CERTIFICATE-----", "", 102 | "\n", "") 103 | return r.Replace(s) 104 | } 105 | -------------------------------------------------------------------------------- /handlers/handlers_suite_test.go: -------------------------------------------------------------------------------- 1 | package handlers_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | 10 | "code.cloudfoundry.org/gorouter/handlers" 11 | ) 12 | 13 | func TestHandlers(t *testing.T) { 14 | RegisterFailHandler(Fail) 15 | RunSpecs(t, "Handlers Suite") 16 | } 17 | 18 | type PrevHandler struct{} 19 | 20 | func (h *PrevHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) { 21 | next(w, req) 22 | } 23 | 24 | type PrevHandlerWithTrace struct{} 25 | 26 | func (h *PrevHandlerWithTrace) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) { 27 | reqInfo, err := handlers.ContextRequestInfo(req) 28 | if err == nil { 29 | reqInfo.TraceInfo = handlers.TraceInfo{ 30 | TraceID: "1111", 31 | SpanID: "2222", 32 | } 33 | } 34 | 35 | next(w, req) 36 | } 37 | -------------------------------------------------------------------------------- /handlers/healthcheck.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | 7 | "code.cloudfoundry.org/gorouter/common/health" 8 | ) 9 | 10 | type healthcheck struct { 11 | health *health.Health 12 | logger *slog.Logger 13 | } 14 | 15 | func NewHealthcheck(health *health.Health, logger *slog.Logger) http.Handler { 16 | return &healthcheck{ 17 | health: health, 18 | logger: logger, 19 | } 20 | } 21 | 22 | func (h *healthcheck) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 23 | 24 | rw.Header().Set("Cache-Control", "private, max-age=0") 25 | rw.Header().Set("Expires", "0") 26 | 27 | if h.health.Health() != health.Healthy { 28 | rw.WriteHeader(http.StatusServiceUnavailable) 29 | r.Close = true 30 | return 31 | } 32 | 33 | rw.WriteHeader(http.StatusOK) 34 | // #nosec G104 - ignore errors when writing HTTP responses so we don't spam our logs during a DoS 35 | rw.Write([]byte("ok\n")) 36 | r.Close = true 37 | } 38 | -------------------------------------------------------------------------------- /handlers/healthcheck_test.go: -------------------------------------------------------------------------------- 1 | package handlers_test 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | 11 | "code.cloudfoundry.org/gorouter/common/health" 12 | "code.cloudfoundry.org/gorouter/handlers" 13 | "code.cloudfoundry.org/gorouter/test_util" 14 | ) 15 | 16 | var _ = Describe("Healthcheck", func() { 17 | var ( 18 | handler http.Handler 19 | logger *test_util.TestLogger 20 | resp *httptest.ResponseRecorder 21 | req *http.Request 22 | healthStatus *health.Health 23 | ) 24 | 25 | BeforeEach(func() { 26 | logger = test_util.NewTestLogger("healthcheck") 27 | req = test_util.NewRequest("GET", "example.com", "/", nil) 28 | resp = httptest.NewRecorder() 29 | healthStatus = &health.Health{} 30 | healthStatus.SetHealth(health.Healthy) 31 | 32 | handler = handlers.NewHealthcheck(healthStatus, logger.Logger) 33 | }) 34 | 35 | It("closes the request", func() { 36 | handler.ServeHTTP(resp, req) 37 | Expect(req.Close).To(BeTrue()) 38 | }) 39 | 40 | It("responds with 200 OK", func() { 41 | handler.ServeHTTP(resp, req) 42 | Expect(resp.Code).To(Equal(200)) 43 | bodyString, err := io.ReadAll(resp.Body) 44 | Expect(err).ToNot(HaveOccurred()) 45 | Expect(bodyString).To(ContainSubstring("ok\n")) 46 | }) 47 | 48 | It("sets the Cache-Control and Expires headers", func() { 49 | handler.ServeHTTP(resp, req) 50 | Expect(resp.Header().Get("Cache-Control")).To(Equal("private, max-age=0")) 51 | Expect(resp.Header().Get("Expires")).To(Equal("0")) 52 | }) 53 | 54 | Context("when draining is in progress", func() { 55 | BeforeEach(func() { 56 | healthStatus.SetHealth(health.Degraded) 57 | }) 58 | 59 | It("responds with a 503 Service Unavailable", func() { 60 | handler.ServeHTTP(resp, req) 61 | Expect(resp.Code).To(Equal(503)) 62 | }) 63 | 64 | It("sets the Cache-Control and Expires headers", func() { 65 | handler.ServeHTTP(resp, req) 66 | Expect(resp.Header().Get("Cache-Control")).To(Equal("private, max-age=0")) 67 | Expect(resp.Header().Get("Expires")).To(Equal("0")) 68 | }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /handlers/helpers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "net/http" 7 | "strings" 8 | 9 | router_http "code.cloudfoundry.org/gorouter/common/http" 10 | "code.cloudfoundry.org/gorouter/config" 11 | "code.cloudfoundry.org/gorouter/route" 12 | ) 13 | 14 | const ( 15 | cacheMaxAgeSeconds = 2 16 | VcapCookieId = "__VCAP_ID__" 17 | ) 18 | 19 | func AddRouterErrorHeader(rw http.ResponseWriter, val string) { 20 | rw.Header().Set(router_http.CfRouterError, val) 21 | } 22 | 23 | func addInvalidResponseCacheControlHeader(rw http.ResponseWriter) { 24 | rw.Header().Set( 25 | "Cache-Control", 26 | fmt.Sprintf("public,max-age=%d", cacheMaxAgeSeconds), 27 | ) 28 | } 29 | 30 | func addNoCacheControlHeader(rw http.ResponseWriter) { 31 | rw.Header().Set( 32 | "Cache-Control", 33 | "no-cache, no-store", 34 | ) 35 | } 36 | 37 | func hostWithoutPort(reqHost string) string { 38 | host := reqHost 39 | 40 | // Remove : 41 | pos := strings.Index(host, ":") 42 | if pos >= 0 { 43 | host = host[0:pos] 44 | } 45 | 46 | return host 47 | } 48 | 49 | func IsWebSocketUpgrade(request *http.Request) bool { 50 | // websocket should be case insensitive per RFC6455 4.2.1 51 | return strings.ToLower(upgradeHeader(request)) == "websocket" 52 | } 53 | 54 | func upgradeHeader(request *http.Request) string { 55 | // handle multiple Connection field-values, either in a comma-separated string or multiple field-headers 56 | for _, v := range request.Header[http.CanonicalHeaderKey("Connection")] { 57 | // upgrade should be case-insensitive per RFC6455 4.2.1 58 | if strings.Contains(strings.ToLower(v), "upgrade") { 59 | return request.Header.Get("Upgrade") 60 | } 61 | } 62 | 63 | return "" 64 | } 65 | 66 | func EndpointIteratorForRequest(logger *slog.Logger, request *http.Request, stickySessionCookieNames config.StringSet, authNegotiateSticky bool, azPreference string, az string) (route.EndpointIterator, error) { 67 | reqInfo, err := ContextRequestInfo(request) 68 | if err != nil { 69 | return nil, fmt.Errorf("could not find reqInfo in context") 70 | } 71 | stickyEndpointID, mustBeSticky := GetStickySession(request, stickySessionCookieNames, authNegotiateSticky) 72 | return reqInfo.RoutePool.Endpoints(logger, stickyEndpointID, mustBeSticky, azPreference, az), nil 73 | } 74 | 75 | func GetStickySession(request *http.Request, stickySessionCookieNames config.StringSet, authNegotiateSticky bool) (string, bool) { 76 | if authNegotiateSticky { 77 | containsAuthNegotiateHeader := strings.HasPrefix(strings.ToLower(request.Header.Get("Authorization")), "negotiate") 78 | if containsAuthNegotiateHeader { 79 | if sticky, err := request.Cookie(VcapCookieId); err == nil { 80 | return sticky.Value, true 81 | } 82 | } 83 | } 84 | // Try choosing a backend using sticky session 85 | for stickyCookieName := range stickySessionCookieNames { 86 | if _, err := request.Cookie(stickyCookieName); err == nil { 87 | if sticky, err := request.Cookie(VcapCookieId); err == nil { 88 | return sticky.Value, false 89 | } 90 | } 91 | } 92 | return "", false 93 | } 94 | -------------------------------------------------------------------------------- /handlers/hop_by_hop.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | "strings" 7 | 8 | "code.cloudfoundry.org/gorouter/config" 9 | ) 10 | 11 | type HopByHop struct { 12 | cfg *config.Config 13 | logger *slog.Logger 14 | } 15 | 16 | // NewHopByHop creates a new handler that sanitizes hop-by-hop headers based on the HopByHopHeadersToFilter config 17 | func NewHopByHop(cfg *config.Config, logger *slog.Logger) *HopByHop { 18 | return &HopByHop{ 19 | logger: logger, 20 | cfg: cfg, 21 | } 22 | } 23 | 24 | func (h *HopByHop) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 25 | h.SanitizeRequestConnection(r) 26 | next(rw, r) 27 | } 28 | 29 | func (h *HopByHop) SanitizeRequestConnection(r *http.Request) { 30 | if len(h.cfg.HopByHopHeadersToFilter) == 0 { 31 | return 32 | } 33 | connections := r.Header.Values("Connection") 34 | for index, connection := range connections { 35 | if connection != "" { 36 | values := strings.Split(connection, ",") 37 | connectionHeader := []string{} 38 | for i := range values { 39 | trimmedValue := strings.TrimSpace(values[i]) 40 | found := false 41 | for _, item := range h.cfg.HopByHopHeadersToFilter { 42 | if strings.EqualFold(item, trimmedValue) { 43 | found = true 44 | break 45 | } 46 | } 47 | if !found { 48 | connectionHeader = append(connectionHeader, trimmedValue) 49 | } 50 | } 51 | r.Header[http.CanonicalHeaderKey("Connection")][index] = strings.Join(connectionHeader, ", ") 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /handlers/http_rewrite.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/urfave/negroni/v3" 7 | 8 | "code.cloudfoundry.org/gorouter/config" 9 | "code.cloudfoundry.org/gorouter/proxy/utils" 10 | ) 11 | 12 | type httpRewriteHandler struct { 13 | responseHeaderRewriters []utils.HeaderRewriter 14 | } 15 | 16 | func headerNameValuesToHTTPHeader(headerNameValues []config.HeaderNameValue) http.Header { 17 | h := http.Header{} 18 | for _, hv := range headerNameValues { 19 | h.Add(hv.Name, hv.Value) 20 | } 21 | return h 22 | } 23 | 24 | func NewHTTPRewriteHandler(cfg config.HTTPRewrite, headersToAlwaysRemove []string) negroni.Handler { 25 | addHeadersIfNotPresent := headerNameValuesToHTTPHeader( 26 | cfg.Responses.AddHeadersIfNotPresent, 27 | ) 28 | headers := cfg.Responses.RemoveHeaders 29 | 30 | for _, header := range headersToAlwaysRemove { 31 | headers = append(headers, config.HeaderNameValue{Name: header}) 32 | } 33 | 34 | removeHeaders := headerNameValuesToHTTPHeader( 35 | headers, 36 | ) 37 | return &httpRewriteHandler{ 38 | responseHeaderRewriters: []utils.HeaderRewriter{ 39 | &utils.RemoveHeaderRewriter{Header: removeHeaders}, 40 | &utils.AddHeaderIfNotPresentRewriter{Header: addHeadersIfNotPresent}, 41 | }, 42 | } 43 | } 44 | 45 | func (p *httpRewriteHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 46 | proxyWriter := rw.(utils.ProxyResponseWriter) 47 | for _, rewriter := range p.responseHeaderRewriters { 48 | proxyWriter.AddHeaderRewriter(rewriter) 49 | } 50 | next(rw, r) 51 | } 52 | -------------------------------------------------------------------------------- /handlers/httplatencyprometheus.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/urfave/negroni/v3" 8 | 9 | "code.cloudfoundry.org/gorouter/metrics" 10 | ) 11 | 12 | type httpLatencyPrometheusHandler struct { 13 | reporter metrics.MetricReporter 14 | } 15 | 16 | // NewHTTPLatencyPrometheus creates a new handler that handles prometheus metrics for latency 17 | func NewHTTPLatencyPrometheus(reporter metrics.MetricReporter) negroni.Handler { 18 | return &httpLatencyPrometheusHandler{ 19 | reporter: reporter, 20 | } 21 | } 22 | 23 | // ServeHTTP handles emitting a StartStop event after the request has been completed 24 | func (hl *httpLatencyPrometheusHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 25 | start := time.Now() 26 | next(rw, r) 27 | stop := time.Now() 28 | 29 | latency := stop.Sub(start) / time.Second 30 | 31 | sourceId := "gorouter" 32 | endpoint, err := GetEndpoint(r.Context()) 33 | if err == nil { 34 | if endpoint.Tags["source_id"] != "" { 35 | sourceId = endpoint.Tags["source_id"] 36 | } 37 | } 38 | 39 | hl.reporter.CaptureHTTPLatency(latency, sourceId) 40 | } 41 | -------------------------------------------------------------------------------- /handlers/httpstartstop.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "log/slog" 5 | "maps" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/cloudfoundry/dropsonde" 10 | "github.com/cloudfoundry/dropsonde/emitter" 11 | "github.com/cloudfoundry/dropsonde/factories" 12 | "github.com/cloudfoundry/sonde-go/events" 13 | uuid "github.com/nu7hatch/gouuid" 14 | "github.com/urfave/negroni/v3" 15 | "google.golang.org/protobuf/proto" 16 | 17 | log "code.cloudfoundry.org/gorouter/logger" 18 | "code.cloudfoundry.org/gorouter/proxy/utils" 19 | ) 20 | 21 | type httpStartStopHandler struct { 22 | emitter dropsonde.EventEmitter 23 | logger *slog.Logger 24 | } 25 | 26 | // NewHTTPStartStop creates a new handler that handles emitting frontend 27 | // HTTP StartStop events 28 | func NewHTTPStartStop(emitter dropsonde.EventEmitter, logger *slog.Logger) negroni.Handler { 29 | return &httpStartStopHandler{ 30 | emitter: emitter, 31 | logger: logger, 32 | } 33 | } 34 | 35 | // ServeHTTP handles emitting a StartStop event after the request has been completed 36 | func (hh *httpStartStopHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 37 | logger := LoggerWithTraceInfo(hh.logger, r) 38 | 39 | requestID, err := uuid.ParseHex(r.Header.Get(VcapRequestIdHeader)) 40 | if err != nil { 41 | log.Panic(logger, "start-stop-handler-err", slog.String("error", "X-Vcap-Request-Id not found")) 42 | return 43 | } 44 | prw, ok := rw.(utils.ProxyResponseWriter) 45 | if !ok { 46 | log.Panic(logger, "request-info-err", slog.String("error", "ProxyResponseWriter not found")) 47 | return 48 | } 49 | 50 | // Remove these headers if pre-set so they aren't logged in the event. 51 | // ProxyRoundTripper will set them to correct values later 52 | r.Header.Del("X-CF-ApplicationID") 53 | r.Header.Del("X-CF-InstanceIndex") 54 | r.Header.Del("X-CF-InstanceID") 55 | 56 | startTime := time.Now() 57 | 58 | next(rw, r) 59 | 60 | // #nosec G115 - http status codes are `int` values, but dropsonde requires int32. per the RFC, it will only ever be 3 digits so ignore any loss/overflow due to conversion 61 | startStopEvent := factories.NewHttpStartStop(r, int32(prw.Status()), int64(prw.Size()), events.PeerType_Server, requestID) 62 | startStopEvent.StartTimestamp = proto.Int64(startTime.UnixNano()) 63 | 64 | envelope, err := emitter.Wrap(startStopEvent, hh.emitter.Origin()) 65 | if err != nil { 66 | logger.Info("failed-to-create-startstop-envelope", log.ErrAttr(err)) 67 | return 68 | } 69 | 70 | info, err := ContextRequestInfo(r) 71 | if err != nil { 72 | logger.Error("request-info-err", log.ErrAttr(err)) 73 | } else { 74 | envelope.Tags = hh.envelopeTags(info) 75 | } 76 | 77 | err = hh.emitter.EmitEnvelope(envelope) 78 | if err != nil { 79 | logger.Info("failed-to-emit-startstop-event", log.ErrAttr(err)) 80 | } 81 | } 82 | 83 | func (hh *httpStartStopHandler) envelopeTags(ri *RequestInfo) map[string]string { 84 | tags := make(map[string]string) 85 | endpoint := ri.RouteEndpoint 86 | if endpoint != nil { 87 | maps.Copy(tags, endpoint.Tags) 88 | } 89 | if ri.TraceInfo.SpanID != "" && ri.TraceInfo.TraceID != "" { 90 | tags["span_id"] = ri.TraceInfo.SpanID 91 | tags["trace_id"] = ri.TraceInfo.TraceID 92 | } 93 | return tags 94 | } 95 | -------------------------------------------------------------------------------- /handlers/max_request_size.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "net/http" 7 | 8 | router_http "code.cloudfoundry.org/gorouter/common/http" 9 | "code.cloudfoundry.org/gorouter/config" 10 | log "code.cloudfoundry.org/gorouter/logger" 11 | ) 12 | 13 | type MaxRequestSize struct { 14 | cfg *config.Config 15 | MaxSize int 16 | MaxCount int 17 | logger *slog.Logger 18 | } 19 | 20 | const ONE_MB = 1024 * 1024 // bytes * kb 21 | 22 | // NewAccessLog creates a new handler that handles logging requests to the 23 | // access log 24 | func NewMaxRequestSize(cfg *config.Config, logger *slog.Logger) *MaxRequestSize { 25 | maxSize := cfg.MaxRequestHeaderBytes 26 | 27 | if maxSize < 1 { 28 | maxSize = ONE_MB 29 | } 30 | 31 | if maxSize > ONE_MB { 32 | logger.Warn("innefectual-max-header-bytes-value", slog.String("error", fmt.Sprintf("Values over %d are limited by http.Server", maxSize))) 33 | maxSize = ONE_MB 34 | } 35 | 36 | return &MaxRequestSize{ 37 | MaxSize: maxSize, 38 | MaxCount: cfg.MaxRequestHeaders, 39 | logger: logger, 40 | cfg: cfg, 41 | } 42 | } 43 | 44 | func (m *MaxRequestSize) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 45 | logger := LoggerWithTraceInfo(m.logger, r) 46 | 47 | // Four additional bytes for the two spaces and \r\n: 48 | // GET / HTTP/1.1\r\n 49 | reqSize := len(r.Method) + len(r.URL.RequestURI()) + len(r.Proto) + 4 50 | 51 | // Host header which is not passed on to us, plus eight bytes for 'Host: ' and \r\n 52 | reqSize += len(r.Host) + 8 53 | 54 | hdrCount := 0 55 | 56 | // Go doesn't split header values on commas, instead it only splits the value when it's 57 | // provided via repeated header keys. Therefore we have to account for each value of a repeated 58 | // header as well as its key. 59 | for k, vv := range r.Header { 60 | for _, v := range vv { 61 | // Four additional bytes for the colon and space after the header key and \r\n. 62 | reqSize += len(k) + len(v) + 4 63 | hdrCount++ 64 | } 65 | } 66 | 67 | if reqSize >= m.MaxSize || (m.MaxCount > 0 && hdrCount > m.MaxCount) { 68 | reqInfo, err := ContextRequestInfo(r) 69 | if err != nil { 70 | logger.Error("request-info-err", log.ErrAttr(err)) 71 | } else { 72 | endpointIterator, err := EndpointIteratorForRequest(logger, r, m.cfg.StickySessionCookieNames, m.cfg.StickySessionsForAuthNegotiate, m.cfg.LoadBalanceAZPreference, m.cfg.Zone) 73 | if err != nil { 74 | logger.Error("failed-to-find-endpoint-for-req-during-431-short-circuit", log.ErrAttr(err)) 75 | } else { 76 | reqInfo.RouteEndpoint = endpointIterator.Next(0) 77 | } 78 | } 79 | rw.Header().Set(router_http.CfRouterError, "max-request-size-exceeded") 80 | rw.WriteHeader(http.StatusRequestHeaderFieldsTooLarge) 81 | r.Close = true 82 | return 83 | } 84 | next(rw, r) 85 | } 86 | -------------------------------------------------------------------------------- /handlers/paniccheck.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "net/http" 7 | "runtime/debug" 8 | 9 | "github.com/urfave/negroni/v3" 10 | 11 | "code.cloudfoundry.org/gorouter/common/health" 12 | router_http "code.cloudfoundry.org/gorouter/common/http" 13 | log "code.cloudfoundry.org/gorouter/logger" 14 | ) 15 | 16 | type panicCheck struct { 17 | health *health.Health 18 | logger *slog.Logger 19 | } 20 | 21 | // NewPanicCheck creates a handler responsible for checking for panics and setting the Healthcheck to fail. 22 | func NewPanicCheck(health *health.Health, logger *slog.Logger) negroni.Handler { 23 | return &panicCheck{ 24 | health: health, 25 | logger: logger, 26 | } 27 | } 28 | 29 | func (p *panicCheck) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 30 | defer func() { 31 | if rec := recover(); rec != nil { 32 | switch rec { 33 | case http.ErrAbortHandler: 34 | // The ErrAbortHandler panic occurs when a client goes away in the middle of a request 35 | // this is a panic we expect to see in normal operation and is safe to allow the panic 36 | // http.Server will handle it appropriately 37 | panic(http.ErrAbortHandler) 38 | default: 39 | err, ok := rec.(error) 40 | if !ok { 41 | err = fmt.Errorf("%v", rec) 42 | } 43 | logger := LoggerWithTraceInfo(p.logger, r) 44 | logger.Error("panic-check", slog.String("host", r.Host), log.ErrAttr(err), slog.String("stacktrace", string(debug.Stack()))) 45 | 46 | rw.Header().Set(router_http.CfRouterError, "unknown_failure") 47 | rw.WriteHeader(http.StatusBadGateway) 48 | r.Close = true 49 | } 50 | } 51 | }() 52 | 53 | next(rw, r) 54 | } 55 | -------------------------------------------------------------------------------- /handlers/protocolcheck.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "net" 9 | "net/http" 10 | 11 | "github.com/urfave/negroni/v3" 12 | 13 | "code.cloudfoundry.org/gorouter/errorwriter" 14 | ) 15 | 16 | type protocolCheck struct { 17 | logger *slog.Logger 18 | errorWriter errorwriter.ErrorWriter 19 | enableHTTP2 bool 20 | } 21 | 22 | // NewProtocolCheck creates a handler responsible for checking the protocol of 23 | // the request 24 | func NewProtocolCheck(logger *slog.Logger, errorWriter errorwriter.ErrorWriter, enableHTTP2 bool) negroni.Handler { 25 | return &protocolCheck{ 26 | logger: logger, 27 | errorWriter: errorWriter, 28 | enableHTTP2: enableHTTP2, 29 | } 30 | } 31 | 32 | func (p *protocolCheck) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 33 | logger := LoggerWithTraceInfo(p.logger, r) 34 | if !p.isProtocolSupported(r) { 35 | // must be hijacked, otherwise no response is sent back 36 | conn, buf, err := p.hijack(rw) 37 | if err != nil { 38 | p.errorWriter.WriteError( 39 | rw, 40 | http.StatusBadRequest, 41 | "Unsupported protocol", 42 | logger, 43 | ) 44 | return 45 | } 46 | 47 | fmt.Fprintf(buf, "HTTP/1.0 400 Bad Request\r\n\r\n") 48 | // #nosec G104 - ignore errors when writing HTTP responses so we don't spam our logs during a DoS 49 | buf.Flush() 50 | // #nosec G104 - ignore errors when writing HTTP responses so we don't spam our logs during a DoS 51 | conn.Close() 52 | return 53 | } 54 | 55 | next(rw, r) 56 | } 57 | 58 | func (p *protocolCheck) hijack(rw http.ResponseWriter) (net.Conn, *bufio.ReadWriter, error) { 59 | hijacker, ok := rw.(http.Hijacker) 60 | if !ok { 61 | return nil, nil, errors.New("response writer cannot hijack") 62 | } 63 | return hijacker.Hijack() 64 | } 65 | 66 | func (p *protocolCheck) isProtocolSupported(request *http.Request) bool { 67 | return (p.enableHTTP2 && request.ProtoMajor == 2) || (request.ProtoMajor == 1 && (request.ProtoMinor == 0 || request.ProtoMinor == 1)) 68 | } 69 | -------------------------------------------------------------------------------- /handlers/proxy_healthcheck.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/urfave/negroni/v3" 7 | 8 | "code.cloudfoundry.org/gorouter/common/health" 9 | ) 10 | 11 | type proxyHealthcheck struct { 12 | userAgent string 13 | health *health.Health 14 | } 15 | 16 | // NewHealthcheck creates a handler that responds to healthcheck requests. 17 | // If userAgent is set to a non-empty string, it will use that user agent to 18 | // differentiate between healthcheck requests and non-healthcheck requests. 19 | // Otherwise, it will treat all requests as healthcheck requests. 20 | func NewProxyHealthcheck(userAgent string, health *health.Health) negroni.Handler { 21 | return &proxyHealthcheck{ 22 | userAgent: userAgent, 23 | health: health, 24 | } 25 | } 26 | 27 | func (h *proxyHealthcheck) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 28 | // If reqeust is not intended for healthcheck 29 | if r.Header.Get("User-Agent") != h.userAgent { 30 | next(rw, r) 31 | return 32 | } 33 | 34 | rw.Header().Set("Cache-Control", "private, max-age=0") 35 | rw.Header().Set("Expires", "0") 36 | 37 | if h.health.Health() != health.Healthy { 38 | rw.WriteHeader(http.StatusServiceUnavailable) 39 | r.Close = true 40 | return 41 | } 42 | 43 | rw.WriteHeader(http.StatusOK) 44 | // #nosec G104 - ignore errors when writing HTTP responses so we don't spam our logs during a DoS 45 | rw.Write([]byte("ok\n")) 46 | r.Close = true 47 | } 48 | -------------------------------------------------------------------------------- /handlers/proxy_picker.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httputil" 6 | "strings" 7 | 8 | "github.com/urfave/negroni/v3" 9 | ) 10 | 11 | type proxyPicker struct { 12 | directorProxy *httputil.ReverseProxy 13 | expect100ContinueRProxy *httputil.ReverseProxy 14 | } 15 | 16 | // Creates a per-request decision on which reverse proxy to use, based on whether 17 | // a request contained an `Expect: 100-continue` header 18 | func NewProxyPicker(directorProxy *httputil.ReverseProxy, expect100ContinueRProxy *httputil.ReverseProxy) negroni.Handler { 19 | return &proxyPicker{ 20 | directorProxy: directorProxy, 21 | expect100ContinueRProxy: expect100ContinueRProxy, 22 | } 23 | } 24 | 25 | func (pp *proxyPicker) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 26 | pickedProxy := pp.directorProxy 27 | if strings.ToLower(r.Header.Get("Expect")) == "100-continue" { 28 | pickedProxy = pp.expect100ContinueRProxy 29 | } 30 | 31 | pickedProxy.ServeHTTP(rw, r) 32 | next(rw, r) 33 | } 34 | -------------------------------------------------------------------------------- /handlers/proxy_picker_test.go: -------------------------------------------------------------------------------- 1 | package handlers_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/http/httputil" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/urfave/negroni/v3" 11 | 12 | "code.cloudfoundry.org/gorouter/handlers" 13 | "code.cloudfoundry.org/gorouter/test_util" 14 | ) 15 | 16 | var _ = Describe("Proxy Picker", func() { 17 | var ( 18 | handler negroni.Handler 19 | resp *httptest.ResponseRecorder 20 | req *http.Request 21 | nextHandler http.HandlerFunc 22 | nextCalled, rproxyCalled, expect100ContinueRProxyCalled bool 23 | rproxy, expect100ContinueRProxy *httputil.ReverseProxy 24 | ) 25 | BeforeEach(func() { 26 | req = test_util.NewRequest("GET", "example.com", "/", nil) 27 | resp = httptest.NewRecorder() 28 | 29 | rproxy = &httputil.ReverseProxy{ 30 | Director: func(r *http.Request) { 31 | rproxyCalled = true 32 | }, 33 | } 34 | expect100ContinueRProxy = &httputil.ReverseProxy{ 35 | Rewrite: func(t *httputil.ProxyRequest) { 36 | expect100ContinueRProxyCalled = true 37 | }, 38 | } 39 | 40 | nextCalled = false 41 | rproxyCalled = false 42 | expect100ContinueRProxyCalled = false 43 | 44 | handler = handlers.NewProxyPicker(rproxy, expect100ContinueRProxy) 45 | nextHandler = http.HandlerFunc(func(http.ResponseWriter, *http.Request) { 46 | nextCalled = true 47 | }) 48 | 49 | }) 50 | 51 | Context("when the request has an Expect: 100-continue", func() { 52 | BeforeEach(func() { 53 | req.Header.Set("Expect", "100-continue") 54 | }) 55 | It("Chooses the expect100ContinueRProxy", func() { 56 | handler.ServeHTTP(resp, req, nextHandler) 57 | Expect(expect100ContinueRProxyCalled).To(BeTrue()) 58 | Expect(rproxyCalled).To(BeFalse()) 59 | }) 60 | 61 | Context("when upper/lower case mixtures", func() { 62 | BeforeEach(func() { 63 | req.Header.Set("Expect", "100-CoNTiNuE") 64 | }) 65 | It("is case insensitive", func() { 66 | handler.ServeHTTP(resp, req, nextHandler) 67 | Expect(expect100ContinueRProxyCalled).To(BeTrue()) 68 | Expect(rproxyCalled).To(BeFalse()) 69 | }) 70 | }) 71 | }) 72 | Context("when the request does not have an Expect: 100-continue", func() { 73 | It("Chooses the main rproxy", func() { 74 | handler.ServeHTTP(resp, req, nextHandler) 75 | Expect(expect100ContinueRProxyCalled).To(BeFalse()) 76 | Expect(rproxyCalled).To(BeTrue()) 77 | }) 78 | 79 | }) 80 | It("calls next()", func() { 81 | handler.ServeHTTP(resp, req, nextHandler) 82 | Expect(nextCalled).To(BeTrue()) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /handlers/proxywriter.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | 7 | "github.com/urfave/negroni/v3" 8 | 9 | log "code.cloudfoundry.org/gorouter/logger" 10 | "code.cloudfoundry.org/gorouter/proxy/utils" 11 | ) 12 | 13 | type proxyWriterHandler struct { 14 | logger *slog.Logger 15 | } 16 | 17 | // NewProxyWriter creates a handler responsible for setting a proxy 18 | // responseWriter on the request and response 19 | func NewProxyWriter(logger *slog.Logger) negroni.Handler { 20 | return &proxyWriterHandler{ 21 | logger: logger, 22 | } 23 | } 24 | 25 | // ServeHTTP wraps the responseWriter in a ProxyResponseWriter 26 | func (p *proxyWriterHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 27 | reqInfo, err := ContextRequestInfo(r) 28 | if err != nil { 29 | log.Panic(p.logger, "request-info-err", log.ErrAttr(err)) 30 | return 31 | } 32 | proxyWriter := utils.NewProxyResponseWriter(rw) 33 | reqInfo.ProxyResponseWriter = proxyWriter 34 | next(proxyWriter, r) 35 | } 36 | -------------------------------------------------------------------------------- /handlers/proxywriter_test.go: -------------------------------------------------------------------------------- 1 | package handlers_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | "github.com/urfave/negroni/v3" 12 | 13 | "code.cloudfoundry.org/gorouter/handlers" 14 | "code.cloudfoundry.org/gorouter/proxy/utils" 15 | "code.cloudfoundry.org/gorouter/test_util" 16 | ) 17 | 18 | var _ = Describe("ProxyWriter", func() { 19 | var ( 20 | handler *negroni.Negroni 21 | 22 | resp http.ResponseWriter 23 | req *http.Request 24 | 25 | nextCalled bool 26 | logger *test_util.TestLogger 27 | 28 | reqChan chan *http.Request 29 | respChan chan http.ResponseWriter 30 | ) 31 | 32 | nextHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 33 | _, err := io.ReadAll(req.Body) 34 | Expect(err).NotTo(HaveOccurred()) 35 | 36 | rw.WriteHeader(http.StatusTeapot) 37 | rw.Write([]byte("I'm a little teapot, short and stout.")) 38 | 39 | reqChan <- req 40 | respChan <- rw 41 | nextCalled = true 42 | }) 43 | 44 | BeforeEach(func() { 45 | logger = test_util.NewTestLogger("test") 46 | body := bytes.NewBufferString("What are you?") 47 | req = test_util.NewRequest("GET", "example.com", "/", body) 48 | resp = httptest.NewRecorder() 49 | 50 | handler = negroni.New() 51 | handler.Use(handlers.NewRequestInfo()) 52 | handler.Use(handlers.NewProxyWriter(logger.Logger)) 53 | handler.UseHandlerFunc(nextHandler) 54 | 55 | reqChan = make(chan *http.Request, 1) 56 | respChan = make(chan http.ResponseWriter, 1) 57 | 58 | nextCalled = false 59 | }) 60 | 61 | AfterEach(func() { 62 | close(reqChan) 63 | close(respChan) 64 | }) 65 | 66 | It("sets the proxy response writer on the request context", func() { 67 | handler.ServeHTTP(resp, req) 68 | var contextReq *http.Request 69 | Eventually(reqChan).Should(Receive(&contextReq)) 70 | reqInfo, err := handlers.ContextRequestInfo(contextReq) 71 | Expect(err).ToNot(HaveOccurred()) 72 | Expect(reqInfo.ProxyResponseWriter).ToNot(BeNil()) 73 | Expect(nextCalled).To(BeTrue(), "Expected the next handler to be called.") 74 | }) 75 | 76 | It("passes the proxy response writer to the next handler", func() { 77 | handler.ServeHTTP(resp, req) 78 | var rw http.ResponseWriter 79 | Eventually(respChan).Should(Receive(&rw)) 80 | Expect(rw).ToNot(BeNil()) 81 | Expect(rw).To(BeAssignableToTypeOf(utils.NewProxyResponseWriter(resp))) 82 | Expect(nextCalled).To(BeTrue(), "Expected the next handler to be called.") 83 | }) 84 | 85 | Context("when request info is not set on the request context", func() { 86 | var badHandler *negroni.Negroni 87 | BeforeEach(func() { 88 | badHandler = negroni.New() 89 | badHandler.Use(handlers.NewProxyWriter(logger.Logger)) 90 | badHandler.UseHandlerFunc(nextHandler) 91 | }) 92 | It("calls Panic on the logger", func() { 93 | Expect(func() { badHandler.ServeHTTP(resp, req) }).To(Panic()) 94 | Expect(nextCalled).To(BeFalse()) 95 | }) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /handlers/query_param.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/urfave/negroni/v3" 9 | 10 | router_http "code.cloudfoundry.org/gorouter/common/http" 11 | ) 12 | 13 | type queryParam struct { 14 | logger *slog.Logger 15 | } 16 | 17 | // NewQueryParam creates a new handler that emits warnings if requests came in with semicolons un-escaped 18 | func NewQueryParam(logger *slog.Logger) negroni.Handler { 19 | return &queryParam{logger: logger} 20 | } 21 | 22 | func (q *queryParam) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 23 | logger := LoggerWithTraceInfo(q.logger, r) 24 | semicolonInParams := strings.Contains(r.RequestURI, ";") 25 | if semicolonInParams { 26 | logger.Warn("deprecated-semicolon-params", slog.String("vcap_request_id", r.Header.Get(VcapRequestIdHeader))) 27 | } 28 | 29 | next(rw, r) 30 | 31 | if semicolonInParams { 32 | rw.Header().Add(router_http.CfRouterError, "deprecated-semicolon-params") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /handlers/reporter.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | "net/textproto" 7 | "time" 8 | 9 | "github.com/urfave/negroni/v3" 10 | 11 | log "code.cloudfoundry.org/gorouter/logger" 12 | "code.cloudfoundry.org/gorouter/metrics" 13 | "code.cloudfoundry.org/gorouter/proxy/utils" 14 | ) 15 | 16 | type reporterHandler struct { 17 | reporter metrics.MetricReporter 18 | logger *slog.Logger 19 | } 20 | 21 | // NewReporter creates a new handler that handles reporting backend 22 | // responses to metrics and missing Content-Length header 23 | func NewReporter(reporter metrics.MetricReporter, logger *slog.Logger) negroni.Handler { 24 | return &reporterHandler{ 25 | reporter: reporter, 26 | logger: logger, 27 | } 28 | } 29 | 30 | // ServeHTTP handles reporting the response after the request has been completed 31 | func (rh *reporterHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 32 | logger := LoggerWithTraceInfo(rh.logger, r) 33 | requestInfo, err := ContextRequestInfo(r) 34 | // logger.Panic does not cause gorouter to exit 1 but rather throw panic with 35 | // stacktrace in error log 36 | if err != nil { 37 | log.Panic(logger, "request-info-err", log.ErrAttr(err)) 38 | return 39 | } 40 | if !validContentLength(r.Header) { 41 | rh.reporter.CaptureEmptyContentLengthHeader() 42 | } 43 | 44 | next(rw, r) 45 | 46 | requestInfo.FinishedAt = time.Now() 47 | if requestInfo.RouteEndpoint == nil { 48 | return 49 | } 50 | 51 | proxyWriter := rw.(utils.ProxyResponseWriter) 52 | rh.reporter.CaptureRoutingResponse(proxyWriter.Status()) 53 | 54 | if requestInfo.AppRequestFinishedAt.Equal(time.Time{}) { 55 | return 56 | } 57 | rh.reporter.CaptureRoutingResponseLatency( 58 | requestInfo.RouteEndpoint, proxyWriter.Status(), 59 | requestInfo.ReceivedAt, requestInfo.AppRequestFinishedAt.Sub(requestInfo.ReceivedAt), 60 | ) 61 | rh.calculateGorouterTime(requestInfo) 62 | rh.reporter.CaptureGorouterTime(requestInfo.GorouterTime) 63 | } 64 | 65 | // calculateGorouterTime 66 | // calculate the gorouter time by subtracting app response time from the total roundtrip time. 67 | // Parameters: 68 | // - requestInfo *RequestInfo 69 | func (rh *reporterHandler) calculateGorouterTime(requestInfo *RequestInfo) { 70 | requestInfo.GorouterTime = -1 71 | appTime := requestInfo.AppRequestFinishedAt.Sub(requestInfo.AppRequestStartedAt).Seconds() 72 | rtTime := requestInfo.FinishedAt.Sub(requestInfo.ReceivedAt).Seconds() 73 | if rtTime >= 0 && appTime >= 0 { 74 | requestInfo.GorouterTime = rtTime - appTime 75 | } 76 | } 77 | 78 | // validContentLength ensures that if the `Content-Length` header is set, it is not empty. 79 | // Request that don't have a `Content-Length` header are OK. 80 | // 81 | // Based on https://github.com/golang/go/blob/33496c2dd310aad1d56bae9febcbd2f02b4985cb/src/net/http/transfer.go#L1051 82 | // http.Header.Get() will return "" for empty headers, or when the header is not set at all. 83 | func validContentLength(header http.Header) bool { 84 | clHeaders := header["Content-Length"] 85 | 86 | if len(clHeaders) == 0 { 87 | return true 88 | } 89 | cl := textproto.TrimString(clHeaders[0]) 90 | 91 | // The Content-Length must be a valid numeric value. 92 | // See: https://datatracker.ietf.org/doc/html/rfc2616/#section-14.13 93 | return cl != "" 94 | } 95 | -------------------------------------------------------------------------------- /handlers/request_id.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | 7 | "github.com/urfave/negroni/v3" 8 | 9 | log "code.cloudfoundry.org/gorouter/logger" 10 | ) 11 | 12 | const ( 13 | VcapRequestIdHeader = "X-Vcap-Request-Id" 14 | ) 15 | 16 | type setVcapRequestIdHeader struct { 17 | logger *slog.Logger 18 | } 19 | 20 | func NewVcapRequestIdHeader(logger *slog.Logger) negroni.Handler { 21 | return &setVcapRequestIdHeader{ 22 | logger: logger, 23 | } 24 | } 25 | 26 | func (s *setVcapRequestIdHeader) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 27 | // The X-Vcap-Request-Id must be set before the request is passed into the 28 | // dropsonde InstrumentedHandler 29 | 30 | requestInfo, err := ContextRequestInfo(r) 31 | if err != nil { 32 | s.logger.Error("failed-to-get-request-info", log.ErrAttr(err)) 33 | return 34 | } 35 | 36 | logger := LoggerWithTraceInfo(s.logger, r) 37 | 38 | traceInfo, err := requestInfo.ProvideTraceInfo() 39 | if err != nil { 40 | logger.Error("failed-to-get-trace-info", log.ErrAttr(err)) 41 | return 42 | } 43 | 44 | r.Header.Set(VcapRequestIdHeader, traceInfo.UUID) 45 | logger.Debug("vcap-request-id-header-set", slog.String("VcapRequestIdHeader", traceInfo.UUID)) 46 | 47 | next(rw, r) 48 | } 49 | -------------------------------------------------------------------------------- /handlers/w3c_tracestate.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // W3CTracestateEntry represents a Tracestate entry: a key value pair 10 | type W3CTracestateEntry struct { 11 | Key string 12 | Val string 13 | } 14 | 15 | func (s W3CTracestateEntry) String() string { 16 | return fmt.Sprintf("%s=%s", s.Key, s.Val) 17 | } 18 | 19 | // W3CTracestate is an alias for a slice W3CTracestateEntry; has helper funcs 20 | type W3CTracestate []W3CTracestateEntry 21 | 22 | func (s W3CTracestate) String() string { 23 | states := make([]string, 0) 24 | 25 | for i := 1; i <= len(s); i++ { 26 | states = append(states, s[len(s)-i].String()) 27 | } 28 | 29 | return strings.Join(states, ",") 30 | } 31 | 32 | func NextW3CTracestate(tenantID string, parentID []byte) W3CTracestateEntry { 33 | var key string 34 | 35 | if tenantID == "" { 36 | key = W3CVendorID 37 | } else { 38 | key = fmt.Sprintf("%s@%s", tenantID, W3CVendorID) 39 | } 40 | 41 | return W3CTracestateEntry{Key: key, Val: hex.EncodeToString(parentID)} 42 | } 43 | 44 | func (s W3CTracestate) Next(tenantID string, parentID []byte) W3CTracestate { 45 | entry := NextW3CTracestate(tenantID, parentID) 46 | 47 | newEntries := make(W3CTracestate, 0) 48 | 49 | // We should not persist entries which have the same key 50 | for _, existingEntry := range s { 51 | if existingEntry.Key != entry.Key { 52 | newEntries = append(newEntries, existingEntry) 53 | } 54 | } 55 | 56 | return append(newEntries, entry) 57 | } 58 | 59 | func ParseW3CTracestate(header string) W3CTracestate { 60 | parsed := make(W3CTracestate, 0) 61 | 62 | // Arbitrarily ignore large traces for performance reasons 63 | if len(header) > 2048 { 64 | return parsed 65 | } 66 | 67 | states := strings.Split(header, ",") 68 | 69 | // We loop in reverse because the headers are oldest at the end 70 | for i := 1; i <= len(states); i++ { 71 | pair := states[len(states)-i] 72 | traceKey, value, found := strings.Cut(strings.TrimSpace(pair), "=") 73 | if found { 74 | parsed = append(parsed, W3CTracestateEntry{Key: traceKey, Val: value}) 75 | } 76 | } 77 | 78 | return parsed 79 | } 80 | 81 | // NewW3CTracestate generates a new set of W3C tracestate pairs according to 82 | // https://www.w3.org/TR/trace-context/#version-format 83 | // Initially it is populated with the current tracestate determined by 84 | // arguments tenantID and parentID 85 | func NewW3CTracestate(tenantID string, parentID []byte) W3CTracestate { 86 | return W3CTracestate{NextW3CTracestate(tenantID, parentID)} 87 | } 88 | -------------------------------------------------------------------------------- /handlers/x_forwarded_proto.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type XForwardedProto struct { 8 | SkipSanitization func(req *http.Request) bool 9 | ForceForwardedProtoHttps bool 10 | SanitizeForwardedProto bool 11 | } 12 | 13 | func (h *XForwardedProto) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 14 | newReq := new(http.Request) 15 | *newReq = *r 16 | skip := h.SkipSanitization(r) 17 | if !skip { 18 | if h.ForceForwardedProtoHttps { 19 | newReq.Header.Set("X-Forwarded-Proto", "https") 20 | } else if h.SanitizeForwardedProto || newReq.Header.Get("X-Forwarded-Proto") == "" { 21 | scheme := "http" 22 | if newReq.TLS != nil { 23 | scheme = "https" 24 | } 25 | newReq.Header.Set("X-Forwarded-Proto", scheme) 26 | } 27 | } 28 | 29 | next(rw, newReq) 30 | } 31 | -------------------------------------------------------------------------------- /handlers/zipkin.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | 7 | "github.com/openzipkin/zipkin-go/propagation/b3" 8 | "github.com/urfave/negroni/v3" 9 | 10 | log "code.cloudfoundry.org/gorouter/logger" 11 | ) 12 | 13 | // Zipkin is a handler that sets Zipkin headers on requests 14 | type Zipkin struct { 15 | zipkinEnabled bool 16 | logger *slog.Logger 17 | } 18 | 19 | var _ negroni.Handler = new(Zipkin) 20 | 21 | // NewZipkin creates a new handler that sets Zipkin headers on requests 22 | func NewZipkin(enabled bool, logger *slog.Logger) *Zipkin { 23 | return &Zipkin{ 24 | zipkinEnabled: enabled, 25 | logger: logger, 26 | } 27 | } 28 | 29 | func (z *Zipkin) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 30 | defer next(rw, r) 31 | 32 | logger := LoggerWithTraceInfo(z.logger, r) 33 | 34 | if !z.zipkinEnabled { 35 | return 36 | } 37 | 38 | requestInfo, err := ContextRequestInfo(r) 39 | if err != nil { 40 | logger.Error("failed-to-get-request-info", log.ErrAttr(err)) 41 | return 42 | } 43 | 44 | existingContext := r.Header.Get(b3.Context) 45 | if existingContext != "" { 46 | logger.Debug("b3-header-exists", 47 | slog.String("b3", existingContext), 48 | ) 49 | 50 | sc, err := b3.ParseSingleHeader(existingContext) 51 | if err != nil { 52 | logger.Error("failed-to-parse-single-header", log.ErrAttr(err)) 53 | } else { 54 | err = requestInfo.SetTraceInfo(sc.TraceID.String(), sc.ID.String()) 55 | if err != nil { 56 | logger.Error("failed-to-set-trace-info", log.ErrAttr(err)) 57 | } else { 58 | return 59 | } 60 | } 61 | } 62 | 63 | existingTraceID := r.Header.Get(b3.TraceID) 64 | existingSpanID := r.Header.Get(b3.SpanID) 65 | if existingTraceID != "" && existingSpanID != "" { 66 | sc, err := b3.ParseHeaders( 67 | existingTraceID, 68 | existingSpanID, 69 | r.Header.Get(b3.ParentSpanID), 70 | r.Header.Get(b3.Sampled), 71 | r.Header.Get(b3.Flags), 72 | ) 73 | if err != nil { 74 | logger.Info("failed-to-parse-b3-trace-id", log.ErrAttr(err)) 75 | return 76 | } 77 | r.Header.Set(b3.Context, b3.BuildSingleHeader(*sc)) 78 | 79 | logger.Debug("b3-trace-id-span-id-header-exists", 80 | slog.String("trace-id", existingTraceID), 81 | slog.String("span-id", existingSpanID), 82 | ) 83 | 84 | err = requestInfo.SetTraceInfo(sc.TraceID.String(), sc.ID.String()) 85 | if err != nil { 86 | logger.Error("failed-to-set-trace-info", log.ErrAttr(err)) 87 | } else { 88 | return 89 | } 90 | } 91 | 92 | traceInfo, err := requestInfo.ProvideTraceInfo() 93 | if err != nil { 94 | logger.Error("failed-to-get-trace-info", log.ErrAttr(err)) 95 | return 96 | } 97 | 98 | r.Header.Set(b3.TraceID, traceInfo.TraceID) 99 | r.Header.Set(b3.SpanID, traceInfo.SpanID) 100 | r.Header.Set(b3.Context, traceInfo.TraceID+"-"+traceInfo.SpanID) 101 | } 102 | 103 | // HeadersToLog specifies the headers which should be logged if Zipkin headers 104 | // are enabled 105 | func (z *Zipkin) HeadersToLog() []string { 106 | if !z.zipkinEnabled { 107 | return []string{} 108 | } 109 | 110 | return []string{ 111 | b3.TraceID, 112 | b3.SpanID, 113 | b3.ParentSpanID, 114 | b3.Context, 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /integration/access_log_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "net/http" 5 | 6 | "code.cloudfoundry.org/gorouter/test/common" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("Access Log", func() { 13 | var ( 14 | testState *testState 15 | done chan bool 16 | logs <-chan string 17 | ) 18 | 19 | BeforeEach(func() { 20 | testState = NewTestState() 21 | }) 22 | 23 | JustBeforeEach(func() { 24 | testState.StartGorouterOrFail() 25 | }) 26 | 27 | AfterEach(func() { 28 | testState.StopAndCleanup() 29 | }) 30 | 31 | Context("when using syslog", func() { 32 | BeforeEach(func() { 33 | // disable file logging 34 | testState.cfg.AccessLog.EnableStreaming = true 35 | testState.cfg.AccessLog.File = "" 36 | // generic tag 37 | testState.cfg.Logging.Syslog = "gorouter" 38 | }) 39 | 40 | Context("via UDP", func() { 41 | BeforeEach(func() { 42 | testState.cfg.Logging.SyslogNetwork = "udp" 43 | done = make(chan bool) 44 | testState.cfg.Logging.SyslogAddr, logs = common.TestUdp(done) 45 | }) 46 | 47 | AfterEach(func() { 48 | close(done) 49 | }) 50 | 51 | It("properly emits access logs", func() { 52 | req := testState.newGetRequest("https://foobar.cloudfoundry.org") 53 | res, err := testState.client.Do(req) 54 | Expect(err).NotTo(HaveOccurred()) 55 | Expect(res.StatusCode).To(Equal(http.StatusNotFound)) 56 | 57 | log := <-logs 58 | 59 | Expect(log).To(ContainSubstring(`x_cf_routererror:"unknown_route"`)) 60 | Expect(log).To(ContainSubstring(`"GET / HTTP/1.1" 404`)) 61 | Expect(log).To(ContainSubstring("foobar.cloudfoundry.org")) 62 | 63 | // ensure we don't see any excess access logs 64 | Consistently(func() int { return len(logs) }).Should(Equal(0)) 65 | }) 66 | }) 67 | 68 | Context("via TCP", func() { 69 | BeforeEach(func() { 70 | testState.cfg.Logging.SyslogNetwork = "tcp" 71 | done = make(chan bool) 72 | testState.cfg.Logging.SyslogAddr, logs = common.TestTcp(done) 73 | }) 74 | 75 | AfterEach(func() { 76 | close(done) 77 | }) 78 | 79 | It("properly emits successful requests", func() { 80 | req := testState.newGetRequest("https://foobar.cloudfoundry.org") 81 | res, err := testState.client.Do(req) 82 | Expect(err).NotTo(HaveOccurred()) 83 | Expect(res.StatusCode).To(Equal(http.StatusNotFound)) 84 | 85 | log := <-logs 86 | 87 | Expect(log).To(ContainSubstring(`x_cf_routererror:"unknown_route"`)) 88 | Expect(log).To(ContainSubstring(`"GET / HTTP/1.1" 404`)) 89 | Expect(log).To(ContainSubstring("foobar.cloudfoundry.org")) 90 | 91 | // ensure we don't see any excess access logs 92 | Consistently(func() int { return len(logs) }).Should(Equal(0)) 93 | }) 94 | }) 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /integration/large_request_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | 12 | "code.cloudfoundry.org/gorouter/route" 13 | "code.cloudfoundry.org/gorouter/test/common" 14 | "code.cloudfoundry.org/gorouter/test_util" 15 | ) 16 | 17 | var _ = Describe("Large requests", func() { 18 | var ( 19 | testState *testState 20 | appURL string 21 | echoApp *common.TestApp 22 | ) 23 | 24 | BeforeEach(func() { 25 | testState = NewTestState() 26 | testState.EnableAccessLog() 27 | testState.EnableMetron() 28 | testState.cfg.MaxRequestHeaderBytes = 1 * 1024 // 1kb 29 | testState.StartGorouterOrFail() 30 | 31 | appURL = "echo-app." + test_util.LocalhostDNS 32 | 33 | echoApp = newEchoApp([]route.Uri{route.Uri(appURL)}, testState.cfg.Port, testState.mbusClient, time.Millisecond, "") 34 | echoApp.TlsRegister(testState.trustedBackendServerCertSAN) 35 | echoApp.TlsListen(testState.trustedBackendTLSConfig) 36 | echoApp.WaitUntilReady() 37 | }) 38 | 39 | AfterEach(func() { 40 | if testState != nil { 41 | testState.StopAndCleanup() 42 | } 43 | }) 44 | 45 | It("logs requests that exceed the MaxHeaderBytes configuration (but are lower than 1MB)", func() { 46 | pathSize := 2 * 1024 // 2kb 47 | path := strings.Repeat("a", pathSize) 48 | 49 | req := testState.newGetRequest(fmt.Sprintf("http://%s/%s", appURL, path)) 50 | 51 | resp, err := testState.client.Do(req) 52 | Expect(err).NotTo(HaveOccurred()) 53 | 54 | Expect(resp.StatusCode).To(Equal(431)) 55 | 56 | getAccessLogContents := func() string { 57 | accessLogContents, err := os.ReadFile(testState.AccessLogFilePath()) 58 | Expect(err).NotTo(HaveOccurred()) 59 | return string(accessLogContents) 60 | } 61 | 62 | Eventually(getAccessLogContents).Should(MatchRegexp("echo-app.*/aaaaaaaa.*431.*x_cf_routererror:\"max-request-size-exceeded\"")) 63 | Eventually(func() []string { 64 | var messages []string 65 | events := testState.MetronEvents() 66 | for _, event := range events { 67 | if event.EventType == "LogMessage" { 68 | messages = append(messages, event.Name) 69 | } 70 | } 71 | return messages 72 | }).Should(ContainElement(MatchRegexp("echo-app.*/aaaaaaaa.*431.*x_cf_routererror:\"max-request-size-exceeded\""))) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /integration/large_upload_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | nats "github.com/nats-io/nats.go" 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | 15 | "code.cloudfoundry.org/gorouter/route" 16 | "code.cloudfoundry.org/gorouter/test/common" 17 | "code.cloudfoundry.org/gorouter/test_util" 18 | ) 19 | 20 | var _ = Describe("Large upload", func() { 21 | var ( 22 | testState *testState 23 | ) 24 | 25 | BeforeEach(func() { 26 | testState = NewTestState() 27 | testState.StartGorouterOrFail() 28 | }) 29 | 30 | AfterEach(func() { 31 | if testState != nil { 32 | testState.StopAndCleanup() 33 | } 34 | }) 35 | 36 | Context("when a client tries to upload a large file", func() { 37 | var appURL string 38 | var echoApp *common.TestApp 39 | 40 | BeforeEach(func() { 41 | appURL = "echo-app." + test_util.LocalhostDNS 42 | 43 | echoApp = newEchoApp([]route.Uri{route.Uri(appURL)}, testState.cfg.Port, testState.mbusClient, time.Millisecond, "") 44 | echoApp.TlsRegister(testState.trustedBackendServerCertSAN) 45 | errChan := echoApp.TlsListen(testState.trustedBackendTLSConfig) 46 | Consistently(errChan).ShouldNot(Receive()) 47 | }) 48 | 49 | It("the connection remains open for the entire upload", func() { 50 | // We are afraid that this test might become flaky at some point 51 | // If it does, try increasing the size of the payload 52 | // or maybe decreasing it... 53 | 54 | // We have empirically tested that this number needs to be quite large in 55 | // order for the test to be testing the right thing 56 | 57 | payloadSize := 2 << 24 58 | // 2^24 ~= 17Mb 59 | 60 | payload := strings.Repeat("a", payloadSize) 61 | 62 | req := testState.newPostRequest( 63 | fmt.Sprintf("http://%s", appURL), 64 | bytes.NewReader([]byte(payload)), 65 | ) 66 | resp, err := testState.client.Do(req) 67 | Expect(err).NotTo(HaveOccurred()) 68 | Expect(resp.StatusCode).To(Equal(200)) 69 | respBody, err := io.ReadAll(resp.Body) 70 | Expect(err).NotTo(HaveOccurred()) 71 | resp.Body.Close() 72 | 73 | Expect(respBody).To(HaveLen(payloadSize)) 74 | }) 75 | }) 76 | }) 77 | 78 | func newEchoApp(urls []route.Uri, rPort uint16, mbusClient *nats.Conn, delay time.Duration, routeServiceUrl string) *common.TestApp { 79 | app := common.NewTestApp(urls, rPort, mbusClient, nil, routeServiceUrl) 80 | app.AddHandler("/", func(w http.ResponseWriter, r *http.Request) { 81 | defer GinkgoRecover() 82 | 83 | if r.Method == http.MethodPost { 84 | buf := make([]byte, 4096) 85 | 86 | i := 0 87 | for { 88 | n, err := r.Body.Read(buf) 89 | if n > 0 { 90 | i++ 91 | _, err = w.Write(buf[:n]) 92 | Expect(err).NotTo(HaveOccurred(), "Encountered unexpected write error") 93 | } else if err != nil { 94 | if err != io.EOF { 95 | Expect(err).NotTo(HaveOccurred(), "Encountered unexpected read error") 96 | } 97 | break 98 | } 99 | } 100 | } 101 | }) 102 | 103 | return app 104 | } 105 | -------------------------------------------------------------------------------- /integration/redirect_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("Headers", func() { 13 | var ( 14 | testState *testState 15 | 16 | testAppRoute string 17 | testApp *StateTrackingTestApp 18 | ) 19 | 20 | BeforeEach(func() { 21 | testState = NewTestState() 22 | testApp = NewUnstartedTestApp(http.HandlerFunc( 23 | func(w http.ResponseWriter, r *http.Request) { 24 | defer GinkgoRecover() 25 | _, err := io.ReadAll(r.Body) 26 | Expect(err).NotTo(HaveOccurred()) 27 | w.Header().Set("Location", "redirect.com") 28 | w.WriteHeader(http.StatusFound) 29 | })) 30 | testAppRoute = "potato.potato" 31 | }) 32 | 33 | AfterEach(func() { 34 | if testState != nil { 35 | testState.StopAndCleanup() 36 | } 37 | testApp.Close() 38 | }) 39 | 40 | Context("When an app returns a 3xx-redirect", func() { 41 | BeforeEach(func() { 42 | testState.StartGorouterOrFail() 43 | testApp.Start() 44 | testState.register(testApp.Server, testAppRoute) 45 | }) 46 | 47 | It("does not follow the redirect and instead forwards it to the client", func() { 48 | req := testState.newGetRequest(fmt.Sprintf("http://%s", testAppRoute)) 49 | 50 | // this makes the test client NOT follow redirects, so that we can 51 | // test that the return code is indeed 3xx 52 | testState.client.CheckRedirect = func(req *http.Request, via []*http.Request) error { 53 | return http.ErrUseLastResponse 54 | } 55 | 56 | resp, err := testState.client.Do(req) 57 | Expect(err).NotTo(HaveOccurred()) 58 | Expect(resp.StatusCode).To(Equal(http.StatusFound)) 59 | 60 | _, err = io.ReadAll(resp.Body) 61 | Expect(err).NotTo(HaveOccurred()) 62 | resp.Body.Close() 63 | }) 64 | }) 65 | 66 | }) 67 | -------------------------------------------------------------------------------- /integration/web_socket_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "time" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | 12 | "code.cloudfoundry.org/gorouter/route" 13 | "code.cloudfoundry.org/gorouter/test" 14 | "code.cloudfoundry.org/gorouter/test/common" 15 | "code.cloudfoundry.org/gorouter/test_util" 16 | ) 17 | 18 | var _ = Describe("Websockets", func() { 19 | var ( 20 | testState *testState 21 | ) 22 | 23 | BeforeEach(func() { 24 | testState = NewTestState() 25 | testState.StartGorouterOrFail() 26 | }) 27 | 28 | AfterEach(func() { 29 | if testState != nil { 30 | testState.StopAndCleanup() 31 | } 32 | }) 33 | 34 | Context("When gorouter attempts to connect to a websocket app that fails", func() { 35 | assertWebsocketFailure := func(wsApp *common.TestApp) { 36 | routesURI := fmt.Sprintf("http://%s:%s@%s:%d/routes", testState.cfg.Status.User, testState.cfg.Status.Pass, "localhost", testState.cfg.Status.Routes.Port) 37 | 38 | Eventually(func() bool { return appRegistered(routesURI, wsApp) }, "2s", "500ms").Should(BeTrue()) 39 | 40 | wsApp.WaitUntilReady() 41 | 42 | conn, err := net.Dial("tcp", fmt.Sprintf("ws-app.%s:%d", test_util.LocalhostDNS, testState.cfg.Port)) 43 | Expect(err).NotTo(HaveOccurred()) 44 | 45 | x := test_util.NewHttpConn(conn) 46 | 47 | req := test_util.NewRequest("GET", "ws-app."+test_util.LocalhostDNS, "/chat", nil) 48 | req.Header.Set("Upgrade", "websocket") 49 | req.Header.Set("Connection", "upgrade") 50 | x.WriteRequest(req) 51 | 52 | resp, _ := x.ReadResponse() 53 | Expect(resp.StatusCode).To(Equal(http.StatusBadGateway)) 54 | 55 | x.Close() 56 | } 57 | 58 | It("returns a status code indicating failure", func() { 59 | wsApp := test.NewFailingWebSocketApp([]route.Uri{"ws-app." + test_util.LocalhostDNS}, testState.cfg.Port, testState.mbusClient, time.Millisecond, "") 60 | wsApp.TlsRegister(testState.trustedBackendServerCertSAN) 61 | wsApp.TlsListen(testState.trustedBackendTLSConfig) 62 | 63 | assertWebsocketFailure(wsApp) 64 | }) 65 | 66 | }) 67 | 68 | }) 69 | -------------------------------------------------------------------------------- /logger/logger_suite_test.go: -------------------------------------------------------------------------------- 1 | package logger_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestLogger(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Logger Suite") 13 | } 14 | -------------------------------------------------------------------------------- /logger/logger_test_init.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | //import ( 4 | // "log/slog" 5 | // 6 | // "go.uber.org/zap/zapcore" 7 | //) 8 | // 9 | //func InitializeLogger(level string, timestampFormat string, writeSyncer zapcore.WriteSyncer) *slog.Logger { 10 | // return initializeLogger(level, timestampFormat, writeSyncer) 11 | //} 12 | -------------------------------------------------------------------------------- /mbus/mbus_suite_test.go: -------------------------------------------------------------------------------- 1 | package mbus_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestMbus(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Mbus Suite") 13 | } 14 | -------------------------------------------------------------------------------- /mbus/registry_message_test.go: -------------------------------------------------------------------------------- 1 | package mbus_test 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | 9 | . "code.cloudfoundry.org/gorouter/mbus" 10 | ) 11 | 12 | var _ = Describe("RegistryMessage", func() { 13 | Describe("ValidateMessage", func() { 14 | var message *RegistryMessage 15 | var payload []byte 16 | 17 | JustBeforeEach(func() { 18 | message = new(RegistryMessage) 19 | err := json.Unmarshal(payload, message) 20 | Expect(err).NotTo(HaveOccurred()) 21 | }) 22 | 23 | Describe("With a payload with no route service url", func() { 24 | BeforeEach(func() { 25 | payload = []byte(`{"dea":"dea1","app":"app1","uris":["test.com"],"host":"1.2.3.4","port":1234,"tags":{},"private_instance_id":"private_instance_id"}`) 26 | }) 27 | 28 | It("passes validation", func() { 29 | Expect(message.ValidateMessage()).To(BeTrue()) 30 | }) 31 | }) 32 | 33 | Describe("With a payload with an empty route service url", func() { 34 | BeforeEach(func() { 35 | payload = []byte(`{"dea":"dea1","app":"app1","uris":["test.com"],"host":"1.2.3.4","port":1234,"tags":{},"route_service_url":"","private_instance_id":"private_instance_id"}`) 36 | }) 37 | 38 | It("passes validation", func() { 39 | Expect(message.ValidateMessage()).To(BeTrue()) 40 | }) 41 | }) 42 | 43 | Describe("With a payload with an https route service url", func() { 44 | BeforeEach(func() { 45 | payload = []byte(`{"dea":"dea1","app":"app1","uris":["test.com"],"host":"1.2.3.4","port":1234,"tags":{},"route_service_url":"https://www.my-route.me","private_instance_id":"private_instance_id"}`) 46 | }) 47 | 48 | It("passes validation", func() { 49 | Expect(message.ValidateMessage()).To(BeTrue()) 50 | }) 51 | }) 52 | 53 | Describe("With a payload with an http route service url", func() { 54 | BeforeEach(func() { 55 | payload = []byte(`{"dea":"dea1","app":"app1","uris":["test.com"],"host":"1.2.3.4","port":1234,"tags":{},"route_service_url":"http://www.my-insecure-route.com","private_instance_id":"private_instance_id"}`) 56 | }) 57 | 58 | It("fails validation", func() { 59 | Expect(message.ValidateMessage()).To(BeFalse()) 60 | }) 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /metrics/metrics_suite_test.go: -------------------------------------------------------------------------------- 1 | package metrics_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestMetrics(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Metrics Suite") 13 | } 14 | -------------------------------------------------------------------------------- /metrics/monitor/fd_monitor.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | "os/exec" 8 | "runtime" 9 | "strings" 10 | "time" 11 | 12 | log "code.cloudfoundry.org/gorouter/logger" 13 | "code.cloudfoundry.org/gorouter/metrics" 14 | ) 15 | 16 | type FileDescriptor struct { 17 | path string 18 | ticker *time.Ticker 19 | reporter metrics.MetricReporter 20 | logger *slog.Logger 21 | } 22 | 23 | func NewFileDescriptor(path string, ticker *time.Ticker, reporter metrics.MetricReporter, logger *slog.Logger) *FileDescriptor { 24 | return &FileDescriptor{ 25 | path: path, 26 | ticker: ticker, 27 | reporter: reporter, 28 | logger: logger, 29 | } 30 | } 31 | 32 | func (f *FileDescriptor) Run(signals <-chan os.Signal, ready chan<- struct{}) error { 33 | close(ready) 34 | for { 35 | select { 36 | case <-f.ticker.C: 37 | numFds := 0 38 | if runtime.GOOS == "linux" { 39 | dirEntries, err := os.ReadDir(f.path) 40 | if err != nil { 41 | f.logger.Error("error-reading-filedescriptor-path", log.ErrAttr(err)) 42 | break 43 | } 44 | numFds = symlinks(dirEntries) 45 | } else if runtime.GOOS == "darwin" { 46 | dirEntries, err := os.ReadDir(f.path) 47 | if err != nil { 48 | // no /proc on MacOS, falling back to lsof 49 | out, err := exec.Command("/bin/sh", "-c", fmt.Sprintf("lsof -p %v", os.Getpid())).Output() 50 | if err != nil { 51 | f.logger.Error("error-running-lsof", log.ErrAttr(err)) 52 | break 53 | } 54 | lines := strings.Split(string(out), "\n") 55 | numFds = len(lines) - 1 //cut the table header 56 | } else { 57 | numFds = symlinks(dirEntries) 58 | } 59 | } 60 | f.reporter.CaptureFoundFileDescriptors(numFds) 61 | 62 | case <-signals: 63 | f.logger.Info("exited") 64 | return nil 65 | } 66 | } 67 | } 68 | 69 | func symlinks(fileInfos []os.DirEntry) (count int) { 70 | for i := 0; i < len(fileInfos); i++ { 71 | if fileInfos[i].Type()&os.ModeSymlink == os.ModeSymlink { 72 | count++ 73 | } 74 | } 75 | return count 76 | } 77 | -------------------------------------------------------------------------------- /metrics/monitor/fd_monitor_test.go: -------------------------------------------------------------------------------- 1 | package monitor_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strconv" 7 | "time" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | "github.com/tedsuo/ifrit" 12 | 13 | "code.cloudfoundry.org/gorouter/metrics/fakes" 14 | "code.cloudfoundry.org/gorouter/metrics/monitor" 15 | "code.cloudfoundry.org/gorouter/test_util" 16 | ) 17 | 18 | var _ = Describe("FileDescriptor", func() { 19 | var ( 20 | reporter *fakes.FakeMetricReporter 21 | procPath string 22 | tr *time.Ticker 23 | logger *test_util.TestLogger 24 | ) 25 | 26 | BeforeEach(func() { 27 | tr = time.NewTicker(1 * time.Second) 28 | reporter = new(fakes.FakeMetricReporter) 29 | logger = test_util.NewTestLogger("test") 30 | }) 31 | 32 | AfterEach(func() { 33 | tr.Stop() 34 | Expect(os.RemoveAll(procPath)).To(Succeed()) 35 | }) 36 | 37 | It("exits when os signal is received", func() { 38 | fdMonitor := monitor.NewFileDescriptor(procPath, tr, reporter, logger.Logger) 39 | process := ifrit.Invoke(fdMonitor) 40 | Eventually(process.Ready()).Should(BeClosed()) 41 | 42 | process.Signal(os.Interrupt) 43 | var err error 44 | Eventually(process.Wait()).Should(Receive(&err)) 45 | Expect(err).ToNot(HaveOccurred()) 46 | 47 | }) 48 | 49 | It("monitors all the open file descriptors for a given pid", func() { 50 | procPath = createTestPath("", 10) 51 | fdMonitor := monitor.NewFileDescriptor(procPath, tr, reporter, logger.Logger) 52 | process := ifrit.Invoke(fdMonitor) 53 | Eventually(process.Ready()).Should(BeClosed()) 54 | 55 | Eventually(reporter.CaptureFoundFileDescriptorsCallCount, "2s").Should(Equal(1)) 56 | files := reporter.CaptureFoundFileDescriptorsArgsForCall(0) 57 | Expect(files).To(BeEquivalentTo(10)) 58 | 59 | // create some more FDs 60 | createTestPath(procPath, 20) 61 | 62 | Eventually(reporter.CaptureFoundFileDescriptorsCallCount, "2s").Should(Equal(2)) 63 | files = reporter.CaptureFoundFileDescriptorsArgsForCall(1) 64 | Expect(files).To(BeEquivalentTo(20)) 65 | }) 66 | }) 67 | 68 | func createTestPath(path string, symlink int) string { 69 | // Create symlink structure similar to /proc/pid/fd in linux file system 70 | createSymlink := func(dir string, n int) { 71 | fd, err := os.CreateTemp(dir, "socket") 72 | Expect(err).NotTo(HaveOccurred()) 73 | for i := 0; i < n; i++ { 74 | fdId := strconv.Itoa(i) 75 | symlink := filepath.Join(dir, fdId) 76 | os.Symlink(fd.Name()+fdId, symlink) 77 | 78 | } 79 | } 80 | if path != "" { 81 | createSymlink(path, symlink) 82 | return path 83 | } 84 | procPath, err := os.MkdirTemp("", "proc") 85 | Expect(err).NotTo(HaveOccurred()) 86 | createSymlink(procPath, symlink) 87 | return procPath 88 | } 89 | -------------------------------------------------------------------------------- /metrics/monitor/nats_monitor.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | "time" 7 | 8 | log "code.cloudfoundry.org/gorouter/logger" 9 | "code.cloudfoundry.org/gorouter/metrics" 10 | ) 11 | 12 | //go:generate counterfeiter -o ../fakes/fake_subscriber.go . Subscriber 13 | type Subscriber interface { 14 | Pending() (int, error) 15 | Dropped() (int, error) 16 | } 17 | 18 | type NATSMonitor struct { 19 | Subscriber Subscriber 20 | Reporter metrics.MetricReporter 21 | TickChan <-chan time.Time 22 | Logger *slog.Logger 23 | } 24 | 25 | func (n *NATSMonitor) Run(signals <-chan os.Signal, ready chan<- struct{}) error { 26 | close(ready) 27 | for { 28 | select { 29 | case <-n.TickChan: 30 | queuedMsgs, err := n.Subscriber.Pending() 31 | if err != nil { 32 | n.Logger.Error("error-retrieving-nats-subscription-pending-messages", log.ErrAttr(err)) 33 | } 34 | n.Reporter.CaptureNATSBufferedMessages(queuedMsgs) 35 | 36 | droppedMsgs, err := n.Subscriber.Dropped() 37 | if err != nil { 38 | n.Logger.Error("error-retrieving-nats-subscription-dropped-messages", log.ErrAttr(err)) 39 | } 40 | n.Reporter.CaptureNATSDroppedMessages(droppedMsgs) 41 | case <-signals: 42 | n.Logger.Info("exited") 43 | return nil 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /metrics/monitor/nats_monitor_test.go: -------------------------------------------------------------------------------- 1 | package monitor_test 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "time" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gbytes" 11 | "github.com/tedsuo/ifrit" 12 | 13 | "code.cloudfoundry.org/gorouter/metrics/fakes" 14 | "code.cloudfoundry.org/gorouter/metrics/monitor" 15 | "code.cloudfoundry.org/gorouter/test_util" 16 | ) 17 | 18 | var _ = Describe("NATSMonitor", func() { 19 | var ( 20 | subscriber *fakes.FakeSubscriber 21 | reporter *fakes.FakeMetricReporter 22 | ch chan time.Time 23 | natsMonitor *monitor.NATSMonitor 24 | logger *test_util.TestLogger 25 | process ifrit.Process 26 | ) 27 | 28 | BeforeEach(func() { 29 | ch = make(chan time.Time) 30 | subscriber = new(fakes.FakeSubscriber) 31 | reporter = new(fakes.FakeMetricReporter) 32 | 33 | logger = test_util.NewTestLogger("test") 34 | 35 | natsMonitor = &monitor.NATSMonitor{ 36 | Subscriber: subscriber, 37 | Reporter: reporter, 38 | TickChan: ch, 39 | Logger: logger.Logger, 40 | } 41 | 42 | process = ifrit.Invoke(natsMonitor) 43 | Eventually(process.Ready()).Should(BeClosed()) 44 | }) 45 | 46 | It("exits when os signal is received", func() { 47 | process.Signal(os.Interrupt) 48 | var err error 49 | Eventually(process.Wait()).Should(Receive(&err)) 50 | Expect(err).ToNot(HaveOccurred()) 51 | }) 52 | 53 | It("sends a buffered_messages metric on a time interval", func() { 54 | subscriber.PendingReturns(1000, nil) 55 | ch <- time.Time{} 56 | ch <- time.Time{} // an extra tick is to make sure the time ticked at least once 57 | 58 | Expect(subscriber.PendingCallCount()).To(BeNumerically(">=", 1)) 59 | Expect(reporter.CaptureNATSBufferedMessagesCallCount()).To(BeNumerically(">=", 1)) 60 | messages := reporter.CaptureNATSBufferedMessagesArgsForCall(0) 61 | Expect(messages).To(Equal(1000)) 62 | }) 63 | 64 | It("sends a total_dropped_messages metric on a time interval", func() { 65 | subscriber.DroppedReturns(2000, nil) 66 | ch <- time.Time{} 67 | ch <- time.Time{} // an extra tick is to make sure the time ticked at least once 68 | 69 | Expect(subscriber.DroppedCallCount()).To(BeNumerically(">=", 1)) 70 | Expect(reporter.CaptureNATSDroppedMessagesCallCount()).To(BeNumerically(">=", 1)) 71 | messages := reporter.CaptureNATSDroppedMessagesArgsForCall(1) 72 | Expect(messages).To(Equal(2000)) 73 | }) 74 | 75 | Context("when it fails to retrieve queued messages", func() { 76 | BeforeEach(func() { 77 | subscriber.PendingReturns(-1, errors.New("failed")) 78 | }) 79 | It("should log an error when it fails to retrieve queued messages", func() { 80 | ch <- time.Time{} 81 | ch <- time.Time{} 82 | 83 | Eventually(logger).Should(gbytes.Say("error-retrieving-nats-subscription-pending-messages")) 84 | }) 85 | }) 86 | 87 | Context("when it fails to retrieve dropped messages", func() { 88 | BeforeEach(func() { 89 | subscriber.DroppedReturns(-1, errors.New("failed")) 90 | }) 91 | It("should log an error when it fails to retrieve queued messages", func() { 92 | ch <- time.Time{} 93 | ch <- time.Time{} 94 | 95 | Eventually(logger).Should(gbytes.Say("error-retrieving-nats-subscription-dropped-messages")) 96 | }) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /metrics/monitor/uptime_monitor.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "log/slog" 5 | "time" 6 | 7 | "github.com/cloudfoundry/dropsonde/metrics" 8 | 9 | log "code.cloudfoundry.org/gorouter/logger" 10 | ) 11 | 12 | type Uptime struct { 13 | logger *slog.Logger 14 | interval time.Duration 15 | started int64 16 | doneChan chan chan struct{} 17 | } 18 | 19 | func NewUptime(interval time.Duration, logger *slog.Logger) *Uptime { 20 | return &Uptime{ 21 | interval: interval, 22 | started: time.Now().Unix(), 23 | doneChan: make(chan chan struct{}), 24 | logger: logger, 25 | } 26 | } 27 | 28 | func (u *Uptime) Start() { 29 | ticker := time.NewTicker(u.interval) 30 | 31 | for { 32 | select { 33 | case <-ticker.C: 34 | err := metrics.SendValue("uptime", float64(time.Now().Unix()-u.started), "seconds") 35 | if err != nil { 36 | u.logger.Debug("failed-to-send-metric", log.ErrAttr(err), slog.String("metric", "uptime")) 37 | } 38 | case stopped := <-u.doneChan: 39 | ticker.Stop() 40 | close(stopped) 41 | return 42 | } 43 | } 44 | } 45 | 46 | func (u *Uptime) Stop() { 47 | stopped := make(chan struct{}) 48 | u.doneChan <- stopped 49 | <-stopped 50 | } 51 | -------------------------------------------------------------------------------- /metrics/monitor/uptime_monitor_test.go: -------------------------------------------------------------------------------- 1 | package monitor_test 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/cloudfoundry/sonde-go/events" 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | "google.golang.org/protobuf/proto" 10 | 11 | "code.cloudfoundry.org/gorouter/metrics/monitor" 12 | "code.cloudfoundry.org/gorouter/test_util" 13 | ) 14 | 15 | const ( 16 | interval = 100 * time.Millisecond 17 | ) 18 | 19 | var _ = Describe("Uptime", func() { 20 | var ( 21 | uptime *monitor.Uptime 22 | logger *test_util.TestLogger 23 | ) 24 | 25 | BeforeEach(func() { 26 | logger = test_util.NewTestLogger("test") 27 | fakeEventEmitter.Reset() 28 | uptime = monitor.NewUptime(interval, logger.Logger) 29 | go uptime.Start() 30 | }) 31 | 32 | Context("stops automatically", func() { 33 | 34 | AfterEach(func() { 35 | uptime.Stop() 36 | }) 37 | 38 | It("returns a value metric containing uptime after specified time", func() { 39 | Eventually(fakeEventEmitter.GetMessages).Should(HaveLen(1)) 40 | 41 | metric := fakeEventEmitter.GetMessages()[0].Event.(*events.ValueMetric) 42 | Expect(metric.Name).To(Equal(proto.String("uptime"))) 43 | Expect(metric.Unit).To(Equal(proto.String("seconds"))) 44 | }) 45 | 46 | It("reports increasing uptime value", func() { 47 | Eventually(fakeEventEmitter.GetMessages).Should(HaveLen(1)) 48 | metric := fakeEventEmitter.GetMessages()[0].Event.(*events.ValueMetric) 49 | uptime := *(metric.Value) 50 | 51 | Eventually(getLatestUptime, "2s").Should(BeNumerically(">", uptime)) 52 | }) 53 | }) 54 | 55 | It("stops the monitor and respective ticker", func() { 56 | Eventually(func() int { return len(fakeEventEmitter.GetMessages()) }).Should(BeNumerically(">=", 1)) 57 | 58 | uptime.Stop() 59 | 60 | current := getLatestUptime() 61 | Consistently(getLatestUptime, 2).Should(Equal(current)) 62 | }) 63 | }) 64 | 65 | func getLatestUptime() float64 { 66 | lastMsgIndex := len(fakeEventEmitter.GetMessages()) - 1 67 | return *fakeEventEmitter.GetMessages()[lastMsgIndex].Event.(*events.ValueMetric).Value 68 | } 69 | -------------------------------------------------------------------------------- /metrics/monitor/uptime_suite_test.go: -------------------------------------------------------------------------------- 1 | package monitor_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudfoundry/dropsonde/emitter/fake" 7 | "github.com/cloudfoundry/dropsonde/metric_sender" 8 | "github.com/cloudfoundry/dropsonde/metrics" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | func TestMonitor(t *testing.T) { 14 | RegisterFailHandler(Fail) 15 | RunSpecs(t, "Monitor Suite") 16 | } 17 | 18 | var ( 19 | fakeEventEmitter *fake.FakeEventEmitter 20 | ) 21 | 22 | var _ = BeforeSuite(func() { 23 | fakeEventEmitter = fake.NewFakeEventEmitter("MonitorTest") 24 | sender := metric_sender.NewMetricSender(fakeEventEmitter) 25 | //batcher := metricbatcher.New(sender, 100*time.Millisecond) 26 | metrics.Initialize(sender, nil) 27 | }) 28 | 29 | var _ = AfterSuite(func() { 30 | fakeEventEmitter.Close() 31 | }) 32 | -------------------------------------------------------------------------------- /metrics_prometheus/metrics_suite_test.go: -------------------------------------------------------------------------------- 1 | package metrics_prometheus 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestMetrics(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Metrics Suite") 13 | } 14 | -------------------------------------------------------------------------------- /proxy/buffer_pool.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "net/http/httputil" 5 | "sync" 6 | ) 7 | 8 | type bufferPool struct { 9 | pool *sync.Pool 10 | } 11 | 12 | func NewBufferPool() httputil.BufferPool { 13 | return &bufferPool{ 14 | pool: new(sync.Pool), 15 | } 16 | } 17 | 18 | func (b *bufferPool) Get() []byte { 19 | buf := b.pool.Get() 20 | if buf == nil { 21 | return make([]byte, 8192) 22 | } 23 | return *buf.(*[]byte) 24 | } 25 | 26 | func (b *bufferPool) Put(buf []byte) { 27 | b.pool.Put(&buf) 28 | } 29 | -------------------------------------------------------------------------------- /proxy/fails/basic_classifiers.go: -------------------------------------------------------------------------------- 1 | package fails 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "errors" 8 | "net" 9 | "strings" 10 | ) 11 | 12 | var IdempotentRequestEOFError = errors.New("EOF (via idempotent request)") 13 | 14 | var IncompleteRequestError = errors.New("incomplete request") 15 | 16 | var AttemptedTLSWithNonTLSBackend = ClassifierFunc(func(err error) bool { 17 | return errors.As(err, &tls.RecordHeaderError{}) 18 | }) 19 | 20 | var Dial = ClassifierFunc(func(err error) bool { 21 | var opErr *net.OpError 22 | if errors.As(err, &opErr) { 23 | return opErr.Op == "dial" 24 | } 25 | return false 26 | }) 27 | 28 | var ContextCancelled = ClassifierFunc(func(err error) bool { 29 | return errors.Is(err, context.Canceled) 30 | }) 31 | 32 | var ConnectionResetOnRead = ClassifierFunc(func(err error) bool { 33 | var opErr *net.OpError 34 | if errors.As(err, &opErr) { 35 | return opErr.Err.Error() == "read: connection reset by peer" 36 | } 37 | return false 38 | }) 39 | 40 | var RemoteFailedCertCheck = ClassifierFunc(func(err error) bool { 41 | var opErr *net.OpError 42 | if errors.As(err, &opErr) { 43 | if opErr.Op == "remote error" { 44 | if opErr.Err.Error() == "tls: bad certificate" || 45 | opErr.Err.Error() == "tls: unknown certificate authority" || 46 | opErr.Err.Error() == "tls: certificate required" { 47 | return true 48 | } 49 | } 50 | } 51 | return false 52 | }) 53 | 54 | var RemoteHandshakeTimeout = ClassifierFunc(func(err error) bool { 55 | return err != nil && strings.Contains(err.Error(), "net/http: TLS handshake timeout") 56 | }) 57 | 58 | var ExpiredOrNotYetValidCertFailure = ClassifierFunc(func(err error) bool { 59 | var certErr x509.CertificateInvalidError 60 | if errors.As(err, &certErr) { 61 | return certErr.Reason == x509.Expired 62 | } 63 | return false 64 | }) 65 | 66 | var RemoteHandshakeFailure = ClassifierFunc(func(err error) bool { 67 | var opErr *net.OpError 68 | if errors.As(err, &opErr) { 69 | return opErr != nil && opErr.Error() == "remote error: tls: handshake failure" 70 | } 71 | return false 72 | }) 73 | 74 | var HostnameMismatch = ClassifierFunc(func(err error) bool { 75 | return errors.As(err, &x509.HostnameError{}) 76 | }) 77 | 78 | var UntrustedCert = ClassifierFunc(func(err error) bool { 79 | var tlsCertError *tls.CertificateVerificationError 80 | switch { 81 | case errors.As(err, &x509.UnknownAuthorityError{}), errors.As(err, &tlsCertError): 82 | return true 83 | default: 84 | return false 85 | } 86 | }) 87 | 88 | var IdempotentRequestEOF = ClassifierFunc(func(err error) bool { 89 | return errors.Is(err, IdempotentRequestEOFError) 90 | }) 91 | 92 | var IncompleteRequest = ClassifierFunc(func(err error) bool { 93 | return errors.Is(err, IncompleteRequestError) 94 | }) 95 | -------------------------------------------------------------------------------- /proxy/fails/classifier.go: -------------------------------------------------------------------------------- 1 | package fails 2 | 3 | //go:generate counterfeiter -o fakes/fake_classifier.go --fake-name Classifier . Classifier 4 | type Classifier interface { 5 | Classify(err error) bool 6 | } 7 | 8 | type ClassifierFunc func(err error) bool 9 | 10 | func (f ClassifierFunc) Classify(err error) bool { return f(err) } 11 | -------------------------------------------------------------------------------- /proxy/fails/classifier_group.go: -------------------------------------------------------------------------------- 1 | package fails 2 | 3 | type ClassifierGroup []Classifier 4 | 5 | // RetriableClassifiers include backend errors that are safe to retry 6 | // 7 | // Backend errors are only safe to retry if we can be certain that they have 8 | // occurred before any http request data has been sent from gorouter to the 9 | // backend application. 10 | // 11 | // Otherwise, there’s risk of a mutating non-idempotent request (e.g. send 12 | // payment) being silently retried without the client knowing. 13 | var RetriableClassifiers = ClassifierGroup{ 14 | Dial, 15 | AttemptedTLSWithNonTLSBackend, 16 | HostnameMismatch, 17 | RemoteFailedCertCheck, 18 | RemoteHandshakeFailure, 19 | RemoteHandshakeTimeout, 20 | UntrustedCert, 21 | ExpiredOrNotYetValidCertFailure, 22 | IdempotentRequestEOF, 23 | IncompleteRequest, 24 | } 25 | 26 | var FailableClassifiers = ClassifierGroup{ 27 | Dial, 28 | AttemptedTLSWithNonTLSBackend, 29 | HostnameMismatch, 30 | RemoteFailedCertCheck, 31 | RemoteHandshakeFailure, 32 | RemoteHandshakeTimeout, 33 | UntrustedCert, 34 | ExpiredOrNotYetValidCertFailure, 35 | ConnectionResetOnRead, 36 | } 37 | 38 | var PrunableClassifiers = ClassifierGroup{ 39 | Dial, 40 | AttemptedTLSWithNonTLSBackend, 41 | HostnameMismatch, 42 | RemoteFailedCertCheck, 43 | RemoteHandshakeFailure, 44 | RemoteHandshakeTimeout, 45 | UntrustedCert, 46 | ExpiredOrNotYetValidCertFailure, 47 | } 48 | 49 | // Classify returns true on errors that are retryable 50 | func (cg ClassifierGroup) Classify(err error) bool { 51 | for _, classifier := range cg { 52 | if classifier.Classify(err) { 53 | return true 54 | } 55 | } 56 | return false 57 | } 58 | -------------------------------------------------------------------------------- /proxy/fails/fails_suite_test.go: -------------------------------------------------------------------------------- 1 | package fails_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestFails(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Fails Suite") 13 | } 14 | -------------------------------------------------------------------------------- /proxy/fails/fakes/fake_classifier.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package fakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "code.cloudfoundry.org/gorouter/proxy/fails" 8 | ) 9 | 10 | type Classifier struct { 11 | ClassifyStub func(error) bool 12 | classifyMutex sync.RWMutex 13 | classifyArgsForCall []struct { 14 | arg1 error 15 | } 16 | classifyReturns struct { 17 | result1 bool 18 | } 19 | classifyReturnsOnCall map[int]struct { 20 | result1 bool 21 | } 22 | invocations map[string][][]interface{} 23 | invocationsMutex sync.RWMutex 24 | } 25 | 26 | func (fake *Classifier) Classify(arg1 error) bool { 27 | fake.classifyMutex.Lock() 28 | ret, specificReturn := fake.classifyReturnsOnCall[len(fake.classifyArgsForCall)] 29 | fake.classifyArgsForCall = append(fake.classifyArgsForCall, struct { 30 | arg1 error 31 | }{arg1}) 32 | stub := fake.ClassifyStub 33 | fakeReturns := fake.classifyReturns 34 | fake.recordInvocation("Classify", []interface{}{arg1}) 35 | fake.classifyMutex.Unlock() 36 | if stub != nil { 37 | return stub(arg1) 38 | } 39 | if specificReturn { 40 | return ret.result1 41 | } 42 | return fakeReturns.result1 43 | } 44 | 45 | func (fake *Classifier) ClassifyCallCount() int { 46 | fake.classifyMutex.RLock() 47 | defer fake.classifyMutex.RUnlock() 48 | return len(fake.classifyArgsForCall) 49 | } 50 | 51 | func (fake *Classifier) ClassifyCalls(stub func(error) bool) { 52 | fake.classifyMutex.Lock() 53 | defer fake.classifyMutex.Unlock() 54 | fake.ClassifyStub = stub 55 | } 56 | 57 | func (fake *Classifier) ClassifyArgsForCall(i int) error { 58 | fake.classifyMutex.RLock() 59 | defer fake.classifyMutex.RUnlock() 60 | argsForCall := fake.classifyArgsForCall[i] 61 | return argsForCall.arg1 62 | } 63 | 64 | func (fake *Classifier) ClassifyReturns(result1 bool) { 65 | fake.classifyMutex.Lock() 66 | defer fake.classifyMutex.Unlock() 67 | fake.ClassifyStub = nil 68 | fake.classifyReturns = struct { 69 | result1 bool 70 | }{result1} 71 | } 72 | 73 | func (fake *Classifier) ClassifyReturnsOnCall(i int, result1 bool) { 74 | fake.classifyMutex.Lock() 75 | defer fake.classifyMutex.Unlock() 76 | fake.ClassifyStub = nil 77 | if fake.classifyReturnsOnCall == nil { 78 | fake.classifyReturnsOnCall = make(map[int]struct { 79 | result1 bool 80 | }) 81 | } 82 | fake.classifyReturnsOnCall[i] = struct { 83 | result1 bool 84 | }{result1} 85 | } 86 | 87 | func (fake *Classifier) Invocations() map[string][][]interface{} { 88 | fake.invocationsMutex.RLock() 89 | defer fake.invocationsMutex.RUnlock() 90 | fake.classifyMutex.RLock() 91 | defer fake.classifyMutex.RUnlock() 92 | copiedInvocations := map[string][][]interface{}{} 93 | for key, value := range fake.invocations { 94 | copiedInvocations[key] = value 95 | } 96 | return copiedInvocations 97 | } 98 | 99 | func (fake *Classifier) recordInvocation(key string, args []interface{}) { 100 | fake.invocationsMutex.Lock() 101 | defer fake.invocationsMutex.Unlock() 102 | if fake.invocations == nil { 103 | fake.invocations = map[string][][]interface{}{} 104 | } 105 | if fake.invocations[key] == nil { 106 | fake.invocations[key] = [][]interface{}{} 107 | } 108 | fake.invocations[key] = append(fake.invocations[key], args) 109 | } 110 | 111 | var _ fails.Classifier = new(Classifier) 112 | -------------------------------------------------------------------------------- /proxy/modifyresponse.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | router_http "code.cloudfoundry.org/gorouter/common/http" 8 | "code.cloudfoundry.org/gorouter/handlers" 9 | ) 10 | 11 | func (p *proxy) modifyResponse(res *http.Response) error { 12 | req := res.Request 13 | if req == nil { 14 | return errors.New("Response does not have an attached request") 15 | } 16 | if res.Header.Get(handlers.VcapRequestIdHeader) == "" { 17 | vcapID := req.Header.Get(handlers.VcapRequestIdHeader) 18 | res.Header.Set(handlers.VcapRequestIdHeader, vcapID) 19 | } 20 | 21 | reqInfo, err := handlers.ContextRequestInfo(req) 22 | if err != nil { 23 | return err 24 | } 25 | endpoint := reqInfo.RouteEndpoint 26 | if endpoint == nil { 27 | return errors.New("reqInfo.RouteEndpoint is empty on a successful response") 28 | } 29 | routePool := reqInfo.RoutePool 30 | if routePool == nil { 31 | return errors.New("reqInfo.RoutePool is empty on a successful response") 32 | } 33 | 34 | if p.config.TraceKey != "" && req.Header.Get(router_http.VcapTraceHeader) == p.config.TraceKey { 35 | res.Header.Set(router_http.VcapRouterHeader, p.config.Ip) 36 | res.Header.Set(router_http.VcapBackendHeader, endpoint.CanonicalAddr()) 37 | res.Header.Set(router_http.CfRouteEndpointHeader, endpoint.CanonicalAddr()) 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /proxy/round_tripper/dropsonde_round_tripper.go: -------------------------------------------------------------------------------- 1 | package round_tripper 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/cloudfoundry/dropsonde" 7 | 8 | "code.cloudfoundry.org/gorouter/proxy/utils" 9 | ) 10 | 11 | func NewDropsondeRoundTripper(p ProxyRoundTripper) ProxyRoundTripper { 12 | return &dropsondeRoundTripper{ 13 | p: p, 14 | d: dropsonde.InstrumentedRoundTripper(p), 15 | } 16 | } 17 | 18 | type dropsondeRoundTripper struct { 19 | p ProxyRoundTripper 20 | d http.RoundTripper 21 | } 22 | 23 | func (d *dropsondeRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { 24 | return d.d.RoundTrip(r) 25 | } 26 | 27 | func (d *dropsondeRoundTripper) CancelRequest(r *http.Request) { 28 | d.p.CancelRequest(r) 29 | } 30 | 31 | type FactoryImpl struct { 32 | BackendTemplate *http.Transport 33 | RouteServiceTemplate *http.Transport 34 | IsInstrumented bool 35 | } 36 | 37 | func (t *FactoryImpl) New(expectedServerName string, isRouteService bool, isHttp2 bool) ProxyRoundTripper { 38 | var template *http.Transport 39 | if isRouteService { 40 | template = t.RouteServiceTemplate 41 | } else { 42 | template = t.BackendTemplate 43 | } 44 | 45 | customTLSConfig := utils.TLSConfigWithServerName(expectedServerName, template.TLSClientConfig, isRouteService) 46 | 47 | newTransport := &http.Transport{ 48 | DialContext: template.DialContext, 49 | DisableKeepAlives: template.DisableKeepAlives, 50 | MaxIdleConns: template.MaxIdleConns, 51 | IdleConnTimeout: template.IdleConnTimeout, 52 | MaxIdleConnsPerHost: template.MaxIdleConnsPerHost, 53 | DisableCompression: template.DisableCompression, 54 | TLSClientConfig: customTLSConfig, 55 | TLSHandshakeTimeout: template.TLSHandshakeTimeout, 56 | ForceAttemptHTTP2: isHttp2, 57 | ExpectContinueTimeout: template.ExpectContinueTimeout, 58 | MaxResponseHeaderBytes: template.MaxResponseHeaderBytes, 59 | } 60 | if t.IsInstrumented { 61 | return NewDropsondeRoundTripper(newTransport) 62 | } else { 63 | return newTransport 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /proxy/round_tripper/error_handler.go: -------------------------------------------------------------------------------- 1 | package round_tripper 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | router_http "code.cloudfoundry.org/gorouter/common/http" 8 | "code.cloudfoundry.org/gorouter/metrics" 9 | "code.cloudfoundry.org/gorouter/proxy/fails" 10 | "code.cloudfoundry.org/gorouter/proxy/utils" 11 | ) 12 | 13 | type ErrorSpec struct { 14 | Classifier fails.Classifier 15 | Message string 16 | Code int 17 | HandleError func(reporter metrics.MetricReporter) 18 | } 19 | 20 | func handleHostnameMismatch(reporter metrics.MetricReporter) { 21 | reporter.CaptureBackendInvalidID() 22 | } 23 | 24 | func handleSSLHandshake(reporter metrics.MetricReporter) { 25 | reporter.CaptureBackendTLSHandshakeFailed() 26 | } 27 | 28 | func handleUntrustedCert(reporter metrics.MetricReporter) { 29 | reporter.CaptureBackendInvalidTLSCert() 30 | } 31 | 32 | var DefaultErrorSpecs = []ErrorSpec{ 33 | {fails.AttemptedTLSWithNonTLSBackend, SSLHandshakeMessage, 525, handleSSLHandshake}, 34 | {fails.HostnameMismatch, HostnameErrorMessage, http.StatusServiceUnavailable, handleHostnameMismatch}, 35 | {fails.UntrustedCert, InvalidCertificateMessage, 526, handleUntrustedCert}, 36 | {fails.RemoteFailedCertCheck, SSLCertRequiredMessage, 496, nil}, 37 | {fails.ContextCancelled, ContextCancelledMessage, 499, nil}, 38 | {fails.RemoteHandshakeFailure, SSLHandshakeMessage, 525, handleSSLHandshake}, 39 | } 40 | 41 | type ErrorHandler struct { 42 | MetricReporter metrics.MetricReporter 43 | ErrorSpecs []ErrorSpec 44 | } 45 | 46 | func (eh *ErrorHandler) HandleError(responseWriter utils.ProxyResponseWriter, err error) { 47 | msg := "endpoint_failure" 48 | if err != nil { 49 | msg = fmt.Sprintf("%s (%s)", msg, err) 50 | } 51 | 52 | responseWriter.Header().Set(router_http.CfRouterError, msg) 53 | 54 | eh.writeErrorCode(err, responseWriter) 55 | responseWriter.Header().Del("Connection") 56 | responseWriter.Done() 57 | } 58 | 59 | func (eh *ErrorHandler) writeErrorCode(err error, responseWriter http.ResponseWriter) { 60 | for _, spec := range eh.ErrorSpecs { 61 | if spec.Classifier.Classify(err) { 62 | if spec.HandleError != nil { 63 | spec.HandleError(eh.MetricReporter) 64 | } 65 | http.Error(responseWriter, spec.Message, spec.Code) 66 | return 67 | } 68 | } 69 | 70 | // default case 71 | http.Error(responseWriter, BadGatewayMessage, http.StatusBadGateway) 72 | eh.MetricReporter.CaptureBadGateway() 73 | } 74 | -------------------------------------------------------------------------------- /proxy/round_tripper/fakes/fake_error_handler.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package fakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "code.cloudfoundry.org/gorouter/proxy/utils" 8 | ) 9 | 10 | type ErrorHandler struct { 11 | HandleErrorStub func(utils.ProxyResponseWriter, error) 12 | handleErrorMutex sync.RWMutex 13 | handleErrorArgsForCall []struct { 14 | arg1 utils.ProxyResponseWriter 15 | arg2 error 16 | } 17 | invocations map[string][][]interface{} 18 | invocationsMutex sync.RWMutex 19 | } 20 | 21 | func (fake *ErrorHandler) HandleError(arg1 utils.ProxyResponseWriter, arg2 error) { 22 | fake.handleErrorMutex.Lock() 23 | fake.handleErrorArgsForCall = append(fake.handleErrorArgsForCall, struct { 24 | arg1 utils.ProxyResponseWriter 25 | arg2 error 26 | }{arg1, arg2}) 27 | stub := fake.HandleErrorStub 28 | fake.recordInvocation("HandleError", []interface{}{arg1, arg2}) 29 | fake.handleErrorMutex.Unlock() 30 | if stub != nil { 31 | fake.HandleErrorStub(arg1, arg2) 32 | } 33 | } 34 | 35 | func (fake *ErrorHandler) HandleErrorCallCount() int { 36 | fake.handleErrorMutex.RLock() 37 | defer fake.handleErrorMutex.RUnlock() 38 | return len(fake.handleErrorArgsForCall) 39 | } 40 | 41 | func (fake *ErrorHandler) HandleErrorCalls(stub func(utils.ProxyResponseWriter, error)) { 42 | fake.handleErrorMutex.Lock() 43 | defer fake.handleErrorMutex.Unlock() 44 | fake.HandleErrorStub = stub 45 | } 46 | 47 | func (fake *ErrorHandler) HandleErrorArgsForCall(i int) (utils.ProxyResponseWriter, error) { 48 | fake.handleErrorMutex.RLock() 49 | defer fake.handleErrorMutex.RUnlock() 50 | argsForCall := fake.handleErrorArgsForCall[i] 51 | return argsForCall.arg1, argsForCall.arg2 52 | } 53 | 54 | func (fake *ErrorHandler) Invocations() map[string][][]interface{} { 55 | fake.invocationsMutex.RLock() 56 | defer fake.invocationsMutex.RUnlock() 57 | fake.handleErrorMutex.RLock() 58 | defer fake.handleErrorMutex.RUnlock() 59 | copiedInvocations := map[string][][]interface{}{} 60 | for key, value := range fake.invocations { 61 | copiedInvocations[key] = value 62 | } 63 | return copiedInvocations 64 | } 65 | 66 | func (fake *ErrorHandler) recordInvocation(key string, args []interface{}) { 67 | fake.invocationsMutex.Lock() 68 | defer fake.invocationsMutex.Unlock() 69 | if fake.invocations == nil { 70 | fake.invocations = map[string][][]interface{}{} 71 | } 72 | if fake.invocations[key] == nil { 73 | fake.invocations[key] = [][]interface{}{} 74 | } 75 | fake.invocations[key] = append(fake.invocations[key], args) 76 | } 77 | -------------------------------------------------------------------------------- /proxy/round_tripper/round_tripper_suite_test.go: -------------------------------------------------------------------------------- 1 | package round_tripper_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestRoundTripper(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "RoundTripper Suite") 13 | } 14 | -------------------------------------------------------------------------------- /proxy/test_helpers/helper.go: -------------------------------------------------------------------------------- 1 | package test_helpers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "time" 7 | 8 | "code.cloudfoundry.org/gorouter/metrics" 9 | "code.cloudfoundry.org/gorouter/route" 10 | "code.cloudfoundry.org/gorouter/stats" 11 | ) 12 | 13 | type NullVarz struct{} 14 | 15 | func (NullVarz) MarshalJSON() ([]byte, error) { return json.Marshal(nil) } 16 | func (NullVarz) ActiveApps() *stats.ActiveApps { return stats.NewActiveApps() } 17 | func (NullVarz) CaptureBadRequest() {} 18 | func (NullVarz) CaptureBadGateway() {} 19 | func (NullVarz) CaptureRoutingRequest(b *route.Endpoint) {} 20 | func (NullVarz) CaptureRoutingResponse(int) {} 21 | func (NullVarz) CaptureRoutingResponseLatency(*route.Endpoint, int, time.Time, time.Duration) { 22 | } 23 | func (NullVarz) CaptureRouteServiceResponse(*http.Response) {} 24 | func (NullVarz) CaptureRegistryMessage(msg metrics.ComponentTagged) {} 25 | -------------------------------------------------------------------------------- /proxy/utils/headerrewriter.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type HeaderRewriter interface { 8 | RewriteHeader(http.Header) 9 | } 10 | 11 | // AddHeaderIfNotPresentRewriter: Adds headers only if they are not present 12 | // in the current http.Header. 13 | // The http.Header must be built using the method Add() to canonalize the keys 14 | type AddHeaderIfNotPresentRewriter struct { 15 | Header http.Header 16 | } 17 | 18 | func (i *AddHeaderIfNotPresentRewriter) RewriteHeader(header http.Header) { 19 | for h, v := range i.Header { 20 | if _, ok := header[h]; !ok { 21 | header[h] = v 22 | } 23 | } 24 | } 25 | 26 | // RemoveHeaderRewriter: Removes any value associated to a header 27 | // The http.Header must be built using the method Add() to canonalize the keys 28 | type RemoveHeaderRewriter struct { 29 | Header http.Header 30 | } 31 | 32 | func (i *RemoveHeaderRewriter) RewriteHeader(header http.Header) { 33 | for h := range i.Header { 34 | header.Del(h) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /proxy/utils/headerrewriter_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | 9 | "code.cloudfoundry.org/gorouter/proxy/utils" 10 | ) 11 | 12 | var _ = Describe("AddHeaderIfNotPresentRewriter", func() { 13 | It("adds headers if missing in the original Header", func() { 14 | header := http.Header{} 15 | 16 | headerToAdd := http.Header{} 17 | headerToAdd.Add("foo", "bar1") 18 | headerToAdd.Add("foo", "bar2") 19 | 20 | rewriter := utils.AddHeaderIfNotPresentRewriter{Header: headerToAdd} 21 | 22 | rewriter.RewriteHeader(header) 23 | 24 | Expect(header).To(HaveKey("Foo")) 25 | Expect(header["Foo"]).To(ConsistOf("bar1", "bar2")) 26 | }) 27 | 28 | It("does not add headers if present in the original Header", func() { 29 | header := http.Header{} 30 | header.Add("foo", "original") 31 | 32 | headerToAdd := http.Header{} 33 | headerToAdd.Add("foo", "bar1") 34 | headerToAdd.Add("foo", "bar2") 35 | 36 | rewriter := utils.AddHeaderIfNotPresentRewriter{Header: headerToAdd} 37 | 38 | rewriter.RewriteHeader(header) 39 | 40 | Expect(header).To(HaveKey("Foo")) 41 | Expect(header["Foo"]).To(ConsistOf("original")) 42 | }) 43 | 44 | It("headers match based with the canonicalized case-insentive key", func() { 45 | header := http.Header{} 46 | header.Add("FOO", "original") 47 | 48 | headerToAdd := http.Header{} 49 | headerToAdd.Add("fOo", "bar1") 50 | 51 | rewriter := utils.AddHeaderIfNotPresentRewriter{Header: headerToAdd} 52 | 53 | rewriter.RewriteHeader(header) 54 | 55 | Expect(header.Get("fOo")).To(Equal("original")) 56 | Expect(header.Get("Foo")).To(Equal("original")) 57 | }) 58 | }) 59 | 60 | var _ = Describe("RemoveHeaderRewriter", func() { 61 | It("remove headers with same name and only those", func() { 62 | header := http.Header{} 63 | header.Add("foo1", "bar1") 64 | header.Add("foo1", "bar2") 65 | header.Add("foo2", "bar1") 66 | header.Add("foo3", "bar1") 67 | header.Add("foo3", "bar2") 68 | 69 | headerToRemove := http.Header{} 70 | headerToRemove.Add("foo1", "") 71 | headerToRemove.Add("foo2", "") 72 | 73 | rewriter := utils.RemoveHeaderRewriter{Header: headerToRemove} 74 | 75 | rewriter.RewriteHeader(header) 76 | 77 | Expect(header).ToNot(HaveKey("Foo1")) 78 | Expect(header).ToNot(HaveKey("Foo2")) 79 | Expect(header).To(HaveKey("Foo3")) 80 | Expect(header["Foo3"]).To(ConsistOf("bar1", "bar2")) 81 | }) 82 | 83 | It("headers match based with the canonicalized case-insentive key", func() { 84 | header := http.Header{} 85 | header.Add("X-Foo", "foo") 86 | header.Add("x-BAR", "bar") 87 | header.Add("x-foobar", "foobar") 88 | 89 | headerToRemove := http.Header{} 90 | headerToRemove.Add("X-fOo", "") 91 | headerToRemove.Add("x-bar", "") 92 | headerToRemove.Add("x-FoObAr", "") 93 | 94 | rewriter := utils.RemoveHeaderRewriter{Header: headerToRemove} 95 | 96 | Expect(header.Get("X-Foo")).ToNot(BeEmpty()) 97 | Expect(header.Get("X-Bar")).ToNot(BeEmpty()) 98 | Expect(header.Get("x-foobar")).ToNot(BeEmpty()) 99 | 100 | rewriter.RewriteHeader(header) 101 | 102 | Expect(header.Get("X-Foo")).To(BeEmpty()) 103 | Expect(header.Get("X-Bar")).To(BeEmpty()) 104 | Expect(header.Get("x-foobar")).To(BeEmpty()) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /proxy/utils/logging.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // CollectHeadersToLog returns an ordered slice of slices of headers to be 4 | // logged and returns a unique, ordered slice of headers to be logged 5 | func CollectHeadersToLog(headerGroups ...[]string) []string { 6 | // We want the headers to be ordered, so we need a slice and a map 7 | var ( 8 | collectedHeaders = make([]string, 0) 9 | seenHeaders = make(map[string]bool) 10 | ) 11 | 12 | for _, headerGroup := range headerGroups { 13 | for _, header := range headerGroup { 14 | 15 | if _, seen := seenHeaders[header]; !seen { 16 | 17 | seenHeaders[header] = true 18 | collectedHeaders = append(collectedHeaders, header) 19 | 20 | } 21 | } 22 | 23 | } 24 | 25 | return collectedHeaders 26 | } 27 | -------------------------------------------------------------------------------- /proxy/utils/logging_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "code.cloudfoundry.org/gorouter/proxy/utils" 8 | ) 9 | 10 | var _ = Describe("CollectHeadersToLog", func() { 11 | Context("when there are no headers to be logged", func() { 12 | It("returns no headers", func() { 13 | Expect(utils.CollectHeadersToLog()).To(HaveLen(0)) 14 | }) 15 | }) 16 | 17 | Context("when there are some headers to be logged", func() { 18 | It("returns the headers in order", func() { 19 | headersToLog := utils.CollectHeadersToLog( 20 | []string{"X-Forwarded-For", "Host", "Content-Length"}, 21 | ) 22 | 23 | Expect(headersToLog).To(HaveLen(3)) 24 | Expect(headersToLog[0]).To(Equal("X-Forwarded-For")) 25 | Expect(headersToLog[1]).To(Equal("Host")) 26 | Expect(headersToLog[2]).To(Equal("Content-Length")) 27 | }) 28 | }) 29 | 30 | Context("when there are multiple groups of headers to be logged", func() { 31 | Context("when there are no duplicates", func() { 32 | It("returns the headers in order", func() { 33 | headersToLog := utils.CollectHeadersToLog( 34 | []string{"X-Forwarded-For", "Host", "Content-Length"}, 35 | []string{"X-Forwarded-Proto", "Content-Type"}, 36 | ) 37 | 38 | Expect(headersToLog).To(HaveLen(5)) 39 | Expect(headersToLog[0]).To(Equal("X-Forwarded-For")) 40 | Expect(headersToLog[1]).To(Equal("Host")) 41 | Expect(headersToLog[2]).To(Equal("Content-Length")) 42 | Expect(headersToLog[3]).To(Equal("X-Forwarded-Proto")) 43 | Expect(headersToLog[4]).To(Equal("Content-Type")) 44 | }) 45 | }) 46 | Context("when there are duplicates", func() { 47 | It("returns the headers in order", func() { 48 | headersToLog := utils.CollectHeadersToLog( 49 | []string{"X-Forwarded-For", "Host", "Content-Length"}, 50 | []string{"X-Forwarded-Proto", "Content-Type", "Host"}, 51 | ) 52 | 53 | Expect(headersToLog).To(HaveLen(5)) 54 | Expect(headersToLog[0]).To(Equal("X-Forwarded-For")) 55 | Expect(headersToLog[1]).To(Equal("Host")) 56 | Expect(headersToLog[2]).To(Equal("Content-Length")) 57 | Expect(headersToLog[3]).To(Equal("X-Forwarded-Proto")) 58 | Expect(headersToLog[4]).To(Equal("Content-Type")) 59 | }) 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /proxy/utils/response_reader.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | type ReadResponseResult struct { 11 | response *http.Response 12 | error error 13 | } 14 | 15 | type TimeoutError struct{} 16 | 17 | func (t TimeoutError) Error() string { 18 | return "timeout waiting for http response from backend" 19 | } 20 | 21 | // ReadResponseWithTimeout extends http.ReadResponse but it utilizes a timeout 22 | func ReadResponseWithTimeout(r *bufio.Reader, req *http.Request, timeout time.Duration) (*http.Response, error) { 23 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 24 | defer cancel() 25 | 26 | read := make(chan ReadResponseResult) 27 | defer close(read) 28 | 29 | go waitForReadResponse(ctx, read, r, req) 30 | 31 | select { 32 | case s := <-read: 33 | return s.response, s.error 34 | case <-ctx.Done(): 35 | return nil, TimeoutError{} 36 | } 37 | } 38 | 39 | func waitForReadResponse(ctx context.Context, c chan<- ReadResponseResult, r *bufio.Reader, req *http.Request) { 40 | resp, err := http.ReadResponse(r, req) 41 | 42 | select { 43 | case <-ctx.Done(): 44 | default: 45 | c <- ReadResponseResult{resp, err} 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /proxy/utils/response_reader_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "time" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | 12 | "code.cloudfoundry.org/gorouter/proxy/utils" 13 | "code.cloudfoundry.org/gorouter/test_util" 14 | ) 15 | 16 | var _ = Describe("ResponseReader", func() { 17 | Describe("ReadResponseWithTimeout", func() { 18 | var ( 19 | timeout time.Duration 20 | reader *bufio.Reader 21 | ) 22 | 23 | BeforeEach(func() { 24 | timeout = 50 * time.Millisecond 25 | reader = bufio.NewReader(io.MultiReader(bytes.NewBufferString("HTTP/1.1 200\r\n\r\n"), nil)) 26 | }) 27 | 28 | It("reads the response before the timeout", func() { 29 | resp, err := utils.ReadResponseWithTimeout(reader, nil, timeout) 30 | Expect(err).NotTo(HaveOccurred()) 31 | Expect(resp.StatusCode).To(Equal(200)) 32 | }) 33 | 34 | It("returns an error when response is invalid", func() { 35 | badReader := bufio.NewReader(io.MultiReader(bytes.NewBufferString("Invalid HTTP\r\n\r\n"), nil)) 36 | resp, err := utils.ReadResponseWithTimeout(badReader, nil, timeout) 37 | Expect(err).To(HaveOccurred()) 38 | Expect(err.Error()).To(ContainSubstring("malformed HTTP")) 39 | Expect(resp).To(BeNil()) 40 | }) 41 | 42 | Context("when read response times out", func() { 43 | var ( 44 | slowReader *bufio.Reader 45 | sleepDuration time.Duration 46 | ) 47 | 48 | BeforeEach(func() { 49 | sleepDuration = 100 * time.Millisecond 50 | slowReader = bufio.NewReader(&test_util.SlowReadCloser{SleepDuration: sleepDuration}) 51 | }) 52 | 53 | It("returns an error", func() { 54 | resp, err := utils.ReadResponseWithTimeout(slowReader, nil, timeout) 55 | Expect(err).To(HaveOccurred()) 56 | Expect(err.Error()).To(ContainSubstring("timeout")) 57 | Expect(resp).To(BeNil()) 58 | }) 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /proxy/utils/responsewriter.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "net" 7 | "net/http" 8 | ) 9 | 10 | type ProxyResponseWriter interface { 11 | Header() http.Header 12 | Hijack() (net.Conn, *bufio.ReadWriter, error) 13 | Write(b []byte) (int, error) 14 | WriteHeader(s int) 15 | Done() 16 | Flush() 17 | Status() int 18 | SetStatus(status int) 19 | Size() int 20 | AddHeaderRewriter(HeaderRewriter) 21 | } 22 | 23 | type proxyResponseWriter struct { 24 | w http.ResponseWriter 25 | status int 26 | size int 27 | 28 | flusher http.Flusher 29 | done bool 30 | 31 | headerRewriters []HeaderRewriter 32 | } 33 | 34 | func NewProxyResponseWriter(w http.ResponseWriter) *proxyResponseWriter { 35 | proxyWriter := &proxyResponseWriter{ 36 | w: w, 37 | flusher: w.(http.Flusher), 38 | } 39 | 40 | return proxyWriter 41 | } 42 | 43 | func (p *proxyResponseWriter) Header() http.Header { 44 | return p.w.Header() 45 | } 46 | 47 | func (p *proxyResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 48 | hijacker, ok := p.w.(http.Hijacker) 49 | if !ok { 50 | return nil, nil, errors.New("response writer cannot hijack") 51 | } 52 | return hijacker.Hijack() 53 | } 54 | 55 | func (p *proxyResponseWriter) Write(b []byte) (int, error) { 56 | if p.done { 57 | return 0, nil 58 | } 59 | 60 | if p.status == 0 { 61 | p.WriteHeader(http.StatusOK) 62 | } 63 | size, err := p.w.Write(b) 64 | p.size += size 65 | return size, err 66 | } 67 | 68 | func (p *proxyResponseWriter) WriteHeader(s int) { 69 | if p.done { 70 | return 71 | } 72 | 73 | // if Content-Type not in response, nil out to suppress Go's auto-detect 74 | if _, ok := p.w.Header()["Content-Type"]; !ok { 75 | p.w.Header()["Content-Type"] = nil 76 | } 77 | 78 | for _, headerRewriter := range p.headerRewriters { 79 | headerRewriter.RewriteHeader(p.w.Header()) 80 | } 81 | 82 | p.w.WriteHeader(s) 83 | 84 | if p.status == 0 || (p.status >= 100 && p.status <= 199) { 85 | p.status = s 86 | } 87 | } 88 | 89 | func (p *proxyResponseWriter) Done() { 90 | p.done = true 91 | } 92 | 93 | func (p *proxyResponseWriter) Flush() { 94 | if p.flusher != nil { 95 | p.flusher.Flush() 96 | } 97 | } 98 | 99 | func (p *proxyResponseWriter) Status() int { 100 | return p.status 101 | } 102 | 103 | // SetStatus should be used when the ResponseWriter has been hijacked 104 | // so WriteHeader is not valid but still needs to save a status code 105 | func (p *proxyResponseWriter) SetStatus(status int) { 106 | p.status = status 107 | } 108 | 109 | func (p *proxyResponseWriter) Size() int { 110 | return p.size 111 | } 112 | 113 | // Satisfy http.ResponseController support (Go 1.20+) 114 | func (p *proxyResponseWriter) Unwrap() http.ResponseWriter { 115 | return p.w 116 | } 117 | 118 | func (p *proxyResponseWriter) AddHeaderRewriter(r HeaderRewriter) { 119 | p.headerRewriters = append(p.headerRewriters, r) 120 | } 121 | -------------------------------------------------------------------------------- /proxy/utils/tls_config.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "crypto/tls" 4 | 5 | func TLSConfigWithServerName(newServerName string, template *tls.Config, isRouteService bool) *tls.Config { 6 | config := &tls.Config{ 7 | CipherSuites: template.CipherSuites, 8 | InsecureSkipVerify: template.InsecureSkipVerify, 9 | RootCAs: template.RootCAs, 10 | ServerName: newServerName, 11 | Certificates: template.Certificates, 12 | } 13 | 14 | if isRouteService { 15 | config.MinVersion = template.MinVersion 16 | config.MaxVersion = template.MaxVersion 17 | } 18 | return config 19 | } 20 | -------------------------------------------------------------------------------- /proxy/utils/utils_suite_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestUtils(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Utils Suite") 13 | } 14 | -------------------------------------------------------------------------------- /registry/container/container_suite_test.go: -------------------------------------------------------------------------------- 1 | package container_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestContainer(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Container Suite") 13 | } 14 | -------------------------------------------------------------------------------- /registry/registry_suite_test.go: -------------------------------------------------------------------------------- 1 | package registry_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestRegistry(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Registry Suite") 13 | } 14 | -------------------------------------------------------------------------------- /route/pool_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package route_test 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | 8 | "code.cloudfoundry.org/gorouter/route" 9 | ) 10 | 11 | var ( 12 | endpoint1 = route.Endpoint{ 13 | ApplicationId: "abc", 14 | Protocol: "http2", 15 | Tags: map[string]string{"tag1": "value1", "tag2": "value2"}, 16 | ServerCertDomainSAN: "host.domain.tld", 17 | PrivateInstanceId: "1234-5678-91011-0000", 18 | PrivateInstanceIndex: "", 19 | Stats: nil, 20 | IsolationSegment: "", 21 | UpdatedAt: time.Time{}, 22 | RoundTripperInit: sync.Once{}, 23 | } 24 | endpoint2 = route.Endpoint{ 25 | ApplicationId: "def", 26 | Protocol: "http2", 27 | Tags: map[string]string{"tag1": "value1", "tag2": "value2"}, 28 | ServerCertDomainSAN: "host.domain.tld", 29 | PrivateInstanceId: "1234-5678-91011-0000", 30 | PrivateInstanceIndex: "", 31 | Stats: nil, 32 | IsolationSegment: "", 33 | UpdatedAt: time.Time{}, 34 | RoundTripperInit: sync.Once{}, 35 | } 36 | endpoint3 = route.Endpoint{ 37 | ApplicationId: "abc", 38 | Protocol: "http2", 39 | Tags: map[string]string{"tag1": "value1", "tag2": "value2"}, 40 | ServerCertDomainSAN: "host.domain.tld", 41 | PrivateInstanceId: "1234-5678-91011-0000", 42 | PrivateInstanceIndex: "", 43 | Stats: nil, 44 | IsolationSegment: "", 45 | UpdatedAt: time.Time{}, 46 | RoundTripperInit: sync.Once{}, 47 | } 48 | result = false 49 | ) 50 | 51 | func BenchmarkEndpointEquals(b *testing.B) { 52 | for i := 0; i < b.N; i++ { 53 | result = endpoint1.Equal(&endpoint3) 54 | } 55 | b.ReportAllocs() 56 | } 57 | 58 | func BenchmarkEndpointNotEquals(b *testing.B) { 59 | for i := 0; i < b.N; i++ { 60 | result = endpoint1.Equal(&endpoint2) 61 | } 62 | b.ReportAllocs() 63 | } 64 | -------------------------------------------------------------------------------- /route/route_suite_test.go: -------------------------------------------------------------------------------- 1 | package route_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestRoute(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Route Suite") 13 | } 14 | -------------------------------------------------------------------------------- /route/uris.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | type Uri string 9 | 10 | func (u Uri) ToLower() Uri { 11 | return Uri(strings.ToLower(u.String())) 12 | } 13 | 14 | func (u Uri) NextWildcard() (Uri, error) { 15 | uri := strings.TrimPrefix(u.String(), "*.") 16 | 17 | i := strings.Index(uri, ".") 18 | if i == -1 { 19 | return u, errors.New("no next wildcard available") 20 | } 21 | suffix := uri[i+1:] 22 | return Uri("*." + suffix), nil 23 | } 24 | 25 | func (u Uri) String() string { 26 | return strings.TrimSuffix(string(u), "/") 27 | } 28 | 29 | func (u Uri) RouteKey() Uri { 30 | key := u.ToLower() 31 | if idx := strings.Index(string(key), "?"); idx >= 0 { 32 | key = key[0:idx] 33 | } 34 | return key 35 | } 36 | -------------------------------------------------------------------------------- /route/uris_test.go: -------------------------------------------------------------------------------- 1 | package route_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "code.cloudfoundry.org/gorouter/route" 8 | ) 9 | 10 | var _ = Describe("URIs", func() { 11 | 12 | Context("RouteKey", func() { 13 | 14 | var key route.Uri 15 | 16 | It("creates a route key based on uri", func() { 17 | key = route.Uri("dora.app.com").RouteKey() 18 | Expect(key.String()).To(Equal("dora.app.com")) 19 | 20 | key = route.Uri("dora.app.com/").RouteKey() 21 | Expect(key.String()).To(Equal("dora.app.com")) 22 | 23 | key = route.Uri("dora.app.com/v1").RouteKey() 24 | Expect(key.String()).To(Equal("dora.app.com/v1")) 25 | 26 | }) 27 | 28 | Context("has a context path", func() { 29 | 30 | It("creates route key with context path", func() { 31 | key = route.Uri("dora.app.com/v1").RouteKey() 32 | Expect(key.String()).To(Equal("dora.app.com/v1")) 33 | 34 | key = route.Uri("dora.app.com/v1/abc").RouteKey() 35 | Expect(key.String()).To(Equal("dora.app.com/v1/abc")) 36 | }) 37 | 38 | Context("has query string in uri", func() { 39 | 40 | It("strips query string for route key", func() { 41 | key = route.Uri("dora.app.com/v1?foo=bar").RouteKey() 42 | Expect(key.String()).To(Equal("dora.app.com/v1")) 43 | 44 | key = route.Uri("dora.app.com/v1?foo=bar&baz=bing").RouteKey() 45 | Expect(key.String()).To(Equal("dora.app.com/v1")) 46 | 47 | key = route.Uri("dora.app.com/v1/abc?foo=bar&baz=bing").RouteKey() 48 | Expect(key.String()).To(Equal("dora.app.com/v1/abc")) 49 | }) 50 | 51 | }) 52 | }) 53 | 54 | Context("has query string in uri", func() { 55 | 56 | It("strips query string for route key", func() { 57 | key = route.Uri("dora.app.com?foo=bar").RouteKey() 58 | Expect(key.String()).To(Equal("dora.app.com")) 59 | 60 | }) 61 | 62 | }) 63 | 64 | Context("has mixed case in uri", func() { 65 | 66 | It("converts the uri to lowercase", func() { 67 | key = route.Uri("DoRa.ApP.CoM").RouteKey() 68 | Expect(key.String()).To(Equal("dora.app.com")) 69 | 70 | key = route.Uri("DORA.APP.COM/").RouteKey() 71 | Expect(key.String()).To(Equal("dora.app.com")) 72 | }) 73 | 74 | }) 75 | 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /route_fetcher/route_fetcher_suite_test.go: -------------------------------------------------------------------------------- 1 | package route_fetcher_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestRouteFetcher(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "RouteFetcher Suite") 13 | } 14 | -------------------------------------------------------------------------------- /router/health_listener.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "log/slog" 7 | "net" 8 | "net/http" 9 | "time" 10 | 11 | log "code.cloudfoundry.org/gorouter/logger" 12 | ) 13 | 14 | type HealthListener struct { 15 | HealthCheck http.Handler 16 | TLSConfig *tls.Config 17 | Port uint16 18 | Router *Router 19 | Logger *slog.Logger 20 | 21 | listener net.Listener 22 | tlsListener net.Listener 23 | } 24 | 25 | func (hl *HealthListener) ListenAndServe() error { 26 | mux := http.NewServeMux() 27 | mux.HandleFunc("/health", func(w http.ResponseWriter, req *http.Request) { 28 | hl.HealthCheck.ServeHTTP(w, req) 29 | }) 30 | mux.HandleFunc("/is-process-alive-do-not-use-for-loadbalancing", func(w http.ResponseWriter, req *http.Request) { 31 | w.WriteHeader(http.StatusOK) 32 | // #nosec G104 - ignore errors when writing HTTP responses so we don't spam our logs during a DoS 33 | w.Write([]byte("ok\n")) 34 | req.Close = true 35 | }) 36 | 37 | addr := fmt.Sprintf("0.0.0.0:%d", hl.Port) 38 | s := &http.Server{ 39 | Addr: addr, 40 | Handler: mux, 41 | ReadTimeout: 10 * time.Second, 42 | WriteTimeout: 10 * time.Second, 43 | } 44 | 45 | var err error 46 | hl.listener, err = net.Listen("tcp", addr) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | healthListener := hl.listener 52 | if hl.TLSConfig != nil { 53 | hl.tlsListener = tls.NewListener(hl.listener, hl.TLSConfig) 54 | healthListener = hl.tlsListener 55 | } 56 | go func() { 57 | err := s.Serve(healthListener) 58 | if !hl.Router.IsStopping() { 59 | hl.Logger.Error("health-listener-failed", log.ErrAttr(err)) 60 | } 61 | }() 62 | return nil 63 | } 64 | 65 | func (hl *HealthListener) Stop() { 66 | if hl.listener != nil { 67 | err := hl.listener.Close() 68 | if err != nil { 69 | hl.Logger.Error("failed-closing-health-listener", log.ErrAttr(err)) 70 | } 71 | } 72 | if hl.tlsListener != nil { 73 | err := hl.tlsListener.Close() 74 | if err != nil { 75 | hl.Logger.Error("failed-closing-health-tls-listener", log.ErrAttr(err)) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /router/helper_test.go: -------------------------------------------------------------------------------- 1 | package router_test 2 | 3 | import ( 4 | "code.cloudfoundry.org/gorouter/registry" 5 | "code.cloudfoundry.org/gorouter/test/common" 6 | ) 7 | 8 | func appRegistered(registry *registry.RouteRegistry, app *common.TestApp) bool { 9 | for _, url := range app.Urls() { 10 | pool := registry.Lookup(url) 11 | if pool == nil { 12 | return false 13 | } 14 | } 15 | 16 | return true 17 | } 18 | -------------------------------------------------------------------------------- /router/route_service_server_test.go: -------------------------------------------------------------------------------- 1 | package router_test 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | 13 | "code.cloudfoundry.org/gorouter/config" 14 | "code.cloudfoundry.org/gorouter/router" 15 | ) 16 | 17 | var _ = Describe("RouteServicesServer", func() { 18 | var ( 19 | rss *router.RouteServicesServer 20 | handler http.Handler 21 | errChan chan error 22 | req *http.Request 23 | cfg *config.Config 24 | ) 25 | 26 | BeforeEach(func() { 27 | handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 28 | w.WriteHeader(http.StatusTeapot) 29 | }) 30 | 31 | var err error 32 | cfg, err = config.DefaultConfig() 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | req, err = http.NewRequest("GET", "/foo", nil) 36 | Expect(err).NotTo(HaveOccurred()) 37 | }) 38 | 39 | JustBeforeEach(func() { 40 | var err error 41 | rss, err = router.NewRouteServicesServer(cfg) 42 | Expect(err).NotTo(HaveOccurred()) 43 | 44 | errChan = make(chan error) 45 | 46 | Expect(rss.Serve(handler, errChan)).To(Succeed()) 47 | }) 48 | 49 | AfterEach(func() { 50 | rss.Stop() 51 | Eventually(errChan).Should(Receive()) 52 | }) 53 | 54 | Describe("Serve", func() { 55 | It("responds to a TLS request using the client cert", func() { 56 | resp, err := rss.GetRoundTripper().RoundTrip(req) 57 | Expect(err).NotTo(HaveOccurred()) 58 | resp.Body.Close() 59 | 60 | Expect(resp.StatusCode).To(Equal(http.StatusTeapot)) 61 | }) 62 | }) 63 | 64 | Describe("ReadHeaderTimeout", func() { 65 | BeforeEach(func() { 66 | cfg.ReadHeaderTimeout = 100 * time.Millisecond 67 | }) 68 | 69 | It("closes requests when their header write exceeds ReadHeaderTimeout", func() { 70 | roundTripper := rss.GetRoundTripper() 71 | conn, err := tls.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", cfg.RouteServicesServerPort), roundTripper.TLSClientConfig()) 72 | Expect(err).NotTo(HaveOccurred()) 73 | defer conn.Close() 74 | 75 | writer := bufio.NewWriter(conn) 76 | 77 | fmt.Fprintf(writer, "GET /some-request HTTP/1.1\r\n") 78 | 79 | // started writing headers 80 | fmt.Fprintf(writer, "Host: localhost\r\n") 81 | writer.Flush() 82 | 83 | time.Sleep(300 * time.Millisecond) 84 | 85 | fmt.Fprintf(writer, "User-Agent: CustomClient/1.0\r\n") 86 | writer.Flush() 87 | 88 | // done 89 | fmt.Fprintf(writer, "\r\n") 90 | writer.Flush() 91 | 92 | resp := bufio.NewReader(conn) 93 | _, err = resp.ReadString('\n') 94 | Expect(err).To(HaveOccurred()) 95 | }) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /router/router_suite_test.go: -------------------------------------------------------------------------------- 1 | package router_test 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | "github.com/cloudfoundry/dropsonde" 10 | "github.com/cloudfoundry/dropsonde/emitter/fake" 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | func TestRouter(t *testing.T) { 16 | RegisterFailHandler(Fail) 17 | log.SetOutput(GinkgoWriter) 18 | RunSpecs(t, "Router Suite") 19 | } 20 | 21 | var originalDefaultTransport *http.Transport 22 | 23 | var _ = SynchronizedBeforeSuite(func() []byte { 24 | originalDefaultTransport = http.DefaultTransport.(*http.Transport) 25 | fakeEmitter := fake.NewFakeEventEmitter("fake") 26 | dropsonde.InitializeWithEmitter(fakeEmitter) 27 | return nil 28 | }, func([]byte) { 29 | SetDefaultEventuallyPollingInterval(100 * time.Millisecond) 30 | SetDefaultConsistentlyDuration(1 * time.Second) 31 | SetDefaultConsistentlyPollingInterval(10 * time.Millisecond) 32 | }) 33 | -------------------------------------------------------------------------------- /router/routes_listener.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "time" 9 | 10 | common "code.cloudfoundry.org/gorouter/common/http" 11 | "code.cloudfoundry.org/gorouter/config" 12 | ) 13 | 14 | type RoutesListener struct { 15 | Config *config.Config 16 | RouteRegistry json.Marshaler 17 | 18 | listener net.Listener 19 | } 20 | 21 | func (rl *RoutesListener) ListenAndServe() error { 22 | hs := http.NewServeMux() 23 | hs.HandleFunc("/routes", func(w http.ResponseWriter, req *http.Request) { 24 | w.Header().Set("Connection", "close") 25 | w.Header().Set("Content-Type", "application/json") 26 | w.WriteHeader(http.StatusOK) 27 | 28 | enc := json.NewEncoder(w) 29 | // #nosec G104 - ignore errors when writing HTTP responses so we don't spam our logs during a DoS 30 | enc.Encode(rl.RouteRegistry) 31 | }) 32 | 33 | f := func(user, password string) bool { 34 | return user == rl.Config.Status.User && password == rl.Config.Status.Pass 35 | } 36 | 37 | addr := fmt.Sprintf("127.0.0.1:%d", rl.Config.Status.Routes.Port) 38 | s := &http.Server{ 39 | Addr: addr, 40 | Handler: &common.BasicAuth{Handler: hs, Authenticator: f}, 41 | ReadTimeout: 10 * time.Second, 42 | WriteTimeout: 10 * time.Second, 43 | } 44 | 45 | l, err := net.Listen("tcp", addr) 46 | if err != nil { 47 | return err 48 | } 49 | rl.listener = l 50 | 51 | go func() { 52 | err = s.Serve(l) 53 | }() 54 | return nil 55 | } 56 | 57 | func (rl *RoutesListener) Stop() error { 58 | if rl.listener != nil { 59 | err := rl.listener.Close() 60 | if err != nil { 61 | return err 62 | } 63 | } 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /routeservice/routeservice_suite_test.go: -------------------------------------------------------------------------------- 1 | package routeservice_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestRouteService(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "RouteService Suite") 13 | } 14 | -------------------------------------------------------------------------------- /routeservice/signature.go: -------------------------------------------------------------------------------- 1 | package routeservice 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "time" 8 | 9 | "code.cloudfoundry.org/gorouter/common/secure" 10 | ) 11 | 12 | type SignatureContents struct { 13 | ForwardedUrl string `json:"forwarded_url"` 14 | RequestedTime time.Time `json:"requested_time"` 15 | } 16 | 17 | type Metadata struct { 18 | Nonce []byte `json:"nonce"` 19 | } 20 | 21 | func BuildSignatureAndMetadata( 22 | crypto secure.Crypto, 23 | signatureContents *SignatureContents, 24 | ) (string, string, error) { 25 | 26 | signatureContentsJson, err := json.Marshal(&signatureContents) 27 | if err != nil { 28 | return "", "", err 29 | } 30 | 31 | signatureJsonEncrypted, nonce, err := crypto.Encrypt(signatureContentsJson) 32 | if err != nil { 33 | return "", "", err 34 | } 35 | 36 | metadata := Metadata{ 37 | Nonce: nonce, 38 | } 39 | 40 | metadataJson, err := json.Marshal(&metadata) 41 | if err != nil { 42 | return "", "", err 43 | } 44 | 45 | metadataHeader := base64.URLEncoding.EncodeToString(metadataJson) 46 | signatureHeader := base64.URLEncoding.EncodeToString(signatureJsonEncrypted) 47 | 48 | return signatureHeader, metadataHeader, nil 49 | } 50 | 51 | func SignatureContentsFromHeaders(signatureHeader, metadataHeader string, crypto secure.Crypto) (SignatureContents, error) { 52 | metadata := Metadata{} 53 | signatureContents := SignatureContents{} 54 | 55 | if metadataHeader == "" { 56 | return signatureContents, errors.New("No metadata found") 57 | } 58 | 59 | metadataDecoded, err := base64.URLEncoding.DecodeString(metadataHeader) 60 | if err != nil { 61 | return signatureContents, err 62 | } 63 | 64 | err = json.Unmarshal(metadataDecoded, &metadata) 65 | if err != nil { 66 | return signatureContents, err 67 | } 68 | 69 | signatureDecoded, err := base64.URLEncoding.DecodeString(signatureHeader) 70 | if err != nil { 71 | return signatureContents, err 72 | } 73 | 74 | signatureDecrypted, err := crypto.Decrypt(signatureDecoded, metadata.Nonce) 75 | if err != nil { 76 | return signatureContents, err 77 | } 78 | 79 | err = json.Unmarshal([]byte(signatureDecrypted), &signatureContents) 80 | 81 | return signatureContents, err 82 | } 83 | -------------------------------------------------------------------------------- /routeservice/signature_test.go: -------------------------------------------------------------------------------- 1 | package routeservice_test 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "strings" 8 | "time" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | 13 | "code.cloudfoundry.org/gorouter/common/secure/fakes" 14 | "code.cloudfoundry.org/gorouter/routeservice" 15 | ) 16 | 17 | var _ = Describe("Route Service Signature", func() { 18 | var ( 19 | crypto = new(fakes.FakeCrypto) 20 | signatureContents *routeservice.SignatureContents 21 | ) 22 | 23 | BeforeEach(func() { 24 | crypto.DecryptStub = func(cipherText, nonce []byte) ([]byte, error) { 25 | decryptedStr := string(cipherText) 26 | 27 | decryptedStr = strings.Replace(decryptedStr, "encrypted", "", -1) 28 | decryptedStr = strings.Replace(decryptedStr, string(nonce), "", -1) 29 | return []byte(decryptedStr), nil 30 | } 31 | 32 | crypto.EncryptStub = func(plainText []byte) ([]byte, []byte, error) { 33 | nonce := []byte("some-nonce") 34 | cipherText := append(plainText, "encrypted"...) 35 | cipherText = append(cipherText, nonce...) 36 | return cipherText, nonce, nil 37 | } 38 | 39 | signatureContents = &routeservice.SignatureContents{RequestedTime: time.Now()} 40 | }) 41 | 42 | Describe("Build Signature and Metadata", func() { 43 | It("builds signature and metadata headers", func() { 44 | signatureHeader, metadata, err := routeservice.BuildSignatureAndMetadata(crypto, signatureContents) 45 | Expect(err).ToNot(HaveOccurred()) 46 | Expect(signatureHeader).ToNot(BeEmpty()) 47 | metadataDecoded, err := base64.URLEncoding.DecodeString(metadata) 48 | Expect(err).ToNot(HaveOccurred()) 49 | metadataStruct := routeservice.Metadata{} 50 | err = json.Unmarshal([]byte(metadataDecoded), &metadataStruct) 51 | Expect(err).ToNot(HaveOccurred()) 52 | Expect(metadataStruct.Nonce).To(Equal([]byte("some-nonce"))) 53 | }) 54 | 55 | Context("when unable to encrypt the signature", func() { 56 | BeforeEach(func() { 57 | crypto.EncryptReturns([]byte{}, []byte{}, errors.New("No entropy")) 58 | }) 59 | 60 | It("returns an error", func() { 61 | _, _, err := routeservice.BuildSignatureAndMetadata(crypto, signatureContents) 62 | Expect(err).To(HaveOccurred()) 63 | }) 64 | }) 65 | }) 66 | 67 | Describe("Parse headers into signatureContent", func() { 68 | var ( 69 | signatureHeader string 70 | metadataHeader string 71 | ) 72 | 73 | BeforeEach(func() { 74 | var err error 75 | signatureHeader, metadataHeader, err = routeservice.BuildSignatureAndMetadata(crypto, signatureContents) 76 | Expect(err).ToNot(HaveOccurred()) 77 | }) 78 | 79 | It("parses signatureContents from signature and metadata headers", func() { 80 | decryptedSignature, err := routeservice.SignatureContentsFromHeaders(signatureHeader, metadataHeader, crypto) 81 | Expect(err).ToNot(HaveOccurred()) 82 | Expect(signatureContents.RequestedTime.Sub(decryptedSignature.RequestedTime)).To(Equal(time.Duration(0))) 83 | }) 84 | }) 85 | 86 | }) 87 | -------------------------------------------------------------------------------- /stats/active_apps_test.go: -------------------------------------------------------------------------------- 1 | package stats_test 2 | 3 | import ( 4 | "time" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | 9 | . "code.cloudfoundry.org/gorouter/stats" 10 | ) 11 | 12 | var _ = Describe("ActiveApps", func() { 13 | var activeApps *ActiveApps 14 | 15 | BeforeEach(func() { 16 | activeApps = NewActiveApps() 17 | }) 18 | 19 | It("marks application ids active", func() { 20 | activeApps.Mark("a", time.Unix(1, 0)) 21 | apps := activeApps.ActiveSince(time.Unix(1, 0)) 22 | Expect(apps).To(HaveLen(1)) 23 | }) 24 | 25 | It("marks existing applications", func() { 26 | activeApps.Mark("b", time.Unix(1, 0)) 27 | apps := activeApps.ActiveSince(time.Unix(1, 0)) 28 | Expect(apps).To(HaveLen(1)) 29 | 30 | activeApps.Mark("b", time.Unix(2, 0)) 31 | apps = activeApps.ActiveSince(time.Unix(1, 0)) 32 | Expect(apps).To(HaveLen(1)) 33 | }) 34 | 35 | It("trims aging application ids", func() { 36 | for i, x := range []string{"a", "b", "c"} { 37 | activeApps.Mark(x, time.Unix(int64(i+1), 0)) 38 | } 39 | apps := activeApps.ActiveSince(time.Unix(0, 0)) 40 | Expect(apps).To(HaveLen(3)) 41 | 42 | activeApps.Trim(time.Unix(1, 0)) 43 | apps = activeApps.ActiveSince(time.Unix(0, 0)) 44 | Expect(apps).To(HaveLen(2)) 45 | 46 | activeApps.Trim(time.Unix(2, 0)) 47 | apps = activeApps.ActiveSince(time.Unix(0, 0)) 48 | Expect(apps).To(HaveLen(1)) 49 | 50 | activeApps.Trim(time.Unix(3, 0)) 51 | apps = activeApps.ActiveSince(time.Unix(0, 0)) 52 | Expect(apps).To(HaveLen(0)) 53 | }) 54 | 55 | It("returns application ids active since a point in time", func() { 56 | activeApps.Mark("a", time.Unix(1, 0)) 57 | Expect(activeApps.ActiveSince(time.Unix(1, 0))).To(Equal([]string{"a"})) 58 | Expect(activeApps.ActiveSince(time.Unix(3, 0))).To(Equal([]string{})) 59 | Expect(activeApps.ActiveSince(time.Unix(5, 0))).To(Equal([]string{})) 60 | 61 | activeApps.Mark("b", time.Unix(3, 0)) 62 | Expect(activeApps.ActiveSince(time.Unix(1, 0))).To(Equal([]string{"b", "a"})) 63 | Expect(activeApps.ActiveSince(time.Unix(3, 0))).To(Equal([]string{"b"})) 64 | Expect(activeApps.ActiveSince(time.Unix(5, 0))).To(Equal([]string{})) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /stats/container/heap.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | type HeapType interface { 4 | SetIndex(i, j int) 5 | } 6 | 7 | type Heap struct { 8 | HeapType 9 | h []interface{} 10 | } 11 | 12 | func (x *Heap) Len() int { 13 | return len(x.h) 14 | } 15 | 16 | func (x *Heap) Swap(i, j int) { 17 | x.h[i], x.h[j] = x.h[j], x.h[i] 18 | x.SetIndex(i, i) 19 | x.SetIndex(j, j) 20 | } 21 | 22 | func (x *Heap) Push(a interface{}) { 23 | x.h = append(x.h, a) 24 | n := len(x.h) 25 | x.SetIndex(n-1, n-1) 26 | } 27 | 28 | func (x *Heap) Pop() interface{} { 29 | n := len(x.h) 30 | x.SetIndex(n-1, -1) 31 | y := x.h[n-1] 32 | x.h = x.h[0 : n-1] 33 | return y 34 | } 35 | 36 | func (x *Heap) Get(indx int) interface{} { 37 | return x.h[indx] 38 | } 39 | 40 | func (x *Heap) Copy() Heap { 41 | y := *x 42 | y.h = make([]interface{}, len(x.h)) 43 | copy(y.h, x.h) 44 | return y 45 | } 46 | -------------------------------------------------------------------------------- /stats/stats_suite_test.go: -------------------------------------------------------------------------------- 1 | package stats_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestStats(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Stats Suite") 13 | } 14 | -------------------------------------------------------------------------------- /stats/top_apps_test.go: -------------------------------------------------------------------------------- 1 | package stats_test 2 | 3 | import ( 4 | "time" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | 9 | . "code.cloudfoundry.org/gorouter/stats" 10 | ) 11 | 12 | var _ = Describe("TopApps", func() { 13 | 14 | var topApps *TopApps 15 | 16 | BeforeEach(func() { 17 | topApps = NewTopApps() 18 | }) 19 | 20 | It("marks application ids", func() { 21 | topApps.Mark("a", time.Unix(1, 0)) 22 | topApps.Mark("b", time.Unix(1, 0)) 23 | apps := topApps.TopSince(time.Unix(0, 0), 5) 24 | Expect(apps).To(HaveLen(2)) 25 | }) 26 | 27 | It("mark updates existing application ids", func() { 28 | topApps.Mark("b", time.Unix(1, 0)) 29 | topApps.Mark("b", time.Unix(1, 0)) 30 | 31 | apps := topApps.TopSince(time.Unix(0, 0), 5) 32 | Expect(apps).To(HaveLen(1)) 33 | }) 34 | 35 | It("trims aging application ids", func() { 36 | for i, x := range []string{"a", "b", "c"} { 37 | topApps.Mark(x, time.Unix(int64(i+1), 0)) 38 | } 39 | 40 | apps := topApps.TopSince(time.Unix(0, 0), 5) 41 | Expect(apps).To(HaveLen(3)) 42 | 43 | topApps.Trim(time.Unix(1, 0)) 44 | apps = topApps.TopSince(time.Unix(1, 0), 5) 45 | Expect(apps).To(HaveLen(2)) 46 | 47 | topApps.Trim(time.Unix(2, 0)) 48 | apps = topApps.TopSince(time.Unix(2, 0), 5) 49 | Expect(apps).To(HaveLen(1)) 50 | 51 | topApps.Trim(time.Unix(3, 0)) 52 | apps = topApps.TopSince(time.Unix(3, 0), 5) 53 | Expect(apps).To(HaveLen(0)) 54 | }) 55 | 56 | It("reports top application ids", func() { 57 | f := func(x ...TopAppsTopEntry) []TopAppsTopEntry { 58 | if x == nil { 59 | x = make([]TopAppsTopEntry, 0) 60 | } 61 | return x 62 | } 63 | 64 | g := func(x string, y int64) TopAppsTopEntry { 65 | return TopAppsTopEntry{ApplicationId: x, Requests: y} 66 | } 67 | 68 | x := []string{"a", "b", "c"} 69 | for i, y := range x { 70 | for j := 0; j < len(x); j++ { 71 | topApps.Mark(y, time.Unix(int64(i+j), 0)) 72 | } 73 | } 74 | 75 | Expect(topApps.TopSince(time.Unix(2, 0), 3)).To(Equal(f(g("c", 3), g("b", 2), g("a", 1)))) 76 | Expect(topApps.TopSince(time.Unix(3, 0), 3)).To(Equal(f(g("c", 2), g("b", 1)))) 77 | Expect(topApps.TopSince(time.Unix(4, 0), 3)).To(Equal(f(g("c", 1)))) 78 | Expect(topApps.TopSince(time.Unix(5, 0), 3)).To(Equal(f())) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /test/common/network.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "net" 7 | 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | // TestUdp sets up a UDP listener which accepts the first connection and reads individual datagrams 12 | // sent over it into the returned channel. The channel is buffered. The listen address is returned 13 | // as well. 14 | func TestUdp(done <-chan bool) (string, <-chan string) { 15 | conn, err := net.ListenUDP("udp", &net.UDPAddr{ 16 | IP: net.IP{127, 0, 0, 1}, 17 | Port: 0, 18 | }) 19 | Expect(err).NotTo(HaveOccurred()) 20 | go closeDone(done, conn) 21 | 22 | out := make(chan string, 10) 23 | go func() { 24 | var ( 25 | n int 26 | err error 27 | buf = make([]byte, 65_535) 28 | ) 29 | for err == nil { 30 | n, _, err = conn.ReadFrom(buf) 31 | out <- string(buf[:n]) 32 | } 33 | }() 34 | 35 | return conn.LocalAddr().String(), out 36 | } 37 | 38 | // TestTcp sets up a TCP listener which accepts the first connection and reads individual lines 39 | // sent over it into the returned channel. The channel is buffered. The listen address is returned 40 | // as well. 41 | func TestTcp(done <-chan bool) (string, <-chan string) { 42 | l, err := net.ListenTCP("tcp", &net.TCPAddr{ 43 | IP: net.IP{127, 0, 0, 1}, 44 | Port: 0, 45 | }) 46 | Expect(err).NotTo(HaveOccurred()) 47 | go closeDone(done, l) 48 | 49 | out := make(chan string, 10) 50 | go func() { 51 | conn, err := l.Accept() 52 | Expect(err).NotTo(HaveOccurred()) 53 | go closeDone(done, conn) 54 | 55 | scanner := bufio.NewScanner(conn) 56 | for scanner.Scan() { 57 | out <- scanner.Text() 58 | } 59 | }() 60 | 61 | return l.Addr().String(), out 62 | } 63 | 64 | func closeDone(done <-chan bool, closer io.Closer) { 65 | <-done 66 | _ = closer.Close() 67 | } 68 | -------------------------------------------------------------------------------- /test/greet_app.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/nats-io/nats.go" 9 | 10 | "code.cloudfoundry.org/gorouter/route" 11 | "code.cloudfoundry.org/gorouter/test/common" 12 | ) 13 | 14 | func NewGreetApp(urls []route.Uri, rPort uint16, mbusClient *nats.Conn, tags map[string]string) *common.TestApp { 15 | app := common.NewTestApp(urls, rPort, mbusClient, tags, "") 16 | app.AddHandler("/", greetHandler) 17 | app.AddHandler("/forwardedprotoheader", headerHandler) 18 | 19 | return app 20 | } 21 | 22 | func headerHandler(w http.ResponseWriter, r *http.Request) { 23 | // #nosec G104 - ignore errors when writing HTTP responses so we don't spam our logs during a DoS 24 | io.WriteString(w, fmt.Sprintf("%+v", r.Header.Get("X-Forwarded-Proto"))) 25 | } 26 | func greetHandler(w http.ResponseWriter, r *http.Request) { 27 | // #nosec G104 - ignore errors when writing HTTP responses so we don't spam our logs during a DoS 28 | io.WriteString(w, fmt.Sprintf("Hello, %s", r.RemoteAddr)) 29 | } 30 | -------------------------------------------------------------------------------- /test/nginx-app/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | daemon off; 3 | 4 | error_log stderr; 5 | events { worker_connections 1024; } 6 | 7 | http { 8 | charset utf-8; 9 | log_format cloudfoundry 'NginxLog "$request" $status $body_bytes_sent'; 10 | access_log /dev/stdout cloudfoundry; 11 | default_type application/octet-stream; 12 | sendfile on; 13 | 14 | tcp_nopush on; 15 | keepalive_timeout 30; 16 | port_in_redirect off; 17 | 18 | server { 19 | listen {{.Port}}; 20 | root {{.ServerRoot}}; 21 | index index.html index.htm Default.htm; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/nginx-app/public/index.html: -------------------------------------------------------------------------------- 1 | hi 2 | -------------------------------------------------------------------------------- /test/sticky_app.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/nats-io/nats.go" 9 | 10 | "code.cloudfoundry.org/gorouter/route" 11 | "code.cloudfoundry.org/gorouter/test/common" 12 | ) 13 | 14 | func NewStickyApp(urls []route.Uri, rPort uint16, mbusClient *nats.Conn, tags map[string]string, stickyCookieName string) *common.TestApp { 15 | app := common.NewTestApp(urls, rPort, mbusClient, tags, "") 16 | app.AddHandler("/sticky", stickyHandler(app.Port(), stickyCookieName)) 17 | 18 | return app 19 | } 20 | 21 | func stickyHandler(port uint16, stickyCookieName string) func(http.ResponseWriter, *http.Request) { 22 | return func(w http.ResponseWriter, r *http.Request) { 23 | cookie := &http.Cookie{ 24 | Name: stickyCookieName, 25 | Value: "xxx", 26 | } 27 | http.SetCookie(w, cookie) 28 | // #nosec G104 - ignore errors writing http responses to prevent logs from filling up during a DoS 29 | io.WriteString(w, fmt.Sprintf("%d", port)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/websocket_app.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | nats "github.com/nats-io/nats.go" 11 | "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | 14 | "code.cloudfoundry.org/gorouter/route" 15 | "code.cloudfoundry.org/gorouter/test/common" 16 | "code.cloudfoundry.org/gorouter/test_util" 17 | ) 18 | 19 | func NewWebSocketApp(urls []route.Uri, rPort uint16, mbusClient *nats.Conn, delay time.Duration, routeServiceUrl string) *common.TestApp { 20 | app := common.NewTestApp(urls, rPort, mbusClient, nil, routeServiceUrl) 21 | app.AddHandler("/", func(w http.ResponseWriter, r *http.Request) { 22 | defer ginkgo.GinkgoRecover() 23 | 24 | Expect(r.Header.Get("Upgrade")).To(Equal("websocket")) 25 | Expect(strings.ToLower(r.Header.Get("Connection"))).To(Equal("upgrade")) 26 | 27 | conn, _, err := w.(http.Hijacker).Hijack() 28 | Expect(err).ToNot(HaveOccurred()) 29 | x := test_util.NewHttpConn(conn) 30 | 31 | resp := test_util.NewResponse(http.StatusSwitchingProtocols) 32 | resp.Header.Set("Upgrade", "websocket") 33 | resp.Header.Set("Connection", "upgrade") 34 | 35 | time.Sleep(delay) 36 | 37 | x.WriteResponse(resp) 38 | 39 | x.CheckLine("hello from client") 40 | // #nosec G104 - ignore errors when writing HTTP responses so we don't spam our logs during a DoS 41 | x.WriteLine("hello from server") 42 | }) 43 | 44 | return app 45 | } 46 | 47 | func NewFailingWebSocketApp(urls []route.Uri, rPort uint16, mbusClient *nats.Conn, delay time.Duration, routeServiceUrl string) *common.TestApp { 48 | app := common.NewTestApp(urls, rPort, mbusClient, nil, routeServiceUrl) 49 | app.AddHandler("/", func(w http.ResponseWriter, r *http.Request) { 50 | defer ginkgo.GinkgoRecover() 51 | 52 | Expect(r.Header.Get("Upgrade")).To(Equal("websocket")) 53 | Expect(strings.ToLower(r.Header.Get("Connection"))).To(Equal("upgrade")) 54 | 55 | conn, _, err := w.(http.Hijacker).Hijack() 56 | Expect(err).ToNot(HaveOccurred()) 57 | x := test_util.NewHttpConn(conn) 58 | err = x.Close() 59 | Expect(err).ToNot(HaveOccurred()) 60 | 61 | }) 62 | 63 | return app 64 | } 65 | 66 | func NewNotUpgradingWebSocketApp(urls []route.Uri, rPort uint16, mbusClient *nats.Conn, routeServiceUrl string) *common.TestApp { 67 | app := common.NewTestApp(urls, rPort, mbusClient, nil, routeServiceUrl) 68 | app.AddHandler("/", func(w http.ResponseWriter, r *http.Request) { 69 | defer ginkgo.GinkgoRecover() 70 | 71 | Expect(r.Header.Get("Upgrade")).To(Equal("websocket")) 72 | Expect(strings.ToLower(r.Header.Get("Connection"))).To(Equal("upgrade")) 73 | 74 | conn, _, err := w.(http.Hijacker).Hijack() 75 | Expect(err).ToNot(HaveOccurred()) 76 | x := test_util.NewHttpConn(conn) 77 | 78 | resp := test_util.NewResponse(http.StatusNotFound) 79 | resp.ContentLength = -1 80 | resp.Header.Set("Upgrade", "websocket") 81 | resp.Header.Set("Connection", "upgrade") 82 | 83 | resp.Body = io.NopCloser(io.MultiReader( 84 | bytes.NewBufferString("\r\nbeginning of the response body goes here\r\n\r\n"), 85 | bytes.NewBuffer(make([]byte, 10024)), // bigger than the internal buffer of the http stdlib 86 | bytes.NewBufferString("\r\nmore response here, probably won't be seen by client\r\n"), 87 | ), 88 | ) 89 | x.WriteResponse(resp) 90 | }) 91 | 92 | return app 93 | } 94 | -------------------------------------------------------------------------------- /test_util/failure_reporter.go: -------------------------------------------------------------------------------- 1 | package test_util 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/cloudfoundry/custom-cats-reporters/honeycomb/client" 8 | "github.com/onsi/ginkgo/v2/config" 9 | "github.com/onsi/ginkgo/v2/types" 10 | ) 11 | 12 | type FailureReporter struct { 13 | client client.Client 14 | } 15 | 16 | func NewFailureReporter(client client.Client) FailureReporter { 17 | return FailureReporter{ 18 | client: client, 19 | } 20 | } 21 | 22 | func (fr FailureReporter) SpecDidComplete(ss *types.SpecSummary) { 23 | if ss.HasFailureState() { 24 | _ = fr.client.SendEvent( 25 | map[string]string{ 26 | "State": getTestState(ss.State), 27 | "Description": strings.Join(ss.ComponentTexts, " | "), 28 | "FailureMessage": ss.Failure.Message, 29 | "FailureLocation": ss.Failure.Location.String(), 30 | "FailureOutput": ss.CapturedOutput, 31 | "ComponentCodeLocation": ss.Failure.ComponentCodeLocation.String(), 32 | "RunTimeInSeconds": fmt.Sprintf("%f", ss.RunTime.Seconds()), 33 | }, 34 | map[string]string{}, 35 | map[string]string{}, 36 | ) 37 | } 38 | } 39 | 40 | func (fr FailureReporter) SuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { 41 | } 42 | func (fr FailureReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { 43 | } 44 | func (fr FailureReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) {} 45 | func (fr FailureReporter) SpecWillRun(specSummary *types.SpecSummary) {} 46 | func (fr FailureReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) {} 47 | func (fr FailureReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) {} 48 | func (fr FailureReporter) SuiteDidEnd(summary *types.SuiteSummary) {} 49 | 50 | func getTestState(state types.SpecState) string { 51 | switch state { 52 | case types.SpecStatePassed: 53 | return "passed" 54 | case types.SpecStateFailed: 55 | return "failed" 56 | case types.SpecStatePending: 57 | return "pending" 58 | case types.SpecStateSkipped: 59 | return "skipped" 60 | case types.SpecStatePanicked: 61 | return "panicked" 62 | case types.SpecStateTimedout: 63 | return "timedOut" 64 | case types.SpecStateInvalid: 65 | return "invalid" 66 | default: 67 | panic("unknown spec state") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test_util/fake_file.go: -------------------------------------------------------------------------------- 1 | package test_util 2 | 3 | import "sync" 4 | 5 | type FakeFile struct { 6 | mutex sync.Mutex 7 | payload []byte 8 | } 9 | 10 | func (f *FakeFile) Write(data []byte) (int, error) { 11 | f.mutex.Lock() 12 | f.payload = data 13 | f.mutex.Unlock() 14 | return len(data), nil 15 | } 16 | 17 | func (f *FakeFile) Read(data *[]byte) (int, error) { 18 | f.mutex.Lock() 19 | *data = f.payload 20 | f.mutex.Unlock() 21 | return len(*data), nil 22 | } 23 | -------------------------------------------------------------------------------- /test_util/fake_metron.go: -------------------------------------------------------------------------------- 1 | package test_util 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "sync" 7 | 8 | "github.com/cloudfoundry/sonde-go/events" 9 | "google.golang.org/protobuf/proto" 10 | ) 11 | 12 | type Event struct { 13 | EventType string 14 | Name string 15 | Origin string 16 | Value float64 17 | } 18 | 19 | type FakeMetron interface { 20 | AllEvents() []Event 21 | 22 | Address() string 23 | Close() error 24 | Port() int 25 | } 26 | 27 | type fakeMetron struct { 28 | lock *sync.Mutex 29 | receivedEvents []Event 30 | listener net.PacketConn 31 | port int 32 | } 33 | 34 | func NewFakeMetron() *fakeMetron { 35 | port := NextAvailPort() 36 | addr := fmt.Sprintf("127.0.0.1:%d", port) 37 | listener, err := net.ListenPacket("udp4", addr) 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | metron := &fakeMetron{ 43 | lock: &sync.Mutex{}, 44 | listener: listener, 45 | port: int(port), 46 | } 47 | go metron.listenForEvents() 48 | return metron 49 | } 50 | 51 | func (f *fakeMetron) Address() string { 52 | return f.listener.LocalAddr().String() 53 | } 54 | 55 | func (f *fakeMetron) Port() int { 56 | return f.port 57 | } 58 | 59 | func (f *fakeMetron) Close() error { 60 | return f.listener.Close() 61 | } 62 | 63 | func (f *fakeMetron) AllEvents() []Event { 64 | f.lock.Lock() 65 | defer f.lock.Unlock() 66 | 67 | ret := make([]Event, len(f.receivedEvents)) 68 | copy(ret, f.receivedEvents) 69 | return ret 70 | } 71 | 72 | // modified from https://github.com/cloudfoundry/dropsonde/blob/9b2cd8f8f9e99dca1f764ca4511d6011b4f44d0c/integration_test/dropsonde_end_to_end_test.go 73 | func (f *fakeMetron) listenForEvents() { 74 | for { 75 | buffer := make([]byte, 1024*128) 76 | n, _, err := f.listener.ReadFrom(buffer) 77 | if err != nil { 78 | return 79 | } 80 | 81 | if n == 0 { 82 | panic("Received empty packet") 83 | } 84 | envelope := new(events.Envelope) 85 | err = proto.Unmarshal(buffer[0:n], envelope) 86 | if err != nil { 87 | panic(fmt.Errorf("Error unmarshalling envelope: %s", err.Error())) 88 | } 89 | 90 | var eventId = envelope.GetEventType().String() 91 | 92 | newEvent := Event{EventType: eventId} 93 | 94 | switch envelope.GetEventType() { 95 | case events.Envelope_HttpStartStop: 96 | newEvent.Name = envelope.GetHttpStartStop().GetPeerType().String() 97 | case events.Envelope_ValueMetric: 98 | valMetric := envelope.GetValueMetric() 99 | newEvent.Name = valMetric.GetName() 100 | newEvent.Value = valMetric.GetValue() 101 | case events.Envelope_CounterEvent: 102 | countMetric := envelope.GetCounterEvent() 103 | newEvent.Name = countMetric.GetName() 104 | newEvent.Value = float64(countMetric.GetDelta()) 105 | case events.Envelope_LogMessage: 106 | logMessage := envelope.GetLogMessage() 107 | newEvent.Name = string(logMessage.Message) 108 | default: 109 | panic("Unexpected message type: " + envelope.GetEventType().String()) 110 | 111 | } 112 | 113 | newEvent.Origin = envelope.GetOrigin() 114 | 115 | f.lock.Lock() 116 | f.receivedEvents = append(f.receivedEvents, newEvent) 117 | f.lock.Unlock() 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /test_util/localhost_dns.go: -------------------------------------------------------------------------------- 1 | package test_util 2 | 3 | // Our tests assume that there exists a DNS entry *.localhost.routing.cf-app.com that maps to 127.0.0.1 4 | // If that DNS entry does not work, then many, many tests will fail 5 | const LocalhostDNS = "localhost.routing.cf-app.com" 6 | -------------------------------------------------------------------------------- /test_util/nats_client.go: -------------------------------------------------------------------------------- 1 | package test_util 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "os" 8 | "os/exec" 9 | "strconv" 10 | "time" 11 | 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | type Nats struct { 16 | port uint16 17 | cmd *exec.Cmd 18 | address string 19 | } 20 | 21 | func NewNats(port uint16) *Nats { 22 | return &Nats{ 23 | port: port, 24 | address: fmt.Sprintf("127.0.0.1:%d", port), 25 | } 26 | } 27 | 28 | func (n *Nats) Port() uint16 { 29 | return n.port 30 | } 31 | 32 | func (n *Nats) Start() { 33 | natsServer, exists := os.LookupEnv("NATS_SERVER_BINARY") 34 | if !exists { 35 | fmt.Println("You need nats-server installed and set NATS_SERVER_BINARY env variable") 36 | os.Exit(1) 37 | } 38 | cmd := exec.Command(natsServer, "-p", strconv.Itoa(int(n.port)), "--user", "nats", "--pass", "nats") 39 | err := cmd.Start() 40 | Expect(err).ToNot(HaveOccurred()) 41 | n.cmd = cmd 42 | 43 | err = n.waitUntilNatsUp() 44 | Expect(err).ToNot(HaveOccurred()) 45 | } 46 | 47 | func (n *Nats) Stop() { 48 | err := n.cmd.Process.Kill() 49 | Expect(err).ToNot(HaveOccurred()) 50 | err = n.cmd.Wait() 51 | Expect(err).ToNot(HaveOccurred()) 52 | 53 | err = n.waitUntilNatsDown() 54 | Expect(err).ToNot(HaveOccurred()) 55 | } 56 | 57 | func (n *Nats) waitUntilNatsUp() error { 58 | maxWait := 10 59 | for i := 0; i < maxWait; i++ { 60 | time.Sleep(500 * time.Millisecond) 61 | _, err := net.Dial("tcp", n.address) 62 | if err == nil { 63 | return nil 64 | } 65 | } 66 | 67 | return errors.New("Waited too long for NATS to start") 68 | } 69 | 70 | func (n *Nats) waitUntilNatsDown() error { 71 | maxWait := 10 72 | for i := 0; i < maxWait; i++ { 73 | time.Sleep(500 * time.Millisecond) 74 | _, err := net.Dial("tcp", n.address) 75 | if err != nil { 76 | return nil 77 | } 78 | } 79 | 80 | return errors.New("Waited too long for NATS to stop") 81 | } 82 | -------------------------------------------------------------------------------- /test_util/nats_runner.go: -------------------------------------------------------------------------------- 1 | package test_util 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/nats-io/nats.go" 11 | "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | "github.com/onsi/gomega/gexec" 14 | ) 15 | 16 | type NATSRunner struct { 17 | port int 18 | natsSession *gexec.Session 19 | MessageBus *nats.Conn 20 | } 21 | 22 | func NewNATSRunner(port int) *NATSRunner { 23 | return &NATSRunner{ 24 | port: port, 25 | } 26 | } 27 | 28 | func (runner *NATSRunner) Start() { 29 | if runner.natsSession != nil { 30 | panic("starting an already started NATS runner!!!") 31 | } 32 | 33 | natsServer, exists := os.LookupEnv("NATS_SERVER_BINARY") 34 | if !exists { 35 | fmt.Println("You need nats-server installed and set NATS_SERVER_BINARY env variable") 36 | os.Exit(1) 37 | } 38 | 39 | cmd := exec.Command(natsServer, "-p", strconv.Itoa(runner.port)) 40 | sess, err := gexec.Start( 41 | cmd, 42 | gexec.NewPrefixedWriter("\x1b[32m[o]\x1b[34m[nats-server]\x1b[0m ", ginkgo.GinkgoWriter), 43 | gexec.NewPrefixedWriter("\x1b[91m[e]\x1b[34m[nats-server]\x1b[0m ", ginkgo.GinkgoWriter), 44 | ) 45 | Expect(err).NotTo(HaveOccurred(), "Make sure to have nats-server on your path") 46 | 47 | runner.natsSession = sess 48 | 49 | Expect(err).NotTo(HaveOccurred()) 50 | 51 | var messageBus *nats.Conn 52 | Eventually(func() error { 53 | messageBus, err = nats.Connect(fmt.Sprintf("nats://127.0.0.1:%d", runner.port)) 54 | return err 55 | }, 5, 0.1).ShouldNot(HaveOccurred()) 56 | 57 | runner.MessageBus = messageBus 58 | } 59 | 60 | func (runner *NATSRunner) Stop() { 61 | runner.KillWithFire() 62 | } 63 | 64 | func (runner *NATSRunner) KillWithFire() { 65 | if runner.natsSession != nil { 66 | runner.natsSession.Kill().Wait(5 * time.Second) 67 | runner.MessageBus = nil 68 | runner.natsSession = nil 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test_util/ports.go: -------------------------------------------------------------------------------- 1 | package test_util 2 | 3 | import ( 4 | "sync" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | ) 8 | 9 | var ( 10 | lastPortUsed uint16 11 | portLock sync.Mutex 12 | once sync.Once 13 | ) 14 | 15 | func NextAvailPort() uint16 { 16 | portLock.Lock() 17 | defer portLock.Unlock() 18 | 19 | if lastPortUsed == 0 { 20 | once.Do(func() { 21 | const portRangeStart = 25000 22 | // #nosec G115 - if we have negative or > 65k parallel ginkgo threads there's something worse happening 23 | lastPortUsed = portRangeStart + uint16(GinkgoParallelProcess()) 24 | }) 25 | } 26 | 27 | suiteCfg, _ := GinkgoConfiguration() 28 | // #nosec G115 - if we have negative or > 65k parallel ginkgo threads there's something worse happening 29 | lastPortUsed += uint16(suiteCfg.ParallelTotal) 30 | return lastPortUsed 31 | } 32 | -------------------------------------------------------------------------------- /test_util/rss/README.md: -------------------------------------------------------------------------------- 1 | # RSS CLI 2 | Command line tool for reading and writing route service signatures. 3 | 4 | ## Building 5 | 6 | ```bash 7 | cd ./test_util/rss 8 | go build 9 | ``` 10 | 11 | ## Using RSS cli 12 | 13 | ``` 14 | NAME: 15 | rss - A CLI for generating and opening a route service signature. 16 | 17 | USAGE: 18 | rss [global options] command [command options] [arguments...] 19 | 20 | VERSION: 21 | 0.1.0 22 | 23 | AUTHOR(S): 24 | Cloud Foundry Routing Team 25 | 26 | COMMANDS: 27 | generate, g Generates a Route Service Signature 28 | read, r, o Decodes and decrypts a route service signature 29 | help, h Shows a list of commands or help for one command 30 | 31 | GLOBAL OPTIONS: 32 | --help, -h show help 33 | --version, -v print the version 34 | ``` 35 | 36 | For example: 37 | 38 | In the following example, we will generate a random key and then encrypt / decrypt signature. 39 | 40 | - Generate key 41 | mkdir ~/.rss 42 | echo "my-super-secret-password" > ~/.rss/key 43 | 44 | - Encrypt using key 45 | ./rss generate --url http://myapp.com 46 | 47 | Encoded Signature: 48 | AfirJQ7-m-AMByj7y5e4Z0U0_gi6EF29all4mlsyc94YbPu1OYBCL9cT0kyCTkOuOPAbfeZHUs6fHfgrPK54a6BmoKZOdSJO_YWU4F65ja2ZyXH36dlLAD3cHlh4KCyTdBwLQ88M8U39X2A= 49 | 50 | Encoded Metadata: 51 | eyJpdiI6Im1aN0JVY0NQVTVPazdFR1EiLCJub25jZSI6IldXSzNrYWIvVDJlK2w5aU4ifQ== 52 | 53 | - Decrypt the signed header with key and metadata 54 | 55 | ./rss read --signature AfirJQ7-m-AMByj7y5e4Z0U0_gi6EF29all4mlsyc94YbPu1OYBCL9cT0kyCTkOuOPAbfeZHUs6fHfgrPK54a6BmoKZOdSJO_YWU4F65ja2ZyXH36dlLAD3cHlh4KCyTdBwLQ88M8U39X2A= --metadata eyJpdiI6Im1aN0JVY0NQVTVPazdFR1EiLCJub25jZSI6IldXSzNrYWIvVDJlK2w5aU4ifQ== 56 | 57 | Decoded Signature: 58 | { 59 | "forwarded_url": "http://myapp.com", 60 | "requested_time": "2015-08-13T11:16:48.081153827-07:00" 61 | } 62 | -------------------------------------------------------------------------------- /test_util/rss/commands/generate.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/urfave/cli" 10 | 11 | "code.cloudfoundry.org/gorouter/routeservice" 12 | "code.cloudfoundry.org/gorouter/test_util/rss/common" 13 | ) 14 | 15 | func GenerateSignature(c *cli.Context) { 16 | url := c.String("url") 17 | 18 | if url == "" { 19 | // #nosec G104 - this will never return an error since we hardcode "read" which is the command calling this function to begin with 20 | cli.ShowCommandHelp(c, "generate") 21 | os.Exit(1) 22 | } 23 | 24 | crypto, err := common.CreateCrypto(c) 25 | if err != nil { 26 | os.Exit(1) 27 | } 28 | 29 | signatureContents, err := createSigFromArgs(c) 30 | if err != nil { 31 | os.Exit(1) 32 | } 33 | 34 | sigEncoded, metaEncoded, err := routeservice.BuildSignatureAndMetadata(crypto, &signatureContents) 35 | if err != nil { 36 | fmt.Printf("Failed to create signature: %s", err.Error()) 37 | os.Exit(1) 38 | } 39 | 40 | fmt.Printf("Encoded Signature:\n%s\n\n", sigEncoded) 41 | fmt.Printf("Encoded Metadata:\n%s\n\n", metaEncoded) 42 | } 43 | 44 | func createSigFromArgs(c *cli.Context) (routeservice.SignatureContents, error) { 45 | signatureContents := routeservice.SignatureContents{} 46 | url := c.String("url") 47 | 48 | var sigTime time.Time 49 | 50 | timeStr := c.String("time") 51 | 52 | if timeStr != "" { 53 | unix, err := strconv.ParseInt(timeStr, 10, 64) 54 | if err != nil { 55 | fmt.Printf("Invalid time format: %s", timeStr) 56 | return signatureContents, err 57 | } 58 | 59 | sigTime = time.Unix(unix, 0) 60 | } else { 61 | sigTime = time.Now() 62 | } 63 | 64 | return routeservice.SignatureContents{ 65 | RequestedTime: sigTime, 66 | ForwardedUrl: url, 67 | }, nil 68 | } 69 | -------------------------------------------------------------------------------- /test_util/rss/commands/read.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/urfave/cli" 9 | 10 | "code.cloudfoundry.org/gorouter/routeservice" 11 | "code.cloudfoundry.org/gorouter/test_util/rss/common" 12 | ) 13 | 14 | func ReadSignature(c *cli.Context) { 15 | sigEncoded := c.String("signature") 16 | metaEncoded := c.String("metadata") 17 | 18 | if sigEncoded == "" || metaEncoded == "" { 19 | // #nosec G104 - this will never return an error since we hardcode "read" which is the command calling this function to begin with 20 | cli.ShowCommandHelp(c, "read") 21 | os.Exit(1) 22 | } 23 | 24 | crypto, err := common.CreateCrypto(c) 25 | if err != nil { 26 | os.Exit(1) 27 | } 28 | 29 | signatureContents, err := routeservice.SignatureContentsFromHeaders(sigEncoded, metaEncoded, crypto) 30 | 31 | if err != nil { 32 | fmt.Printf("Failed to read signature: %s\n", err.Error()) 33 | os.Exit(1) 34 | } 35 | 36 | printSignatureContents(signatureContents) 37 | } 38 | 39 | func printSignatureContents(signatureContents routeservice.SignatureContents) { 40 | signatureJson, _ := json.MarshalIndent(&signatureContents, "", " ") 41 | fmt.Printf("Decoded Signature:\n%s\n\n", signatureJson) 42 | } 43 | -------------------------------------------------------------------------------- /test_util/rss/common/utils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/user" 8 | 9 | "github.com/urfave/cli" 10 | 11 | "code.cloudfoundry.org/gorouter/common/secure" 12 | ) 13 | 14 | func CreateCrypto(c *cli.Context) (*secure.AesGCM, error) { 15 | keyPath := c.String("key-path") 16 | 17 | if keyPath == "" { 18 | usr, err := user.Current() 19 | if err != nil { 20 | fmt.Println(err.Error()) 21 | } 22 | keyPath = usr.HomeDir + "/.rss/key" 23 | } 24 | 25 | key, err := os.ReadFile(keyPath) 26 | if err != nil { 27 | fmt.Printf("Unable to read key file: %s\n%s\n", keyPath, err.Error()) 28 | return nil, err 29 | } 30 | 31 | key = bytes.Trim(key, "\n") 32 | secretPbkdf := secure.NewPbkdf2(key, 16) 33 | crypto, err := secure.NewAesGCM(secretPbkdf) 34 | if err != nil { 35 | fmt.Printf("Error creating crypto: %s\n", err) 36 | return nil, err 37 | } 38 | return crypto, nil 39 | } 40 | -------------------------------------------------------------------------------- /test_util/rss/fixtures/invalidkey: -------------------------------------------------------------------------------- 1 | invalidkey 2 | -------------------------------------------------------------------------------- /test_util/rss/fixtures/key: -------------------------------------------------------------------------------- 1 | route-services-key 2 | -------------------------------------------------------------------------------- /test_util/rss/fixtures/otherkey: -------------------------------------------------------------------------------- 1 | qxNO61/w+gD9zVXCsk8H2+rw877JlbNQOvoag1bhz+c= 2 | -------------------------------------------------------------------------------- /test_util/rss/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/urfave/cli" 9 | 10 | "code.cloudfoundry.org/gorouter/test_util/rss/commands" 11 | ) 12 | 13 | var keyFlag = cli.StringFlag{ 14 | Name: "key-path, p, k", 15 | Usage: "Path of the key file used to decrypt a route service signature", 16 | } 17 | 18 | var timeFlag = cli.StringFlag{ 19 | Name: "time, t", 20 | Usage: "Timestamp the signature", 21 | } 22 | 23 | var urlFlag = cli.StringFlag{ 24 | Name: "url, u", 25 | Usage: "Client url (required)", 26 | } 27 | 28 | var signatureFlag = cli.StringFlag{ 29 | Name: "signature, s", 30 | Usage: "Route service signature, base64 encoded (Required)", 31 | } 32 | 33 | var metadataFlag = cli.StringFlag{ 34 | Name: "metadata, m", 35 | Usage: "Route service metadata, base64 encoded (Required)", 36 | } 37 | 38 | var genFlags = []cli.Flag{urlFlag, timeFlag, keyFlag} 39 | 40 | var readFlags = []cli.Flag{signatureFlag, metadataFlag, keyFlag} 41 | 42 | var cliCommands = []cli.Command{ 43 | { 44 | Name: "generate", 45 | Usage: "Generates a Route Service Signature", 46 | Aliases: []string{"g"}, 47 | Description: "Generates a Route Service Signature with the current time", 48 | Action: commands.GenerateSignature, 49 | Flags: genFlags, 50 | }, 51 | { 52 | Name: "read", 53 | Usage: "Decodes and decrypts a route service signature", 54 | Aliases: []string{"r", "o"}, 55 | Description: `Decodes and decrypts a route service signature using the key file: 56 | key can be passed in as an argument`, 57 | Action: commands.ReadSignature, 58 | Flags: readFlags, 59 | }, 60 | } 61 | 62 | func main() { 63 | fmt.Println() 64 | app := cli.NewApp() 65 | app.Name = "rss" 66 | app.Usage = "A CLI for generating and opening a route service signature." 67 | authors := []cli.Author{cli.Author{Name: "Cloud Foundry Routing Team", Email: "cf-dev@lists.cloudfoundry.org"}} 68 | app.Authors = authors 69 | app.Commands = cliCommands 70 | app.CommandNotFound = commandNotFound 71 | app.Version = "0.1.0" 72 | 73 | err := app.Run(os.Args) 74 | if err != nil { 75 | log.Fatalf("Error: %s", err) 76 | } 77 | os.Exit(0) 78 | } 79 | 80 | func commandNotFound(c *cli.Context, cmd string) { 81 | fmt.Println("Not a valid command:", cmd) 82 | os.Exit(1) 83 | } 84 | -------------------------------------------------------------------------------- /test_util/rss/main_suite_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "os/user" 7 | "testing" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | "github.com/onsi/gomega/gexec" 12 | ) 13 | 14 | var rssPath string 15 | var rssCommand = func(args ...string) *exec.Cmd { 16 | command := exec.Command(rssPath, args...) 17 | return command 18 | } 19 | 20 | const keyPath = "fixtures/key" 21 | 22 | func TestRssCli(t *testing.T) { 23 | RegisterFailHandler(Fail) 24 | RunSpecs(t, "RSS Cli Suite") 25 | } 26 | 27 | var _ = SynchronizedBeforeSuite(func() []byte { 28 | 29 | createDefaultKey() 30 | 31 | cliPath, err := gexec.Build("code.cloudfoundry.org/gorouter/test_util/rss") 32 | Expect(err).NotTo(HaveOccurred()) 33 | return []byte(cliPath) 34 | }, func(cliPath []byte) { 35 | rssPath = string(cliPath) 36 | }) 37 | 38 | var _ = SynchronizedAfterSuite(func() { 39 | }, func() { 40 | removeDefaultKey() 41 | gexec.CleanupBuildArtifacts() 42 | }) 43 | 44 | func createDefaultKey() { 45 | keyDir := getKeyDir() 46 | 47 | if !isDirExist(keyDir) { 48 | err := os.Mkdir(keyDir, os.ModePerm) 49 | Expect(err).NotTo(HaveOccurred()) 50 | } 51 | 52 | copyFile(keyPath, keyDir+"/key") 53 | } 54 | 55 | func removeDefaultKey() { 56 | keyDir := getKeyDir() 57 | err := os.RemoveAll(keyDir) 58 | Expect(err).NotTo(HaveOccurred()) 59 | } 60 | 61 | func getKeyDir() string { 62 | usr, err := user.Current() 63 | Expect(err).NotTo(HaveOccurred()) 64 | return usr.HomeDir + "/.rss" 65 | } 66 | 67 | func isDirExist(dir string) bool { 68 | _, err := os.Stat(dir) 69 | return !os.IsNotExist(err) 70 | } 71 | 72 | func copyFile(src, dest string) { 73 | data, err := os.ReadFile(src) 74 | Expect(err).NotTo(HaveOccurred()) 75 | writeToFile(data, dest) 76 | } 77 | 78 | func writeToFile(data []byte, fileName string) { 79 | var file *os.File 80 | var err error 81 | file, err = os.Create(fileName) 82 | Expect(err).NotTo(HaveOccurred()) 83 | 84 | _, err = file.Write(data) 85 | Expect(err).NotTo(HaveOccurred()) 86 | 87 | err = file.Close() 88 | Expect(err).NotTo(HaveOccurred()) 89 | } 90 | -------------------------------------------------------------------------------- /test_util/test_logger.go: -------------------------------------------------------------------------------- 1 | package test_util 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | "github.com/onsi/gomega/gbytes" 11 | "go.uber.org/zap/zapcore" 12 | 13 | log "code.cloudfoundry.org/gorouter/logger" 14 | ) 15 | 16 | // We add 1 to zap's default values to match our level definitions 17 | // https://github.com/uber-go/zap/blob/47f41350ff078ea1415b63c117bf1475b7bbe72c/level.go#L36 18 | func levelNumber(level zapcore.Level) int { 19 | return int(level) + 1 20 | } 21 | 22 | // TestLogger implements an slog logger that can be used with Ginkgo tests 23 | type TestLogger struct { 24 | *slog.Logger 25 | *TestSink 26 | } 27 | 28 | // Taken from go.uber.org/zap 29 | type TestSink struct { 30 | *gbytes.Buffer 31 | } 32 | 33 | // NewTestLogger returns a new slog logger using a zap handler 34 | func NewTestLogger(component string) *TestLogger { 35 | sink := &TestSink{ 36 | Buffer: gbytes.NewBuffer(), 37 | } 38 | var testLogger *slog.Logger 39 | if component != "" { 40 | testLogger = log.CreateLoggerWithSource(component, "") 41 | } else { 42 | testLogger = log.CreateLogger() 43 | } 44 | 45 | log.SetDynamicWriteSyncer(zapcore.NewMultiWriteSyncer(sink, zapcore.AddSync(ginkgo.GinkgoWriter))) 46 | log.SetLoggingLevel("Debug") 47 | return &TestLogger{ 48 | Logger: testLogger, 49 | TestSink: sink, 50 | } 51 | } 52 | 53 | func (s *TestSink) Sync() error { 54 | return nil 55 | } 56 | 57 | func (s *TestSink) Lines() []string { 58 | output := strings.Split(string(s.Contents()), "\n") 59 | return output[:len(output)-1] 60 | } 61 | 62 | // Buffer returns the gbytes buffer that was used as the sink 63 | func (z *TestLogger) Buffer() *gbytes.Buffer { 64 | return z.TestSink.Buffer 65 | } 66 | 67 | func (z *TestLogger) Lines(level zapcore.Level) []string { 68 | r, _ := regexp.Compile(fmt.Sprintf(".*\"log_level\":%d.*}\n", levelNumber(level))) 69 | return r.FindAllString(string(z.TestSink.Buffer.Contents()), -1) 70 | } 71 | -------------------------------------------------------------------------------- /varz/varz_suite_test.go: -------------------------------------------------------------------------------- 1 | package varz_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestVarz(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Varz Suite") 13 | } 14 | --------------------------------------------------------------------------------