├── CODEOWNERS ├── janus-logo.png ├── .whitesource ├── book.json ├── examples ├── plugin-cb │ ├── img │ │ ├── hystrix-home.png │ │ └── hystrix-dashboard.png │ ├── stubs │ │ ├── service.json │ │ └── status.json │ ├── docker-compose.yml │ ├── janus.toml │ ├── apis │ │ └── example.json │ └── README.md ├── front-proxy-mongo │ ├── seed.Dockerfile │ ├── janus.toml │ ├── apis │ │ └── example.json │ └── docker-compose.yml ├── front-proxy │ ├── stubs │ │ ├── service.json │ │ └── status.json │ ├── docker-compose.yml │ ├── apis │ │ └── example.json │ └── janus.toml ├── front-proxy-auth │ ├── stubs │ │ ├── service.json │ │ ├── service1 │ │ │ ├── service.json │ │ │ └── status.json │ │ ├── status.json │ │ └── auth │ │ │ └── token.json │ ├── janus.toml │ ├── docker-compose.yml │ ├── auth │ │ └── auth.json │ └── apis │ │ └── example.json ├── front-proxy-cassandra │ ├── apis │ │ ├── example.json │ │ └── example2.json │ ├── janus.toml │ └── docker-compose.yml ├── janus-tls │ ├── docker-compose.yml │ ├── apis │ │ └── example.json │ ├── janus.toml │ ├── janus.crt │ └── janus.key └── front-proxy-cluster │ ├── apis │ └── example.json │ ├── janus.toml │ └── docker-compose.yml ├── janus ├── Chart.yaml ├── templates │ ├── pdb.yaml │ ├── tests │ │ └── test-connection.yaml │ ├── service.yaml │ ├── ingress.yaml │ ├── NOTES.txt │ └── _helpers.tpl ├── .helmignore └── values.yaml ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── workflows │ └── release.yml ├── pkg ├── config │ └── doc.go ├── api │ ├── doc.go │ ├── error.go │ ├── file_repository_test.go │ └── repository.go ├── test │ ├── model.go │ ├── handler.go │ └── http.go ├── plugin │ ├── retry │ │ ├── setup_test.go │ │ ├── middleware_test.go │ │ ├── setup.go │ │ └── middleware.go │ ├── bodylmt │ │ ├── setup_test.go │ │ ├── middleware_test.go │ │ ├── setup.go │ │ └── middleware.go │ ├── compression │ │ ├── setup_test.go │ │ └── setup.go │ ├── oauth2 │ │ ├── setup_test.go │ │ ├── file_repository_test.go │ │ ├── middleware_access_rules.go │ │ ├── file_repository.go │ │ ├── error.go │ │ └── in_memory_repository.go │ ├── rate │ │ ├── setup_integration_test.go │ │ ├── rate_limit_logger_test.go │ │ └── rate_limit_logger.go │ ├── requesttransformer │ │ ├── setup.go │ │ └── setup_test.go │ ├── responsetransformer │ │ ├── setup.go │ │ └── setup_test.go │ ├── basic │ │ ├── encrypt │ │ │ └── encrypt.go │ │ ├── errors.go │ │ ├── setup_test.go │ │ ├── in_memory_repository_test.go │ │ ├── middleware.go │ │ ├── in_memory_repository.go │ │ └── middleware_test.go │ ├── organization │ │ └── errors.go │ ├── events.go │ ├── cb │ │ ├── stats_collector_test.go │ │ ├── middleware_test.go │ │ └── middleware.go │ └── cors │ │ └── setup.go ├── middleware │ ├── recovery.go │ ├── logger_test.go │ ├── request_id.go │ ├── recovery_test.go │ ├── stats_tagger.go │ ├── debug_trace.go │ ├── logger.go │ ├── stats.go │ ├── debug_trace_test.go │ └── host_matcher.go ├── web │ ├── checker_test.go │ ├── handlers.go │ └── options.go ├── proxy │ ├── transport │ │ ├── registry.go │ │ └── options.go │ ├── balancer │ │ ├── rr.go │ │ ├── weight.go │ │ ├── rr_test.go │ │ └── balancer.go │ ├── reverse_proxy_test.go │ └── register_options.go ├── render │ ├── responder.go │ └── responder_test.go ├── router │ ├── listen_path_parameter_name_extractor_test.go │ ├── listen_path_matcher.go │ ├── listen_path_parameter_name_extractor.go │ ├── listen_path_matcher_test.go │ └── router.go ├── jwt │ ├── token_test.go │ ├── token.go │ ├── provider │ │ ├── verifier.go │ │ └── provider.go │ ├── basic │ │ └── provider.go │ ├── middleware.go │ ├── guard.go │ └── github │ │ ├── organization_verifier.go │ │ └── team_verifier.go ├── observability │ └── request_id.go ├── metrics │ └── metrics_context.go ├── server │ └── option.go └── errors │ └── error_test.go ├── docs ├── proxy │ ├── conclusion.md │ ├── README.md │ ├── terminology.md │ ├── request_host_header.md │ ├── wildcard_hostnames.md │ ├── overview.md │ ├── append_uri_property.md │ ├── load_balacing.md │ ├── request_http_method.md │ ├── request_uri.md │ ├── routing_priorities.md │ └── preserve_host_property.md ├── install │ ├── configuration.md │ ├── README.md │ └── docker.md ├── plugins │ ├── compression.md │ ├── body_limit.md │ ├── retry.md │ ├── oauth.md │ ├── cb.md │ ├── README.md │ └── cors.md ├── clustering │ └── clustering.md ├── known_issues │ └── http_keepalive.md ├── misc │ ├── health_checks.md │ ├── monitoring.md │ └── tracing.md ├── README.md ├── upgrade │ ├── 3x.md │ └── 3.7.x.md ├── config │ └── proxy.md └── quick_start │ ├── README.md │ └── file_system.md ├── assets ├── stubs │ ├── upstreams │ │ ├── error.json │ │ ├── fallback.json │ │ ├── recipes.json │ │ ├── hello-world.json │ │ ├── recipes-menu.json │ │ ├── status-ok.json │ │ ├── status-broken.json │ │ └── status-partial.json │ └── auth-service │ │ └── token.json ├── auth │ └── auth.json ├── apis │ └── example_SingleDefinition.json └── docker-compose.yml ├── main.go ├── .golangci.yml ├── doc.go ├── .editorconfig ├── entry-dev.sh ├── features ├── Home.feature └── bootstrap │ ├── context_misc.go │ └── context_api.go ├── cmd ├── context.go ├── check.go └── root.go ├── .gitignore ├── cassandra ├── session.go ├── wrapper │ ├── sessionretry.go │ └── session.go └── schema.sql ├── .goreleaser.yml ├── Dockerfile ├── Makefile ├── LICENSE ├── Dockerfile.dev └── .circleci └── config.yml /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hellofresh/janus-owners 2 | docs/* @italolelis 3 | -------------------------------------------------------------------------------- /janus-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motiv-labs/janus/HEAD/janus-logo.png -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "settingsInheritedFrom": "hellofresh/whitesource-config@master" 3 | } -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "docs", 3 | "title": "Janus docs", 4 | "plugins": ["codetabs"] 5 | } 6 | -------------------------------------------------------------------------------- /examples/plugin-cb/img/hystrix-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motiv-labs/janus/HEAD/examples/plugin-cb/img/hystrix-home.png -------------------------------------------------------------------------------- /janus/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Kubernetes 4 | name: janus 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What does this PR do? 2 | A few sentences describing the overall goals of the pull request's commits. 3 | -------------------------------------------------------------------------------- /examples/plugin-cb/img/hystrix-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motiv-labs/janus/HEAD/examples/plugin-cb/img/hystrix-dashboard.png -------------------------------------------------------------------------------- /pkg/config/doc.go: -------------------------------------------------------------------------------- 1 | // Package config provides the configuration for Janus. All configurations should be set in environment variables. 2 | package config 3 | -------------------------------------------------------------------------------- /pkg/api/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package api holds the models for the API Definition. On this package you can also find the 3 | default errors, handlers, loaders and repositories. 4 | */ 5 | package api 6 | -------------------------------------------------------------------------------- /examples/front-proxy-mongo/seed.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mongo 2 | 3 | COPY apis/example.json /init.json 4 | CMD mongoimport --host janus-database --db janus --collection api_specs --type json --file /init.json --jsonArray 5 | -------------------------------------------------------------------------------- /docs/proxy/conclusion.md: -------------------------------------------------------------------------------- 1 | ### Conclusion 2 | 3 | Through this guide, we hope you gained knowledge of the underlying proxying 4 | mechanism of Janus, from how is a request matched to an API, to how to allow for 5 | using the WebSocket protocol or setup SSL for an API. 6 | -------------------------------------------------------------------------------- /pkg/test/model.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | // Tag represents the recipe tags 4 | type Tag string 5 | 6 | // Recipe represents a hellofresh recipe 7 | type Recipe struct { 8 | Name string `bson:"name" json:"name"` 9 | Tags []Tag `bson:"tags" json:"tags"` 10 | } 11 | -------------------------------------------------------------------------------- /examples/plugin-cb/stubs/service.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "url": "/" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": {"message":"Hello World!"}, 9 | "headers": { 10 | "Content-Type": "application/json" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/front-proxy/stubs/service.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "url": "/" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": {"message":"Hello World!"}, 9 | "headers": { 10 | "Content-Type": "application/json" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/front-proxy-auth/stubs/service.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "url": "/" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": {"message":"Hello World!"}, 9 | "headers": { 10 | "Content-Type": "application/json" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/plugin-cb/stubs/status.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "url": "/status" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": {"message":"All up and running"}, 9 | "headers": { 10 | "Content-Type": "application/json" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/front-proxy-auth/stubs/service1/service.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "url": "/" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": {"message":"Hello World!"}, 9 | "headers": { 10 | "Content-Type": "application/json" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/front-proxy/stubs/status.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "url": "/status" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": {"message":"All up and running"}, 9 | "headers": { 10 | "Content-Type": "application/json" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /assets/stubs/upstreams/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "url": "/error" 5 | }, 6 | "response": { 7 | "status": 500, 8 | "jsonBody": {"message":"An unexpected error has occurred"}, 9 | "headers": { 10 | "Content-Type": "application/json" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/front-proxy-auth/stubs/status.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "url": "/status" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": {"message":"All up and running"}, 9 | "headers": { 10 | "Content-Type": "application/json" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/install/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | You have multiple ways of configuring Janus. You can choose from `environment variables`, `YAML`, `JSON` or `TOML` files. 4 | Our recomendation is to use TOML configuration, since it makes it easier to read. You can check an example of the configuration [here](/janus.sample.toml) 5 | -------------------------------------------------------------------------------- /examples/front-proxy-auth/stubs/service1/status.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "url": "/status" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": {"message":"All up and running"}, 9 | "headers": { 10 | "Content-Type": "application/json" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /assets/stubs/upstreams/fallback.json: -------------------------------------------------------------------------------- 1 | { 2 | "priority":10, 3 | "request": { 4 | "method": "ANY", 5 | "urlPattern": ".*" 6 | }, 7 | "response": { 8 | "status": 404, 9 | "jsonBody": {"message":"Endpoint not found"}, 10 | "headers": { 11 | "Content-Type": "application/json" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/hellofresh/janus/cmd" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | var version = "0.0.0-dev" 9 | 10 | func main() { 11 | rootCmd := cmd.NewRootCmd(version) 12 | 13 | if err := rootCmd.Execute(); err != nil { 14 | log.WithError(err).Fatal("Could not run command") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # See https://golangci-lint.run/usage/configuration/#config-file for more information 2 | run: 3 | timeout: 5m 4 | linters: 5 | disable-all: true 6 | enable: 7 | - gofmt 8 | - golint 9 | - goimports 10 | fast: false 11 | linters-settings: 12 | gofmt: 13 | simplify: false 14 | issues: 15 | exclude-use-default: false 16 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package janus is a lightweight, API Gateway and Management Platform enables you to control who accesses your API, 3 | when they access it and how they access it. API Gateway will also record detailed analytics on how your 4 | users are interacting with your API and when things go wrong. 5 | 6 | For a full guide visit https://github.com/hellofresh/janus 7 | */ 8 | package main 9 | -------------------------------------------------------------------------------- /assets/stubs/upstreams/recipes.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "urlPathPattern": "/recipes/5252b1b5301bbf46038b473f" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "headers": { 9 | "Content-Type": "application/json; charset=utf-8" 10 | }, 11 | "jsonBody": {"slug": "I'm a slug"} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /assets/stubs/upstreams/hello-world.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "urlPathPattern": "/hello-world(/.+)?" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "headers": { 9 | "Content-Type": "application/json; charset=utf-8" 10 | }, 11 | "jsonBody": { 12 | "hello": "world" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /janus/templates/pdb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1beta1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | name: {{ include "janus.fullname" . }} 5 | labels: 6 | {{ include "janus.labels" . | indent 4 }} 7 | spec: 8 | minAvailable: {{ .Values.deployment.minAvailable }} 9 | selector: 10 | matchLabels: 11 | app.kubernetes.io/name: {{ include "janus.name" . }} 12 | app.kubernetes.io/instance: {{ .Release.Name }} 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | [Short description of problem here] 2 | 3 | **Reproduction Steps:** 4 | 5 | 1. [First Step] 6 | 2. [Second Step] 7 | 3. [Other Steps...] 8 | 9 | **Expected behavior:** 10 | 11 | [Describe expected behavior here] 12 | 13 | **Observed behavior:** 14 | 15 | [Describe observed behavior here] 16 | 17 | **Janus version:** [Enter Atom version here] 18 | **OS and version:** [Enter OS name and version here] 19 | -------------------------------------------------------------------------------- /assets/stubs/upstreams/recipes-menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "urlPathPattern": "/recipes/5252b1b5301bbf46038b473f/menus/6362b1b5301bbf46038b4766" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "headers": { 9 | "Content-Type": "application/json; charset=utf-8" 10 | }, 11 | "jsonBody": {"description": "A menu description"} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/plugins/compression.md: -------------------------------------------------------------------------------- 1 | # Compression 2 | 3 | Enables gzip compression if the client supports it. By default, responses are not gzipped. If enabled, the default settings will ensure that images, videos, and archives (already compressed) are not gzipped. 4 | 5 | The plain compression config is good enough for most things, but you can gain more control if needed: 6 | 7 | ```json 8 | "compression": { 9 | "enabled": true 10 | } 11 | ``` 12 | -------------------------------------------------------------------------------- /janus/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /pkg/plugin/retry/setup_test.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hellofresh/janus/pkg/plugin" 7 | "github.com/hellofresh/janus/pkg/proxy" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSetup(t *testing.T) { 12 | def := proxy.NewRouterDefinition(proxy.NewDefinition()) 13 | err := setupRetry(def, make(plugin.Config)) 14 | assert.NoError(t, err) 15 | 16 | assert.Len(t, def.Middleware(), 1) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/plugin/bodylmt/setup_test.go: -------------------------------------------------------------------------------- 1 | package bodylmt 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hellofresh/janus/pkg/plugin" 7 | "github.com/hellofresh/janus/pkg/proxy" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSetup(t *testing.T) { 12 | def := proxy.NewRouterDefinition(proxy.NewDefinition()) 13 | err := setupBodyLimit(def, make(plugin.Config)) 14 | assert.NoError(t, err) 15 | 16 | assert.Len(t, def.Middleware(), 1) 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_size = 4 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | 12 | [*.{yml,yaml}] 13 | indent_size = 2 14 | 15 | [*.go] 16 | indent_style = tab 17 | indent_size = 4 18 | 19 | [*.json] 20 | indent_size = 4 21 | indent_style = space 22 | 23 | [Makefile] 24 | indent_style = tab 25 | indent_size = 4 26 | -------------------------------------------------------------------------------- /examples/front-proxy-cassandra/apis/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "my-endpoint", 3 | "active" : true, 4 | "proxy" : { 5 | "listen_path" : "/example/*", 6 | "upstreams" : { 7 | "balancing": "roundrobin", 8 | "targets": [ 9 | {"target": "http://www.mocky.io/v2/595625d22900008702cd71e8"} 10 | ] 11 | }, 12 | "methods" : ["GET"] 13 | }, 14 | "plugins": [ 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /pkg/plugin/compression/setup_test.go: -------------------------------------------------------------------------------- 1 | package compression 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hellofresh/janus/pkg/plugin" 7 | "github.com/hellofresh/janus/pkg/proxy" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSetup(t *testing.T) { 12 | def := proxy.NewRouterDefinition(proxy.NewDefinition()) 13 | err := setupCompression(def, make(plugin.Config)) 14 | assert.NoError(t, err) 15 | 16 | assert.Len(t, def.Middleware(), 1) 17 | } 18 | -------------------------------------------------------------------------------- /entry-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd /janus 3 | if [ "$debug" == 1 ]; then 4 | echo "about to compile go for debugging" 5 | go build -gcflags "all=-N -l" -o main . 6 | else 7 | echo "about to compile go" 8 | CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . 9 | fi 10 | echo "compile finished" 11 | if [ "$debug" == 1 ]; then 12 | dlv --listen=:40000 --headless=true --continue --accept-multiclient --api-version=2 exec ./main start 13 | else 14 | ./main start 15 | fi 16 | -------------------------------------------------------------------------------- /examples/front-proxy/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | 4 | janus: 5 | image: hellofreshtech/janus 6 | ports: 7 | - "8080:8080" 8 | - "8081:8081" 9 | depends_on: 10 | - service1 11 | volumes: 12 | - ./janus.toml:/etc/janus/janus.toml 13 | - ./apis:/etc/janus/apis 14 | 15 | service1: 16 | image: rodolpheche/wiremock 17 | ports: 18 | - '9089:8080' 19 | volumes: 20 | - ./stubs:/home/wiremock/mappings 21 | -------------------------------------------------------------------------------- /features/Home.feature: -------------------------------------------------------------------------------- 1 | Feature: Retrieve welcome line of the service. 2 | As an anonymous user, I need to be able to see welcome line of the system. 3 | In order to know that the service is up and running. 4 | 5 | Scenario: Check welcome line of the service 6 | When I request "/" API path with "GET" method 7 | Then I should receive 200 response code 8 | And header "Content-Type" should be "application/json" 9 | And the response should contain "Welcome to Janus" 10 | -------------------------------------------------------------------------------- /pkg/plugin/compression/setup.go: -------------------------------------------------------------------------------- 1 | package compression 2 | 3 | import ( 4 | "github.com/go-chi/chi/middleware" 5 | "github.com/hellofresh/janus/pkg/plugin" 6 | "github.com/hellofresh/janus/pkg/proxy" 7 | ) 8 | 9 | func init() { 10 | plugin.RegisterPlugin("compression", plugin.Plugin{ 11 | Action: setupCompression, 12 | }) 13 | } 14 | 15 | func setupCompression(def *proxy.RouterDefinition, rawConfig plugin.Config) error { 16 | def.AddMiddleware(middleware.DefaultCompress) 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/plugin/oauth2/setup_test.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hellofresh/janus/pkg/plugin" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestOAuth2Config(t *testing.T) { 12 | var config Config 13 | rawConfig := map[string]interface{}{ 14 | "server_name": "test", 15 | } 16 | 17 | err := plugin.Decode(rawConfig, &config) 18 | require.NoError(t, err) 19 | assert.Equal(t, "test", config.ServerName) 20 | } 21 | -------------------------------------------------------------------------------- /assets/stubs/auth-service/token.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "POST", 4 | "url": "/token" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": { 9 | "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbnVzIn0.PvBI5yIdPVtR8RVJWWZEEEVv9Bk83Q_rS7vYcKNX1wM", 10 | "expires_in": 21600, 11 | "token_type": "Bearer" 12 | }, 13 | "headers": { 14 | "Content-Type": "application/json" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/front-proxy-auth/stubs/auth/token.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "POST", 4 | "url": "/token" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": { 9 | "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbnVzIn0.PvBI5yIdPVtR8RVJWWZEEEVv9Bk83Q_rS7vYcKNX1wM", 10 | "expires_in": 21600, 11 | "token_type": "Bearer" 12 | }, 13 | "headers": { 14 | "Content-Type": "application/json" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cmd/context.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | ) 9 | 10 | // ContextWithSignal create a context cancelled when SIGINT or SIGTERM are notified 11 | func ContextWithSignal(ctx context.Context) context.Context { 12 | newCtx, cancel := context.WithCancel(ctx) 13 | signals := make(chan os.Signal) 14 | signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) 15 | go func() { 16 | select { 17 | case <-signals: 18 | cancel() 19 | close(signals) 20 | } 21 | }() 22 | return newCtx 23 | } 24 | -------------------------------------------------------------------------------- /janus/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "janus.fullname" ( dict "Values" .Values "Chart" .Chart "Release" .Release "name" .Values.name ) }}-test-connection" 5 | labels: 6 | {{ include "janus.labels" . | indent 4 }} 7 | annotations: 8 | "helm.sh/hook": test-success 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "janus.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /janus/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "janus.fullname" ( dict "Values" .Values "Chart" .Chart "Release" .Release "name" .Values.service.name ) }} 5 | labels: 6 | {{ include "janus.labels" . | indent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | {{- with .Values.service.ports }} 10 | ports: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | selector: 14 | app.kubernetes.io/name: {{ include "janus.name" . }} 15 | app.kubernetes.io/instance: {{ .Release.Name }} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | main 6 | 7 | # Folders 8 | _obj 9 | _test 10 | .idea 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | profile.coverprofile 24 | overalls.coverprofile 25 | 26 | *.exe 27 | *.test 28 | *.prof 29 | gin-bin 30 | 31 | _book 32 | /vendor 33 | node_modules/ 34 | 35 | # HF config files 36 | /dist 37 | debug 38 | -------------------------------------------------------------------------------- /pkg/middleware/recovery.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "net/http" 4 | 5 | // NewRecovery creates a new instance of Recovery 6 | func NewRecovery(recoverFunc func(w http.ResponseWriter, r *http.Request, err interface{})) func(http.Handler) http.Handler { 7 | return func(handler http.Handler) http.Handler { 8 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 9 | defer func() { 10 | if err := recover(); err != nil { 11 | recoverFunc(w, r, err) 12 | } 13 | }() 14 | 15 | handler.ServeHTTP(w, r) 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/front-proxy-cassandra/apis/example2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "my-endpoint-2", 3 | "active" : true, 4 | "proxy" : { 5 | "listen_path" : "/example/path/*", 6 | "upstreams" : { 7 | "balancing": "roundrobin", 8 | "targets": [ 9 | {"target": "http://www.mocky.io/v2/595625d22900008702cd71e8"} 10 | ] 11 | }, 12 | "methods" : ["GET"] 13 | }, 14 | "plugins": [ 15 | { 16 | "name": "basic_auth", 17 | "enabled": true 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /examples/janus-tls/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | janus: 5 | image: hellofreshtech/janus 6 | ports: 7 | - "8443:8443" 8 | - "8444:8444" 9 | depends_on: 10 | - service1 11 | volumes: 12 | - ./apis:/etc/janus/apis 13 | - ./janus.toml:/etc/janus/janus.toml 14 | - ./janus.crt:/etc/janus/janus.crt 15 | - ./janus.key:/etc/janus/janus.key 16 | 17 | service1: 18 | image: rodolpheche/wiremock 19 | ports: 20 | - '9089:8080' 21 | volumes: 22 | - ../front-proxy/stubs:/home/wiremock/mappings 23 | -------------------------------------------------------------------------------- /pkg/plugin/rate/setup_integration_test.go: -------------------------------------------------------------------------------- 1 | package rate 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/hellofresh/janus/pkg/proxy" 9 | ) 10 | 11 | func TestRateLimitPluginRedisPolicy(t *testing.T) { 12 | rawConfig := map[string]interface{}{ 13 | "limit": "10-S", 14 | "policy": "redis", 15 | "redis": map[string]interface{}{ 16 | "dsn": "localhost", 17 | "prefix": "test", 18 | }, 19 | } 20 | 21 | def := proxy.NewRouterDefinition(proxy.NewDefinition()) 22 | err := setupRateLimit(def, rawConfig) 23 | 24 | assert.Error(t, err) 25 | } 26 | -------------------------------------------------------------------------------- /examples/front-proxy/apis/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "example", 3 | "active" : true, 4 | "proxy" : { 5 | "preserve_host" : false, 6 | "listen_path" : "/example/*", 7 | "upstreams" : { 8 | "balancing": "roundrobin", 9 | "targets": [ 10 | {"target": "http://service1:8080/"} 11 | ] 12 | }, 13 | "strip_path" : false, 14 | "append_path" : false, 15 | "methods" : ["GET"] 16 | }, 17 | "health_check": { 18 | "url": "http://service1:8080/status", 19 | "timeout": 3 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/janus-tls/apis/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "example", 3 | "active" : true, 4 | "proxy" : { 5 | "preserve_host" : false, 6 | "listen_path" : "/example/*", 7 | "upstreams" : { 8 | "balancing": "roundrobin", 9 | "targets": [ 10 | {"target": "http://service1:8080/"} 11 | ] 12 | }, 13 | "strip_path" : false, 14 | "append_path" : false, 15 | "methods" : ["GET"] 16 | }, 17 | "health_check": { 18 | "url": "http://service1:8080/status", 19 | "timeout": 3 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/middleware/logger_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "testing" 5 | 6 | "net/http" 7 | 8 | "github.com/hellofresh/janus/pkg/test" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestSuccessfulLog(t *testing.T) { 13 | mw := NewLogger() 14 | w, err := test.Record( 15 | "GET", 16 | "/", 17 | map[string]string{ 18 | "Content-Type": "application/json", 19 | }, 20 | mw.Handler(http.HandlerFunc(test.Ping)), 21 | ) 22 | assert.NoError(t, err) 23 | 24 | assert.Equal(t, http.StatusOK, w.Code) 25 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 26 | } 27 | -------------------------------------------------------------------------------- /docs/proxy/README.md: -------------------------------------------------------------------------------- 1 | # Proxy Reference 2 | 3 | Janus listens for traffic on four ports, which by default are: 4 | 5 | `:8080` on which Janus listens for incoming HTTP traffic from your clients, and forwards it to your upstream services. 6 | 7 | `:8443` on which Janus listens for incoming HTTPS traffic. This port has a similar behavior as the `:8080` port, except that it expects HTTPS traffic only. This port can be disabled via the configuration file. 8 | 9 | `:8081` on which the [Admin API](admin_api.md) used to configure Janus listens. 10 | 11 | `:8444` on which the [Admin API](admin_api.md) listens for HTTPS traffic. 12 | -------------------------------------------------------------------------------- /examples/front-proxy-cluster/apis/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "example", 3 | "active" : true, 4 | "proxy" : { 5 | "preserve_host" : false, 6 | "listen_path" : "/example/*", 7 | "upstreams" : { 8 | "balancing": "roundrobin", 9 | "targets": [ 10 | {"target": "http://service1:8080/"} 11 | ] 12 | }, 13 | "strip_path" : false, 14 | "append_path" : false, 15 | "methods" : ["GET"] 16 | }, 17 | "health_check": { 18 | "url": "http://service1:8080/status", 19 | "timeout": 3 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/plugin/requesttransformer/setup.go: -------------------------------------------------------------------------------- 1 | package requesttransformer 2 | 3 | import ( 4 | "github.com/hellofresh/janus/pkg/plugin" 5 | "github.com/hellofresh/janus/pkg/proxy" 6 | ) 7 | 8 | func init() { 9 | plugin.RegisterPlugin("request_transformer", plugin.Plugin{ 10 | Action: setupRequestTransformer, 11 | }) 12 | } 13 | 14 | func setupRequestTransformer(def *proxy.RouterDefinition, rawConfig plugin.Config) error { 15 | var config Config 16 | err := plugin.Decode(rawConfig, &config) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | def.AddMiddleware(NewRequestTransformer(config)) 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /examples/front-proxy/janus.toml: -------------------------------------------------------------------------------- 1 | ################################################################ 2 | # Global configuration 3 | ################################################################ 4 | port = 8080 5 | 6 | [log] 7 | level = "debug" 8 | 9 | ################################################################ 10 | # API configuration backend 11 | ################################################################ 12 | [web] 13 | port = 8081 14 | 15 | [web.credentials] 16 | secret = "secret" 17 | 18 | [web.credentials.basic] 19 | users = {admin = "admin"} 20 | 21 | [database] 22 | dsn = "file:///etc/janus" 23 | -------------------------------------------------------------------------------- /examples/plugin-cb/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | 4 | janus: 5 | image: hellofreshtech/janus 6 | ports: 7 | - "8080:8080" 8 | - "8081:8081" 9 | depends_on: 10 | - service1 11 | volumes: 12 | - ./janus.toml:/etc/janus/janus.toml 13 | - ./apis:/etc/janus/apis 14 | 15 | service1: 16 | image: rodolpheche/wiremock 17 | ports: 18 | - '9089:8080' 19 | volumes: 20 | - ./stubs:/home/wiremock/mappings 21 | 22 | hystrix: 23 | image: mlabouardy/hystrix-dashboard 24 | depends_on: 25 | - janus 26 | ports: 27 | - 9002:9002 28 | -------------------------------------------------------------------------------- /examples/plugin-cb/janus.toml: -------------------------------------------------------------------------------- 1 | ################################################################ 2 | # Global configuration 3 | ################################################################ 4 | port = 8080 5 | 6 | [log] 7 | level = "debug" 8 | 9 | ################################################################ 10 | # API configuration backend 11 | ################################################################ 12 | [web] 13 | port = 8081 14 | 15 | [web.credentials] 16 | secret = "secret" 17 | 18 | [web.credentials.basic] 19 | users = {admin = "admin"} 20 | 21 | [database] 22 | dsn = "file:///etc/janus" 23 | -------------------------------------------------------------------------------- /pkg/plugin/responsetransformer/setup.go: -------------------------------------------------------------------------------- 1 | package responsetransformer 2 | 3 | import ( 4 | "github.com/hellofresh/janus/pkg/plugin" 5 | "github.com/hellofresh/janus/pkg/proxy" 6 | ) 7 | 8 | func init() { 9 | plugin.RegisterPlugin("response_transformer", plugin.Plugin{ 10 | Action: setupResponseTransformer, 11 | }) 12 | } 13 | 14 | func setupResponseTransformer(def *proxy.RouterDefinition, rawConfig plugin.Config) error { 15 | var config Config 16 | err := plugin.Decode(rawConfig, &config) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | def.AddMiddleware(NewResponseTransformer(config)) 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /examples/front-proxy-auth/janus.toml: -------------------------------------------------------------------------------- 1 | ################################################################ 2 | # Global configuration 3 | ################################################################ 4 | port = 8080 5 | 6 | [log] 7 | level = "debug" 8 | 9 | ################################################################ 10 | # API configuration backend 11 | ################################################################ 12 | [web] 13 | port = 8081 14 | 15 | [web.credentials] 16 | secret = "secret" 17 | 18 | [web.credentials.basic] 19 | users = {admin = "admin"} 20 | 21 | [database] 22 | dsn = "file:///etc/janus" 23 | -------------------------------------------------------------------------------- /docs/proxy/terminology.md: -------------------------------------------------------------------------------- 1 | ### Terminology 2 | 3 | `API`: This term refers to the API entity of Janus. You configure your APIs, that point to your own upstream services, through the Admin API. 4 | 5 | `Middleware`: This refers to Janus "middleware", which are pieces of business logic that run in the proxying lifecycle. Middleware can be configured through the Admin API - either globally (all incoming traffic) or on a per-API basis. 6 | 7 | `Client`: Refers to the downstream client making requests to Janus's proxy port. 8 | 9 | `Upstream service`: Refers to your own API/service sitting behind Janus, to which client requests are forwarded. 10 | -------------------------------------------------------------------------------- /cassandra/session.go: -------------------------------------------------------------------------------- 1 | package cassandra 2 | 3 | import ( 4 | "github.com/hellofresh/janus/cassandra/wrapper" 5 | ) 6 | const ( 7 | // Cassandra cluster host 8 | ClusterHostName = "db" 9 | // System keyspace 10 | SystemKeyspace = "system" 11 | // Github taxi dispatcher keyspace 12 | AppKeyspace = "janus" 13 | // default timeout 14 | Timeout = 300 15 | ) 16 | 17 | // SessionHolder holds our connection to Cassandra 18 | var sessionHolder wrapper.Holder 19 | 20 | func GetSession() wrapper.SessionInterface { 21 | return sessionHolder.GetSession() 22 | } 23 | 24 | func SetSessionHolder(holder wrapper.Holder) { 25 | sessionHolder = holder 26 | } 27 | -------------------------------------------------------------------------------- /examples/front-proxy-cassandra/janus.toml: -------------------------------------------------------------------------------- 1 | ################################################################ 2 | # Global configuration 3 | ################################################################ 4 | port = 8080 5 | 6 | [log] 7 | level = "debug" 8 | 9 | ################################################################ 10 | # API configuration backend 11 | ################################################################ 12 | [web] 13 | port = 8081 14 | 15 | [web.credentials] 16 | secret = "secret" 17 | 18 | [web.credentials.basic] 19 | users = {admin = "admin"} 20 | 21 | [database] 22 | dsn = "cassandra:///db/system/janus/300" 23 | -------------------------------------------------------------------------------- /examples/front-proxy-mongo/janus.toml: -------------------------------------------------------------------------------- 1 | ################################################################ 2 | # Global configuration 3 | ################################################################ 4 | port = 8080 5 | 6 | [log] 7 | level = "debug" 8 | 9 | ################################################################ 10 | # API configuration backend 11 | ################################################################ 12 | [web] 13 | port = 8081 14 | 15 | [web.credentials] 16 | secret = "secret" 17 | 18 | [web.credentials.basic] 19 | users = {admin = "admin"} 20 | 21 | [database] 22 | dsn = "mongodb://janus-database:27017/janus" 23 | -------------------------------------------------------------------------------- /pkg/web/checker_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "testing" 5 | 6 | "net/http" 7 | 8 | "github.com/hellofresh/janus/pkg/api" 9 | "github.com/hellofresh/janus/pkg/router" 10 | "github.com/hellofresh/janus/pkg/test" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestRegister(t *testing.T) { 15 | r := router.NewChiRouter() 16 | r.GET("/status", NewOverviewHandler(&api.Configuration{})) 17 | 18 | ts := test.NewServer(r) 19 | defer ts.Close() 20 | 21 | res, _ := ts.Do(http.MethodGet, "/status", make(map[string]string)) 22 | assert.Equal(t, http.StatusOK, res.StatusCode) 23 | assert.Equal(t, "application/json", res.Header.Get("Content-Type")) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/plugin/basic/encrypt/encrypt.go: -------------------------------------------------------------------------------- 1 | package encrypt 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | //Hash implements root.Hash 6 | type Hash struct{} 7 | 8 | //Generate a salted hash for the input string 9 | func (c *Hash) Generate(s string) (string, error) { 10 | saltedBytes := []byte(s) 11 | hashedBytes, err := bcrypt.GenerateFromPassword(saltedBytes, bcrypt.DefaultCost) 12 | if err != nil { 13 | return "", err 14 | } 15 | 16 | hash := string(hashedBytes[:]) 17 | return hash, nil 18 | } 19 | 20 | func (c *Hash) Compare(hash string, s string) error { 21 | incoming := []byte(s) 22 | existing := []byte(hash) 23 | return bcrypt.CompareHashAndPassword(existing, incoming) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/plugin/oauth2/file_repository_test.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNewFileSystemRepository(t *testing.T) { 13 | wd, err := os.Getwd() 14 | assert.NoError(t, err) 15 | 16 | // ./../../assets/auth 17 | exampleAPIsPath := filepath.Join(wd, "..", "..", "..", "assets", "stubs", "auth-service") 18 | info, err := os.Stat(exampleAPIsPath) 19 | require.NoError(t, err) 20 | require.True(t, info.IsDir()) 21 | 22 | fsRepo, err := NewFileSystemRepository(exampleAPIsPath) 23 | require.NoError(t, err) 24 | assert.NotNil(t, fsRepo) 25 | } 26 | -------------------------------------------------------------------------------- /examples/front-proxy-cluster/janus.toml: -------------------------------------------------------------------------------- 1 | ################################################################ 2 | # Global configuration 3 | ################################################################ 4 | port = 8080 5 | 6 | [log] 7 | level = "debug" 8 | 9 | ################################################################ 10 | # API configuration backend 11 | ################################################################ 12 | [web] 13 | port = 8081 14 | 15 | [web.credentials] 16 | secret = "secret" 17 | 18 | [web.credentials.basic] 19 | users = {admin = "admin"} 20 | 21 | [database] 22 | dsn = "mongodb://janus-database:27017/janus" 23 | 24 | [cluster] 25 | UpdateFrequency = "5s" 26 | -------------------------------------------------------------------------------- /pkg/test/handler.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/hellofresh/janus/pkg/errors" 7 | ) 8 | 9 | // Ping is a test handler 10 | func Ping(w http.ResponseWriter, r *http.Request) { 11 | w.Header().Add("Content-Type", "application/json") 12 | w.Write([]byte("OK\n")) 13 | } 14 | 15 | // FailWith is a test handler that fails 16 | func FailWith(statusCode int) http.Handler { 17 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | w.WriteHeader(statusCode) 19 | }) 20 | } 21 | 22 | // RecoveryHandler represents the recovery handler 23 | func RecoveryHandler(w http.ResponseWriter, r *http.Request, err interface{}) { 24 | errors.Handler(w, r, err) 25 | } 26 | -------------------------------------------------------------------------------- /examples/front-proxy-mongo/apis/example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name" : "example", 4 | "active" : true, 5 | "proxy" : { 6 | "preserve_host" : false, 7 | "listen_path" : "/example/*", 8 | "upstreams" : { 9 | "balancing": "roundrobin", 10 | "targets": [ 11 | {"target": "http://service1:8080/"} 12 | ] 13 | }, 14 | "strip_path" : false, 15 | "append_path" : false, 16 | "methods" : ["GET"] 17 | }, 18 | "health_check": { 19 | "url": "http://service1:8080/status", 20 | "timeout": 3 21 | } 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /assets/stubs/upstreams/status-ok.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "urlPath": "/status-ok" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "headers": { 9 | "Content-Type": "application/json; charset=utf-8" 10 | }, 11 | "jsonBody": { 12 | "status": "OK", 13 | "timestamp": "2017-07-03T14:48:14.563630521Z", 14 | "system": { 15 | "version": "go1.8.3", 16 | "goroutines_count": 15, 17 | "total_alloc_bytes": 46186776, 18 | "heap_objects_count": 44186, 19 | "alloc_bytes": 5733552 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/proxy/transport/registry.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | ) 7 | 8 | type registry struct { 9 | sync.RWMutex 10 | store map[string]*http.Transport 11 | } 12 | 13 | func newRegistry() *registry { 14 | r := new(registry) 15 | r.store = make(map[string]*http.Transport) 16 | 17 | return r 18 | } 19 | 20 | func (r *registry) get(key string) (*http.Transport, bool) { 21 | r.RLock() 22 | defer r.RUnlock() 23 | 24 | // return r.store[key] does not work here, says too few argument to return 25 | tr, ok := r.store[key] 26 | return tr, ok 27 | } 28 | 29 | func (r *registry) put(key string, tr *http.Transport) { 30 | r.Lock() 31 | defer r.Unlock() 32 | 33 | r.store[key] = tr 34 | } 35 | -------------------------------------------------------------------------------- /examples/front-proxy-auth/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | 4 | janus: 5 | image: hellofreshtech/janus 6 | ports: 7 | - "8080:8080" 8 | - "8081:8081" 9 | depends_on: 10 | - service1 11 | - auth-service 12 | volumes: 13 | - ./janus.toml:/etc/janus/janus.toml 14 | - ./apis:/etc/janus/apis 15 | - ./auth:/etc/janus/auth 16 | 17 | service1: 18 | image: rodolpheche/wiremock 19 | ports: 20 | - '9089:8080' 21 | volumes: 22 | - ./stubs/service1:/home/wiremock/mappings 23 | 24 | auth-service: 25 | image: rodolpheche/wiremock 26 | ports: 27 | - '9088:8080' 28 | volumes: 29 | - ./stubs/auth:/home/wiremock/mappings 30 | -------------------------------------------------------------------------------- /pkg/plugin/basic/errors.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/hellofresh/janus/pkg/errors" 7 | ) 8 | 9 | var ( 10 | // ErrNotAuthorized is used when the the access is not permisted 11 | ErrNotAuthorized = errors.New(http.StatusUnauthorized, "not authorized") 12 | // ErrUserNotFound is used when an user is not found 13 | ErrUserNotFound = errors.New(http.StatusNotFound, "user not found") 14 | // ErrUserExists is used when an user already exists 15 | ErrUserExists = errors.New(http.StatusNotFound, "user already exists") 16 | // ErrInvalidAdminRouter is used when an invalid admin router is given 17 | ErrInvalidAdminRouter = errors.New(http.StatusNotFound, "invalid admin router given") 18 | ) 19 | -------------------------------------------------------------------------------- /pkg/render/responder.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | ) 8 | 9 | // M is a simple abstraction for a map interface 10 | type M map[string]interface{} 11 | 12 | // JSON marshals 'v' to JSON, automatically escaping HTML and setting the 13 | // Content-Type as application/json. 14 | func JSON(w http.ResponseWriter, code int, v interface{}) { 15 | buf := &bytes.Buffer{} 16 | enc := json.NewEncoder(buf) 17 | enc.SetEscapeHTML(true) 18 | if err := enc.Encode(v); err != nil { 19 | http.Error(w, err.Error(), http.StatusInternalServerError) 20 | return 21 | } 22 | 23 | w.Header().Set("Content-Type", "application/json") 24 | w.WriteHeader(code) 25 | w.Write(buf.Bytes()) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/plugin/organization/errors.go: -------------------------------------------------------------------------------- 1 | package organization 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/hellofresh/janus/pkg/errors" 7 | ) 8 | 9 | var ( 10 | // ErrNotAuthorized is used when the the access is not permisted 11 | ErrNotAuthorized = errors.New(http.StatusUnauthorized, "not authorized") 12 | // ErrUserNotFound is used when an user is not found 13 | ErrUserNotFound = errors.New(http.StatusNotFound, "user not found") 14 | // ErrUserExists is used when an user already exists 15 | ErrUserExists = errors.New(http.StatusNotFound, "user already exists") 16 | // ErrInvalidAdminRouter is used when an invalid admin router is given 17 | ErrInvalidAdminRouter = errors.New(http.StatusNotFound, "invalid admin router given") 18 | ) 19 | -------------------------------------------------------------------------------- /pkg/router/listen_path_parameter_name_extractor_test.go: -------------------------------------------------------------------------------- 1 | package router_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hellofresh/janus/pkg/router" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestExtractCorrectParameters(t *testing.T) { 11 | extractor := router.NewListenPathParamNameExtractor() 12 | 13 | assert.Equal(t, []string{}, extractor.Extract("/recipes/")) 14 | assert.Equal(t, []string{}, extractor.Extract("/recipes?take=100")) 15 | assert.Equal(t, []string{"id"}, extractor.Extract("/recipes/{id}/favorites")) 16 | assert.Equal(t, []string{"id", "slug"}, extractor.Extract("/recipes/{id}/favorites/{slug}")) 17 | assert.Equal(t, []string{"id", "slug"}, extractor.Extract("/recipes/{id}/favorites/{slug}?q=123")) 18 | } 19 | -------------------------------------------------------------------------------- /cassandra/wrapper/sessionretry.go: -------------------------------------------------------------------------------- 1 | package wrapper 2 | 3 | import ( 4 | "github.com/gocql/gocql" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | // sessionRetry is an implementation of SessionInterface 9 | type sessionRetry struct { 10 | goCqlSession *gocql.Session 11 | } 12 | 13 | // Query wrapper to be able to return our own QueryInterface 14 | func (s sessionRetry) Query( stmt string, values ...interface{}) QueryInterface { 15 | log.Debug("running SessionRetry Query() method") 16 | 17 | return queryRetry{goCqlQuery: s.goCqlSession.Query(stmt, values...)} 18 | } 19 | 20 | // Close wrapper to be able to run goCql method 21 | func (s sessionRetry) Close() { 22 | log.Debug("running SessionRetry Close() method") 23 | 24 | s.goCqlSession.Close() 25 | } 26 | -------------------------------------------------------------------------------- /assets/auth/auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "local", 3 | "oauth_endpoints" : { 4 | "token" : { 5 | "preserve_host" : false, 6 | "listen_path" : "/auth/token", 7 | "upstreams" : { 8 | "balancing": "roundrobin", 9 | "targets": [ 10 | {"target": "http://localhost:9088/token"} 11 | ] 12 | }, 13 | "strip_path" : true, 14 | "append_path" : false, 15 | "methods" : [ 16 | "GET", 17 | "POST" 18 | ] 19 | } 20 | }, 21 | "token_strategy" : { 22 | "name" : "jwt", 23 | "settings" : [ 24 | {"alg": "HS256", "key" : "secret"} 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cmd/check.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hellofresh/janus/pkg/config" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // NewCheckCmd creates a new check command 11 | func NewCheckCmd(ctx context.Context) *cobra.Command { 12 | return &cobra.Command{ 13 | Use: "check [config-file]", 14 | Short: "Check the validity of a given Janus configuration file. (default /etc/janus/janus.toml)", 15 | Args: cobra.MinimumNArgs(1), 16 | RunE: RunCheck, 17 | } 18 | } 19 | 20 | // RunCheck is the run command to check Janus configurations 21 | func RunCheck(cmd *cobra.Command, args []string) error { 22 | _, err := config.Load(args[0]) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | cmd.Println("The configuration file is valid") 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /examples/front-proxy-auth/auth/auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "local", 3 | "oauth_endpoints" : { 4 | "token" : { 5 | "preserve_host" : false, 6 | "listen_path" : "/auth/token", 7 | "upstreams" : { 8 | "balancing": "roundrobin", 9 | "targets": [ 10 | {"target": "http://auth-service:8080/token"} 11 | ] 12 | }, 13 | "strip_path" : true, 14 | "append_path" : false, 15 | "methods" : [ 16 | "GET", 17 | "POST" 18 | ] 19 | } 20 | }, 21 | "token_strategy" : { 22 | "name" : "jwt", 23 | "settings" : [ 24 | {"alg": "HS256", "key" : "secret"} 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/proxy/request_host_header.md: -------------------------------------------------------------------------------- 1 | ### Request Host header 2 | 3 | Routing a request based on its Host header is the most straightforward way to proxy traffic through Janus, as this is the intended usage of the HTTP Host header. Janus makes it easy to do so via the hosts field of the API entity. 4 | 5 | `hosts` accepts multiple values, which must be in an array format when specifying them via the Admin API: 6 | 7 | ```json 8 | { 9 | "hosts": ["my-api.com", "example.com", "service.com"] 10 | } 11 | ``` 12 | 13 | To satisfy the hosts condition of this API, any incoming request from a client must now have its Host header set to one of: 14 | 15 | ```http 16 | Host: my-api.com 17 | ``` 18 | 19 | or: 20 | 21 | ```http 22 | Host: example.com 23 | ``` 24 | 25 | or: 26 | 27 | ```http 28 | Host: service.com 29 | ``` 30 | -------------------------------------------------------------------------------- /examples/front-proxy-auth/apis/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "example", 3 | "active" : true, 4 | "proxy" : { 5 | "preserve_host" : false, 6 | "listen_path" : "/example/*", 7 | "upstreams" : { 8 | "balancing": "roundrobin", 9 | "targets": [ 10 | {"target": "http://service1:8080/"} 11 | ] 12 | }, 13 | "strip_path" : false, 14 | "append_path" : false, 15 | "methods" : ["GET"] 16 | }, 17 | "health_check": { 18 | "url": "http://service1:8080/status", 19 | "timeout": 3 20 | }, 21 | "plugins": [ 22 | { 23 | "name": "oauth2", 24 | "enabled" : true, 25 | "config": { 26 | "server_name": "local" 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /pkg/jwt/token_test.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/dgrijalva/jwt-go" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestIssueAdminToken(t *testing.T) { 13 | alg := "HS256" 14 | key := time.Now().Format(time.RFC3339Nano) 15 | claimsID := time.Now().Format(time.RFC3339Nano) 16 | 17 | accessToken, err := IssueAdminToken(SigningMethod{alg, key}, jwt.MapClaims{"id": claimsID}, time.Hour) 18 | require.NoError(t, err) 19 | 20 | config := NewParserConfig(0, SigningMethod{Alg: alg, Key: key}) 21 | parser := NewParser(config) 22 | 23 | token, err := parser.Parse(accessToken.Token) 24 | require.NoError(t, err) 25 | 26 | claims, ok := parser.GetMapClaims(token) 27 | assert.True(t, ok) 28 | assert.Equal(t, claimsID, claims["id"]) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/middleware/request_id.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gofrs/uuid" 7 | "github.com/hellofresh/janus/pkg/observability" 8 | ) 9 | 10 | type reqIDKeyType int 11 | 12 | const ( 13 | reqIDKey reqIDKeyType = iota 14 | requestIDHeader = "X-Request-ID" 15 | ) 16 | 17 | // RequestID middleware 18 | func RequestID(handler http.Handler) http.Handler { 19 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 | requestID := r.Header.Get(requestIDHeader) 21 | if requestID == "" { 22 | requestID = uuid.Must(uuid.NewV4()).String() 23 | } 24 | 25 | r.Header.Set(requestIDHeader, requestID) 26 | w.Header().Set(requestIDHeader, requestID) 27 | 28 | handler.ServeHTTP(w, r.WithContext(observability.RequestIDToContext(r.Context(), requestID))) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /assets/stubs/upstreams/status-broken.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "urlPath": "/status-broken" 5 | }, 6 | "response": { 7 | "status": 503, 8 | "headers": { 9 | "Content-Type": "application/json; charset=utf-8" 10 | }, 11 | "jsonBody": { 12 | "status": "Unavailable", 13 | "timestamp": "2017-07-03T14:48:14.563630521Z", 14 | "failures": { 15 | "mongodb": "Failed during MongoDB health check" 16 | }, 17 | "system": { 18 | "version": "go1.8.3", 19 | "goroutines_count": 15, 20 | "total_alloc_bytes": 46186776, 21 | "heap_objects_count": 44186, 22 | "alloc_bytes": 5733552 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pkg/observability/request_id.go: -------------------------------------------------------------------------------- 1 | package observability 2 | 3 | import "context" 4 | 5 | type reqIDKeyType int 6 | 7 | const reqIDKey reqIDKeyType = iota 8 | 9 | // RequestIDToContext puts a request ID to context for future use 10 | func RequestIDToContext(ctx context.Context, requestID string) context.Context { 11 | if ctx == nil { 12 | panic("Can not put request ID to empty context") 13 | } 14 | 15 | return context.WithValue(ctx, reqIDKey, requestID) 16 | } 17 | 18 | // RequestIDFromContext tries to extract request ID from context if present, otherwise returns empty string 19 | func RequestIDFromContext(ctx context.Context) string { 20 | if ctx == nil { 21 | panic("Can not get request ID from empty context") 22 | } 23 | 24 | if requestID, ok := ctx.Value(reqIDKey).(string); ok { 25 | return requestID 26 | } 27 | 28 | return "" 29 | } 30 | -------------------------------------------------------------------------------- /features/bootstrap/context_misc.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/cucumber/godog" 7 | ) 8 | 9 | const durationAWhile = time.Second 10 | 11 | // RegisterMiscContext registers godog suite context for handling misc steps 12 | func RegisterMiscContext(ctx *godog.ScenarioContext) { 13 | scenarioCtx := &miscContext{} 14 | 15 | ctx.Step(`^I wait for a while$`, scenarioCtx.iWaitForAWhile) 16 | ctx.Step(`^I wait for "([^"]*)"$`, scenarioCtx.iWaitFor) 17 | } 18 | 19 | type miscContext struct{} 20 | 21 | func (c *miscContext) iWaitForAWhile() error { 22 | time.Sleep(durationAWhile) 23 | return nil 24 | } 25 | 26 | func (c *miscContext) iWaitFor(duration string) error { 27 | parsedDuration, err := time.ParseDuration(duration) 28 | if nil != err { 29 | return err 30 | } 31 | time.Sleep(parsedDuration) 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/middleware/recovery_test.go: -------------------------------------------------------------------------------- 1 | package middleware_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "net/http" 7 | 8 | "github.com/hellofresh/janus/pkg/errors" 9 | "github.com/hellofresh/janus/pkg/middleware" 10 | "github.com/hellofresh/janus/pkg/test" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestSuccessfulRecovery(t *testing.T) { 15 | mw := middleware.NewRecovery(errors.RecoveryHandler) 16 | w, err := test.Record( 17 | "GET", 18 | "/", 19 | map[string]string{ 20 | "Content-Type": "application/json", 21 | }, 22 | mw(http.HandlerFunc(doPanic)), 23 | ) 24 | assert.NoError(t, err) 25 | 26 | assert.Equal(t, http.StatusBadRequest, w.Code) 27 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 28 | } 29 | 30 | func doPanic(w http.ResponseWriter, r *http.Request) { 31 | panic(errors.ErrInvalidID) 32 | } 33 | -------------------------------------------------------------------------------- /assets/stubs/upstreams/status-partial.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "urlPath": "/status-partial" 5 | }, 6 | "response": { 7 | "status": 400, 8 | "headers": { 9 | "Content-Type": "application/json; charset=utf-8" 10 | }, 11 | "jsonBody": { 12 | "status": "Partially Available", 13 | "timestamp": "2017-07-03T14:48:14.563630521Z", 14 | "failures": { 15 | "rabbitmq": "Failed during RabbitMQ health check" 16 | }, 17 | "system": { 18 | "version": "go1.8.3", 19 | "goroutines_count": 15, 20 | "total_alloc_bytes": 46186776, 21 | "heap_objects_count": 44186, 22 | "alloc_bytes": 5733552 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pkg/middleware/stats_tagger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go.opencensus.io/tag" 7 | ) 8 | 9 | // StatsTagger is a middleware that takes a list of tags and adds them into context to be propagated 10 | type StatsTagger struct { 11 | tags []tag.Mutator 12 | } 13 | 14 | // NewStatsTagger creates a new instance of StatsTagger 15 | func NewStatsTagger(tags []tag.Mutator) *StatsTagger { 16 | metricKeyInserter := &StatsTagger{tags} 17 | return metricKeyInserter 18 | } 19 | 20 | // Handler is the middleware function 21 | func (h *StatsTagger) Handler(handler http.Handler) http.Handler { 22 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 23 | ctx := r.Context() 24 | 25 | for _, t := range h.tags { 26 | ctx, _ = tag.New(ctx, t) 27 | } 28 | 29 | handler.ServeHTTP(w, r.WithContext(ctx)) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/router/listen_path_matcher.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import "regexp" 4 | 5 | const ( 6 | matchRule = `(\/\*(.+)?)` 7 | ) 8 | 9 | // ListenPathMatcher is responsible for matching a listen path to a set of rules 10 | type ListenPathMatcher struct { 11 | reg *regexp.Regexp 12 | } 13 | 14 | // NewListenPathMatcher creates a new instance ListenPathMatcher 15 | func NewListenPathMatcher() *ListenPathMatcher { 16 | return &ListenPathMatcher{regexp.MustCompile(matchRule)} 17 | } 18 | 19 | // Match verifies if a listen path matches the given rule 20 | func (l *ListenPathMatcher) Match(listenPath string) bool { 21 | return l.reg.MatchString(listenPath) 22 | } 23 | 24 | // Extract takes the usable part of the listen path based on the provided rule 25 | func (l *ListenPathMatcher) Extract(listenPath string) string { 26 | return l.reg.ReplaceAllString(listenPath, "") 27 | } 28 | -------------------------------------------------------------------------------- /pkg/proxy/balancer/rr.go: -------------------------------------------------------------------------------- 1 | package balancer 2 | 3 | import "sync" 4 | 5 | type ( 6 | // RoundrobinBalancer balancer 7 | RoundrobinBalancer struct { 8 | current int // current backend position 9 | mu sync.RWMutex 10 | } 11 | ) 12 | 13 | // NewRoundrobinBalancer creates a new instance of Roundrobin 14 | func NewRoundrobinBalancer() *RoundrobinBalancer { 15 | return &RoundrobinBalancer{} 16 | } 17 | 18 | // Elect backend using roundrobin strategy 19 | func (b *RoundrobinBalancer) Elect(hosts []*Target) (*Target, error) { 20 | if len(hosts) == 0 { 21 | return nil, ErrEmptyBackendList 22 | } 23 | 24 | if len(hosts) == 1 { 25 | return hosts[0], nil 26 | } 27 | 28 | if b.current >= len(hosts) { 29 | b.current = 0 30 | } 31 | 32 | host := hosts[b.current] 33 | 34 | b.mu.Lock() 35 | defer b.mu.Unlock() 36 | b.current++ 37 | 38 | return host, nil 39 | } 40 | -------------------------------------------------------------------------------- /assets/apis/example_SingleDefinition.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "example", 3 | "active" : true, 4 | "proxy" : { 5 | "preserve_host" : false, 6 | "listen_path" : "/example/*", 7 | "upstreams" : { 8 | "balancing": "roundrobin", 9 | "targets": [ 10 | {"target": "http://localhost:9089/hello-world"} 11 | ] 12 | }, 13 | "strip_path" : false, 14 | "append_path" : false, 15 | "methods" : ["GET"] 16 | }, 17 | "plugins": [ 18 | { 19 | "name": "rate_limit", 20 | "enabled": true, 21 | "config": { 22 | "limit": "5-M", 23 | "policy": "local" 24 | } 25 | } 26 | ], 27 | "health_check": { 28 | "url": "http://localhost:9089/status", 29 | "timeout": 3 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/install/README.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | You can install Janus using docker or manually deploying the binary. 4 | 5 | ### Docker 6 | 7 | The simplest way of installing janus is to run the docker image for it. Just check the [docker-compose.yml](/examples/front-proxy/docker-compose.yml) example and then run it. 8 | 9 | ```sh 10 | docker-compose up -d 11 | ``` 12 | 13 | Now you should be able to get a response from the gateway. 14 | 15 | Try the following command: 16 | 17 | ```sh 18 | http http://localhost:8080/ 19 | ``` 20 | 21 | You can find more details about how to use Janus docker image [here](docker.md). 22 | 23 | ### Manual 24 | 25 | You can get the binary and play with it in your own environment (or even deploy it where ever you like). 26 | Just go to the [releases](https://github.com/hellofresh/janus/releases) page and download the latest one for your platform. 27 | -------------------------------------------------------------------------------- /pkg/metrics/metrics_context.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hellofresh/stats-go" 7 | "github.com/hellofresh/stats-go/client" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type statsKeyType int 12 | 13 | const statsKey statsKeyType = iota 14 | 15 | // NewContext returns a context that has a stats Client 16 | func NewContext(ctx context.Context, client client.Client) context.Context { 17 | return context.WithValue(ctx, statsKey, client) 18 | } 19 | 20 | // WithContext returns a stats Client with as much context as possible 21 | func WithContext(ctx context.Context) client.Client { 22 | ctxStats, ok := ctx.Value(statsKey).(client.Client) 23 | if !ok { 24 | log.Error("Could not retrieve stats client from the context") 25 | 26 | ctxStats, _ := stats.NewClient("noop://") 27 | return ctxStats 28 | } 29 | return ctxStats 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v2 15 | - name: Docker Login 16 | if: success() && startsWith(github.ref, 'refs/tags/') 17 | env: 18 | DOCKER_USERNAME: hellofreshtech 19 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 20 | run: | 21 | echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin 22 | - name: Run GoReleaser 23 | uses: goreleaser/goreleaser-action@v2 24 | if: success() && startsWith(github.ref, 'refs/tags/') 25 | with: 26 | args: release --rm-dist 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /cassandra/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE KEYSPACE IF NOT EXISTS janus with replication = {'class': 'SimpleStrategy', 'replication_factor': 1}; 2 | USE janus; 3 | 4 | CREATE TABLE IF NOT EXISTS janus.user ( 5 | username text, 6 | password text, 7 | PRIMARY KEY (username)); 8 | 9 | CREATE TABLE IF NOT EXISTS janus.api_definition ( 10 | name text, 11 | definition text, 12 | PRIMARY KEY (name)); 13 | 14 | CREATE TABLE IF NOT EXISTS janus.oauth ( 15 | name text, 16 | oauth text, 17 | PRIMARY KEY (name)); 18 | 19 | CREATE TABLE IF NOT EXISTS janus.organization ( 20 | username text, 21 | password text, 22 | organization text, 23 | PRIMARY KEY (username)); 24 | 25 | CREATE TABLE IF NOT EXISTS janus.organization_config ( 26 | organization text, 27 | priority int, 28 | content_per_day int, 29 | config text, 30 | PRIMARY KEY (organization)); 31 | -------------------------------------------------------------------------------- /docs/plugins/body_limit.md: -------------------------------------------------------------------------------- 1 | # Body Limit 2 | 3 | Block incoming requests whose body is greater than a specific size in megabytes. 4 | 5 | ## Configuration 6 | 7 | The plain request transformer config: 8 | 9 | ```json 10 | "body_limit": { 11 | "enabled": true, 12 | "config": { 13 | "limit": "40M" 14 | } 15 | } 16 | ``` 17 | 18 | Here is a simple definition of the available configurations. 19 | 20 | | Configuration | Description | 21 | |-------------------------------|---------------------------------------------------------------------| 22 | | name | Name of the plugin to use, in this case: body_limit | 23 | | config.limit | Allowed request payload size. You can set the size in `B` for bytes,`K` for kilobytes, `M` for megabytes, `G` for gigabytes and `T` for terabytes | 24 | -------------------------------------------------------------------------------- /examples/janus-tls/janus.toml: -------------------------------------------------------------------------------- 1 | ################################################################ 2 | # Global configuration 3 | ################################################################ 4 | port = 8443 5 | CertFile = "/etc/janus/janus.crt" 6 | KeyFile = "/etc/janus/janus.key" 7 | 8 | [log] 9 | level = "debug" 10 | 11 | [tls] 12 | port = 8433 13 | redirect = true 14 | CertFile = "janus.crt" 15 | KeyFile = "janus.key" 16 | 17 | ################################################################ 18 | # API configuration backend 19 | ################################################################ 20 | [web] 21 | port = 8444 22 | CertFile = "/etc/janus/janus.crt" 23 | KeyFile = "/etc/janus/janus.key" 24 | 25 | [web.credentials] 26 | secret = "secret" 27 | 28 | [web.credentials.basic] 29 | users = {admin = "admin"} 30 | 31 | [database] 32 | dsn = "file:///etc/janus" 33 | -------------------------------------------------------------------------------- /docs/plugins/retry.md: -------------------------------------------------------------------------------- 1 | # Retry 2 | 3 | The retry plugin allows you to configure retry rules for your proxy. This enables you to be more resilient for any network or any other kind of failure. 4 | 5 | ## Configuration 6 | 7 | The plain retry config: 8 | 9 | ```json 10 | { 11 | "name" : "retry", 12 | "enabled" : false, 13 | "config" : { 14 | "attempts" : 3, 15 | "backoff": "1s" 16 | } 17 | } 18 | ``` 19 | 20 | Configuration | Description 21 | :---|:---| 22 | | attempts | Number of attempts | 23 | | backoff | Time that we should wait to retry. This must be given in the [ParseDuration](https://golang.org/pkg/time/#ParseDuration) format. Defaults to `1s` | 24 | | predicate | The rule that we will check to define if the request was successful or not. You have access to `statusCode` and all the `request` object. Defaults to `statusCode == 0 || statusCode >= 500` | 25 | -------------------------------------------------------------------------------- /examples/front-proxy-mongo/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | janus: 5 | image: motivlabs/janus:debug 6 | ports: 7 | - "8082:8080" 8 | - "8083:8081" 9 | - "40001:40000" 10 | depends_on: 11 | - service1 12 | - janus-database 13 | environment: 14 | - debug=1 15 | volumes: 16 | - ./janus.toml:/etc/janus/janus.toml 17 | - ~/dev/motiv/janus:/janus 18 | 19 | janus-database: 20 | image: mongo 21 | ports: 22 | - "27017:27017" 23 | 24 | # This container is just a helper to seed the database 25 | mongo-seed: 26 | build: 27 | context: . 28 | dockerfile: seed.Dockerfile 29 | depends_on: 30 | - janus-database 31 | 32 | service1: 33 | image: rodolpheche/wiremock 34 | ports: 35 | - '9090:8080' 36 | volumes: 37 | - ../front-proxy/stubs:/home/wiremock/mappings 38 | -------------------------------------------------------------------------------- /pkg/api/error.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/hellofresh/janus/pkg/errors" 7 | ) 8 | 9 | var ( 10 | // ErrAPIDefinitionNotFound is used when the api was not found in the datastore 11 | ErrAPIDefinitionNotFound = errors.New(http.StatusNotFound, "api definition not found") 12 | 13 | // ErrAPINameExists is used when the API name is already registered on the datastore 14 | ErrAPINameExists = errors.New(http.StatusConflict, "api name is already registered") 15 | 16 | // ErrAPIListenPathExists is used when the API listen path is already registered on the datastore 17 | ErrAPIListenPathExists = errors.New(http.StatusConflict, "api listen path is already registered") 18 | 19 | // ErrDBContextNotSet is used when the database request context is not set 20 | ErrDBContextNotSet = errors.New(http.StatusInternalServerError, "DB context was not set for this request") 21 | ) 22 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: janus 2 | 3 | builds: 4 | - id: binary-build 5 | main: main.go 6 | binary: janus 7 | ldflags: 8 | - -s -w -X main.version={{.Version}} 9 | env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - darwin 14 | - windows 15 | - freebsd 16 | - openbsd 17 | goarch: 18 | - amd64 19 | - arm 20 | - arm64 21 | - 386 22 | ignore: 23 | - goos: darwin 24 | goarch: 386 25 | - goos: freebsd 26 | goarch: arm64 27 | 28 | dockers: 29 | - goos: linux 30 | goarch: amd64 31 | binaries: 32 | - janus 33 | image_templates: 34 | - "hellofreshtech/janus:latest" 35 | - "hellofreshtech/janus:{{.Tag}}" 36 | dockerfile: Dockerfile 37 | 38 | changelog: 39 | sort: asc 40 | filters: 41 | exclude: 42 | - Merge pull request 43 | - Merge branch 44 | -------------------------------------------------------------------------------- /docs/clustering/clustering.md: -------------------------------------------------------------------------------- 1 | # Clustering / High Availability 2 | 3 | Multiple Janus nodes pointing to the same datastore must belong to the same "Janus Cluster". 4 | 5 | A Janus cluster allows you to scale the system horizontally by adding more machines to handle a bigger load of incoming requests, and they all share the same data since they point to the same datastore. 6 | 7 | A Janus cluster can be created in one datacenter, or in multiple datacenters, in both cloud or on-premise environments. Janus will take care of joining and leaving a node automatically in a cluster, as long as the node is configured properly. 8 | 9 | ## Configuration update 10 | 11 | Some backends requires that you define an update interval, which is used to check for changes 12 | on that storage. You can do that by setting the cluster configuration like this: 13 | 14 | ```toml 15 | [cluster] 16 | UpdateFrequency = "5s" 17 | ``` 18 | 19 | -------------------------------------------------------------------------------- /examples/front-proxy-cassandra/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | janus: 5 | image: motivlabs/janus:debug 6 | ports: 7 | - "8080:8080" 8 | - "8081:8081" 9 | - "40000:40000" 10 | environment: 11 | - debug=1 12 | depends_on: 13 | - service1 14 | - janus-database 15 | volumes: 16 | - ./janus.toml:/etc/janus/janus.toml 17 | - ~/dev/motiv/janus:/janus 18 | 19 | janus-database: 20 | image: cassandra:latest 21 | container_name: db 22 | ports: 23 | - "9042:9042" 24 | environment: 25 | - MAX_HEAP_SIZE=1G 26 | - HEAP_NEWSIZE=250M 27 | - JAVA_OPTS="-Dcassandra.skip_wait_for_gossip_to_settle=0 -Dcassandra.num_tokens=1 -Dcassandra.initial_token=1" 28 | 29 | service1: 30 | image: rodolpheche/wiremock 31 | ports: 32 | - '9089:8080' 33 | volumes: 34 | - ../front-proxy/stubs:/home/wiremock/mappings 35 | -------------------------------------------------------------------------------- /docs/proxy/wildcard_hostnames.md: -------------------------------------------------------------------------------- 1 | #### Using wildcard hostnames 2 | 3 | To provide flexibility, Janus allows you to specify hostnames with wildcards in the hosts field. Wildcard hostnames allow any matching Host header to satisfy the condition, and thus match a given API. 4 | 5 | Wildcard hostnames must contain only one asterisk at the leftmost or rightmost label of the domain. Examples: 6 | 7 | `*.example.org` would allow Host values such as `a.example.com` and `x.y.example.com` to match. 8 | `example.*` would allow Host values such as `example.com` and `example.org` to match. 9 | A complete example would look like this: 10 | 11 | ```json 12 | { 13 | "name": "My API", 14 | "hosts": ["*.example.com", "service.com"] 15 | } 16 | ``` 17 | 18 | Which would allow the following requests to match this API: 19 | 20 | ```http 21 | GET / HTTP/1.1 22 | Host: an.example.com 23 | ``` 24 | 25 | ```http 26 | GET / HTTP/1.1 27 | Host: service.com 28 | ``` 29 | -------------------------------------------------------------------------------- /cassandra/wrapper/session.go: -------------------------------------------------------------------------------- 1 | package wrapper 2 | 3 | // Initializer is a common interface for functionality to start a new session 4 | type Initializer interface { 5 | NewSession() (Holder, error) 6 | } 7 | 8 | // Holder allows to store a close sessions 9 | type Holder interface { 10 | GetSession() SessionInterface 11 | CloseSession() 12 | } 13 | 14 | // SessionInterface is an interface to wrap gocql methods used in Motiv 15 | type SessionInterface interface { 16 | Query( stmt string, values ...interface{}) QueryInterface 17 | Close() 18 | } 19 | 20 | type QueryInterface interface { 21 | Exec() error 22 | Scan( dest ...interface{}) error 23 | Iter() IterInterface 24 | PageState(state []byte, ) QueryInterface 25 | PageSize(n int, ) QueryInterface 26 | } 27 | 28 | type IterInterface interface { 29 | Scan( dest ...interface{}) bool 30 | WillSwitchPage() bool 31 | PageState() []byte 32 | Close() error 33 | ScanAndClose( handle func() bool, dest ...interface{}) error 34 | } 35 | -------------------------------------------------------------------------------- /pkg/plugin/bodylmt/middleware_test.go: -------------------------------------------------------------------------------- 1 | package bodylmt 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/hellofresh/janus/pkg/test" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestBodyLmtValidSize(t *testing.T) { 14 | mw := NewBodyLimitMiddleware("2M") 15 | 16 | content := []byte("Hello, World!") 17 | r := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(content)) 18 | w := httptest.NewRecorder() 19 | 20 | mw(http.HandlerFunc(test.Ping)).ServeHTTP(w, r) 21 | 22 | assert.Equal(t, http.StatusOK, w.Code) 23 | } 24 | 25 | func TestBodyLmtInvalidSize(t *testing.T) { 26 | mw := NewBodyLimitMiddleware("2B") 27 | 28 | content := []byte("Hello, World!") 29 | r := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(content)) 30 | w := httptest.NewRecorder() 31 | 32 | mw(http.HandlerFunc(test.Ping)).ServeHTTP(w, r) 33 | 34 | assert.Equal(t, http.StatusRequestEntityTooLarge, w.Code) 35 | } 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ####### Start from a golang base image ############### 2 | FROM golang:1.13.6-buster as builder 3 | LABEL maintainer="Motiv Labs " 4 | WORKDIR /app 5 | COPY ./ ./ 6 | 7 | RUN go mod download 8 | 9 | RUN make build 10 | 11 | FROM ubuntu:20.04 as prod 12 | 13 | COPY --from=builder /app/cassandra/schema.sql /usr/local/bin 14 | 15 | COPY --from=builder /app/dist/janus /bin/janus 16 | RUN chmod a+x /bin/janus && \ 17 | mkdir -p /etc/janus/apis && \ 18 | mkdir -p /etc/janus/auth 19 | 20 | RUN apt-get update && apt-get install -y --no-install-recommends \ 21 | ca-certificates \ 22 | curl \ 23 | && rm -rf /var/lib/apt/lists/* 24 | 25 | HEALTHCHECK --interval=5s --timeout=5s --retries=3 CMD curl -f http://localhost:8081/status || exit 1 26 | 27 | # Use nobody user + group 28 | USER 65534:65534 29 | 30 | EXPOSE 8080 8081 8443 8444 31 | ENTRYPOINT ["/bin/janus", "start"] 32 | 33 | # just to have it 34 | RUN ["/bin/janus", "--version"] 35 | -------------------------------------------------------------------------------- /docs/proxy/overview.md: -------------------------------------------------------------------------------- 1 | 2 | ### Overview 3 | 4 | From a high level perspective, Janus will listen for HTTP traffic on its configured proxy port (`8080` by default), recognize which upstream service is being requested, run the configured middlewares for that API, and forward the HTTP request upstream to your own API or service. 5 | 6 | When a client makes a request to the proxy port, Janus will decide to which upstream service or API to route (or forward) the incoming request, depending on the API configuration in Janus, which is managed via the Admin API. You can configure APIs with various properties, but the three relevant ones for routing incoming traffic are hosts, uris, and methods. 7 | 8 | If Janus cannot determine to which upstream API a given request should be routed, Janus will respond with: 9 | 10 | ```http 11 | HTTP/1.1 404 Not Found 12 | Content-Type: application/json 13 | Server: Janus/ 14 | 15 | { 16 | "message": "no API found with those values" 17 | } 18 | ``` 19 | -------------------------------------------------------------------------------- /pkg/plugin/rate/rate_limit_logger_test.go: -------------------------------------------------------------------------------- 1 | package rate 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/hellofresh/stats-go" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/ulule/limiter/v3" 10 | "github.com/ulule/limiter/v3/drivers/store/memory" 11 | 12 | "github.com/hellofresh/janus/pkg/test" 13 | ) 14 | 15 | func TestSuccessfulRateLimitLog(t *testing.T) { 16 | statsClient, _ := stats.NewClient("noop://") 17 | limiterStore := memory.NewStore() 18 | rate, _ := limiter.NewRateFromFormatted("100-M") 19 | limiterInstance := limiter.New(limiterStore, rate) 20 | 21 | mw := NewRateLimitLogger(limiterInstance, statsClient, false) 22 | w, err := test.Record( 23 | "GET", 24 | "/", 25 | map[string]string{ 26 | "Content-Type": "application/json", 27 | }, 28 | mw(http.HandlerFunc(test.Ping)), 29 | ) 30 | assert.NoError(t, err) 31 | 32 | assert.Equal(t, http.StatusOK, w.Code) 33 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/plugin/bodylmt/setup.go: -------------------------------------------------------------------------------- 1 | package bodylmt 2 | 3 | import ( 4 | "github.com/asaskevich/govalidator" 5 | "github.com/hellofresh/janus/pkg/plugin" 6 | "github.com/hellofresh/janus/pkg/proxy" 7 | ) 8 | 9 | // Config represents the Body Limit configuration 10 | type Config struct { 11 | Limit string `json:"limit"` 12 | } 13 | 14 | func init() { 15 | plugin.RegisterPlugin("body_limit", plugin.Plugin{ 16 | Action: setupBodyLimit, 17 | Validate: validateConfig, 18 | }) 19 | } 20 | 21 | func setupBodyLimit(def *proxy.RouterDefinition, rawConfig plugin.Config) error { 22 | var config Config 23 | err := plugin.Decode(rawConfig, &config) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | def.AddMiddleware(NewBodyLimitMiddleware(config.Limit)) 29 | return nil 30 | } 31 | 32 | func validateConfig(rawConfig plugin.Config) (bool, error) { 33 | var config Config 34 | err := plugin.Decode(rawConfig, &config) 35 | if err != nil { 36 | return false, err 37 | } 38 | 39 | return govalidator.ValidateStruct(config) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/router/listen_path_parameter_name_extractor.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import "regexp" 4 | 5 | const ( 6 | parameterMatchRule = `\{([^/}]+)\}` 7 | ) 8 | 9 | // ListenPathParameterNameExtractor is responsible for extracting parameters name from the listen path 10 | type ListenPathParameterNameExtractor struct { 11 | reg *regexp.Regexp 12 | } 13 | 14 | // NewListenPathParamNameExtractor creates a new instance ListenPathParameterNameExtractor 15 | func NewListenPathParamNameExtractor() *ListenPathParameterNameExtractor { 16 | return &ListenPathParameterNameExtractor{regexp.MustCompile(parameterMatchRule)} 17 | } 18 | 19 | // Extract takes the usable part of the listen path and extracts parameter names 20 | func (l *ListenPathParameterNameExtractor) Extract(listenPath string) []string { 21 | submatches := l.reg.FindAllStringSubmatch(listenPath, -1) 22 | result := make([]string, 0, len(submatches)) 23 | 24 | for _, submatch := range submatches { 25 | result = append(result, submatch[1]) 26 | } 27 | 28 | return result 29 | } 30 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var configFile string 10 | 11 | // NewRootCmd creates a new instance of the root command 12 | func NewRootCmd(version string) *cobra.Command { 13 | ctx := context.Background() 14 | 15 | cmd := &cobra.Command{ 16 | Use: "janus", 17 | Version: version, 18 | Short: "Janus is an API Gateway", 19 | Long: ` 20 | This is a lightweight API Gateway and Management Platform that enables you 21 | to control who accesses your API, when they access it and how they access it. 22 | API Gateway will also record detailed analytics on how your users are interacting 23 | with your API and when things go wrong. 24 | Complete documentation is available at https://hellofresh.gitbooks.io/janus`, 25 | } 26 | 27 | cmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Config file (default is $PWD/janus.toml)") 28 | 29 | cmd.AddCommand(NewCheckCmd(ctx)) 30 | cmd.AddCommand(NewServerStartCmd(ctx, version)) 31 | 32 | return cmd 33 | } 34 | -------------------------------------------------------------------------------- /pkg/web/handlers.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "net/url" 8 | 9 | "github.com/hellofresh/janus/pkg/render" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // Home handler is just a nice home page message 14 | func Home() http.HandlerFunc { 15 | return func(w http.ResponseWriter, r *http.Request) { 16 | render.JSON(w, http.StatusOK, "Welcome to Janus") 17 | } 18 | } 19 | 20 | // RedirectHTTPS redirects an http request to https 21 | func RedirectHTTPS(port int) http.HandlerFunc { 22 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 23 | host, _, _ := net.SplitHostPort(req.Host) 24 | 25 | target := url.URL{ 26 | Scheme: "https", 27 | Host: fmt.Sprintf("%s:%v", host, port), 28 | Path: req.URL.Path, 29 | } 30 | if len(req.URL.RawQuery) > 0 { 31 | target.RawQuery += "?" + req.URL.RawQuery 32 | } 33 | log.Printf("redirect to: %s", target.String()) 34 | http.Redirect(w, req, target.String(), http.StatusTemporaryRedirect) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/jwt/token.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/dgrijalva/jwt-go" 7 | ) 8 | 9 | // AccessToken represents a token 10 | type AccessToken struct { 11 | Type string `json:"token_type"` 12 | Token string `json:"access_token"` 13 | Expires int64 `json:"expires_in"` 14 | } 15 | 16 | // IssueAdminToken issues admin JWT for API access 17 | func IssueAdminToken(signingMethod SigningMethod, claims jwt.MapClaims, expireIn time.Duration) (*AccessToken, error) { 18 | token := jwt.New(jwt.GetSigningMethod(signingMethod.Alg)) 19 | exp := time.Now().Add(expireIn).Unix() 20 | 21 | token.Claims = claims 22 | claims["exp"] = exp 23 | claims["iat"] = time.Now().Unix() 24 | 25 | accessToken, err := token.SignedString([]byte(signingMethod.Key)) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | // currently only HSXXX algorithms are supported for issuing admin token, so we cast key to bytes array 31 | return &AccessToken{ 32 | Type: "Bearer", 33 | Token: accessToken, 34 | Expires: exp, 35 | }, nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/server/option.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/hellofresh/janus/pkg/api" 5 | "github.com/hellofresh/janus/pkg/config" 6 | "github.com/hellofresh/stats-go/client" 7 | ) 8 | 9 | // Option represents the available options 10 | type Option func(*Server) 11 | 12 | // WithGlobalConfig sets the global configuration 13 | func WithGlobalConfig(globalConfig *config.Specification) Option { 14 | return func(s *Server) { 15 | s.globalConfig = globalConfig 16 | } 17 | } 18 | 19 | // WithMetricsClient sets the metric provider 20 | func WithMetricsClient(client client.Client) Option { 21 | return func(s *Server) { 22 | s.statsClient = client 23 | } 24 | } 25 | 26 | // WithProvider sets the configuration provider 27 | func WithProvider(provider api.Repository) Option { 28 | return func(s *Server) { 29 | s.provider = provider 30 | } 31 | } 32 | 33 | // WithProfiler enables or disables profiler 34 | func WithProfiler(enabled, public bool) Option { 35 | return func(s *Server) { 36 | s.profilingEnabled = enabled 37 | s.profilingPublic = public 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /features/bootstrap/context_api.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/cucumber/godog" 8 | "github.com/cucumber/messages-go/v10" 9 | 10 | "github.com/hellofresh/janus/pkg/api" 11 | ) 12 | 13 | // RegisterAPIContext registers godog suite context for handling API related steps 14 | func RegisterAPIContext(ctx *godog.ScenarioContext, apiRepo api.Repository, ch chan<- api.ConfigurationMessage) { 15 | scenarioCtx := &apiContext{apiRepo: apiRepo, ch: ch} 16 | 17 | ctx.BeforeScenario(scenarioCtx.clearAPI) 18 | } 19 | 20 | type apiContext struct { 21 | apiRepo api.Repository 22 | ch chan<- api.ConfigurationMessage 23 | } 24 | 25 | func (c *apiContext) clearAPI(*messages.Pickle) { 26 | data, err := c.apiRepo.FindAll() 27 | if err != nil { 28 | panic(fmt.Errorf("failed to get all registered route specs: %w", err)) 29 | } 30 | 31 | for _, definition := range data { 32 | c.ch <- api.ConfigurationMessage{ 33 | Operation: api.RemovedOperation, 34 | Configuration: definition, 35 | } 36 | } 37 | 38 | time.Sleep(time.Second) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/jwt/provider/verifier.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // Verifier contains the methods for verification of providers 9 | type Verifier interface { 10 | Verify(r *http.Request, httpClient *http.Client) (bool, error) 11 | } 12 | 13 | // VerifierBasket acts as a collection of verifier 14 | type VerifierBasket struct { 15 | verifiers []Verifier 16 | } 17 | 18 | //NewVerifierBasket creates a new instace of VerifierBasket 19 | func NewVerifierBasket(verifiers ...Verifier) *VerifierBasket { 20 | return &VerifierBasket{verifiers: verifiers} 21 | } 22 | 23 | // Verify checks is the provider is valid 24 | func (vb *VerifierBasket) Verify(r *http.Request, httpClient *http.Client) (bool, error) { 25 | var wrappedErrors error 26 | 27 | for _, verifier := range vb.verifiers { 28 | verified, err := verifier.Verify(r, httpClient) 29 | if err != nil { 30 | wrappedErrors = fmt.Errorf("verification failed: %w", err) 31 | continue 32 | } 33 | if verified { 34 | return true, nil 35 | } 36 | } 37 | 38 | return false, wrappedErrors 39 | } 40 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NO_COLOR=\033[0m 2 | OK_COLOR=\033[32;01m 3 | ERROR_COLOR=\033[31;01m 4 | WARN_COLOR=\033[33;01m 5 | 6 | VERSION ?= "dev-$(shell git rev-parse --short HEAD)" 7 | GO_LINKER_FLAGS=-ldflags="-s -w -X main.version=$(VERSION)" 8 | 9 | .PHONY: all lint test-unit test-integration test-features build 10 | 11 | all: test-unit build 12 | 13 | build: 14 | @echo "$(OK_COLOR)==> Building default binary... $(NO_COLOR)" 15 | @CGO_ENABLED=0 go build ${GO_LINKER_FLAGS} -o "dist/janus" 16 | 17 | test-unit: 18 | @echo "$(OK_COLOR)==> Running unit tests$(NO_COLOR)" 19 | @go test ./... 20 | 21 | test-integration: _mocks 22 | @echo "$(OK_COLOR)==> Running integration tests$(NO_COLOR)" 23 | @go test -cover -tags=integration -coverprofile=coverage.txt -covermode=atomic ./... 24 | 25 | test-features: build _mocks 26 | @/bin/sh -c "./build/features.sh" 27 | 28 | lint: 29 | @echo "$(OK_COLOR)==> Linting with golangci-lint running in docker container$(NO_COLOR)" 30 | @docker run --rm -v $(PWD):/app -w /app golangci/golangci-lint:v1.30.0 golangci-lint run -v 31 | 32 | _mocks: 33 | @/bin/sh -c "./build/mocks.sh" 34 | -------------------------------------------------------------------------------- /janus/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "janus.fullname" ( dict "Values" .Values "Chart" .Chart "Release" .Release "name" .Values.ingress.name ) -}} 3 | apiVersion: extensions/v1beta1 4 | kind: Ingress 5 | metadata: 6 | name: {{ $fullName }} 7 | labels: 8 | {{ include "janus.labels" . | indent 4 }} 9 | {{- with .Values.ingress.annotations }} 10 | annotations: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | {{- if .Values.ingress.tls }} 15 | tls: 16 | {{- range .Values.ingress.tls }} 17 | - hosts: 18 | {{- range .hosts }} 19 | - {{ . | quote }} 20 | {{- end }} 21 | secretName: {{ .secretName }} 22 | {{- end }} 23 | {{- end }} 24 | rules: 25 | {{- range .Values.ingress.hosts }} 26 | - host: {{ .host | quote }} 27 | http: 28 | paths: 29 | {{- range .paths }} 30 | - path: {{ .path }} 31 | backend: 32 | serviceName: {{ $fullName }} 33 | servicePort: {{ .port }} 34 | {{- end }} 35 | {{- end }} 36 | {{- end }} 37 | -------------------------------------------------------------------------------- /assets/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # This file is used to run integration tests on travis 2 | version: '3' 3 | services: 4 | 5 | mongo: 6 | image: mongo:3 7 | ports: 8 | - "27017:27017" 9 | healthcheck: 10 | test: "mongo localhost:27017/test --quiet --eval 'quit(db.runCommand({ ping: 1 }).ok ? 0 : 2)'" 11 | interval: 10s 12 | timeout: 5s 13 | retries: 5 14 | 15 | upstreams: 16 | image: rodolpheche/wiremock:2.27.1-alpine 17 | ports: 18 | - '9089:8080' 19 | 20 | auth-service: 21 | image: rodolpheche/wiremock:2.27.1-alpine 22 | ports: 23 | - '9088:8080' 24 | 25 | jaeger: 26 | image: jaegertracing/all-in-one 27 | environment: 28 | COLLECTOR_ZIPKIN_HTTP_PORT: 9411 29 | ports: 30 | - "5775:5775/udp" 31 | - "6831:6831/udp" 32 | - "6832:6832/udp" 33 | - "5778:5778" 34 | - "16686:16686" 35 | - "14268:14268" 36 | 37 | zipkin: 38 | image: openzipkin/zipkin 39 | environment: 40 | STORAGE_TYPE: mem 41 | JAVA_OPTS: -Dlogging.level.zipkin=DEBUG 42 | ports: 43 | - 9411:9411 44 | -------------------------------------------------------------------------------- /docs/proxy/append_uri_property.md: -------------------------------------------------------------------------------- 1 | ##### The `append_path` property 2 | 3 | You might also want to always append the `listen_path` to the elected upstream target. 4 | To do so, use the `append_path` boolean property by configuring an API like this: 5 | 6 | ```json 7 | { 8 | "name": "My API", 9 | "proxy": { 10 | "append_path" : true, 11 | "listen_path": "/service/*", 12 | "upstreams" : { 13 | "balancing": "roundrobin", 14 | "targets": [ 15 | {"target": "http://my-api.com/example"} 16 | ] 17 | }, 18 | } 19 | } 20 | ``` 21 | 22 | Enabling this flag instructs Janus that when proxying this API, it should **always** 23 | include the matching URI prefix in the upstream request's URI. For example, the 24 | following client's request to the API configured as above: 25 | 26 | ```http 27 | GET /service/path/to/resource HTTP/1.1 28 | Host: my-api.com 29 | ``` 30 | 31 | Will cause Janus to send the following request to your upstream service: 32 | 33 | ```http 34 | GET /example/service/path/to/resource HTTP/1.1 35 | Host: my-api.com 36 | ``` 37 | -------------------------------------------------------------------------------- /pkg/jwt/basic/provider.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "net/http" 5 | 6 | jwt "github.com/dgrijalva/jwt-go" 7 | "github.com/hellofresh/janus/pkg/config" 8 | "github.com/hellofresh/janus/pkg/jwt/provider" 9 | ) 10 | 11 | func init() { 12 | provider.Register("basic", &Provider{}) 13 | } 14 | 15 | // Provider abstracts the authentication for github 16 | type Provider struct { 17 | provider.Verifier 18 | } 19 | 20 | // Build acts like the constructor for a provider 21 | func (gp *Provider) Build(config config.Credentials) provider.Provider { 22 | return &Provider{ 23 | Verifier: provider.NewVerifierBasket( 24 | NewPasswordVerifier(userConfigToTeam(config.Basic.Users)), 25 | ), 26 | } 27 | } 28 | 29 | // GetClaims returns a JWT Map Claim 30 | func (gp *Provider) GetClaims(httpClient *http.Client) (jwt.MapClaims, error) { 31 | return jwt.MapClaims{}, nil 32 | } 33 | 34 | func userConfigToTeam(configUser map[string]string) []*user { 35 | users := []*user{} 36 | for u, p := range configUser { 37 | users = append(users, &user{ 38 | Username: u, 39 | Password: p, 40 | }) 41 | } 42 | return users 43 | } 44 | -------------------------------------------------------------------------------- /pkg/render/responder_test.go: -------------------------------------------------------------------------------- 1 | package render_test 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/hellofresh/janus/pkg/render" 10 | "github.com/hellofresh/janus/pkg/test" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestRespondAsJson(t *testing.T) { 15 | w := httptest.NewRecorder() 16 | 17 | recipe := test.Recipe{Name: "Test"} 18 | render.JSON(w, http.StatusOK, recipe) 19 | 20 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 21 | assert.Equal(t, http.StatusOK, w.Code) 22 | } 23 | 24 | func TestRespondExpectedBody(t *testing.T) { 25 | w := httptest.NewRecorder() 26 | 27 | recipe := test.Recipe{Name: "Test"} 28 | render.JSON(w, http.StatusOK, recipe) 29 | 30 | expectedWriter := httptest.NewRecorder() 31 | json.NewEncoder(expectedWriter).Encode(recipe) 32 | 33 | assert.Equal(t, expectedWriter.Body, w.Body) 34 | } 35 | 36 | func TestWrongJson(t *testing.T) { 37 | w := httptest.NewRecorder() 38 | 39 | render.JSON(w, http.StatusOK, make(chan int)) 40 | 41 | assert.Equal(t, http.StatusInternalServerError, w.Code) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/router/listen_path_matcher_test.go: -------------------------------------------------------------------------------- 1 | package router_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hellofresh/janus/pkg/router" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var ( 11 | matcher = router.NewListenPathMatcher() 12 | ) 13 | 14 | func TestMatchCorrectRoute(t *testing.T) { 15 | assert.True(t, matcher.Match("/test/hello/*")) 16 | assert.True(t, matcher.Match("/test/hello/*/anything/after")) 17 | } 18 | 19 | func TestMatchIncorrectRoute(t *testing.T) { 20 | assert.False(t, matcher.Match("/test/hello")) 21 | assert.False(t, matcher.Match("/test/hello/anything/after")) 22 | } 23 | 24 | func TestExtractCorrectRoute(t *testing.T) { 25 | assert.Equal(t, "/test/hello", matcher.Extract("/test/hello/*")) 26 | assert.Equal(t, "/test/hello", matcher.Extract("/test/hello/*/anything/after")) 27 | assert.Equal(t, "/test/hello", matcher.Extract("/test/hello/*/anything/after/*")) 28 | } 29 | 30 | func TestExtractIncorrectRoute(t *testing.T) { 31 | assert.Equal(t, "/test/hello", matcher.Extract("/test/hello")) 32 | assert.Equal(t, "/test/hello/anything/after", matcher.Extract("/test/hello/anything/after")) 33 | } 34 | -------------------------------------------------------------------------------- /docs/plugins/oauth.md: -------------------------------------------------------------------------------- 1 | # OAuth 2.0 Authentication 2 | 3 | Add an OAuth 2.0 authentication layer with the Authorization Code Grant, Client Credentials, Implicit Grant or Resource Owner Password Credentials Grant flow. 4 | 5 | This plugin allows you to set the enpoints of an authentication provider. This means that Janus is not attached in any way 6 | to the oauth flow and it simply delegate that to the oauth server. 7 | 8 | > Note: As per the OAuth2 specs, this plugin requires the underlying API to be served over HTTPS. To avoid any confusion, we recommend that you configure your underlying API to be only served through HTTPS. 9 | 10 | ## Configuration 11 | 12 | > To enable this plugin to your API you should configure an [OAuth Server](auth/oauth.md) first! 13 | 14 | Here is a simple definition of the available configurations. 15 | 16 | | Configuration | Description | 17 | |-------------------------------|---------------------------------------------------------------------| 18 | | server_name | Defines the `oauth server name` to be used as your oauth provider | 19 | -------------------------------------------------------------------------------- /pkg/web/options.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "github.com/hellofresh/janus/pkg/api" 5 | "github.com/hellofresh/janus/pkg/config" 6 | ) 7 | 8 | // Option represents the available options 9 | type Option func(*Server) 10 | 11 | // WithConfigurations sets the current configurations in memory 12 | func WithConfigurations(cfgs *api.Configuration) Option { 13 | return func(s *Server) { 14 | s.apiHandler.Cfgs = cfgs 15 | } 16 | } 17 | 18 | // WithPort sets the server port 19 | func WithPort(port int) Option { 20 | return func(s *Server) { 21 | s.Port = port 22 | } 23 | } 24 | 25 | // WithCredentials sets the credentials for the server 26 | func WithCredentials(cred config.Credentials) Option { 27 | return func(s *Server) { 28 | s.Credentials = cred 29 | } 30 | } 31 | 32 | // WithTLS sets the TLS configs for the server 33 | func WithTLS(tls config.TLS) Option { 34 | return func(s *Server) { 35 | s.TLS = tls 36 | } 37 | } 38 | 39 | // WithProfiler enables or disables profiler 40 | func WithProfiler(enabled, public bool) Option { 41 | return func(s *Server) { 42 | s.profilingEnabled = enabled 43 | s.profilingPublic = public 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/jwt/middleware.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/hellofresh/janus/pkg/render" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // Payload Represents the context key 11 | type Payload struct{} 12 | 13 | // User represents a logged in user 14 | type User struct { 15 | Username string 16 | Email string 17 | } 18 | 19 | // Middleware struct contains data and logic required for middleware functionality 20 | type Middleware struct { 21 | Guard Guard 22 | } 23 | 24 | // NewMiddleware builds and returns new JWT middleware instance 25 | func NewMiddleware(config Guard) *Middleware { 26 | return &Middleware{config} 27 | } 28 | 29 | // Handler implementation 30 | func (m *Middleware) Handler(handler http.Handler) http.Handler { 31 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 32 | parser := Parser{m.Guard.ParserConfig} 33 | _, err := parser.ParseFromRequest(r) 34 | if err != nil { 35 | log.WithError(err).Debug("failed to parse the token") 36 | render.JSON(w, http.StatusUnauthorized, "failed to parse the token") 37 | return 38 | } 39 | 40 | handler.ServeHTTP(w, r) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 HelloFresh SE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/proxy/balancer/weight.go: -------------------------------------------------------------------------------- 1 | package balancer 2 | 3 | import ( 4 | "errors" 5 | "math/rand" 6 | ) 7 | 8 | type ( 9 | // WeightBalancer balancer 10 | WeightBalancer struct{} 11 | ) 12 | 13 | var ( 14 | // ErrZeroWeight is used when there a zero value weight was given 15 | ErrZeroWeight = errors.New("invalid backend, weight 0 given") 16 | ) 17 | 18 | // NewWeightBalancer creates a new instance of WeightBalancer 19 | func NewWeightBalancer() *WeightBalancer { 20 | return &WeightBalancer{} 21 | } 22 | 23 | // Elect backend using weight strategy 24 | func (b *WeightBalancer) Elect(hosts []*Target) (*Target, error) { 25 | if len(hosts) == 0 { 26 | return nil, ErrEmptyBackendList 27 | } 28 | 29 | totalWeight := 0 30 | for _, host := range hosts { 31 | totalWeight += host.Weight 32 | } 33 | 34 | if totalWeight <= 0 { 35 | return nil, ErrZeroWeight 36 | } 37 | 38 | if len(hosts) == 1 { 39 | return hosts[0], nil 40 | } 41 | 42 | r := rand.Intn(totalWeight) 43 | pos := 0 44 | 45 | for _, host := range hosts { 46 | pos += host.Weight 47 | if r >= pos { 48 | continue 49 | } 50 | return host, nil 51 | } 52 | 53 | return nil, ErrCannotElectBackend 54 | } 55 | -------------------------------------------------------------------------------- /docs/known_issues/http_keepalive.md: -------------------------------------------------------------------------------- 1 | # Stale HTTP Keep-Alive 2 | 3 | Janus proxies requests with HTTP keep-alive enabled to reduce resource usage and latency. 4 | Unless explicitly configured, the proxy uses default idle connection timeout of 90 seconds. 5 | This means keep-alive connections that are not used for 90 seconds are closed, and the next request will use a fresh connection. 6 | 7 | This creates a stale connection if an endpoint is under constant load – therefore never reaching 90 seconds idle timeout. 8 | If a DNS update occurs and the target host of the endpoint points to a different address, this connection will still stay alive indefinitely, proxying requests to the wrong address until either Janus is restarted, or the endpoint gets idle long enough to exceed the 90 seconds of the idle connection timeout so it's closed and a new connection is created. 9 | 10 | To help with this problem, Janus can be started with an optional environment variable `CONN_PURGE_INTERVAL` that flushes the idle HTTP keep-alive connections periodically. 11 | This allows Janus to retain the benefits of HTTP keep-alive connections while limiting the maximum duration of stale connection kept alive. 12 | -------------------------------------------------------------------------------- /examples/front-proxy-cluster/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | 4 | janus1: 5 | image: hellofreshtech/janus 6 | ports: 7 | - "8080:8080" 8 | - "8081:8081" 9 | depends_on: 10 | - service1 11 | - janus-database 12 | volumes: 13 | - ./janus.toml:/etc/janus/janus.toml 14 | - ./apis:/etc/janus/apis 15 | 16 | janus2: 17 | image: hellofreshtech/janus 18 | ports: 19 | - "8082:8080" 20 | - "8083:8081" 21 | depends_on: 22 | - service1 23 | - janus-database 24 | volumes: 25 | - ./janus.toml:/etc/janus/janus.toml 26 | - ./apis:/etc/janus/apis 27 | 28 | janus3: 29 | image: hellofreshtech/janus 30 | ports: 31 | - "8084:8080" 32 | - "8085:8081" 33 | depends_on: 34 | - service1 35 | - janus-database 36 | volumes: 37 | - ./janus.toml:/etc/janus/janus.toml 38 | - ./apis:/etc/janus/apis 39 | 40 | service1: 41 | image: rodolpheche/wiremock 42 | ports: 43 | - '9089:8080' 44 | volumes: 45 | - ../front-proxy/stubs:/home/wiremock/mappings 46 | 47 | janus-database: 48 | image: mongo 49 | ports: 50 | - "27017:27017" 51 | -------------------------------------------------------------------------------- /pkg/plugin/responsetransformer/setup_test.go: -------------------------------------------------------------------------------- 1 | package responsetransformer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hellofresh/janus/pkg/plugin" 7 | "github.com/hellofresh/janus/pkg/proxy" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestResponseTransformerConfig(t *testing.T) { 12 | var config Config 13 | rawConfig := map[string]interface{}{ 14 | "add": map[string]interface{}{ 15 | "headers": map[string]string{ 16 | "NAME": "TEST", 17 | }, 18 | "querystring": map[string]string{ 19 | "name": "test", 20 | }, 21 | }, 22 | } 23 | 24 | err := plugin.Decode(rawConfig, &config) 25 | assert.NoError(t, err) 26 | 27 | assert.IsType(t, map[string]string{}, config.Add.Headers) 28 | assert.Contains(t, config.Add.Headers, "NAME") 29 | } 30 | 31 | func TestResponseTransformerPlugin(t *testing.T) { 32 | rawConfig := map[string]interface{}{ 33 | "add": map[string]interface{}{ 34 | "headers": map[string]string{ 35 | "NAME": "TEST", 36 | }, 37 | }, 38 | } 39 | 40 | def := proxy.NewRouterDefinition(proxy.NewDefinition()) 41 | err := setupResponseTransformer(def, rawConfig) 42 | assert.NoError(t, err) 43 | 44 | assert.Len(t, def.Middleware(), 1) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/jwt/guard.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/hellofresh/janus/pkg/config" 7 | ) 8 | 9 | // Guard struct 10 | type Guard struct { 11 | ParserConfig 12 | 13 | // Duration that a jwt token is valid. Optional, defaults to one hour. 14 | Timeout time.Duration 15 | 16 | // SigningMethod defines new token signing algorithm/key pair. 17 | SigningMethod SigningMethod 18 | 19 | // This field allows clients to refresh their token until MaxRefresh has passed. 20 | // Note that clients can refresh their token in the last moment of MaxRefresh. 21 | // This means that the maximum validity timespan for a token is MaxRefresh + Timeout. 22 | // Optional, defaults to 0 meaning not refreshable. 23 | MaxRefresh time.Duration 24 | } 25 | 26 | // NewGuard creates a new instance of Guard with default handlers 27 | func NewGuard(cred config.Credentials) Guard { 28 | return Guard{ 29 | ParserConfig: ParserConfig{ 30 | SigningMethods: []SigningMethod{{Alg: cred.Algorithm, Key: cred.Secret}}, 31 | TokenLookup: "header:Authorization", 32 | }, 33 | SigningMethod: SigningMethod{Alg: cred.Algorithm, Key: cred.Secret}, 34 | Timeout: cred.Timeout, 35 | MaxRefresh: time.Hour * 24, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/jwt/provider/provider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | 7 | jwt "github.com/dgrijalva/jwt-go" 8 | "github.com/hellofresh/janus/pkg/config" 9 | ) 10 | 11 | var providers *sync.Map 12 | 13 | func init() { 14 | providers = new(sync.Map) 15 | } 16 | 17 | // Provider represents an auth provider 18 | type Provider interface { 19 | Verifier 20 | Build(config config.Credentials) Provider 21 | GetClaims(httpClient *http.Client) (jwt.MapClaims, error) 22 | } 23 | 24 | // Register registers a new provider 25 | func Register(providerName string, providerConstructor Provider) { 26 | providers.Store(providerName, providerConstructor) 27 | } 28 | 29 | // GetProviders returns the list of registered providers 30 | func GetProviders() *sync.Map { 31 | return providers 32 | } 33 | 34 | // Factory represents a factory of providers 35 | type Factory struct{} 36 | 37 | // Build builds one provider based on the auth configuration 38 | func (f *Factory) Build(providerName string, config config.Credentials) Provider { 39 | provider, ok := providers.Load(providerName) 40 | if !ok { 41 | provider, _ = providers.Load("basic") 42 | } 43 | 44 | p := provider.(Provider) 45 | return p.Build(config) 46 | } 47 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # run this from the root of janus 2 | FROM golang:1.14-alpine AS build-debug-common 3 | 4 | ARG VERSION='0.0.1-docker' 5 | 6 | WORKDIR /janus 7 | 8 | COPY . ./ 9 | 10 | # Add tooling to install GCC 11 | RUN apk add build-base 12 | # Add cqlsh to the image. 13 | RUN apk add --update \ 14 | bash \ 15 | curl \ 16 | py-pip 17 | RUN go get github.com/go-delve/delve/cmd/dlv@v1.6.0 18 | 19 | RUN apk add --update bash make git 20 | RUN export JANUS_BUILD_ONLY_DEFAULT=1 && \ 21 | export VERSION=$VERSION && \ 22 | make build 23 | 24 | # --- 25 | 26 | FROM alpine 27 | 28 | COPY --from=build-debug-common /janus/dist/janus / 29 | 30 | RUN apk add --no-cache ca-certificates 31 | RUN mkdir -p /etc/janus/apis && \ 32 | mkdir -p /etc/janus/auth 33 | 34 | RUN apk add --update curl && \ 35 | rm -rf /var/cache/apk/* 36 | 37 | HEALTHCHECK --interval=5s --timeout=5s --retries=3 CMD curl -f http://localhost:8081/status || exit 1 38 | 39 | FROM build-debug-common as dev 40 | EXPOSE 8080 8081 8443 8444 40000 41 | COPY entry-dev.sh /usr/local/bin 42 | COPY cassandra/schema.sql /usr/local/bin 43 | RUN chmod 755 /usr/local/bin/entry-dev.sh 44 | ENTRYPOINT ["/usr/local/bin/entry-dev.sh"] 45 | #ENTRYPOINT ["/janus", "start"] 46 | -------------------------------------------------------------------------------- /pkg/jwt/github/organization_verifier.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // OrganizationVerifier checks if the current user belongs any of the defined organizations 10 | type OrganizationVerifier struct { 11 | organizations []string 12 | gitHubClient Client 13 | } 14 | 15 | // NewOrganizationVerifier creates a new instance of OrganizationVerifier 16 | func NewOrganizationVerifier(organizations []string, gitHubClient Client) *OrganizationVerifier { 17 | return &OrganizationVerifier{ 18 | organizations: organizations, 19 | gitHubClient: gitHubClient, 20 | } 21 | } 22 | 23 | // Verify makes a check and return a boolean if the check was successful or not 24 | func (v *OrganizationVerifier) Verify(r *http.Request, httpClient *http.Client) (bool, error) { 25 | orgs, err := v.gitHubClient.Organizations(httpClient) 26 | if err != nil { 27 | return false, fmt.Errorf("failed to get organizations: %w", err) 28 | } 29 | 30 | for _, name := range orgs { 31 | for _, authorizedOrg := range v.organizations { 32 | if name == authorizedOrg { 33 | return true, nil 34 | } 35 | } 36 | } 37 | 38 | return false, errors.New("you are not part of the allowed organizations") 39 | } 40 | -------------------------------------------------------------------------------- /examples/plugin-cb/apis/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "example", 3 | "active" : true, 4 | "proxy" : { 5 | "preserve_host" : false, 6 | "listen_path" : "/example/*", 7 | "upstreams" : { 8 | "balancing": "roundrobin", 9 | "targets": [ 10 | {"target": "http://service1:8080/"} 11 | ] 12 | }, 13 | "strip_path" : false, 14 | "append_path" : false, 15 | "methods" : ["GET"] 16 | }, 17 | "health_check": { 18 | "url": "http://service1:8080/status", 19 | "timeout": 3 20 | }, 21 | "plugins": [ 22 | { 23 | "name" : "retry", 24 | "enabled" : false, 25 | "config" : { 26 | "attempts" : 3, 27 | "backoff": "1s" 28 | } 29 | }, 30 | { 31 | "name" : "cb", 32 | "enabled" : true, 33 | "config" : { 34 | "timeout" : 1000, 35 | "max_concurrent_requests": 100, 36 | "error_percent_threshold": 50, 37 | "request_volume_threshold": 20, 38 | "sleep_window": 5000, 39 | "predicate": "statusCode == 0 || statusCode >= 500" 40 | } 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /pkg/plugin/bodylmt/middleware.go: -------------------------------------------------------------------------------- 1 | package bodylmt 2 | 3 | import ( 4 | "net/http" 5 | 6 | "code.cloudfoundry.org/bytefmt" 7 | "github.com/hellofresh/janus/pkg/errors" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | var ( 12 | // ErrRequestEntityTooLarge is thrown when a body size is bigger then the limit specified 13 | ErrRequestEntityTooLarge = errors.New(http.StatusRequestEntityTooLarge, http.StatusText(http.StatusRequestEntityTooLarge)) 14 | ) 15 | 16 | // NewBodyLimitMiddleware creates a new body limit middleware 17 | func NewBodyLimitMiddleware(limit string) func(http.Handler) http.Handler { 18 | return func(handler http.Handler) http.Handler { 19 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 | log.WithField("limit", limit).Debug("Starting body limit middleware") 21 | limit, err := bytefmt.ToBytes(limit) 22 | if err != nil { 23 | log.WithError(err).WithField("limit", limit).Error("invalid body-limit") 24 | } 25 | 26 | // Based on content length 27 | if r.ContentLength > int64(limit) { 28 | errors.Handler(w, r, ErrRequestEntityTooLarge) 29 | return 30 | } 31 | 32 | r.Body = http.MaxBytesReader(w, r.Body, int64(limit)) 33 | handler.ServeHTTP(w, r) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/middleware/debug_trace.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go.opencensus.io/plugin/ochttp/propagation/b3" 7 | "go.opencensus.io/trace" 8 | "go.opencensus.io/trace/propagation" 9 | ) 10 | 11 | const ( 12 | // DebugTraceHeader is the header key used for detecting if 13 | // trace should be force sampled and returned in the response 14 | DebugTraceHeader = "X-Debug-Trace" 15 | ) 16 | 17 | // DebugTrace is a middleware that allows debugging requests by providing the Trace ID 18 | // back to the caller in the same header in the response 19 | func DebugTrace(format propagation.HTTPFormat, key string) func(handler http.Handler) http.Handler { 20 | return func(handler http.Handler) http.Handler { 21 | if format == nil { 22 | format = &b3.HTTPFormat{} 23 | } 24 | 25 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 | debugHeader := r.Header.Get(DebugTraceHeader) 27 | if debugHeader == key { 28 | ctx, span := trace.StartSpan(r.Context(), DebugTraceHeader, trace.WithSampler(trace.AlwaysSample())) 29 | r = r.WithContext(ctx) 30 | format.SpanContextToRequest(span.SpanContext(), r) 31 | w.Header().Add(DebugTraceHeader, span.SpanContext().TraceID.String()) 32 | } 33 | 34 | handler.ServeHTTP(w, r) 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/janus-tls/janus.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDXTCCAkWgAwIBAgIJAPPVb4fq4kkvMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQwHhcNMTUxMDE5MTk0MTU4WhcNMTYxMDE4MTk0MTU4WjBF 5 | MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 7 | CgKCAQEAsPnpfnUPbQxSu3oq38OaX/Q6LKZ5gnS04F8kREF2RvCDMWiKOWru+hXb 8 | udkwU7Fx+7BcDBGsnJGFpY23dDcRurxF1DVs1jIFukH/vbYyHE8JQEgvOGSpDEiv 9 | rfbcxqK8E/VMrI10eXYGxWzaTFWQOND2PAJ1b5JvZrrzc8rfJ7h5Q24GKnw1999t 10 | hwsZgpUOh9te7fz1M4XxxRRoliMg0oH9EV3P9Yqq635tjWOix8PcnpcqnRKXVDhk 11 | TcNtE+45RsPoSgM6nkiXt8HP4afaVUAGAzF41kDm94SNexcyk7gyVsLs2cEI61Eu 12 | mhvpP3z91md+eAa3If7kU1w70WiY1wIDAQABo1AwTjAdBgNVHQ4EFgQUue6v2TkZ 13 | 1oR0ZzEnnxfKdsGuBPMwHwYDVR0jBBgwFoAUue6v2TkZ1oR0ZzEnnxfKdsGuBPMw 14 | DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAk+xxO8gC40R7+5WVtWvA 15 | +chNsOoxKyFBOPvGzrYGQbt4OBWKrwQmMXSY3VnjY4GzVaZpOCJOxnupKfZrK4AP 16 | G+M+NI+J6fHJRCQdov7Xoje5M14FmgjRiLg+haDZhh//11C7P6MQPAzGNUTpUyqV 17 | Hsi/wwCYvre5bApb/4uDkDlZkLrgN4e1q8+gh6XLj8NPEOEBEI4VpMVoieC1PwnK 18 | pRfNlTsEhyjeMmOllw9fBKMEvEf1BKsJGaKmQ7zCr1nWznCxyI1Fuf66TfmL8/up 19 | lK6sQysLEOIgn2gZEjQz4O/9Jj9v8+TvyP4GZIDsCiv33AaeKJVuSkoeCH0Ls2V8 20 | aQ== 21 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /pkg/plugin/events.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/hellofresh/janus/cassandra/wrapper" 5 | "github.com/hellofresh/stats-go/client" 6 | "go.mongodb.org/mongo-driver/mongo" 7 | 8 | "github.com/hellofresh/janus/pkg/api" 9 | "github.com/hellofresh/janus/pkg/config" 10 | "github.com/hellofresh/janus/pkg/proxy" 11 | "github.com/hellofresh/janus/pkg/router" 12 | ) 13 | 14 | // Define the event names for the startup and shutdown events 15 | const ( 16 | StartupEvent = "startup" 17 | AdminAPIStartupEvent = "admin_startup" 18 | 19 | ReloadEvent = "reload" 20 | ShutdownEvent = "shutdown" 21 | SetupEvent = "setup" 22 | ) 23 | 24 | // OnStartup represents a event that happens when Janus starts up on the main process 25 | type OnStartup struct { 26 | StatsClient client.Client 27 | MongoDB *mongo.Database 28 | Cassandra wrapper.Holder 29 | Register *proxy.Register 30 | Config *config.Specification 31 | Configuration []*api.Definition 32 | } 33 | 34 | // OnReload represents a event that happens when Janus hot reloads it's configurations 35 | type OnReload struct { 36 | Configurations []*api.Definition 37 | } 38 | 39 | // OnAdminAPIStartup represents a event that happens when Janus starts up the admin API 40 | type OnAdminAPIStartup struct { 41 | Router router.Router 42 | } 43 | -------------------------------------------------------------------------------- /pkg/jwt/github/team_verifier.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // Team represents a github team within the organization 10 | type Team struct { 11 | Name string 12 | Organization string 13 | } 14 | 15 | // TeamVerifier checks if the current user belongs any of the defined teams 16 | type TeamVerifier struct { 17 | teams []Team 18 | gitHubClient Client 19 | } 20 | 21 | // NewTeamVerifier creates a new instance of TeamVerifier 22 | func NewTeamVerifier(teams []Team, gitHubClient Client) *TeamVerifier { 23 | return &TeamVerifier{ 24 | teams: teams, 25 | gitHubClient: gitHubClient, 26 | } 27 | } 28 | 29 | // Verify makes a check and return a boolean if the check was successful or not 30 | func (v *TeamVerifier) Verify(r *http.Request, httpClient *http.Client) (bool, error) { 31 | usersOrgTeams, err := v.gitHubClient.Teams(httpClient) 32 | if err != nil { 33 | return false, fmt.Errorf("failed to get teams: %w", err) 34 | } 35 | 36 | for _, team := range v.teams { 37 | if teams, ok := usersOrgTeams[team.Organization]; ok { 38 | for _, teamUserBelongsTo := range teams { 39 | if teamUserBelongsTo == team.Name { 40 | return true, nil 41 | } 42 | } 43 | } 44 | } 45 | 46 | return false, errors.New("you are not part of the allowed teams") 47 | } 48 | -------------------------------------------------------------------------------- /docs/misc/health_checks.md: -------------------------------------------------------------------------------- 1 | # Health Checks 2 | 3 | Health checks can be added to each API definition by simply setting these properties: 4 | 5 | *url*: The url to be checked 6 | *timeout*: A timeout in seconds for the health check. If the timeout is reached an error is returned. 7 | 8 | You will be able to see all your health checks on the admin REST endpoint `/status`. 9 | If everything is ok you will see something like this: 10 | 11 | ```json 12 | { 13 | "status": "OK", 14 | "timestamp": "2017-06-21T13:06:50.546685883+02:00" 15 | } 16 | ``` 17 | 18 | If you have any problems you'll see something like this: 19 | 20 | ```json 21 | { 22 | "status": "Partially Available", 23 | "timestamp": "2017-06-21T14:44:38.782346389+02:00", 24 | "failures": { 25 | "example": "example is not available at the moment" 26 | } 27 | } 28 | ``` 29 | 30 | Each one of the services must provide an endpoint that Janus can use to check how is the service performing. 31 | The response code that the endpoint returns will define if the service is *healthy*, *partially healthy* or *unhealthy* 32 | 33 | | Code | Description | 34 | |----------------|---------------------------| 35 | | 200 - 399 | Service fully working | 36 | | 400 - 499 | Service partially working | 37 | | 500 > | Service not working | 38 | -------------------------------------------------------------------------------- /pkg/middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/felixge/httpsnoop" 8 | "github.com/hellofresh/janus/pkg/observability" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // Logger struct contains data and logic required for middleware functionality 13 | type Logger struct{} 14 | 15 | // NewLogger builds and returns new Logger middleware instance 16 | func NewLogger() *Logger { 17 | return &Logger{} 18 | } 19 | 20 | // Handler implementation 21 | func (m *Logger) Handler(handler http.Handler) http.Handler { 22 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 23 | log.WithFields(log.Fields{"method": r.Method, "path": r.URL.Path}).Debug("Started request") 24 | 25 | fields := log.Fields{ 26 | "request-id": observability.RequestIDFromContext(r.Context()), 27 | "method": r.Method, 28 | "host": r.Host, 29 | "request": r.RequestURI, 30 | "remote-addr": r.RemoteAddr, 31 | "referer": r.Referer(), 32 | "user-agent": r.UserAgent(), 33 | } 34 | 35 | m := httpsnoop.CaptureMetrics(handler, w, r) 36 | 37 | fields["code"] = m.Code 38 | fields["duration"] = int(m.Duration / time.Millisecond) 39 | fields["duration-fmt"] = m.Duration.String() 40 | 41 | log.WithFields(fields).Info("Completed handling request") 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/proxy/transport/options.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Option represents the transport options 8 | type Option func(*transport) 9 | 10 | // WithInsecureSkipVerify sets tls config insecure skip verify 11 | func WithInsecureSkipVerify(value bool) Option { 12 | return func(t *transport) { 13 | t.insecureSkipVerify = value 14 | } 15 | } 16 | 17 | // WithDialTimeout sets the dial context timeout 18 | func WithDialTimeout(d time.Duration) Option { 19 | return func(t *transport) { 20 | t.dialTimeout = d 21 | } 22 | } 23 | 24 | // WithResponseHeaderTimeout sets the response header timeout 25 | func WithResponseHeaderTimeout(d time.Duration) Option { 26 | return func(t *transport) { 27 | t.responseHeaderTimeout = d 28 | } 29 | } 30 | 31 | // WithIdleConnTimeout sets the maximum amount of time an idle 32 | // (keep-alive) connection will remain idle before closing 33 | // itself. 34 | func WithIdleConnTimeout(d time.Duration) Option { 35 | return func(t *transport) { 36 | t.idleConnTimeout = d 37 | } 38 | } 39 | 40 | // WithIdleConnPurgeTicker purges idle connections on every interval if set 41 | // this is done to prevent permanent keep-alive on connections with high ops 42 | func WithIdleConnPurgeTicker(ticker *time.Ticker) Option { 43 | return func(t *transport) { 44 | t.idleConnPurgeTicker = ticker 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | > An API Gateway written in Go 2 | 3 | This is a lightweight API Gateway and Management Platform that enables you to control who accesses your API, 4 | when they access it and how they access it. API Gateway will also record detailed analytics on how your 5 | users are interacting with your API and when things go wrong. 6 | 7 | ## Why Janus? 8 | 9 | > In ancient Roman religion and myth, Janus (/ˈdʒeɪnəs/; Latin: Ianus, pronounced [ˈjaː.nus]) is the god of beginnings, 10 | gates, transitions, time, doorways, passages, and endings. He is usually depicted as having two faces since he 11 | looks to the future and to the past. [Wikipedia](https://en.wikipedia.org/wiki/Janus) 12 | 13 | We thought it would be nice to name the project after the God of the Gates :smile: 14 | 15 | ## What is an API Gateway? 16 | 17 | An API Gateway sits in front of your application(s) and/or services and manages the heavy lifting of authorisation, 18 | access control and throughput limiting to your services. Ideally, it should mean that you can focus on creating 19 | services instead of implementing management infrastructure. For example, if you have written a really awesome 20 | web service that provides geolocation data for all the cats in NYC, and you want to make it public, 21 | integrating an API gateway is a faster, more secure route than writing your own authorisation middleware. 22 | -------------------------------------------------------------------------------- /pkg/plugin/cb/stats_collector_test.go: -------------------------------------------------------------------------------- 1 | package cb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/hellofresh/stats-go/client" 9 | ) 10 | 11 | func TestCollector(t *testing.T) { 12 | t.Parallel() 13 | 14 | tests := []struct { 15 | scenario string 16 | function func(*testing.T) 17 | }{ 18 | { 19 | scenario: "when a collector can be created", 20 | function: testCollectorCreated, 21 | }, 22 | { 23 | scenario: "when a collector cannot be created because the metrics client is nil", 24 | function: testCollectorNotCreated, 25 | }, 26 | { 27 | scenario: "when a collector registry is given", 28 | function: testCollectorRegistry, 29 | }, 30 | } 31 | 32 | for _, test := range tests { 33 | t.Run(test.scenario, func(t *testing.T) { 34 | test.function(t) 35 | }) 36 | } 37 | } 38 | 39 | func testCollectorCreated(t *testing.T) { 40 | metricsClient := client.NewNoop() 41 | _, err := NewStatsCollector("test", metricsClient) 42 | 43 | require.NoError(t, err) 44 | } 45 | 46 | func testCollectorNotCreated(t *testing.T) { 47 | _, err := NewStatsCollector("test", nil) 48 | 49 | require.Error(t, err) 50 | } 51 | 52 | func testCollectorRegistry(t *testing.T) { 53 | c := NewCollectorRegistry(client.NewNoop()) 54 | require.NotNil(t, c) 55 | 56 | c = NewCollectorRegistry(nil) 57 | require.NotNil(t, c) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/plugin/requesttransformer/setup_test.go: -------------------------------------------------------------------------------- 1 | package requesttransformer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hellofresh/janus/pkg/plugin" 7 | "github.com/hellofresh/janus/pkg/proxy" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestRequestTransformerConfig(t *testing.T) { 12 | var config Config 13 | rawConfig := map[string]interface{}{ 14 | "add": map[string]interface{}{ 15 | "headers": map[string]string{ 16 | "NAME": "TEST", 17 | }, 18 | "querystring": map[string]string{ 19 | "name": "test", 20 | }, 21 | }, 22 | } 23 | 24 | err := plugin.Decode(rawConfig, &config) 25 | assert.NoError(t, err) 26 | 27 | assert.IsType(t, map[string]string{}, config.Add.Headers) 28 | assert.Contains(t, config.Add.Headers, "NAME") 29 | 30 | assert.IsType(t, map[string]string{}, config.Add.QueryString) 31 | assert.Contains(t, config.Add.QueryString, "name") 32 | } 33 | 34 | func TestRequestTransformerPlugin(t *testing.T) { 35 | rawConfig := map[string]interface{}{ 36 | "add": map[string]interface{}{ 37 | "headers": map[string]string{ 38 | "NAME": "TEST", 39 | }, 40 | "querystring": map[string]string{ 41 | "name": "test", 42 | }, 43 | }, 44 | } 45 | 46 | def := proxy.NewRouterDefinition(proxy.NewDefinition()) 47 | err := setupRequestTransformer(def, rawConfig) 48 | assert.NoError(t, err) 49 | 50 | assert.Len(t, def.Middleware(), 1) 51 | } 52 | -------------------------------------------------------------------------------- /docs/proxy/load_balacing.md: -------------------------------------------------------------------------------- 1 | ### Load Balancing 2 | 3 | Janus provides multiple ways of load balancing requests to multiple backend services: a `roundrobin` (or just `rr`) method, 4 | and a `weight` method. 5 | 6 | #### Round Robin 7 | 8 | ```json 9 | { 10 | "name": "My API", 11 | "proxy": { 12 | "listen_path": "/foo/*", 13 | "upstreams" : { 14 | "balancing": "rr", 15 | "targets": [ 16 | {"target": "http://my-api1.com"}, 17 | {"target": "http://my-api2.com"}, 18 | {"target": "http://my-api3.com"} 19 | ] 20 | }, 21 | "methods": ["GET"] 22 | } 23 | } 24 | ``` 25 | 26 | This configuration will apply the `roundrobin` algorithm and balance the requests to your upstreams. 27 | 28 | #### Weight 29 | 30 | ```json 31 | { 32 | "name": "My API", 33 | "proxy": { 34 | "listen_path": "/foo/*", 35 | "upstreams" : { 36 | "balancing": "weight", 37 | "targets": [ 38 | {"target": "http://my-api1.com", "weight": 30}, 39 | {"target": "http://my-api2.com", "weight": 10}, 40 | {"target": "http://my-api3.com", "weight": 60} 41 | ] 42 | }, 43 | "methods": ["GET"] 44 | } 45 | } 46 | ``` 47 | 48 | This configuration will apply the `weight` algorithm and balance the requests to your upstreams. 49 | -------------------------------------------------------------------------------- /docs/upgrade/3x.md: -------------------------------------------------------------------------------- 1 | # 2.x to 3.x Upgrade Notes 2 | 3 | If you're using `MongoDB` as configuration database run the following script against `api_specs` collection: 4 | 5 | ```javascript 6 | db.getCollection('api_specs').find({}).forEach(function(doc) { 7 | doc.plugins = []; 8 | 9 | var corsMeta = doc.cors_meta || {enabled: false}; 10 | doc.plugins.push({ 11 | "name": "cors", 12 | "enabled": !!corsMeta.enabled, 13 | "config": corsMeta 14 | }); 15 | delete doc.plugins[0].config.enabled; 16 | 17 | var rateLimit = doc.rate_limit || {enabled: false, limit: 0}; 18 | doc.plugins.push({ 19 | "name": "rate_limit", 20 | "enabled": !!rateLimit.enabled, 21 | "config": { 22 | "limit": rateLimit.limit, 23 | "policy": "local" 24 | } 25 | }); 26 | 27 | doc.plugins.push({ 28 | "name": "oauth2", 29 | "enabled": !!doc.use_oauth2, 30 | "config": {"server_name": doc.oauth_server_name || null} 31 | }); 32 | 33 | doc.plugins.push({ 34 | "name": "compression", 35 | "enabled": !!doc.use_compression 36 | }); 37 | 38 | delete doc.rate_limit; 39 | delete doc.cors_meta; 40 | delete doc.use_oauth2; 41 | delete doc.use_basic_auth; 42 | delete doc.use_compression; 43 | 44 | doc.updated_at = new Date(); 45 | db.api_specs.update({"_id": doc._id}, doc); 46 | }); 47 | ``` 48 | -------------------------------------------------------------------------------- /pkg/plugin/basic/setup_test.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/hellofresh/janus/pkg/plugin" 9 | "github.com/hellofresh/janus/pkg/proxy" 10 | "github.com/hellofresh/janus/pkg/router" 11 | ) 12 | 13 | func TestSetup(t *testing.T) { 14 | def := proxy.NewRouterDefinition(proxy.NewDefinition()) 15 | 16 | event1 := plugin.OnAdminAPIStartup{Router: router.NewChiRouter()} 17 | err := onAdminAPIStartup(event1) 18 | require.NoError(t, err) 19 | 20 | event2 := plugin.OnStartup{Register: proxy.NewRegister(proxy.WithRouter(router.NewChiRouter()))} 21 | err = onStartup(event2) 22 | require.NoError(t, err) 23 | 24 | err = setupBasicAuth(def, make(plugin.Config)) 25 | require.NoError(t, err) 26 | } 27 | 28 | func TestOnStartupMissingAdminRouter(t *testing.T) { 29 | // reset admin router to avoid dependency from another test 30 | adminRouter = nil 31 | 32 | event := plugin.OnStartup{} 33 | err := onStartup(event) 34 | require.Error(t, err) 35 | require.IsType(t, ErrInvalidAdminRouter, err) 36 | } 37 | 38 | func TestOnStartupWrongEvent(t *testing.T) { 39 | wrongEvent := plugin.OnAdminAPIStartup{} 40 | err := onStartup(wrongEvent) 41 | require.Error(t, err) 42 | } 43 | 44 | func TestOnAdminAPIStartupWrongEvent(t *testing.T) { 45 | wrongEvent := plugin.OnStartup{} 46 | err := onAdminAPIStartup(wrongEvent) 47 | require.Error(t, err) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/plugin/oauth2/middleware_access_rules.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/hellofresh/janus/pkg/jwt" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // NewRevokeRulesMiddleware creates a new revoke rules middleware 11 | func NewRevokeRulesMiddleware(parser *jwt.Parser, accessRules []*AccessRule) func(http.Handler) http.Handler { 12 | return func(handler http.Handler) http.Handler { 13 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | log.WithField("rules", len(accessRules)).Debug("Starting revoke rules middleware") 15 | 16 | // If no rules are set then lets not parse the token to avoid performance issues 17 | if len(accessRules) < 1 { 18 | handler.ServeHTTP(w, r) 19 | return 20 | } 21 | 22 | token, err := parser.ParseFromRequest(r) 23 | if err != nil { 24 | log.WithError(err).Debug("Could not parse the JWT") 25 | handler.ServeHTTP(w, r) 26 | return 27 | } 28 | 29 | if claims, ok := parser.GetMapClaims(token); ok && token.Valid { 30 | for _, rule := range accessRules { 31 | allowed, err := rule.IsAllowed(claims) 32 | if err != nil { 33 | log.WithError(err).Debug("Rule is not allowed") 34 | continue 35 | } 36 | 37 | if allowed { 38 | handler.ServeHTTP(w, r) 39 | } else { 40 | w.WriteHeader(http.StatusUnauthorized) 41 | return 42 | } 43 | } 44 | } 45 | 46 | handler.ServeHTTP(w, r) 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/proxy/request_http_method.md: -------------------------------------------------------------------------------- 1 | #### Request HTTP method 2 | 3 | Client requests can also be routed depending on their 4 | HTTP method by specifying the `methods` field. By default, Janus will route a 5 | request to an API regardless of its HTTP method. But when this field is set, 6 | only requests with the specified HTTP methods will be matched. 7 | 8 | This field also accepts multiple values. Here is an example of an API allowing 9 | routing via `GET` and `HEAD` HTTP methods: 10 | 11 | ```json 12 | { 13 | "name": "My API", 14 | "proxy": { 15 | "strip_path" : true, 16 | "listen_path": "/hello/*", 17 | "upstreams" : { 18 | "balancing": "roundrobin", 19 | "targets": [ 20 | {"target": "http://my-api.com"} 21 | ] 22 | }, 23 | "methods": ["GET", "HEAD"] 24 | } 25 | } 26 | ``` 27 | 28 | Such an API would be matched with the following requests: 29 | 30 | ```http 31 | GET / HTTP/1.1 32 | Host: 33 | ``` 34 | 35 | ```http 36 | HEAD /resource HTTP/1.1 37 | Host: 38 | ``` 39 | 40 | But would not match a `POST` or `DELETE` request. This allows for much more 41 | granularity when configuring APIs and Middlewares. For example, one could imagine 42 | two APIs pointing to the same upstream service: one API allowing unlimited 43 | unauthenticated `GET` requests, and a second API allowing only authenticated 44 | and rate-limited `POST` requests (by applying the authentication and rate 45 | limiting plugins to such requests). 46 | -------------------------------------------------------------------------------- /docs/upgrade/3.7.x.md: -------------------------------------------------------------------------------- 1 | # 3.6.x to 3.7.x Upgrade Notes 2 | 3 | If you're using `MongoDB` as configuration database run the following script against `api_specs` collection: 4 | 5 | ```javascript 6 | db.getCollection('api_specs').find({}).forEach(function(doc) { 7 | if ((!doc.proxy.upstreams || !doc.proxy.upstreams.targets) && !!doc.proxy.upstream_url) { 8 | doc.upstreams = { 9 | "balancing": "roundrobin", 10 | "targets": [{"target": doc.proxy.upstream_url}] 11 | } 12 | 13 | delete doc.proxy.upstream_url; 14 | db.api_specs.update({"_id": doc._id}, doc); 15 | } 16 | }); 17 | ``` 18 | 19 | For the `oauth_servers` collection, run: 20 | 21 | ```javascript 22 | db.getCollection('oauth_servers').find({}).forEach(function(doc) { 23 | var fn = function(p) { 24 | if (!!p && (!p.upstreams || !p.upstreams.targets) && !!p.upstream_url) { 25 | p.upstreams = { 26 | "balancing": "roundrobin", 27 | "targets": [{"target": p.upstream_url}] 28 | } 29 | 30 | delete p.upstream_url; 31 | } 32 | } 33 | 34 | fn(doc.oauth_endpoints.authorize); 35 | fn(doc.oauth_endpoints.token); 36 | fn(doc.oauth_endpoints.info); 37 | fn(doc.oauth_endpoints.revoke); 38 | fn(doc.oauth_endpoints.introspect); 39 | fn(doc.oauth_client_endpoints.create); 40 | fn(doc.oauth_client_endpoints.remove); 41 | 42 | db.oauth_servers.update({"_id": doc._id}, doc); 43 | }); 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/proxy/request_uri.md: -------------------------------------------------------------------------------- 1 | #### Request URI 2 | 3 | Another way for Janus to route a request to a given upstream service is to 4 | specify a request URI via the `proxy.listen_path` property. To satisfy this field's 5 | condition, a client request's URI **must** be prefixed with one of the values 6 | of the `proxy.listen_path` field. 7 | 8 | For example, in an API configured like this: 9 | 10 | ```json 11 | { 12 | "name": "My API", 13 | "proxy": { 14 | "listen_path": "/hello/*", 15 | "upstreams" : { 16 | "balancing": "roundrobin", 17 | "targets": [ 18 | {"target": "http://my-api.com"} 19 | ] 20 | }, 21 | "methods": ["GET"], 22 | } 23 | } 24 | ``` 25 | 26 | The following requests would match the configured API: 27 | 28 | ```http 29 | GET /hello HTTP/1.1 30 | Host: my-api.com 31 | ``` 32 | 33 | ```http 34 | GET /hello/resource?param=value HTTP/1.1 35 | Host: my-api.com 36 | ``` 37 | 38 | ```http 39 | GET /hello/world/resource HTTP/1.1 40 | Host: anything.com 41 | ``` 42 | 43 | For each of these requests, Janus detects that their URI is prefixed with one of 44 | the API's `proxy.listen_path` values. By default, Janus would then forward the request 45 | upstream with the untouched, **same URI**. 46 | 47 | When proxying with URIs prefixes, **the longest URIs get evaluated first**. 48 | This allow you to define two APIs with two URIs: `/service` and 49 | `/service/resource`, and ensure that the former does not "shadow" the latter. 50 | -------------------------------------------------------------------------------- /pkg/errors/error_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | errorsBase "errors" 5 | 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestErrorWithCustomError(t *testing.T) { 14 | w := httptest.NewRecorder() 15 | r, _ := http.NewRequest(http.MethodGet, "/hello/test", nil) 16 | Handler(w, r, New(http.StatusBadRequest, http.StatusText(http.StatusBadRequest))) 17 | 18 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 19 | assert.Equal(t, http.StatusBadRequest, w.Code) 20 | } 21 | 22 | func TestErrorWithDefaultError(t *testing.T) { 23 | w := httptest.NewRecorder() 24 | r, _ := http.NewRequest(http.MethodGet, "/hello/test", nil) 25 | Handler(w, r, errorsBase.New(http.StatusText(http.StatusBadRequest))) 26 | 27 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 28 | assert.Equal(t, http.StatusInternalServerError, w.Code) 29 | } 30 | 31 | func TestErrorNotFound(t *testing.T) { 32 | w := httptest.NewRecorder() 33 | r := httptest.NewRequest("GET", "/", nil) 34 | NotFound(w, r) 35 | 36 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 37 | assert.Equal(t, http.StatusNotFound, w.Code) 38 | } 39 | 40 | func TestRecovery(t *testing.T) { 41 | w := httptest.NewRecorder() 42 | r := httptest.NewRequest("GET", "/", nil) 43 | RecoveryHandler(w, r, ErrInvalidID) 44 | 45 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 46 | assert.Equal(t, http.StatusBadRequest, w.Code) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/proxy/balancer/rr_test.go: -------------------------------------------------------------------------------- 1 | package balancer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | type RoundRobinTestSuite struct { 10 | suite.Suite 11 | hosts []*Target 12 | } 13 | 14 | func (suite *RoundRobinTestSuite) SetupTest() { 15 | suite.hosts = []*Target{ 16 | {Target: "127.0.0.1", Weight: 5}, 17 | {Target: "http://test.com", Weight: 10}, 18 | {Target: "http://example.com", Weight: 8}, 19 | } 20 | } 21 | 22 | func (suite *RoundRobinTestSuite) TestRoundRobinBalancerSuccessfulBalance() { 23 | balancer := NewRoundrobinBalancer() 24 | 25 | electedHost, err := balancer.Elect(suite.hosts) 26 | suite.NoError(err) 27 | suite.Equal(suite.hosts[0], electedHost) 28 | 29 | electedHost, err = balancer.Elect(suite.hosts) 30 | suite.NoError(err) 31 | suite.Equal(suite.hosts[1], electedHost) 32 | 33 | electedHost, err = balancer.Elect(suite.hosts) 34 | suite.NoError(err) 35 | suite.Equal(suite.hosts[2], electedHost) 36 | 37 | electedHost, err = balancer.Elect(suite.hosts) 38 | suite.NoError(err) 39 | suite.Equal(suite.hosts[0], electedHost) 40 | } 41 | 42 | func (suite *RoundRobinTestSuite) TestRoundRobinBalancerEmptyList() { 43 | balancer := NewRoundrobinBalancer() 44 | 45 | _, err := balancer.Elect([]*Target{}) 46 | suite.Error(err) 47 | } 48 | 49 | // In order for 'go test' to run this suite, we need to create 50 | // a normal test function and pass our suite to suite.Run 51 | func TestRoundRobinTestSuite(t *testing.T) { 52 | suite.Run(t, new(RoundRobinTestSuite)) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/middleware/stats.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/felixge/httpsnoop" 7 | "github.com/hellofresh/janus/pkg/metrics" 8 | "github.com/hellofresh/stats-go/client" 9 | "github.com/hellofresh/stats-go/timer" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | const ( 14 | notFoundPath = "/-not-found-" 15 | statsSectionRoundTrip = "round" 16 | ) 17 | 18 | // Stats represents the stats middleware 19 | type Stats struct { 20 | statsClient client.Client 21 | } 22 | 23 | // NewStats creates a new instance of Stats 24 | func NewStats(statsClient client.Client) *Stats { 25 | return &Stats{statsClient} 26 | } 27 | 28 | // Handler is the middleware function 29 | func (m *Stats) Handler(handler http.Handler) http.Handler { 30 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | log.WithField("path", r.URL.Path).Debug("Starting Stats middleware") 32 | r = r.WithContext(metrics.NewContext(r.Context(), m.statsClient)) 33 | 34 | mt := httpsnoop.CaptureMetrics(handler, w, r) 35 | t := timer.NewDuration(mt.Duration) 36 | 37 | success := mt.Code < http.StatusBadRequest 38 | if mt.Code == http.StatusNotFound { 39 | log.WithField("path", r.URL.Path).Warn("Unknown endpoint requested") 40 | r.URL.Path = notFoundPath 41 | } 42 | m.statsClient.TrackRequest(r, t, success) 43 | 44 | m.statsClient.SetHTTPRequestSection(statsSectionRoundTrip). 45 | TrackRequest(r, t, mt.Code < http.StatusInternalServerError). 46 | ResetHTTPRequestSection() 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/plugin/basic/in_memory_repository_test.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func newInMemoryRepo() *InMemoryRepository { 10 | repo := NewInMemoryRepository() 11 | 12 | repo.Add(&User{ 13 | Username: "test1", 14 | Password: "test1", 15 | }) 16 | 17 | repo.Add(&User{ 18 | Username: "test2", 19 | Password: "test2", 20 | }) 21 | 22 | return repo 23 | } 24 | 25 | func TestAdd(t *testing.T) { 26 | repo := newInMemoryRepo() 27 | 28 | err := repo.Add(&User{ 29 | Username: "test3", 30 | Password: "test3", 31 | }) 32 | assert.NoError(t, err) 33 | } 34 | 35 | func TestRemoveExistentUser(t *testing.T) { 36 | repo := newInMemoryRepo() 37 | 38 | err := repo.Remove("test1") 39 | assert.NoError(t, err) 40 | } 41 | 42 | func TestRemoveNonexistentUser(t *testing.T) { 43 | repo := newInMemoryRepo() 44 | 45 | err := repo.Remove("invalid") 46 | assert.Error(t, err) 47 | } 48 | 49 | func TestFindAll(t *testing.T) { 50 | repo := newInMemoryRepo() 51 | 52 | results, err := repo.FindAll() 53 | assert.NoError(t, err) 54 | assert.Len(t, results, 2) 55 | } 56 | 57 | func TestFindByUsername(t *testing.T) { 58 | repo := newInMemoryRepo() 59 | 60 | result, err := repo.FindByUsername("test1") 61 | assert.NoError(t, err) 62 | assert.NotNil(t, result) 63 | } 64 | 65 | func TestNotFindByUsername(t *testing.T) { 66 | repo := newInMemoryRepo() 67 | 68 | result, err := repo.FindByUsername("invalid") 69 | assert.Error(t, err) 70 | assert.Nil(t, result) 71 | } 72 | -------------------------------------------------------------------------------- /docs/config/proxy.md: -------------------------------------------------------------------------------- 1 | # Proxy configuration 2 | 3 | | Configuration | Description | 4 | |-----------------------|----------------------------------------------------------------------------------------| 5 | | preserve_hosts | Enable the [preserve host](/docs/proxy/preserve_host_property.md) definition | 6 | | listen_path | Defines the [endpoint](/docs/proxy/request_uri.md) that will be exposed in Janus | 7 | | upstreams | Defines the [endpoints](/docs/proxy/upstreams.md) that the request will be forwarded to| 8 | | strip_path | Enable the [strip URI](/docs/proxy/strip_uri_property.md) rule on this proxy | 9 | | methods | Defines which [methods](/docs/proxy/request_http_method.md) are enabled for this proxy | 10 | | hosts | Defines which [hosts](/docs/proxy/request_http_header.md) are enabled for this proxy | 11 | | forwarding_timeouts.dial_timeout | The amount of time to wait until a connection to a backend server can be established. Defaults to 30 seconds. If zero, no timeout exists. You must use any format that is compatible with [time.Duration](https://golang.org/pkg/time/#Duration) | 12 | | forwarding_timeouts.response_header_timeout | The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists. You must use any format that is compatible with [time.Duration](https://golang.org/pkg/time/#Duration) | 13 | -------------------------------------------------------------------------------- /pkg/plugin/cors/setup.go: -------------------------------------------------------------------------------- 1 | package cors 2 | 3 | import ( 4 | "github.com/asaskevich/govalidator" 5 | "github.com/hellofresh/janus/pkg/plugin" 6 | "github.com/hellofresh/janus/pkg/proxy" 7 | "github.com/rs/cors" 8 | ) 9 | 10 | // Config represents the CORS configuration 11 | type Config struct { 12 | AllowedOrigins []string `json:"domains"` 13 | AllowedMethods []string `json:"methods"` 14 | AllowedHeaders []string `json:"request_headers"` 15 | ExposedHeaders []string `json:"exposed_headers"` 16 | OptionsPassthrough bool `json:"options_passthrough"` 17 | } 18 | 19 | func init() { 20 | plugin.RegisterPlugin("cors", plugin.Plugin{ 21 | Action: setupCors, 22 | Validate: validateConfig, 23 | }) 24 | } 25 | 26 | func setupCors(def *proxy.RouterDefinition, rawConfig plugin.Config) error { 27 | var config Config 28 | 29 | err := plugin.Decode(rawConfig, &config) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | mw := cors.New(cors.Options{ 35 | AllowedOrigins: config.AllowedOrigins, 36 | AllowedMethods: config.AllowedMethods, 37 | AllowedHeaders: config.AllowedHeaders, 38 | ExposedHeaders: config.ExposedHeaders, 39 | OptionsPassthrough: config.OptionsPassthrough, 40 | AllowCredentials: true, 41 | }) 42 | 43 | def.AddMiddleware(mw.Handler) 44 | return nil 45 | } 46 | 47 | func validateConfig(rawConfig plugin.Config) (bool, error) { 48 | var config Config 49 | err := plugin.Decode(rawConfig, &config) 50 | if err != nil { 51 | return false, err 52 | } 53 | 54 | return govalidator.ValidateStruct(config) 55 | } 56 | -------------------------------------------------------------------------------- /janus/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "janus.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "janus.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "janus.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "janus.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | echo "Visit http://127.0.0.1:8080 to use your application" 20 | kubectl port-forward $POD_NAME 8080:80 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /docs/quick_start/README.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | 1. [Download](#download) 4 | 2. [Install](#install) 5 | 3. [Run](#run) 6 | 3. [Configure](#configure) 7 | 8 | ## Download 9 | 10 | You can get Janus for nearly any OS and architecture. You can get the latest Janus release on [Github](https://github.com/hellofresh/janus/releases). 11 | 12 | ## Install and run 13 | 14 | We highly recommend you to use one of our examples to start. Let's see the [front-proxy-mongo](https://github.com/hellofresh/janus/blob/master/examples/front-proxy-mongo) example: 15 | 16 | Make sure you have docker up and running on your platform and then run. 17 | 18 | ```sh 19 | docker-compose up -d 20 | ``` 21 | 22 | This will spin up a janus server and will have a small proxy configuration that is going to a mock server that we spun up. 23 | 24 | ## Configure 25 | 26 | If you access `http://localhost:8080/example` you should see something like: 27 | 28 | ```json 29 | { 30 | "message": "Hello World!" 31 | } 32 | ``` 33 | 34 | That means that Janus already proxied your request to an upstream. But of course you don't just want to do that. For this reason 35 | now is the perfect time for you to learn about all the available configurations that you can play with. 36 | 37 | > Note: If you are using the file-based configuration you will not be able to use the write administration API to add/modify/remove new endpoints, plugins, etc. Please check this [tutorial](file_system.md) if you'd like to add a new endpoint using the file-based configuration. 38 | 39 | Next, let's learn about how to [configure a new endpoint](authenticating.md). 40 | -------------------------------------------------------------------------------- /pkg/test/http.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "net/http/httptest" 7 | 8 | "github.com/hellofresh/janus/pkg/router" 9 | ) 10 | 11 | // Server represents a testing HTTP Server 12 | type Server struct { 13 | *httptest.Server 14 | } 15 | 16 | // NewServer creates a new instance of Server 17 | func NewServer(r router.Router) *Server { 18 | return &Server{httptest.NewServer(r)} 19 | } 20 | 21 | // Do creates a HTTP request to be tested 22 | func (s *Server) Do(method string, url string, headers map[string]string) (*http.Response, error) { 23 | var u bytes.Buffer 24 | u.WriteString(string(s.URL)) 25 | u.WriteString(url) 26 | 27 | req, err := http.NewRequest(method, u.String(), nil) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | for headerName, headerValue := range headers { 33 | if headerName == "Host" { 34 | req.Host = headerValue 35 | } else { 36 | req.Header.Set(headerName, headerValue) 37 | } 38 | } 39 | 40 | return http.DefaultClient.Do(req) 41 | } 42 | 43 | // Record creates a ResponseRecorder for testing 44 | func Record(method string, url string, headers map[string]string, handleFunc http.Handler) (*httptest.ResponseRecorder, error) { 45 | req, err := http.NewRequest(method, url, nil) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | for headerName, headerValue := range headers { 51 | if headerName == "Host" { 52 | req.Host = headerValue 53 | } else { 54 | req.Header.Set(headerName, headerValue) 55 | } 56 | } 57 | 58 | w := httptest.NewRecorder() 59 | handleFunc.ServeHTTP(w, req) 60 | 61 | return w, nil 62 | } 63 | -------------------------------------------------------------------------------- /docs/plugins/cb.md: -------------------------------------------------------------------------------- 1 | # Circuit Breaker 2 | 3 | Janus has a circuit breaker plugin that can be configured for each endpoint. You can check 4 | our [example](https://github.com/hellofresh/janus/tree/master/examples/plugin-cb) on how 5 | to use the plugin. 6 | 7 | ## Configuration 8 | 9 | The plain cb config: 10 | 11 | ```json 12 | { 13 | "name" : "cb", 14 | "enabled" : true, 15 | "config" : { 16 | "name": "my-circuit-breaker", 17 | "timeout" : 1000, 18 | "max_concurrent_requests": 100, 19 | "error_percent_threshold": 50, 20 | "request_volume_threshold": 20, 21 | "sleep_window": 5000, 22 | "predicate": "statusCode == 0 || statusCode >= 500" 23 | } 24 | } 25 | ``` 26 | 27 | Configuration | Description 28 | :---|:---| 29 | | name | Circuit Breaker name to group stats | 30 | | timeout | Timeout that the CB will wait till the request responds | 31 | | max_concurrent_requests | How many commands of the same type can run at the same time | 32 | | error_percent_threshold | Causes circuits to open once the rolling measure of errors exceeds this percent of requests | 33 | | request_volume_threshold | Is the minimum number of requests needed before a circuit can be tripped due to health | 34 | | sleep_window | Is how long, in milliseconds, to wait after a circuit opens before testing for recovery | 35 | | predicate | The rule that we will check to define if the request was successful or not. You have access to `statusCode` and all the `request` object. Defaults to `statusCode == 0 \|\| statusCode >= 500` | 36 | -------------------------------------------------------------------------------- /pkg/plugin/basic/middleware.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "github.com/hellofresh/janus/pkg/plugin/basic/encrypt" 5 | "net/http" 6 | 7 | "github.com/hellofresh/janus/pkg/errors" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // NewBasicAuth is a HTTP basic auth middleware 12 | func NewBasicAuth(repo Repository) func(http.Handler) http.Handler { 13 | return func(handler http.Handler) http.Handler { 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | log.Debug("Starting basic auth middleware") 16 | logger := log.WithFields(log.Fields{ 17 | "path": r.RequestURI, 18 | "origin": r.RemoteAddr, 19 | }) 20 | 21 | username, password, authOK := r.BasicAuth() 22 | if !authOK { 23 | errors.Handler(w, r, ErrNotAuthorized) 24 | return 25 | } 26 | 27 | var found bool 28 | users, err := repo.FindAll() 29 | if err != nil { 30 | log.WithError(err).Error("Error when getting all users") 31 | errors.Handler(w, r, errors.New(http.StatusInternalServerError, "there was an error when looking for users")) 32 | return 33 | } 34 | 35 | hash := encrypt.Hash{} 36 | 37 | for _, u := range users { 38 | //if username == u.Username && (subtle.ConstantTimeCompare([]byte(password), []byte(u.Password)) == 1) { 39 | if username == u.Username && (hash.Compare(u.Password, password) == nil) { 40 | found = true 41 | break 42 | } 43 | } 44 | 45 | if !found { 46 | logger.Debug("Invalid user/password provided.") 47 | errors.Handler(w, r, ErrNotAuthorized) 48 | return 49 | } 50 | 51 | handler.ServeHTTP(w, r) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/plugin/oauth2/file_repository.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "path/filepath" 7 | "sync" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // FileSystemRepository represents a mongodb repository 13 | type FileSystemRepository struct { 14 | *InMemoryRepository 15 | sync.Mutex 16 | } 17 | 18 | // NewFileSystemRepository creates a mongo OAuth Server repo 19 | func NewFileSystemRepository(dir string) (*FileSystemRepository, error) { 20 | repo := &FileSystemRepository{InMemoryRepository: NewInMemoryRepository()} 21 | 22 | // Grab json files from directory 23 | files, err := ioutil.ReadDir(dir) 24 | if nil != err { 25 | return nil, err 26 | } 27 | 28 | for _, f := range files { 29 | if filepath.Ext(f.Name()) == ".json" { 30 | filePath := filepath.Join(dir, f.Name()) 31 | oauthServerRaw, err := ioutil.ReadFile(filePath) 32 | if err != nil { 33 | log.WithError(err).WithField("path", filePath).Error("Couldn't load the oauth server file") 34 | return nil, err 35 | } 36 | 37 | oauthServer := repo.parseOAuthServer(oauthServerRaw) 38 | if err = repo.Add(oauthServer); err != nil { 39 | log.WithError(err).Error("Can't add the definition to the repository") 40 | return nil, err 41 | } 42 | } 43 | } 44 | 45 | return repo, nil 46 | } 47 | 48 | func (r *FileSystemRepository) parseOAuthServer(oauthServerRaw []byte) *OAuth { 49 | oauthServer := NewOAuth() 50 | if err := json.Unmarshal(oauthServerRaw, oauthServer); err != nil { 51 | log.WithError(err).Error("[RPC] --> Couldn't unmarshal oauth server configuration") 52 | } 53 | 54 | return oauthServer 55 | } 56 | -------------------------------------------------------------------------------- /examples/plugin-cb/README.md: -------------------------------------------------------------------------------- 1 | # Janus - Circuit Breaker Example 2 | 3 | Janus has a circuit breaker plugin that can be configured on each endpoint. To test this example start by running: 4 | 5 | ```sh 6 | docker-compose up -d 7 | ``` 8 | 9 | You can access the [hystrix](https://github.com/Netflix/Hystrix) dashboard on http://localhost9002/hystrix 10 | 11 | ![](img/hystrix-home.png) 12 | 13 | On the URL field please add http://janus:8081/hystrix which is the streaming endpoint in Janus. This endpoint will start streaming metrics about your circuits. 14 | 15 | Now you can start making request to `/example` 16 | 17 | ```sh 18 | curl localhost:8080/example 19 | ``` 20 | 21 | If you take a look on the dashboard you should see something like this: 22 | 23 | ![](img/hystrix-dashboard.png) 24 | 25 | ## Simulating failure 26 | 27 | To simulate failure run: 28 | 29 | ```sh 30 | docker-compose stop service1 31 | ``` 32 | 33 | This will force your proxy to go down and Janus won't be able to reach it. 34 | 35 | Start making a lot of requests to `/example` and see what's going to happen on the dashboard. 36 | 37 | For all the options on how to configure this plugin please visit the [documentation](https://hellofresh.gitbooks.io/janus/plugins/cb.html) page. 38 | 39 | ## Using vegeta for failure simulation 40 | 41 | You can also use [vegeta](https://github.com/tsenart/vegeta) which is a HTTP load testing tool. 42 | 43 | Here we are going to send 10 req/s during 30s. In this time you can easily stop `service1` and see how the circuit will react. 44 | 45 | ```sh 46 | echo "GET http://localhost:8080/example" | vegeta attack -rate=10 -duration=30s | vegeta report 47 | ``` 48 | -------------------------------------------------------------------------------- /pkg/proxy/balancer/balancer.go: -------------------------------------------------------------------------------- 1 | // Package balancer provides a simple interface to create concrete balancer algorightms that can be used to choose 2 | // an upstream 3 | package balancer 4 | 5 | import ( 6 | "errors" 7 | "math/rand" 8 | "reflect" 9 | "time" 10 | ) 11 | 12 | var ( 13 | // ErrEmptyBackendList is used when the list of beckends is empty 14 | ErrEmptyBackendList = errors.New("can not elect backend, Backends empty") 15 | // ErrCannotElectBackend is used a backend cannot be elected 16 | ErrCannotElectBackend = errors.New("cant elect backend") 17 | // ErrUnsupportedAlgorithm is used when an unsupported algorithm is given 18 | ErrUnsupportedAlgorithm = errors.New("unsupported balancing algorithm") 19 | typeRegistry = make(map[string]reflect.Type) 20 | ) 21 | 22 | type ( 23 | // Balancer holds the load balancer methods for many different algorithms 24 | Balancer interface { 25 | Elect(hosts []*Target) (*Target, error) 26 | } 27 | 28 | // Target is an ip address/hostname with a port that identifies an instance of a backend service 29 | Target struct { 30 | Target string 31 | Weight int 32 | } 33 | ) 34 | 35 | func init() { 36 | rand.Seed(time.Now().UnixNano()) 37 | typeRegistry["roundrobin"] = reflect.TypeOf(RoundrobinBalancer{}) 38 | typeRegistry["rr"] = reflect.TypeOf(RoundrobinBalancer{}) 39 | typeRegistry["weight"] = reflect.TypeOf(WeightBalancer{}) 40 | } 41 | 42 | // New creates a new Balancer based on balancing strategy 43 | func New(balance string) (Balancer, error) { 44 | alg, ok := typeRegistry[balance] 45 | if !ok { 46 | return nil, ErrUnsupportedAlgorithm 47 | } 48 | 49 | return reflect.New(alg).Elem().Addr().Interface().(Balancer), nil 50 | } 51 | -------------------------------------------------------------------------------- /janus/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "janus.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "janus.fullname" -}} 15 | {{- if .name -}} 16 | {{- .name | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- if .Values.fullnameOverride -}} 19 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 20 | {{- else -}} 21 | {{- $name := default .Chart.Name .Values.nameOverride -}} 22 | {{- if contains $name .Release.Name -}} 23 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 24 | {{- else -}} 25 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 26 | {{- end -}} 27 | {{- end -}} 28 | {{- end -}} 29 | {{- end -}} 30 | 31 | {{/* 32 | Create chart name and version as used by the chart label. 33 | */}} 34 | {{- define "janus.chart" -}} 35 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 36 | {{- end -}} 37 | 38 | {{/* 39 | Common labels 40 | */}} 41 | {{- define "janus.labels" -}} 42 | app.kubernetes.io/name: {{ include "janus.name" . }} 43 | helm.sh/chart: {{ include "janus.chart" . }} 44 | app.kubernetes.io/instance: {{ .Release.Name }} 45 | {{- if .Chart.AppVersion }} 46 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 47 | {{- end }} 48 | app.kubernetes.io/managed-by: {{ .Release.Service }} 49 | {{- end -}} 50 | -------------------------------------------------------------------------------- /pkg/api/file_repository_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/hellofresh/janus/pkg/proxy" 12 | ) 13 | 14 | func TestNewFileSystemRepository(t *testing.T) { 15 | fsRepo := newRepo(t) 16 | 17 | allDefinitions, err := fsRepo.FindAll() 18 | assert.NoError(t, err) 19 | assert.Equal(t, 3, len(allDefinitions)) 20 | 21 | defToAdd := &Definition{ 22 | Name: "foo-bar", 23 | Proxy: &proxy.Definition{ 24 | ListenPath: "/foo/bar/*", 25 | Upstreams: &proxy.Upstreams{ 26 | Balancing: "roundrobin", 27 | Targets: []*proxy.Target{ 28 | {Target: "http://example.com/foo/bar/"}, 29 | }, 30 | }, 31 | }, 32 | } 33 | err = fsRepo.add(defToAdd) 34 | require.NoError(t, err) 35 | 36 | def, err := fsRepo.findByName(defToAdd.Name) 37 | require.NoError(t, err) 38 | assert.Equal(t, defToAdd.Name, def.Name) 39 | assert.Equal(t, defToAdd.Proxy.ListenPath, def.Proxy.ListenPath) 40 | } 41 | 42 | func TestFileSystemRepository_Add(t *testing.T) { 43 | fsRepo := newRepo(t) 44 | 45 | invalidName := &Definition{Name: ""} 46 | err := fsRepo.add(invalidName) 47 | require.Error(t, err) 48 | } 49 | 50 | func newRepo(t *testing.T) *FileSystemRepository { 51 | wd, err := os.Getwd() 52 | assert.NoError(t, err) 53 | 54 | // ./../../assets/apis 55 | exampleAPIsPath := filepath.Join(wd, "..", "..", "assets", "apis") 56 | info, err := os.Stat(exampleAPIsPath) 57 | require.NoError(t, err) 58 | require.True(t, info.IsDir()) 59 | 60 | fsRepo, err := NewFileSystemRepository(exampleAPIsPath) 61 | require.NoError(t, err) 62 | 63 | return fsRepo 64 | } 65 | -------------------------------------------------------------------------------- /pkg/middleware/debug_trace_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/magiconair/properties/assert" 9 | "go.opencensus.io/plugin/ochttp/propagation/b3" 10 | ) 11 | 12 | func TestDebugTrace(t *testing.T) { 13 | format := &b3.HTTPFormat{} 14 | 15 | testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) 16 | 17 | tests := []struct { 18 | testName string 19 | debugHeader string 20 | expectedTraceHeader bool 21 | }{ 22 | { 23 | testName: "'X-Debug-Trace: secret-key' produces response debug header", 24 | debugHeader: "secret-key", 25 | expectedTraceHeader: true, 26 | }, 27 | { 28 | testName: "'X-Debug-Trace: 0' does not produce response debug header", 29 | debugHeader: "0", 30 | expectedTraceHeader: false, 31 | }, 32 | { 33 | testName: "'X-Debug-Trace: true' does not response debug header", 34 | debugHeader: "true", 35 | expectedTraceHeader: false, 36 | }, 37 | { 38 | testName: "'X-Debug-Trace: ' does not produce response debug header", 39 | debugHeader: "", 40 | expectedTraceHeader: false, 41 | }, 42 | } 43 | 44 | middleware := DebugTrace(format, "secret-key") 45 | 46 | for _, test := range tests { 47 | req, _ := http.NewRequest("GET", "http://hello-world", nil) 48 | req.Header.Add("X-Debug-Trace", test.debugHeader) 49 | 50 | rr := httptest.NewRecorder() 51 | middleware(testHandler).ServeHTTP(rr, req) 52 | hasDebugHeader := rr.Header().Get("X-Debug-Trace") != "" 53 | assert.Equal(t, hasDebugHeader, test.expectedTraceHeader, test.testName) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/janus-tls/janus.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCw+el+dQ9tDFK7 3 | eirfw5pf9DospnmCdLTgXyREQXZG8IMxaIo5au76Fdu52TBTsXH7sFwMEayckYWl 4 | jbd0NxG6vEXUNWzWMgW6Qf+9tjIcTwlASC84ZKkMSK+t9tzGorwT9UysjXR5dgbF 5 | bNpMVZA40PY8AnVvkm9muvNzyt8nuHlDbgYqfDX3322HCxmClQ6H217t/PUzhfHF 6 | FGiWIyDSgf0RXc/1iqrrfm2NY6LHw9yelyqdEpdUOGRNw20T7jlGw+hKAzqeSJe3 7 | wc/hp9pVQAYDMXjWQOb3hI17FzKTuDJWwuzZwQjrUS6aG+k/fP3WZ354Brch/uRT 8 | XDvRaJjXAgMBAAECggEAHvnvO5ojtBOXG4d7n6TuDWODFzOgSwxAaJFemK/Ykvwg 9 | CnLg1sH3yEAxMGtqgQurBsHMqrQhQVpbSSnv9WB6MvQnSMh9H1SsGfjZWYxdYwUW 10 | enDoCvfbevHyBgISjJYJU3j5Da7It0XIU6AE6Z2EW91/a+uGQJwh8ZpBaIAW5S2j 11 | B3k+bASANtwEcDdhGE7iLYeHiAttZo89oSSFZP/mwh84pIU29zUVUtsUaHXrob0p 12 | iyGXKPa8NqTvIsbX5Kh/lbbCO4KwsOqgs/eqL7cLSv2VfTmSQCJz+ikiVzcw/vJU 13 | PaT9H4SCBLP73/Gyjf5P14esWvprPQ3ZnWNNDDGWsQKBgQDoWqxQUy6PKY9or7QH 14 | M985y52Y0QlWdmRaLc8gxfWLU4/3Wn0NH1flkFXJ5X9uZFNoGMQpidJBajepzkNO 15 | /54V+1NCLUWl7SE5gMeFG8QtEE7ISyjut71CUDSn5mOp7EBARmqRpMZhmXT42RZi 16 | 1zVDkG08ArKdH0Jnvkq5lWHGbwKBgQDC/IYJXkd27XZO+Ti8TdzaU+SSJV26aY++ 17 | 0N4pzq0cC6IWadHugH/XrgkfH+ImPzkf6XHrCSqSipJJLZMd473/8IjdOsf54wDP 18 | /yHKPXWhfC4W2L+6+l34Jo/ebnuDVvDme1nKLcdmxhwz4YZfg/TYbWaFzANrl3St 19 | beGg9ENIGQKBgBr6/GtPXWauUsK7NFJpyY/yfthR3Z22nayDCTwrAHovN9ZnIYI2 20 | k4RKoEuTZJqy96Rsy8pvAIUsCk6jbtlrgTXYOzDCBQZhZKxCsehY8wywihVj9NrT 21 | ZxyeJ58fd48xqbxM8O78jTSkFxsWSi0sBDlWOfjv70GjcZiOVir6l6HtAoGBAJeA 22 | MAENcQeV4AviltOwx/4Xmwx23gmeRaMklMn1HQoie9FgbU4cJ7kEL3AwjL3c99y0 23 | vN+7Ion0A0+6iol5z8ISObVzG7gsShBSkwWZlVFgtErqJKb6K5NJGxXf0DYvkkPy 24 | 6cQup7VSDs282HRUiiSzdCpXZvztFCpAq0QtJi3ZAoGACjtJ7zEVs0hB7+sCq/SI 25 | UHjjv/fjGSm1TVDP46Joqbm62FRdYkEhd+pGMjtGs80OhM+psTZIqe/fgKdKl5yX 26 | nS9m6f4ny6XCcilfI3+bxXtsmWnpQnybSU2goe2n+Eoi3RcEB68Hp8U0aPjgDULM 27 | 9YDU/ZMupHh/eT79n67QIXw= 28 | -----END PRIVATE KEY----- -------------------------------------------------------------------------------- /pkg/plugin/oauth2/error.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/hellofresh/janus/pkg/errors" 7 | ) 8 | 9 | var ( 10 | // ErrClientIDNotFound is raised when a client_id was not found 11 | ErrClientIDNotFound = errors.New(http.StatusBadRequest, "Invalid client ID provided") 12 | 13 | // ErrAccessTokenOfOtherOrigin is used when the access token is of other origin 14 | ErrAccessTokenOfOtherOrigin = errors.New(http.StatusUnauthorized, "access token of other origin") 15 | 16 | // ErrOauthServerNotFound is used when the oauth server was not found in the datastore 17 | ErrOauthServerNotFound = errors.New(http.StatusNotFound, "oauth server not found") 18 | 19 | // ErrDBContextNotSet is used when the database request context is not set 20 | ErrDBContextNotSet = errors.New(http.StatusInternalServerError, "DB context was not set for this request") 21 | 22 | // ErrJWTSecretMissing is used when the database request context is not set 23 | ErrJWTSecretMissing = errors.New(http.StatusBadRequest, "You need to set a JWT secret") 24 | 25 | // ErrUnknownManager is used when a manager type is not known 26 | ErrUnknownManager = errors.New(http.StatusBadRequest, "Unknown manager type provided") 27 | 28 | // ErrUnknownStrategy is used when a token strategy is not known 29 | ErrUnknownStrategy = errors.New(http.StatusBadRequest, "Unknown token strategy type provided") 30 | 31 | // ErrInvalidIntrospectionURL is used when an introspection URL is invalid 32 | ErrInvalidIntrospectionURL = errors.New(http.StatusBadRequest, "The provided introspection URL is invalid") 33 | 34 | // ErrOauthServerNameExists is used when the Oauth Server name is already registered on the datastore 35 | ErrOauthServerNameExists = errors.New(http.StatusConflict, "oauth server name is already registered") 36 | ) 37 | -------------------------------------------------------------------------------- /docs/install/docker.md: -------------------------------------------------------------------------------- 1 | # Docker Installation 2 | 3 | Official docker image is available in [Docker Hub repository](https://hub.docker.com/repository/docker/hellofreshtech/janus). 4 | We also have a some cool examples with [Docker Compose template](https://github.com/hellofresh/janus/blob/master/examples) with built-in orchestration and scalability. 5 | 6 | Here is a quick example showing how to link a Janus container to a MongoDB container: 7 | 8 | 1. **Start your database:** 9 | 10 | If you wish to use a database instead of a file system based configuration just start the mongodb container: 11 | 12 | ```sh 13 | $ docker run -d --name janus-database \ 14 | -p 27017:27017 \ 15 | mongo:3.0 16 | ``` 17 | 18 | 2. **Configure the update frequency** 19 | 20 | You should configure how frequently Janus will check for changes on your database. You can set this by changing the cluster configuration: 21 | 22 | ```toml 23 | [cluster] 24 | UpdateFrequency = "5s" 25 | ``` 26 | 27 | You can find more information about Janus clusters in the [clustering](../clustering/clustering.md) section. 28 | 29 | 3. **Start Janus:** 30 | 31 | Start a Janus container and link it to your database container (if you are using it), configuring the `DATABASE_DSN` environment variable with the connection string like `mongodb://janus-database:27017/janus`: 32 | 33 | ```sh 34 | $ docker run -d --name janus \ 35 | --link janus-database:janus-database \ 36 | -e "DATABASE_DSN=mongodb://janus-database:27017/janus" \ 37 | -p 8080:8080 \ 38 | -p 8443:8443 \ 39 | -p 8081:8081 \ 40 | -p 8444:8444 \ 41 | hellofreshtech/janus 42 | ``` 43 | 44 | 3. **Janus is running:** 45 | 46 | ```sh 47 | $ curl http://127.0.0.1:8081/ 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/plugins/README.md: -------------------------------------------------------------------------------- 1 | # Extending Janus 2 | 3 | Janus can be extended with plugins. Plugins add missing functionality to Janus. They are "plugged in" at compile-time. 4 | The plugins are attached to an [API Definition](../api-definition/README.md) and you can enable or disable them at 5 | any time. 6 | 7 | Janus comes with a set of built in plugins that you can add to your API Definitions: 8 | 9 | * [CORS](cors.md) 10 | * [OAuth2](oauth.md) 11 | * [Rate Limit](rate_limit.md) 12 | * [Request Transformer](request_transformer.md) 13 | * [Compression](compression.md) 14 | 15 | ## How can I create a plugin? 16 | 17 | Even though there are different kinds of plugins, the process of creating one is roughly the same for all. 18 | 19 | ### 1. Create a package and register your plugin. 20 | 21 | Start a new Go package with an init function and register your plugin with Janus: 22 | 23 | ```go 24 | import "github.com/hellofresh/janus/pkg/plugin" 25 | 26 | func init() { 27 | // register a "generic" plugin, like a directive or middleware 28 | plugin.RegisterPlugin("name", myPlugin) 29 | } 30 | ``` 31 | 32 | Every plugin must have a name and, when applicable, the name must be unique. 33 | 34 | ### 2. Plug in your plugin. 35 | 36 | To plug your plugin into Janus, import it. This is usually done near the top of [loader.go](../../pgk/loader/loader.go): 37 | 38 | ```go 39 | import _ "your/plugin/package/path/here" 40 | ``` 41 | 42 | ### 3. Write Tests! 43 | 44 | Write tests. Get good coverage where possible, and make sure your assertions test what you think they are testing! Use go vet and go test -race to ensure your plugin is as error-free as possible. 45 | 46 | ### 4. Maintain your plugin. 47 | 48 | People will use plugins that are useful, clearly documented, easy to use, and maintained by their owner. 49 | And congratulations, you're a Janus plugin author! 50 | -------------------------------------------------------------------------------- /pkg/plugin/cb/middleware_test.go: -------------------------------------------------------------------------------- 1 | package cb 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/hellofresh/janus/pkg/test" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestMiddleware(t *testing.T) { 13 | t.Parallel() 14 | 15 | tests := []struct { 16 | scenario string 17 | function func(*testing.T, *http.Request, *httptest.ResponseRecorder) 18 | }{ 19 | { 20 | scenario: "with wrong predicate given", 21 | function: testWrongPredicate, 22 | }, 23 | { 24 | scenario: "when the upstream respond successfully", 25 | function: testSuccessfulUpstreamRetry, 26 | }, 27 | { 28 | scenario: "when the upstream fails to respond", 29 | function: testFailedUpstreamRetry, 30 | }, 31 | } 32 | 33 | for _, test := range tests { 34 | t.Run(test.scenario, func(t *testing.T) { 35 | r := httptest.NewRequest(http.MethodGet, "/", nil) 36 | w := httptest.NewRecorder() 37 | test.function(t, r, w) 38 | }) 39 | } 40 | } 41 | 42 | func testWrongPredicate(t *testing.T, r *http.Request, w *httptest.ResponseRecorder) { 43 | cfg := Config{ 44 | Name: "example", 45 | Predicate: "this is wrong", 46 | } 47 | mw := NewCBMiddleware(cfg) 48 | 49 | mw(http.HandlerFunc(test.Ping)).ServeHTTP(w, r) 50 | 51 | assert.Equal(t, http.StatusOK, w.Code) 52 | } 53 | 54 | func testSuccessfulUpstreamRetry(t *testing.T, r *http.Request, w *httptest.ResponseRecorder) { 55 | mw := NewCBMiddleware(Config{Name: "example"}) 56 | 57 | mw(http.HandlerFunc(test.Ping)).ServeHTTP(w, r) 58 | 59 | assert.Equal(t, http.StatusOK, w.Code) 60 | } 61 | 62 | func testFailedUpstreamRetry(t *testing.T, r *http.Request, w *httptest.ResponseRecorder) { 63 | mw := NewCBMiddleware(Config{Name: "example"}) 64 | 65 | mw(test.FailWith(http.StatusBadGateway)).ServeHTTP(w, r) 66 | 67 | assert.Equal(t, http.StatusBadGateway, w.Code) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/plugin/retry/middleware_test.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | "time" 8 | 9 | "github.com/hellofresh/janus/pkg/test" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestMiddleware(t *testing.T) { 14 | t.Parallel() 15 | 16 | tests := []struct { 17 | scenario string 18 | function func(*testing.T, *http.Request, *httptest.ResponseRecorder) 19 | }{ 20 | { 21 | scenario: "with wrong predicate given", 22 | function: testWrongPredicate, 23 | }, 24 | { 25 | scenario: "when the upstream respond successfully", 26 | function: testSuccessfulUpstreamRetry, 27 | }, 28 | { 29 | scenario: "when the upstream fails to respond", 30 | function: testFailedUpstreamRetry, 31 | }, 32 | } 33 | 34 | for _, test := range tests { 35 | t.Run(test.scenario, func(t *testing.T) { 36 | r := httptest.NewRequest(http.MethodGet, "/", nil) 37 | w := httptest.NewRecorder() 38 | test.function(t, r, w) 39 | }) 40 | } 41 | } 42 | 43 | func testWrongPredicate(t *testing.T, r *http.Request, w *httptest.ResponseRecorder) { 44 | cfg := Config{ 45 | Predicate: "this is wrong", 46 | } 47 | mw := NewRetryMiddleware(cfg) 48 | 49 | mw(http.HandlerFunc(test.Ping)).ServeHTTP(w, r) 50 | 51 | assert.Equal(t, http.StatusOK, w.Code) 52 | } 53 | 54 | func testSuccessfulUpstreamRetry(t *testing.T, r *http.Request, w *httptest.ResponseRecorder) { 55 | mw := NewRetryMiddleware(Config{}) 56 | 57 | mw(http.HandlerFunc(test.Ping)).ServeHTTP(w, r) 58 | 59 | assert.Equal(t, http.StatusOK, w.Code) 60 | } 61 | 62 | func testFailedUpstreamRetry(t *testing.T, r *http.Request, w *httptest.ResponseRecorder) { 63 | mw := NewRetryMiddleware(Config{Attempts: 2, Backoff: Duration(time.Second)}) 64 | 65 | mw(test.FailWith(http.StatusBadGateway)).ServeHTTP(w, r) 66 | 67 | assert.Equal(t, http.StatusBadGateway, w.Code) 68 | } 69 | -------------------------------------------------------------------------------- /docs/plugins/cors.md: -------------------------------------------------------------------------------- 1 | # CORS 2 | 3 | Easily add Cross-origin resource sharing (CORS) to your API by enabling this plugin. 4 | 5 | ## Configuration 6 | 7 | The plain cors config: 8 | 9 | ```json 10 | "cors": { 11 | "enabled": true, 12 | "config": { 13 | "domains": ["*"], 14 | "methods": ["GET", "POST"], 15 | "request_headers": ["X-Custom-Header", "X-Foobar"], 16 | "exposed_headers": ["X-Something-Special"], 17 | "options_passthrough": true 18 | } 19 | } 20 | ``` 21 | 22 | | Configuration | Description | 23 | |---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 24 | | domains | A comma-separated list of allowed domains for the Access-Control-Allow-Origin header. If you wish to allow all origins, add * as a single value to this configuration field. | 25 | | methods | Value for the Access-Control-Allow-Methods header, expects a comma delimited string (e.g. GET,POST). | 26 | | request_headers | Value for the Access-Control-Allow-Headers header, expects a comma delimited string (e.g. Origin, Authorization). | 27 | | exposed_headers | Value for the Access-Control-Expose-Headers header, expects a comma delimited string (e.g. Origin, Authorization). If not specified, no custom headers are exposed. | 28 | | options_passthrough | Instructs preflight to let other potential next handlers to process the OPTIONS method. | 29 | -------------------------------------------------------------------------------- /docs/proxy/routing_priorities.md: -------------------------------------------------------------------------------- 1 | ### Routing priorities 2 | 3 | An API may define matching rules based on its `hosts`, `listen_path`, and `methods` 4 | fields. For Janus to match an incoming request to an API, all existing fields 5 | must be satisfied. However, Janus allows for quite some flexibility by allowing 6 | two or more APIs to be configured with fields containing the same values - when 7 | this occurs, Janus applies a priority rule. 8 | 9 | The rule is that : **when evaluating a request, Janus will first try 10 | to match the APIs with the most rules**. 11 | 12 | For example, two APIs are configured like this: 13 | 14 | ```json 15 | { 16 | "name": "API 1", 17 | "proxy": { 18 | "listen_path": "/", 19 | "upstreams" : { 20 | "balancing": "roundrobin", 21 | "targets": [ 22 | {"target": "http://my-api.com"} 23 | ] 24 | }, 25 | "hosts": ["example.com"] 26 | } 27 | }, 28 | { 29 | "name": "API 2", 30 | "proxy": { 31 | "listen_path": "/", 32 | "upstreams" : { 33 | "balancing": "roundrobin", 34 | "targets": [ 35 | {"target": "http://my-api.com"} 36 | ] 37 | }, 38 | "hosts": ["example.com"], 39 | "methods": ["POST"] 40 | } 41 | } 42 | ``` 43 | 44 | api-2 has a `hosts` field **and** a `methods` field, so it will be 45 | evaluated first by Janus. By doing so, we avoid api-1 "shadowing" calls 46 | intended for api-2. 47 | 48 | Thus, this request will match api-1: 49 | 50 | ```http 51 | GET / HTTP/1.1 52 | Host: example.com 53 | ``` 54 | 55 | And this request will match api-2: 56 | 57 | ```http 58 | POST / HTTP/1.1 59 | Host: example.com 60 | ``` 61 | 62 | Following this logic, if a third API was to be configured with a `hosts` field, 63 | a `methods` field, and a `listen_path` field, it would be evaluated first by Janus. 64 | -------------------------------------------------------------------------------- /pkg/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/chi" 7 | ) 8 | 9 | // Constructor for a piece of middleware. 10 | // Some middleware use this constructor out of the box, 11 | // so in most cases you can just pass somepackage.New 12 | type Constructor func(http.Handler) http.Handler 13 | 14 | // URLParam returns the url parameter from a http.Request object. 15 | func URLParam(r *http.Request, key string) string { 16 | return chi.URLParam(r, key) 17 | } 18 | 19 | // Router defines the basic methods for a router 20 | type Router interface { 21 | ServeHTTP(w http.ResponseWriter, req *http.Request) 22 | Handle(method string, path string, handler http.HandlerFunc, handlers ...Constructor) 23 | Any(path string, handler http.HandlerFunc, handlers ...Constructor) 24 | GET(path string, handler http.HandlerFunc, handlers ...Constructor) 25 | POST(path string, handler http.HandlerFunc, handlers ...Constructor) 26 | PUT(path string, handler http.HandlerFunc, handlers ...Constructor) 27 | DELETE(path string, handler http.HandlerFunc, handlers ...Constructor) 28 | PATCH(path string, handler http.HandlerFunc, handlers ...Constructor) 29 | HEAD(path string, handler http.HandlerFunc, handlers ...Constructor) 30 | OPTIONS(path string, handler http.HandlerFunc, handlers ...Constructor) 31 | TRACE(path string, handler http.HandlerFunc, handlers ...Constructor) 32 | CONNECT(path string, handler http.HandlerFunc, handlers ...Constructor) 33 | Group(path string) Router 34 | Use(handlers ...Constructor) Router 35 | 36 | RoutesCount() int 37 | } 38 | 39 | // Options are the HTTPTreeMuxRouter options 40 | type Options struct { 41 | NotFoundHandler http.HandlerFunc 42 | SafeAddRoutesWhileRunning bool 43 | } 44 | 45 | // DefaultOptions are the default router options 46 | var DefaultOptions = Options{ 47 | NotFoundHandler: http.NotFound, 48 | SafeAddRoutesWhileRunning: true, 49 | } 50 | -------------------------------------------------------------------------------- /docs/misc/monitoring.md: -------------------------------------------------------------------------------- 1 | # Monitoring 2 | 3 | `Janus` uses [OpenCensus](https://opencensus.io) to collect and export metrics. OpenCensus supports several exporters, which are: 4 | - Datadog 5 | - Prometheus 6 | - Stackdriver 7 | 8 | Currently only Prometheus exporter is available in `Janus`. 9 | 10 | This can be configured via the configuration file or environment variable: 11 | 12 | ```toml 13 | # Stats / Metric Collection 14 | 15 | [stats] 16 | # Backend system used to export collected metrics 17 | # 18 | # Valid Values: "datadog", "prometheus", or "stackdriver" 19 | # 20 | # Default: None 21 | # 22 | Exporter: "prometheus" 23 | ``` 24 | 25 | --- 26 | 27 | ###### The following feature is deprecated and it is planned for removal. 28 | 29 | --- 30 | 31 | `Janus` monitoring is built on top of [`hellofresh/stats-go`](https://github.com/hellofresh/stats-go) library. 32 | You can configure it with the following env variables: 33 | 34 | * `STATS_DSN` (default `log://`) - DSN of stats backend 35 | * `STATS_IDS` - second level ID list for URLs to generalise metric names, see details in [Generalise resources by type and stripping resource ID](https://github.com/hellofresh/stats-go#generalise-resources-by-type-and-stripping-resource-id) 36 | * `STATS_AUTO_DISCOVER_THRESHOLD` - threshold for second level IDs autodiscovery, see details in [Generalise resources by type and stripping resource ID](https://github.com/hellofresh/stats-go#generalise-resources-by-type-and-stripping-resource-id) 37 | * `STATS_AUTO_DISCOVER_WHITE_LIST` - white list for second level IDs autodiscovery, see details in [Generalise resources by type and stripping resource ID](https://github.com/hellofresh/stats-go#generalise-resources-by-type-and-stripping-resource-id) 38 | * `STATS_ERRORS_SECTION` (default `error-log`) - section for error logs monitoring, see details in [Usage for error logs monitoring](https://github.com/hellofresh/stats-go#usage-for-error-logs-monitoring-using-githubcomsirupsenlogrus) 39 | -------------------------------------------------------------------------------- /pkg/api/repository.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/url" 9 | "time" 10 | 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | const ( 15 | mongodb = "mongodb" 16 | cassandra = "cassandra" 17 | file = "file" 18 | ) 19 | 20 | // Repository defines the behavior of a proxy specs repository 21 | type Repository interface { 22 | io.Closer 23 | FindAll() ([]*Definition, error) 24 | } 25 | 26 | // Watcher defines how a provider should watch for changes on configurations 27 | type Watcher interface { 28 | Watch(ctx context.Context, cfgChan chan<- ConfigurationChanged) 29 | } 30 | 31 | // Listener defines how a provider should listen for changes on configurations 32 | type Listener interface { 33 | Listen(ctx context.Context, cfgChan <-chan ConfigurationMessage) 34 | } 35 | 36 | // BuildRepository creates a repository instance that will depend on your given DSN 37 | func BuildRepository(dsn string, refreshTime time.Duration) (Repository, error) { 38 | dsnURL, err := url.Parse(dsn) 39 | if err != nil { 40 | return nil, fmt.Errorf("error parsing the DSN: %w", err) 41 | } 42 | 43 | switch dsnURL.Scheme { 44 | case mongodb: 45 | log.Debug("MongoDB configuration chosen") 46 | return NewMongoAppRepository(dsn, refreshTime) 47 | case cassandra: 48 | log.Debugf("Casssandra configuration chosen: dsn is %s, dsnURL is %s", dsnURL.Path, dsnURL) 49 | return NewCassandraRepository(dsnURL.Path, refreshTime) 50 | case file: 51 | log.Debug("File system based configuration chosen") 52 | apiPath := fmt.Sprintf("%s/apis", dsnURL.Path) 53 | 54 | log.WithField("path", apiPath).Debug("Trying to load API configuration files") 55 | repo, err := NewFileSystemRepository(apiPath) 56 | if err != nil { 57 | return nil, fmt.Errorf("could not create a file system repository: %w", err) 58 | } 59 | return repo, nil 60 | default: 61 | return nil, errors.New("selected scheme is not supported to load API definitions") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pkg/plugin/basic/in_memory_repository.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "github.com/hellofresh/janus/pkg/plugin/basic/encrypt" 5 | log "github.com/sirupsen/logrus" 6 | "sync" 7 | ) 8 | 9 | // InMemoryRepository represents a in memory repository 10 | type InMemoryRepository struct { 11 | sync.RWMutex 12 | users map[string]*User 13 | hash encrypt.Hash 14 | } 15 | 16 | // NewInMemoryRepository creates a in memory repository 17 | func NewInMemoryRepository() *InMemoryRepository { 18 | return &InMemoryRepository{users: make(map[string]*User)} 19 | } 20 | 21 | // FindAll fetches all the users available 22 | func (r *InMemoryRepository) FindAll() ([]*User, error) { 23 | r.RLock() 24 | defer r.RUnlock() 25 | 26 | var users []*User 27 | for _, user := range r.users { 28 | users = append(users, user) 29 | } 30 | 31 | return users, nil 32 | } 33 | 34 | // FindByUsername find an user by username 35 | func (r *InMemoryRepository) FindByUsername(username string) (*User, error) { 36 | r.RLock() 37 | defer r.RUnlock() 38 | return r.findByUsername(username) 39 | } 40 | 41 | // Add adds an user to the repository 42 | func (r *InMemoryRepository) Add(user *User) error { 43 | r.Lock() 44 | defer r.Unlock() 45 | 46 | hash, err := r.hash.Generate(user.Password) 47 | if err != nil { 48 | log.Errorf("error hashing password: %v", err) 49 | return err 50 | } 51 | user.Password = hash 52 | 53 | r.users[user.Username] = user 54 | 55 | return nil 56 | } 57 | 58 | // Remove removes an user from the repository 59 | func (r *InMemoryRepository) Remove(username string) error { 60 | r.Lock() 61 | defer r.Unlock() 62 | 63 | if _, err := r.findByUsername(username); err == ErrUserNotFound { 64 | return err 65 | } 66 | 67 | delete(r.users, username) 68 | 69 | return nil 70 | } 71 | 72 | func (r *InMemoryRepository) findByUsername(username string) (*User, error) { 73 | user, ok := r.users[username] 74 | if false == ok { 75 | return nil, ErrUserNotFound 76 | } 77 | 78 | return user, nil 79 | } 80 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | build_test: 3 | #docker: 4 | # - image: docker/compose:1.25.3 5 | 6 | machine: 7 | image: ubuntu-2004:202010-01 8 | 9 | steps: 10 | - checkout 11 | 12 | # - run: 13 | # name: Running Unit Tests 14 | # command: | 15 | # echo "Running unit tests and building binary" 16 | # go mod download 17 | # make all 18 | - run: 19 | name: Build and Push Images 20 | command: | 21 | echo "Logging in to Docker Hub" 22 | docker login --username ${DOCKERHUB_USERNAME} --password ${DOCKERHUB_PASSWORD} 23 | sudo docker login --username ${DOCKERHUB_USERNAME} --password ${DOCKERHUB_PASSWORD} 24 | 25 | echo "Setting Image Name" 26 | IMAGE_NAME="motivlabs/janus:$(date +%Y%m%d%H%M%S)-${CIRCLE_SHA1:0:6}" 27 | echo "Image Name" 28 | echo $IMAGE_NAME 29 | 30 | echo "Building Service Image" 31 | docker build -f ./Dockerfile --target=prod -t ${IMAGE_NAME} -t motivlabs/janus:latest . 32 | echo "Finished Building Service Image" 33 | 34 | echo "Pushing Service Images to Docker Hub" 35 | docker push $IMAGE_NAME 36 | echo "Pushed Extended Image Name" 37 | docker push motivlabs/janus:latest 38 | echo "Pushed Latest Image Name" 39 | 40 | COMMIT_MESSAGE=$(git log --format=oneline -n 1 $CIRCLE_SHA1) 41 | echo "got commit message: ${COMMIT_MESSAGE}" 42 | 43 | echo "Running Script to Update Impulse Docker-Compose with Correct Image" 44 | .circleci/update-impulse.sh "janus" ${IMAGE_NAME} ${GITHUB_OAUTH} "${COMMIT_MESSAGE}" 45 | echo "Impulse Updated" 46 | 47 | workflows: 48 | version: 2 49 | build: 50 | jobs: 51 | - build_test: 52 | context: MotivLabs 53 | filters: 54 | branches: 55 | only: 56 | - master 57 | -------------------------------------------------------------------------------- /pkg/proxy/reverse_proxy_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func newTestRequest() *http.Request { 10 | return &http.Request{ 11 | Method: "", 12 | URL: nil, 13 | Proto: "", 14 | ProtoMajor: 0, 15 | ProtoMinor: 0, 16 | Header: nil, 17 | Body: nil, 18 | GetBody: nil, 19 | ContentLength: 0, 20 | TransferEncoding: nil, 21 | Close: false, 22 | Host: "", 23 | Form: nil, 24 | PostForm: nil, 25 | MultipartForm: nil, 26 | Trailer: nil, 27 | RemoteAddr: "", 28 | RequestURI: "", 29 | TLS: nil, 30 | Cancel: nil, 31 | Response: nil, 32 | } 33 | } 34 | func TestStripPathWithParams(t *testing.T) { 35 | t.Run("properly strips path - params and listenPath", func(t *testing.T) { 36 | req := newTestRequest() 37 | path := "/prepath/my-service/endpoint" 38 | listenPath := "/prepath/{service}/*" 39 | paramNames := []string{"service"} 40 | 41 | old := chiURLParam 42 | defer func() {chiURLParam = old}() 43 | 44 | chiURLParam = func(r *http.Request, key string) string { 45 | return "my-service" 46 | } 47 | returnPath := stripPathWithParams(req, path, listenPath, paramNames) 48 | 49 | assert.Equal(t, "/endpoint", returnPath) 50 | }) 51 | 52 | t.Run("check that strip logic is correct if value is not in path", func(t *testing.T) { 53 | req := newTestRequest() 54 | path := "/prepath/my-service/endpoint" 55 | listenPath := "/prepath/{service}/*" 56 | paramNames := []string{"service"} 57 | 58 | old := chiURLParam 59 | defer func() {chiURLParam = old}() 60 | 61 | chiURLParam = func(r *http.Request, key string) string { 62 | return "other-value" 63 | } 64 | returnPath := stripPathWithParams(req, path, listenPath, paramNames) 65 | 66 | assert.Equal(t, "/my-service/endpoint", returnPath) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/plugin/basic/middleware_test.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "encoding/base64" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/hellofresh/janus/pkg/test" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestAuthorizedAccess(t *testing.T) { 13 | mw := NewBasicAuth(setupRepo()) 14 | 15 | w, err := test.Record( 16 | "GET", 17 | "/", 18 | map[string]string{ 19 | "Content-Type": "application/json", 20 | "Authorization": "Basic " + basicAuth("test", "test"), 21 | }, 22 | mw(http.HandlerFunc(test.Ping)), 23 | ) 24 | assert.NoError(t, err) 25 | 26 | assert.Equal(t, http.StatusOK, w.Code) 27 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 28 | } 29 | 30 | func TestInvalidBasicHeader(t *testing.T) { 31 | mw := NewBasicAuth(setupRepo()) 32 | 33 | w, err := test.Record( 34 | "GET", 35 | "/", 36 | map[string]string{ 37 | "Content-Type": "application/json", 38 | "Authorization": "Basic wrong", 39 | }, 40 | mw(http.HandlerFunc(test.Ping)), 41 | ) 42 | assert.NoError(t, err) 43 | 44 | assert.Equal(t, http.StatusUnauthorized, w.Code) 45 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 46 | } 47 | 48 | func TestUnauthorizedAccess(t *testing.T) { 49 | mw := NewBasicAuth(setupRepo()) 50 | 51 | w, err := test.Record( 52 | "GET", 53 | "/", 54 | map[string]string{ 55 | "Content-Type": "application/json", 56 | "Authorization": "Basic " + basicAuth("wrong", "wrong"), 57 | }, 58 | mw(http.HandlerFunc(test.Ping)), 59 | ) 60 | assert.NoError(t, err) 61 | 62 | assert.Equal(t, http.StatusUnauthorized, w.Code) 63 | assert.Equal(t, "application/json", w.Header().Get("Content-Type")) 64 | } 65 | 66 | func basicAuth(username, password string) string { 67 | auth := username + ":" + password 68 | return base64.StdEncoding.EncodeToString([]byte(auth)) 69 | } 70 | 71 | func setupRepo() Repository { 72 | repo := NewInMemoryRepository() 73 | repo.Add(&User{Username: "test", Password: "test"}) 74 | 75 | return repo 76 | } 77 | -------------------------------------------------------------------------------- /pkg/plugin/retry/setup.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/asaskevich/govalidator" 9 | 10 | "github.com/hellofresh/janus/pkg/plugin" 11 | "github.com/hellofresh/janus/pkg/proxy" 12 | ) 13 | 14 | const ( 15 | strNull = "null" 16 | ) 17 | 18 | type ( 19 | // Config represents the Body Limit configuration 20 | Config struct { 21 | Attempts int `json:"attempts"` 22 | Backoff Duration `json:"backoff"` 23 | Predicate string `json:"predicate"` 24 | } 25 | 26 | // Duration is a wrapper for time.Duration so we can use human readable configs 27 | Duration time.Duration 28 | ) 29 | 30 | // MarshalJSON is the implementation of the MarshalJSON interface 31 | func (d *Duration) MarshalJSON() ([]byte, error) { 32 | s := (*time.Duration)(d).String() 33 | s = strconv.Quote(s) 34 | 35 | return []byte(s), nil 36 | } 37 | 38 | // UnmarshalJSON is the implementation of the UnmarshalJSON interface 39 | func (d *Duration) UnmarshalJSON(data []byte) error { 40 | s := string(data) 41 | if s == strNull { 42 | return errors.New("invalid time duration") 43 | } 44 | 45 | s, err := strconv.Unquote(s) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | t, err := time.ParseDuration(s) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | *d = Duration(t) 56 | return nil 57 | } 58 | 59 | func init() { 60 | plugin.RegisterPlugin("retry", plugin.Plugin{ 61 | Action: setupRetry, 62 | Validate: validateConfig, 63 | }) 64 | } 65 | 66 | func setupRetry(def *proxy.RouterDefinition, rawConfig plugin.Config) error { 67 | var config Config 68 | err := plugin.Decode(rawConfig, &config) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | def.AddMiddleware(NewRetryMiddleware(config)) 74 | return nil 75 | } 76 | 77 | func validateConfig(rawConfig plugin.Config) (bool, error) { 78 | var config Config 79 | err := plugin.Decode(rawConfig, &config) 80 | if err != nil { 81 | return false, err 82 | } 83 | 84 | return govalidator.ValidateStruct(config) 85 | } 86 | -------------------------------------------------------------------------------- /docs/proxy/preserve_host_property.md: -------------------------------------------------------------------------------- 1 | #### The `preserve_host` property 2 | 3 | When proxying, Janus's default behavior is to set the upstream request's Host header to the hostname of the API's elected upstream from the`upstreams.targets` property. The `preserve_host` field accepts a boolean flag instructing Janus not to do so. 4 | 5 | For example, when the `preserve_host` property is not changed and an API is configured like this: 6 | 7 | ```json 8 | { 9 | "name": "My API", 10 | "hosts": ["service.com"], 11 | "proxy": { 12 | "listen_path": "/foo/*", 13 | "upstreams" : { 14 | "balancing": "roundrobin", 15 | "targets": [ 16 | {"target": "http://my-api.com"} 17 | ] 18 | }, 19 | "methods": ["GET"] 20 | } 21 | } 22 | ``` 23 | 24 | A possible request from a client to Janus could be: 25 | 26 | ```http 27 | GET / HTTP/1.1 28 | Host: service.com 29 | ``` 30 | 31 | Janus would extract the Host header value from the the hostname of the API's elected upstream from the `upstreams.target` field, and would send the following request to your upstream service: 32 | 33 | ```http 34 | GET / HTTP/1.1 35 | Host: my-api.com 36 | ``` 37 | 38 | However, by explicitly configuring your API with `preserve_host=true`: 39 | 40 | ```json 41 | { 42 | "name": "My API", 43 | "hosts": ["example.com", "service.com"], 44 | "proxy": { 45 | "listen_path": "/foo/*", 46 | "upstreams" : { 47 | "balancing": "roundrobin", 48 | "targets": [ 49 | {"target": "http://my-api.com"} 50 | ] 51 | }, 52 | "methods": ["GET"], 53 | "preserve_host": true 54 | } 55 | } 56 | ``` 57 | 58 | And assuming the same request from the client: 59 | 60 | ```http 61 | GET / HTTP/1.1 62 | Host: service.com 63 | ``` 64 | 65 | Janus would preserve the Host on the client request and would send the following request to your upstream service: 66 | 67 | ```http 68 | GET / HTTP/1.1 69 | Host: service.com 70 | ``` 71 | -------------------------------------------------------------------------------- /pkg/middleware/host_matcher.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/hellofresh/janus/pkg/errors" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // HostMatcher is a middleware that matches any host with the given list of hosts. 14 | // It also supports regex host like *.example.com 15 | type HostMatcher struct { 16 | plainHosts map[string]bool 17 | wildcardHosts []*regexp.Regexp 18 | } 19 | 20 | // NewHostMatcher creates a new instance of HostMatcher 21 | func NewHostMatcher(hosts []string) *HostMatcher { 22 | matcher := &HostMatcher{plainHosts: make(map[string]bool)} 23 | matcher.prepareIndexes(hosts) 24 | return matcher 25 | } 26 | 27 | // Handler is the middleware function 28 | func (h *HostMatcher) Handler(handler http.Handler) http.Handler { 29 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 30 | log.WithField("path", r.URL.Path).Debug("Starting host matcher middleware") 31 | host := r.Host 32 | 33 | if _, ok := h.plainHosts[host]; ok { 34 | log.WithField("host", host).Debug("Plain host matched") 35 | handler.ServeHTTP(w, r) 36 | return 37 | } 38 | 39 | for _, hostRegex := range h.wildcardHosts { 40 | if hostRegex.MatchString(host) { 41 | log.WithField("host", host).Debug("Wildcard host matched") 42 | handler.ServeHTTP(w, r) 43 | return 44 | } 45 | } 46 | 47 | err := errors.ErrRouteNotFound 48 | log.WithError(err).Error("The host didn't match any of the provided hosts") 49 | errors.Handler(w, r, err) 50 | }) 51 | } 52 | 53 | func (h *HostMatcher) prepareIndexes(hosts []string) { 54 | if len(hosts) > 0 { 55 | for _, host := range hosts { 56 | if strings.Contains(host, "*") { 57 | regexStr := strings.Replace(host, ".", "\\.", -1) 58 | regexStr = strings.Replace(regexStr, "*", ".+", -1) 59 | h.wildcardHosts = append(h.wildcardHosts, regexp.MustCompile(fmt.Sprintf("^%s$", regexStr))) 60 | } else { 61 | h.plainHosts[host] = true 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/plugin/cb/middleware.go: -------------------------------------------------------------------------------- 1 | package cb 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/Knetic/govaluate" 9 | "github.com/afex/hystrix-go/hystrix" 10 | "github.com/felixge/httpsnoop" 11 | log "github.com/sirupsen/logrus" 12 | 13 | janusErr "github.com/hellofresh/janus/pkg/errors" 14 | ) 15 | 16 | const ( 17 | defaultPredicate = "statusCode == 0 || statusCode >= 500" 18 | ) 19 | 20 | // NewCBMiddleware creates a new cb middleware 21 | func NewCBMiddleware(cfg Config) func(http.Handler) http.Handler { 22 | return func(handler http.Handler) http.Handler { 23 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 24 | logger := log.WithFields(log.Fields{ 25 | "name": cfg.Name, 26 | "timeout": cfg.Timeout, 27 | "max_concurrent_requests": cfg.MaxConcurrentRequests, 28 | "error_percent_threshold": cfg.ErrorPercentThreshold, 29 | }) 30 | 31 | logger.Debug("Starting cb middleware") 32 | if cfg.Predicate == "" { 33 | cfg.Predicate = defaultPredicate 34 | } 35 | 36 | expression, err := govaluate.NewEvaluableExpression(cfg.Predicate) 37 | if err != nil { 38 | log.WithError(err).Error("could not create an expression with this predicate") 39 | handler.ServeHTTP(w, r) 40 | return 41 | } 42 | 43 | err = hystrix.Do(cfg.Name, func() error { 44 | m := httpsnoop.CaptureMetrics(handler, w, r) 45 | params := make(map[string]interface{}, 8) 46 | params["statusCode"] = m.Code 47 | params["request"] = r 48 | 49 | result, err := expression.Evaluate(params) 50 | if err != nil { 51 | return errors.New("cannot evaluate the expression") 52 | } 53 | 54 | if result.(bool) { 55 | return fmt.Errorf("%s %s request failed", r.Method, r.URL) 56 | } 57 | 58 | return nil 59 | }, nil) 60 | 61 | if err != nil { 62 | logger.WithError(err).Error("Request failed on the cb middleware") 63 | janusErr.Handler(w, r, fmt.Errorf("request failed: %w", err)) 64 | } 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/plugin/oauth2/in_memory_repository.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // InMemoryRepository represents a in memory repository 8 | type InMemoryRepository struct { 9 | sync.RWMutex 10 | servers map[string]*OAuth 11 | } 12 | 13 | // NewInMemoryRepository creates a in memory repository 14 | func NewInMemoryRepository() *InMemoryRepository { 15 | return &InMemoryRepository{servers: make(map[string]*OAuth)} 16 | } 17 | 18 | // FindAll fetches all the OAuth Servers available 19 | func (r *InMemoryRepository) FindAll() ([]*OAuth, error) { 20 | r.RLock() 21 | defer r.RUnlock() 22 | 23 | var servers []*OAuth 24 | for _, server := range r.servers { 25 | servers = append(servers, server) 26 | } 27 | 28 | return servers, nil 29 | } 30 | 31 | // FindByName find an OAuth Server by name 32 | func (r *InMemoryRepository) FindByName(name string) (*OAuth, error) { 33 | r.RLock() 34 | defer r.RUnlock() 35 | 36 | return r.findByName(name) 37 | } 38 | 39 | // Add add a new OAuth Server to the repository 40 | func (r *InMemoryRepository) Add(server *OAuth) error { 41 | r.Lock() 42 | defer r.Unlock() 43 | 44 | if _, ok := r.servers[server.Name]; ok { 45 | return ErrOauthServerNameExists 46 | } 47 | 48 | r.servers[server.Name] = server 49 | 50 | return nil 51 | } 52 | 53 | // Save saves a OAuth Server to the repository 54 | func (r *InMemoryRepository) Save(server *OAuth) error { 55 | r.Lock() 56 | defer r.Unlock() 57 | 58 | r.servers[server.Name] = server 59 | 60 | return nil 61 | } 62 | 63 | // Remove removes an OAuth Server from the repository 64 | func (r *InMemoryRepository) Remove(name string) error { 65 | r.Lock() 66 | defer r.Unlock() 67 | 68 | if _, err := r.findByName(name); err != nil { 69 | return err 70 | } 71 | 72 | delete(r.servers, name) 73 | 74 | return nil 75 | } 76 | 77 | func (r *InMemoryRepository) findByName(name string) (*OAuth, error) { 78 | server, ok := r.servers[name] 79 | if false == ok { 80 | return nil, ErrOauthServerNotFound 81 | } 82 | 83 | return server, nil 84 | } 85 | -------------------------------------------------------------------------------- /pkg/proxy/register_options.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/hellofresh/janus/pkg/router" 7 | "github.com/hellofresh/stats-go/client" 8 | ) 9 | 10 | // RegisterOption represents the register options 11 | type RegisterOption func(*Register) 12 | 13 | // WithRouter sets the router 14 | func WithRouter(router router.Router) RegisterOption { 15 | return func(r *Register) { 16 | r.router = router 17 | } 18 | } 19 | 20 | // WithFlushInterval sets the Flush interval for copying upgraded connections 21 | func WithFlushInterval(d time.Duration) RegisterOption { 22 | return func(r *Register) { 23 | r.flushInterval = d 24 | } 25 | } 26 | 27 | // WithIdleConnectionsPerHost sets idle connections per host option 28 | func WithIdleConnectionsPerHost(value int) RegisterOption { 29 | return func(r *Register) { 30 | r.idleConnectionsPerHost = value 31 | } 32 | } 33 | 34 | // WithStatsClient sets stats client instance for proxy 35 | func WithStatsClient(statsClient client.Client) RegisterOption { 36 | return func(r *Register) { 37 | r.statsClient = statsClient 38 | } 39 | } 40 | 41 | // WithIdleConnTimeout sets the maximum amount of time an idle 42 | // (keep-alive) connection will remain idle before closing 43 | // itself. 44 | func WithIdleConnTimeout(d time.Duration) RegisterOption { 45 | return func(r *Register) { 46 | r.idleConnTimeout = d 47 | } 48 | } 49 | 50 | // WithIdleConnPurgeTicker purges idle connections on every interval if set 51 | // this is done to prevent permanent keep-alive on connections with high ops 52 | func WithIdleConnPurgeTicker(d time.Duration) RegisterOption { 53 | var ticker *time.Ticker 54 | 55 | if d != 0 { 56 | ticker = time.NewTicker(d) 57 | } 58 | 59 | return func(t *Register) { 60 | t.idleConnPurgeTicker = ticker 61 | } 62 | } 63 | 64 | // WithIsPublicEndpoint adds trace metadata from incoming requests 65 | // as parent span if set to false 66 | func WithIsPublicEndpoint(b bool) RegisterOption { 67 | return func(r *Register) { 68 | r.isPublicEndpoint = b 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /docs/misc/tracing.md: -------------------------------------------------------------------------------- 1 | # Distributed Tracing 2 | 3 | `Janus` uses [OpenCensus](https://opencensus.io) as the standard way to trace requests. It can be used for monitoring microservices-based distributed systems: 4 | 5 | - Distributed context propagation 6 | - Distributed transaction monitoring 7 | - Root cause analysis 8 | - Service dependency analysis 9 | - Performance / latency optimization 10 | 11 | OpenCensus supports several tracing backend systems (i.e. [exporters](https://opencensus.io/exporters/supported-exporters/go/)) which are: 12 | - Azure Monitor 13 | - Honeycomb.io 14 | - AWS X-Ray 15 | - Datadog 16 | - Jaeger 17 | - Stackdriver 18 | - Zipkin 19 | 20 | Currently, only Jaeger exporter is available in `Janus`. 21 | 22 | ```toml 23 | # Tracing Configuration 24 | 25 | [tracing] 26 | # Backend system to export traces to 27 | # 28 | # Default: None 29 | # 30 | Exporter = "jaeger" 31 | 32 | # Service name used in the backend 33 | # 34 | # Default: "janus" 35 | # 36 | ServiceName = "janus" 37 | 38 | # If set to false, trace metadata set in incoming requests will be 39 | # added as the parent span of the trace. 40 | # 41 | # See the following link for more details: 42 | # https://godoc.org/go.opencensus.io/plugin/ochttp#Handler 43 | # 44 | # Default: true 45 | # 46 | IsPublicEndpoint = true 47 | 48 | # SamplingStrategy specifies the sampling strategy 49 | # 50 | # Valid Values: "probabilistic", "always", "never" 51 | # 52 | # Default: "probabilistic" 53 | # 54 | SamplingStrategy = "probabilistic" 55 | 56 | # SamplingParam is an additional value passed to the sampler. 57 | # 58 | # Valid Values: 59 | # - for "always" and "never" sampler, this value is unused 60 | # - for "probabilistic" sampler, a probability between 0 and 1 61 | # 62 | # Default: "0.15" 63 | # 64 | SamplingParam = "0.15" 65 | 66 | [tracing.jaeger] 67 | # SamplingServerURL is the address to the sampling server 68 | # 69 | # Default: None 70 | # 71 | SamplingServerURL: "localhost:6832" 72 | ``` 73 | -------------------------------------------------------------------------------- /pkg/plugin/rate/rate_limit_logger.go: -------------------------------------------------------------------------------- 1 | package rate 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | 8 | "github.com/felixge/httpsnoop" 9 | "github.com/hellofresh/stats-go/bucket" 10 | "github.com/hellofresh/stats-go/client" 11 | log "github.com/sirupsen/logrus" 12 | "github.com/ulule/limiter/v3" 13 | ) 14 | 15 | const ( 16 | limiterSection = "limiter" 17 | limiterMetric = "state" 18 | ) 19 | 20 | // NewRateLimitLogger logs the IP of blocked users with rate limit 21 | func NewRateLimitLogger(lmt *limiter.Limiter, statsClient client.Client, trustForwardHeaders bool) func(handler http.Handler) http.Handler { 22 | return func(handler http.Handler) http.Handler { 23 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 24 | log.Debug("Starting RateLimitLogger.WriterWrapper middleware") 25 | 26 | m := httpsnoop.CaptureMetrics(handler, w, r) 27 | 28 | limiterIP := limiter.GetIP(r, limiter.Options{TrustForwardHeader: trustForwardHeaders}) 29 | if m.Code == http.StatusTooManyRequests { 30 | log.WithFields(log.Fields{ 31 | "ip_address": limiterIP.String(), 32 | "request_uri": r.RequestURI, 33 | }).Warning("Rate Limit exceeded for this IP") 34 | } 35 | 36 | trackLimitState(lmt, statsClient, limiterIP, r) 37 | }) 38 | } 39 | } 40 | 41 | func trackLimitState(lmt *limiter.Limiter, statsClient client.Client, limiterIP net.IP, r *http.Request) { 42 | ctx, err := lmt.Peek(context.Background(), limiterIP.String()) 43 | if err != nil { 44 | log.WithError(err).WithFields(log.Fields{ 45 | "ip_address": limiterIP.String(), 46 | "request_uri": r.RequestURI, 47 | }).Error("Failed to get limiter ctx from request") 48 | return 49 | } 50 | 51 | requestsPerformed := ctx.Limit - ctx.Remaining 52 | limitState := requestsPerformed * 100 / ctx.Limit 53 | 54 | operation := bucket.BuildHTTPRequestMetricOperation(r, statsClient.GetHTTPMetricCallback()) 55 | // replace request method with fixed section name 56 | operation[0] = limiterMetric 57 | 58 | statsClient.TrackState(limiterSection, operation, int(limitState)) 59 | } 60 | -------------------------------------------------------------------------------- /janus/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for janus. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | nameOverride: "" 6 | fullnameOverride: "" 7 | 8 | deployment: 9 | replicaCount: 2 10 | minAvailable: 1 11 | databaseDSN: "mongodb://janus-database:27017/janus" 12 | labels: 13 | app: janus 14 | valuesFrom: 15 | - name: POD_NAME 16 | valueFrom: 17 | fieldRef: 18 | fieldPath: metadata.name 19 | # podAnnotations: 20 | # annotation-key: annotation-value 21 | 22 | imagePullSecrets: [] 23 | 24 | image: 25 | repository: hellofreshtech/janus 26 | tag: latest 27 | pullPolicy: Always 28 | 29 | service: 30 | name: ops-gateway 31 | type: ClusterIP 32 | ports: 33 | - protocol: TCP 34 | port: 80 35 | targetPort: 8080 36 | name: http 37 | - protocol: TCP 38 | port: 443 39 | targetPort: 8080 40 | name: https 41 | - protocol: TCP 42 | port: 8081 43 | targetPort: 8081 44 | name: http-private 45 | 46 | ingress: 47 | enabled: false 48 | name: ops-gateway 49 | annotations: 50 | nginx.ingress.kubernetes.io/force-ssl-redirect: "true" 51 | hosts: 52 | - host: gateway.sample.com 53 | paths: 54 | - path: / 55 | port: 80 56 | - host: admin-gateway.sample.com 57 | paths: 58 | - path: / 59 | port: 8081 60 | 61 | tls: [] 62 | # - secretName: chart-example-tls 63 | # hosts: 64 | # - chart-example.local 65 | 66 | resources: {} 67 | # We usually recommend not to specify default resources and to leave this as a conscious 68 | # choice for the user. This also increases chances charts run on environments with little 69 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 70 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 71 | # limits: 72 | # cpu: 100m 73 | # memory: 128Mi 74 | # requests: 75 | # cpu: 100m 76 | # memory: 128Mi 77 | 78 | nodeSelector: {} 79 | 80 | tolerations: [] 81 | 82 | affinity: {} 83 | -------------------------------------------------------------------------------- /docs/quick_start/file_system.md: -------------------------------------------------------------------------------- 1 | # Adding your API - File System 2 | 3 | By choosing a File System based configuration we have a static way of configure Janus (similar to nginx). 4 | 5 | ## 1. Boot it up 6 | 7 | We highly recommend you to use one of our examples to start. Let's see the [front-proxy](/examples/front-proxy) example: 8 | 9 | Make sure you have docker up and running on your platform and then run. 10 | 11 | ```sh 12 | docker-compose up -d 13 | ``` 14 | 15 | This will spin up a janus server and will have a small proxy configuration that is going to a mock server that we spun up. 16 | 17 | ## 2. Verify that Janus is working 18 | 19 | Issue the following cURL request to verify that Janus is properly forwarding 20 | requests to your API. Note that [by default][proxy-port] Janus handles proxy 21 | requests on port `:8080`: 22 | 23 | If you access `http://localhost:8080/example` you should something like: 24 | 25 | ```json 26 | { 27 | "message": "Hello World!" 28 | } 29 | ``` 30 | 31 | A successful response means Janus is now forwarding requests made to 32 | `http://localhost:8080` to the elected upstream target (chosen by the load balancer) we configured in step #1, 33 | and is forwarding the response back to us. 34 | 35 | ## Understanding the directory structure 36 | 37 | By default all apis configurations are splitted in separated files (both single and multiple api definitions per file are supported) and they are stored in `/etc/janus`. You can change that path by simply defining the configuration `database.dsn`, for instance, you can define the value to `file:///usr/local/janus`. 38 | 39 | There are two required folder that needs to be there: 40 | 41 | - `/etc/janus/apis` - Holds all API definitions 42 | - `/etc/janus/auth` - Holds all your Auth servers configurations 43 | 44 | ## 4. Adding a new endpoint and authentication 45 | 46 | To add a new endpoint or authentication you can see the [Add Endpoint tutorial](add_endpoint.md) but instead of using the admin API you'll add your configuration to a file and reload the docker instance: 47 | 48 | ```sh 49 | docker-compose reload janus 50 | ``` 51 | -------------------------------------------------------------------------------- /pkg/plugin/retry/middleware.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/Knetic/govaluate" 10 | "github.com/felixge/httpsnoop" 11 | "github.com/rafaeljesus/retry-go" 12 | log "github.com/sirupsen/logrus" 13 | 14 | janusErr "github.com/hellofresh/janus/pkg/errors" 15 | "github.com/hellofresh/janus/pkg/metrics" 16 | ) 17 | 18 | const ( 19 | defaultPredicate = "statusCode == 0 || statusCode >= 500" 20 | proxySection = "proxy" 21 | ) 22 | 23 | // NewRetryMiddleware creates a new retry middleware 24 | func NewRetryMiddleware(cfg Config) func(http.Handler) http.Handler { 25 | return func(handler http.Handler) http.Handler { 26 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | log.WithFields(log.Fields{ 28 | "attempts": cfg.Attempts, 29 | "backoff": cfg.Backoff, 30 | }).Debug("Starting retry middleware") 31 | 32 | if cfg.Predicate == "" { 33 | cfg.Predicate = defaultPredicate 34 | } 35 | 36 | expression, err := govaluate.NewEvaluableExpression(cfg.Predicate) 37 | if err != nil { 38 | log.WithError(err).Error("could not create an expression with this predicate") 39 | handler.ServeHTTP(w, r) 40 | return 41 | } 42 | 43 | if err := retry.Do(func() error { 44 | m := httpsnoop.CaptureMetrics(handler, w, r) 45 | 46 | params := make(map[string]interface{}, 8) 47 | params["statusCode"] = m.Code 48 | params["request"] = r 49 | 50 | result, err := expression.Evaluate(params) 51 | if err != nil { 52 | return errors.New("cannot evaluate the expression") 53 | } 54 | 55 | if result.(bool) { 56 | return fmt.Errorf("%s %s request failed", r.Method, r.URL) 57 | } 58 | 59 | return nil 60 | }, cfg.Attempts, time.Duration(cfg.Backoff)); err != nil { 61 | statsClient := metrics.WithContext(r.Context()) 62 | statsClient.SetHTTPRequestSection(proxySection).TrackRequest(r, nil, false).ResetHTTPRequestSection() 63 | janusErr.Handler(w, r, fmt.Errorf("request failed too many times: %w", err)) 64 | } 65 | }) 66 | } 67 | } 68 | --------------------------------------------------------------------------------