├── .build ├── cargo-build-and-test.yml ├── install-rust.yml └── publish-crate.yml ├── .dockerignore ├── .gitignore ├── .rustfmt.toml ├── .vscode └── tasks.json ├── CONFIG.md ├── CONFIG.tpl ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── Makefile.toml ├── README.md ├── README.tpl ├── assets ├── benchmark │ ├── bencher.dockerfile │ ├── benchmark.yml │ └── nginx.conf ├── manual-test.yml ├── misc │ └── Katalyst.postman_collection.json ├── test.crt └── test.rsa ├── azure-pipelines.yml ├── katalyst ├── Cargo.toml ├── Makefile.toml ├── README.md ├── examples │ └── basic.rs └── src │ ├── app.rs │ ├── cli │ └── mod.rs │ ├── config │ ├── builder │ │ ├── mod.rs │ │ ├── module.rs │ │ ├── path.rs │ │ ├── routes.rs │ │ └── service.rs │ ├── mod.rs │ └── parsers │ │ └── mod.rs │ ├── context │ ├── auth.rs │ ├── data.rs │ ├── matched.rs │ ├── mod.rs │ └── requests.rs │ ├── error │ ├── from.rs │ ├── mod.rs │ └── result.rs │ ├── expression │ ├── bindings │ │ ├── auth.rs │ │ ├── content.rs │ │ ├── encoding.rs │ │ ├── http.rs │ │ ├── mod.rs │ │ ├── sys.rs │ │ └── url.rs │ ├── compiler │ │ ├── compiled.rs │ │ ├── mod.rs │ │ └── nodes.rs │ ├── expr.pest │ ├── mod.rs │ └── traits │ │ └── mod.rs │ ├── instance │ ├── hosts.rs │ ├── mod.rs │ ├── route.rs │ └── service.rs │ ├── lib.rs │ ├── main.rs │ ├── modules │ ├── authentication │ │ ├── always.rs │ │ ├── http.rs │ │ ├── mod.rs │ │ ├── never.rs │ │ └── whitelist.rs │ ├── authorization │ │ └── mod.rs │ ├── balancer │ │ ├── least_connection.rs │ │ ├── mod.rs │ │ ├── random.rs │ │ └── round_robin.rs │ ├── cache │ │ ├── cache_handler.rs │ │ ├── memory.rs │ │ └── mod.rs │ ├── def.rs │ ├── handlers │ │ ├── files │ │ │ └── mod.rs │ │ ├── host │ │ │ ├── dispatcher.rs │ │ │ ├── mod.rs │ │ │ ├── transformers.rs │ │ │ └── util.rs │ │ └── mod.rs │ ├── mod.rs │ ├── plugins │ │ ├── content_plugin.rs │ │ └── mod.rs │ ├── registry.rs │ └── result.rs │ ├── parser │ └── mod.rs │ ├── prelude.rs │ ├── server │ ├── mod.rs │ └── pipeline │ │ ├── auth.rs │ │ ├── cache.rs │ │ ├── dispatcher.rs │ │ ├── logger.rs │ │ ├── mapper.rs │ │ ├── matcher.rs │ │ └── mod.rs │ └── util │ ├── client_requests.rs │ ├── locked_resource.rs │ └── mod.rs └── katalyst_macros ├── Cargo.toml └── src ├── attr.rs ├── binding_derive.rs └── lib.rs /.build/cargo-build-and-test.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - script: | 3 | cargo build --all 4 | cargo test --all 5 | displayName: Run cargo build and tests 6 | -------------------------------------------------------------------------------- /.build/install-rust.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | rustup_toolchain: "" 3 | 4 | steps: 5 | - bash: | 6 | TOOLCHAIN="${{parameters['rustup_toolchain']}}" 7 | TOOLCHAIN="${TOOLCHAIN:-$RUSTUP_TOOLCHAIN}" 8 | TOOLCHAIN="${TOOLCHAIN:-stable}" 9 | echo "##vso[task.setvariable variable=TOOLCHAIN;]$TOOLCHAIN" 10 | displayName: Set rust toolchain 11 | - script: | 12 | curl -sSf -o rustup-init.exe https://win.rustup.rs 13 | rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN% 14 | echo "##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin" 15 | displayName: Windows install rust 16 | condition: eq( variables['Agent.OS'], 'Windows_NT' ) 17 | - script: | 18 | curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUSTUP_TOOLCHAIN 19 | echo "##vso[task.setvariable variable=PATH;]$PATH:$HOME/.cargo/bin" 20 | displayName: Install rust 21 | condition: ne( variables['Agent.OS'], 'Windows_NT' ) 22 | -------------------------------------------------------------------------------- /.build/publish-crate.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - script: | 3 | sed -i 'Cargo.toml' -e 's/version = "0.1.3"/version = "$(build_name)"/' 4 | cargo publish --token $(cargo.login) 5 | displayName: Publish Crate 6 | condition: and(eq( variables['is_tag'], 'True' ), eq( variables['rustup_toolchain'], 'stable' )) 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | 4 | # When using VSCode, we want to keep most user settings hidden but allow the tasks.json to be shared 5 | /.vscode/* 6 | !/.vscode/tasks.json 7 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | use_small_heuristics = "Max" 3 | use_field_init_shorthand = true 4 | use_try_shorthand = true 5 | newline_style = "Unix" 6 | #merge_imports = true -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "problemMatcher": [ 4 | "$rustc" 5 | ], 6 | "tasks": [ 7 | { 8 | "label": "Run Katalyst", 9 | "type": "shell", 10 | "command": "cargo make run", 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | } 15 | }, 16 | { 17 | "label": "Run Katalyst and Watch for Changes", 18 | "type": "shell", 19 | "command": "cargo make watch" 20 | }, 21 | { 22 | "label": "Build Katalyst", 23 | "type": "shell", 24 | "command": "cargo make build" 25 | }, 26 | { 27 | "label": "Initial Environment Setup", 28 | "type": "shell", 29 | "command": "cargo make setup" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /CONFIG.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Configuration of Katalyst is available in YAML and JSON formats. When running katalyst as a binary, 4 | the configuration is generally loaded from a file specified with the -c option. 5 | 6 | ## Configuration Format 7 | 8 | Although Katalyst supports both JSON and YAML formats, YAML is generally preferred and works better 9 | with the expression syntax in the configuration files. As such, all examples here will be in the YAML 10 | format. 11 | 12 | A basic config could look something like this: 13 | 14 | ```yaml 15 | service: 16 | interfaces: 17 | - address: "0.0.0.0:8080" 18 | cache: 19 | type: memory_cache 20 | 21 | hosts: 22 | httpbin: 23 | type: round_robin 24 | servers: 25 | - "http://httpbin.org" 26 | 27 | routes: 28 | - path: 29 | type: regex 30 | pattern: "^/$" 31 | handler: 32 | type: host 33 | host: httpbin 34 | path: "/ip" 35 | cache: 36 | type: cache_response 37 | 38 | ``` 39 | 40 | This example configuration is for a server that listens on port 8080, has one downstream host group 41 | configured with one server (httpbin.org), and has one route configured that will match requests to / and 42 | route them to http://httpbin.org/ip 43 | 44 | ## Expressions 45 | 46 | Many configuration sections support configuration through "expressions". These expressions allow adding 47 | custom logic and dynamic behavior within the configuration itself. In addition, the expression syntax allows 48 | nesting expressions such that the result of one expression (which itself may have nested expressions) can be used 49 | by another expression. 50 | 51 | As an example, if you needed to add the base64 encoded remote IP address to a query parameter of a downstream host, 52 | this could be accomplished like so: 53 | 54 | ```yaml 55 | handler: 56 | type: host 57 | host: downstream_host_group 58 | path: "/get" 59 | query: 60 | encoded_ip: "{{ encode.base64(http.ip()) }}" 61 | ``` 62 | 63 | While this specific example is a bit contived, this flexibility allows you to do a number of things based off of 64 | the state of the incoming request. 65 | 66 | TODO: Document the built in expressions 67 | 68 | ## Modules 69 | 70 | Throughout the configuration, custom modules can be used with specialized configuration. The only universal 71 | configuration option for a module is the 'type' field, all other fields are determined by the module itself. 72 | When a module section of the configuration is parsed, that section of the configuration is kept as an 73 | `unstructured::Document` so that the module can define as simple or complex of a configuration as required. 74 | Documentation for the individual modules should contain information about configuration specific to those modules. 75 | 76 | ## Configuration Sections 77 | 78 | As demonstrated in the basic configuration above, these configuration sections are available: 79 | 80 | * **service**: Global service options such as listening addresses, cache store, etc. 81 | * **hosts**: This defines "host groups" which are simply groups of servers that can be referred to by name and are load balanced 82 | * **routes**: The list of routes that exist on this server. Incoming requests are matched to a route by a pattern and sent to a handler. 83 | 84 | ## Service Configuration Options 85 | 86 | TODO: **WIP** 87 | 88 | ### interfaces 89 | 90 | ```yaml 91 | interfaces: 92 | # HTTP Configuration 93 | - address: "0.0.0.0:8080" 94 | # HTTPS Configuration 95 | - address: "0.0.0.0:8443" 96 | ssl: true 97 | ssl_cert: "cert.crt" 98 | ssl_key: "key.rsa" 99 | ``` 100 | 101 | ### cache 102 | 103 | ```yaml 104 | cache: # This is a 'cache' module 105 | type: memory_cache 106 | ``` 107 | 108 | ## Host Configuration Options 109 | 110 | TODO: **WIP** 111 | 112 | ```yaml 113 | hosts: 114 | httpbin: # This is a 'load balancer' module 115 | type: round_robin 116 | servers: 117 | - "http://httpbin.org" 118 | ``` 119 | 120 | ## Route Configuration Options 121 | 122 | TODO: **WIP** 123 | 124 | ```yaml 125 | routes: 126 | - path: 127 | type: regex 128 | pattern: "^/my/profile$" 129 | handler: # this is a 'handler' module 130 | type: host 131 | host: host_group_name 132 | path: "/user/{{ auth.claim('userid') }}/profile" 133 | cache: # this is a 'cache' module 134 | type: cache_response 135 | # Other module types currently supported here: authenticator, authorizer, plugin 136 | ``` 137 | -------------------------------------------------------------------------------- /CONFIG.tpl: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | {{readme}} -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "katalyst_macros", 4 | "katalyst", 5 | ] -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #This runs the builds... 2 | FROM rust:latest as builder 3 | 4 | #We do all this first to generate a good library cache before actually building the final app 5 | ADD Cargo.toml . 6 | ADD katalyst/Cargo.toml katalyst/ 7 | ADD katalyst_macros/Cargo.toml katalyst_macros/ 8 | RUN mkdir -p katalyst/src katalyst_macros/src && touch katalyst/src/lib.rs && touch katalyst/src/main.rs && touch katalyst_macros/src/lib.rs && \ 9 | (cargo build --release >> /dev/null || true) 10 | 11 | #Now we add the actual source and build the app/library itself 12 | ADD katalyst katalyst 13 | ADD katalyst_macros katalyst_macros 14 | RUN rm -rf target/release/deps/*katalyst* && cargo build --release && \ 15 | mkdir -p /pkg/bin /pkg/lib/x86_64-linux-gnu && \ 16 | cp /lib/x86_64-linux-gnu/libgcc_s.so.1 /pkg/lib/x86_64-linux-gnu/libgcc_s.so.1 && \ 17 | cp target/release/katalyst /pkg/bin/katalyst 18 | 19 | #Install into target container 20 | FROM gcr.io/distroless/base 21 | COPY --from=builder /pkg/ / 22 | ENTRYPOINT [ "/bin/katalyst" ] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 proctorlabs 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = "true" 3 | CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = "katalyst_macros" 4 | 5 | [tasks.deb] 6 | [tasks.run] 7 | [tasks.watch] 8 | [tasks.build] 9 | [tasks.build-release] 10 | [tasks.test] 11 | 12 | [tasks.setup] 13 | description = "Setup the repo with hooks to automatically clean when needed" 14 | script = [ 15 | "cat < $(git rev-parse --show-toplevel)/.git/hooks/pre-commit", 16 | "#!/bin/bash", 17 | "cargo make commit-hook", 18 | "git add \\$(git rev-parse --show-toplevel)/README.md", 19 | "EOF", 20 | "chmod +x $(git rev-parse --show-toplevel)/.git/hooks/pre-commit" 21 | ] 22 | 23 | [tasks.docker-build] 24 | description = "Build Docker container with katalyst" 25 | command = "docker" 26 | args = ["build", "-t", "katalyst-build", "."] 27 | workspace = false 28 | 29 | [tasks.docker-run] 30 | description = "Run katalyst docker container" 31 | command = "docker" 32 | args = ["run", "-it", "--rm", "-v", "${PWD}/assets/benchmark/benchmark.yml:/config.yml:ro", "-p", "8000:8000", "katalyst-build", "-c", "/config.yml"] 33 | workspace = false 34 | 35 | [tasks.readme-config] 36 | description = "Regenerate the CONFIG.md from the doc notes in config/mod.rs" 37 | install_crate = "cargo-readme" 38 | command = "cargo" 39 | args = ["readme", "-r", "katalyst", "-i", "src/config/mod.rs", "-t", "../CONFIG.tpl", "-o", "../CONFIG.md"] 40 | workspace = false 41 | 42 | [tasks.readme-main] 43 | description = "Regenerate the README.md from the doc notes in main.rs" 44 | install_crate = "cargo-readme" 45 | command = "cargo" 46 | args = ["readme", "-r", "katalyst", "-i", "src/main.rs", "-t", "../README.tpl", "-o", "../README.md"] 47 | workspace = false 48 | 49 | [tasks.commit-hook] 50 | description = "Methods for the pre-commit hook" 51 | dependencies = ["format", "readme-main", "readme-config"] 52 | workspace = false 53 | 54 | [tasks.start-nginx-host] 55 | description = "Run nginx in a docker container" 56 | script = [ 57 | "docker run -d --name nginx-test -v $(pwd)/assets/benchmark/nginx.conf:/etc/nginx/conf.d/default.conf:ro -p 9999:80 nginx" 58 | ] 59 | workspace = false 60 | 61 | [tasks.stop-nginx-host] 62 | description = "Stop nginx container" 63 | script = [ 64 | "docker stop nginx-test || true", 65 | "docker rm nginx-test || true" 66 | ] 67 | workspace = false 68 | 69 | [tasks.start-katalyst-host] 70 | description = "Start katalyst in container" 71 | dependencies = [ 72 | "build-release" 73 | ] 74 | script = [ 75 | "docker run -d --name katalyst-test --net host -v $(pwd)/target/release/katalyst:/opt/katalyst:ro -v $(pwd)/assets/benchmark/benchmark.yml:/opt/config.yml:ro debian:stable-slim /opt/katalyst -c /opt/config.yml -l error" 76 | ] 77 | workspace = false 78 | 79 | [tasks.stop-katalyst-host] 80 | description = "Stop katalyst container" 81 | script = [ 82 | "docker stop katalyst-test || true", 83 | "docker rm katalyst-test || true" 84 | ] 85 | workspace = false 86 | 87 | [tasks.run-benchmark-client] 88 | description = "Build/Run the benchmark client" 89 | script = [ 90 | "docker build -t bench-client -f assets/benchmark/bencher.dockerfile assets/", 91 | "mkdir -p target/reports", 92 | "echo 'running benchmark against nginx host'", 93 | "docker run -it --rm --net=host -u $(id -u $USER):$(id -u $USER) bench-client -c 100 -n 300000 -k -G 16 -r http://127.0.0.1:9999 | tee target/reports/baseline.txt", 94 | "cat target/reports/baseline.txt", 95 | "echo 'running benchmark against katalyst host'", 96 | "docker run -it --rm --net=host -u $(id -u $USER):$(id -u $USER) bench-client -c 100 -n 300000 -k -G 16 -r http://127.0.0.1:8000 | tee target/reports/katalyst.txt", 97 | "cat target/reports/katalyst.txt" 98 | ] 99 | workspace = false 100 | 101 | [tasks.benchmark] 102 | description = "Run the full benchmark workflow" 103 | dependencies = [ 104 | "start-nginx-host", 105 | "start-katalyst-host", 106 | "run-benchmark-client", 107 | "stop-katalyst-host", 108 | "stop-nginx-host" 109 | ] 110 | workspace = false 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Katalyst API Gateway 2 | 3 | [![Crate 0.2.0](https://img.shields.io/crates/v/katalyst.svg)](https://crates.io/crates/katalyst) 4 | [![Build Status](https://dev.azure.com/proctorlabs/katalyst/_apis/build/status/proctorlabs.katalyst?branchName=master&jobName=Job&configuration=stable)](https://dev.azure.com/proctorlabs/katalyst/_build/latest?definitionId=1&branchName=master) 5 | [![Documentation](https://img.shields.io/badge/docs-current-important.svg)](https://docs.rs/katalyst/) 6 | [![MIT License](https://img.shields.io/github/license/proctorlabs/katalyst.svg)](LICENSE) 7 | [![Maintenance](https://img.shields.io/badge/maintenance-experimental-blue.svg)](https://crates.io/crates/katalyst) 8 | 9 | Katalyst is a high performance and low memory API Gateway. It can be used as either an 10 | appliance through Docker or it can be used as a library. This project is currently under 11 | heavy development and will likely experience many changes and issues as we work towards the 12 | 1.0 release. 13 | 14 | ## Features 15 | 16 | * Configuration via YAML files 17 | * Configuration design done using templating and modular 'expressions' for dynamic route handling 18 | * Request routing with either regex or custom route builders 19 | * Modular design for customization of the gateway, internal modules can be overridden 20 | * Load balance hosts using Round Robin, Least Connection, or Random algorithms 21 | * SSL/TLS Termination 22 | * Highly performant, with at least some use cases and loads outperforming nginx 23 | * Built on the tokio runtime with Hyper, leveraging async I/O where possible 24 | * Does not require rust nightly, despite the heavy async I/O 25 | * Usable as a rust library, standalone application, or lightweight docker container 26 | 27 | ## Library usage 28 | 29 | For library usage, refer to the official [rust documentation](https://docs.rs/katalyst/). 30 | 31 | ## Install 32 | 33 | Current installation of the binary requires Cargo, though other package formats may be coming soon. 34 | 35 | ```bash 36 | # Add --force if you need to overwrite an already installed version 37 | cargo install katalyst 38 | ``` 39 | 40 | ## Usage 41 | 42 | Once installed, starting Katalyst is easy. Use the -c option to specify the config file. 43 | {{version}} 44 | 45 | ```bash 46 | ➤ katalyst -c config.yml 47 | 2019-06-25 19:44:03,103 INFO [katalyst::config::parsers] Loading file from: config.yml 48 | 2019-06-25 19:44:03,105 INFO [katalyst::server] Listening on http://0.0.0.0:8080 49 | 2019-06-25 19:44:03,105 INFO [katalyst::server] Listening on https://0.0.0.0:8443 50 | ... 51 | ``` 52 | 53 | Run with the help command or flags to get all CLI options 54 | 55 | ```bash 56 | ➤ katalyst help 57 | katalyst 0.2.0 58 | Phil Proctor 59 | High performance, modular API Gateway 60 | 61 | USAGE: 62 | katalyst [OPTIONS] [SUBCOMMAND] 63 | 64 | FLAGS: 65 | -h, --help Prints help information 66 | -V, --version Prints version information 67 | 68 | OPTIONS: 69 | -c, --config Config file [default: katalyst.yaml] 70 | -l, --log-level Logging level to use [default: info] 71 | 72 | SUBCOMMANDS: 73 | help Prints this message or the help of the given subcommand(s) 74 | run Start the API Gateway (default) 75 | ``` 76 | 77 | ## Configuration 78 | 79 | Refer to the documentation [here](CONFIG.md) 80 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | # Katalyst API Gateway 2 | 3 | [![Crate {{version}}](https://img.shields.io/crates/v/katalyst.svg)](https://crates.io/crates/katalyst) 4 | [![Build Status](https://dev.azure.com/proctorlabs/katalyst/_apis/build/status/proctorlabs.katalyst?branchName=master&jobName=Job&configuration=stable)](https://dev.azure.com/proctorlabs/katalyst/_build/latest?definitionId=1&branchName=master) 5 | [![Documentation](https://img.shields.io/badge/docs-current-important.svg)](https://docs.rs/katalyst/) 6 | [![MIT License](https://img.shields.io/github/license/proctorlabs/katalyst.svg)](LICENSE) 7 | [![Maintenance](https://img.shields.io/badge/maintenance-experimental-blue.svg)](https://crates.io/crates/katalyst) 8 | 9 | {{readme}} 10 | -------------------------------------------------------------------------------- /assets/benchmark/bencher.dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang 2 | 3 | RUN go get github.com/parkghost/gohttpbench 4 | RUN go build -o gb github.com/parkghost/gohttpbench 5 | 6 | ENTRYPOINT [ "./gb" ] -------------------------------------------------------------------------------- /assets/benchmark/benchmark.yml: -------------------------------------------------------------------------------- 1 | service: 2 | interfaces: 3 | - address: "0.0.0.0:8000" 4 | cache: 5 | type: memory_cache 6 | 7 | hosts: 8 | nginx: 9 | type: round_robin 10 | servers: 11 | - "http://127.0.0.1:9999" 12 | 13 | routes: 14 | - path: 15 | type: regex 16 | pattern: "^/$" 17 | methods: 18 | - get 19 | handler: 20 | type: host 21 | host: nginx 22 | path: "/" 23 | cache: 24 | type: cache_response 25 | -------------------------------------------------------------------------------- /assets/benchmark/nginx.conf: -------------------------------------------------------------------------------- 1 | # This is the configuration used for benchmarking purposes 2 | server { 3 | listen 80; 4 | access_log off; 5 | error_log off; 6 | 7 | location / { 8 | return 200 'Content Sent!'; 9 | add_header Content-Type text/plain; 10 | } 11 | } -------------------------------------------------------------------------------- /assets/manual-test.yml: -------------------------------------------------------------------------------- 1 | service: 2 | interfaces: 3 | - address: "0.0.0.0:8080" 4 | - address: "0.0.0.0:8443" 5 | ssl: true 6 | ssl_cert: "../assets/test.crt" 7 | ssl_key: "../assets/test.rsa" 8 | cache: 9 | type: memory_cache 10 | 11 | hosts: 12 | httpbin: 13 | type: round_robin 14 | servers: 15 | - "http://httpbin.org" 16 | 17 | routes: 18 | - path: 19 | type: regex 20 | pattern: "^/$" 21 | handler: 22 | type: host 23 | host: httpbin 24 | path: "/ip" 25 | cache: 26 | type: cache_response 27 | 28 | - path: 29 | type: regex 30 | pattern: "^/files/(?P.*)$" 31 | handler: 32 | type: file_server 33 | root_path: "../assets/" 34 | selector: "{{ http.matched('file') }}" 35 | 36 | - path: 37 | type: template 38 | template: "/test/{{ url.segment('segment') }}/{{ url.all('end') }}" 39 | methods: 40 | - get 41 | handler: 42 | type: host 43 | host: httpbin 44 | path: "/get" 45 | query: 46 | segment: "{{ http.matched('segment') }}" 47 | end: "{{ http.matched('end') }}" 48 | method: "{{ http.method() }}" 49 | ip: "{{ http.ip() }}" 50 | encoded: "{{ encode.base64('test') }}" 51 | decoded: "{{ decode.base64('dGVzdA==') }}" 52 | test: "{{ sys.env('USER') }}" 53 | headers: 54 | header: "{{ http.header('user-agent') }}" 55 | authenticators: 56 | - type: http 57 | host: httpbin 58 | path: "/ip" 59 | 60 | - path: 61 | type: exact 62 | path: "/test/post" 63 | methods: 64 | - post 65 | plugins: 66 | - type: parse-content 67 | handler: 68 | type: host 69 | host: httpbin 70 | headers: 71 | Capped: "{{ content.val('body') }}" 72 | path: "/post" 73 | 74 | - path: 75 | type: exact 76 | path: "/test/get-to-post" 77 | methods: 78 | - get 79 | authenticators: 80 | - type: never 81 | opts: 82 | - "127.0.0.2" 83 | - "127.0.0.1" 84 | - "127.0.0.4" 85 | handler: 86 | type: host 87 | host: httpbin 88 | path: "/post" 89 | method: post 90 | headers: 91 | Content-Type: application/json 92 | body: | 93 | { 94 | "remote_address": "{{ http.ip() }}" 95 | } 96 | -------------------------------------------------------------------------------- /assets/misc/Katalyst.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "0e914a98-06f5-46b2-8ac0-5fc665653c54", 4 | "name": "Katalyst", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Root", 10 | "request": { 11 | "method": "GET", 12 | "header": [], 13 | "body": { 14 | "mode": "raw", 15 | "raw": "" 16 | }, 17 | "url": { 18 | "raw": "{{host}}/", 19 | "host": [ 20 | "{{host}}" 21 | ], 22 | "path": [ 23 | "" 24 | ] 25 | } 26 | }, 27 | "response": [] 28 | }, 29 | { 30 | "name": "Test basic get", 31 | "request": { 32 | "method": "GET", 33 | "header": [], 34 | "body": { 35 | "mode": "raw", 36 | "raw": "" 37 | }, 38 | "url": { 39 | "raw": "{{host}}/test/something_capped", 40 | "host": [ 41 | "{{host}}" 42 | ], 43 | "path": [ 44 | "test", 45 | "something_capped" 46 | ] 47 | } 48 | }, 49 | "response": [] 50 | }, 51 | { 52 | "name": "Test post", 53 | "request": { 54 | "method": "POST", 55 | "header": [ 56 | { 57 | "key": "Content-Type", 58 | "name": "Content-Type", 59 | "value": "application/json", 60 | "type": "text" 61 | } 62 | ], 63 | "body": { 64 | "mode": "raw", 65 | "raw": "{\n\t\"body\": \"contents\"\n}" 66 | }, 67 | "url": { 68 | "raw": "{{host}}/test/post", 69 | "host": [ 70 | "{{host}}" 71 | ], 72 | "path": [ 73 | "test", 74 | "post" 75 | ] 76 | } 77 | }, 78 | "response": [] 79 | } 80 | ] 81 | } -------------------------------------------------------------------------------- /assets/test.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDazCCAlOgAwIBAgIUVSqMLYedWsmnp9ej570LVNkaTrowDQYJKoZIhvcNAQEL 3 | BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xOTA1MzEwNDIxNDlaFw0yMDA1 5 | MzAwNDIxNDlaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw 6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB 7 | AQUAA4IBDwAwggEKAoIBAQC4w1FZyrARpL8rfVYNQ2/Fw1ubBSff20jwgkjkRLGX 8 | G9ls/bagvUXBgeB3E8ZquXjvY9KQ77iHdGyAMleJ0hFsSvKQVDuZzTMU/nb4cF1s 9 | XOk4983kr6OGpiKP/RKgTOpFrayhohwVo0b+fAPf68qS4r4z/1QGwaxCTtWQocDr 10 | XXjbSjwL06UYzWSxwgaw/sWYS50JeAzin7LVjv1RHSfzwrGwh67cJ+wpNt3kUoB2 11 | WnMPb6ZmprPuURLM1JMZcN2MCzciOSKvWQly5ltVPMnv+RCI4rnmUik9SOKgYqWj 12 | 8+1jup3jOBUhjX2Lxaxxyma5w+0ijSGQRzBARicyue2jAgMBAAGjUzBRMB0GA1Ud 13 | DgQWBBRD4B2vAmCqx1n+GOBjxZloBBQKqDAfBgNVHSMEGDAWgBRD4B2vAmCqx1n+ 14 | GOBjxZloBBQKqDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB/ 15 | Y2qESJYxeZ8FDCT0GoaRtzCnssXn2gEMHakFnZ4+9WWZcRDAYMJwoNcNgpLnoTOk 16 | Sy6RKfQTNwaanASkaYLhJYsoggT8C+QA3jXFNFF/ec/M6j5ns6jTvrWqITfu0gla 17 | CkdxI9AHQgdOQEyoLhMEWDnPk0r2mpTKdxIiN1EqCmxQ78EVvxENF40puRvRBd9F 18 | vA1RqCn7TuZObGwzlrgX8HaLwdoSmfNYvNCNu3VD3A8u4vhIZXSSwmUPozhxI4Pb 19 | 42bdfb0qWeOzyCPKIah+gVnqL8u5hqihSdSyvsit8mUMOXN5PtEZFf2O+Wipxgjb 20 | D5BuU8W75K0bJkRnhhxf 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /assets/test.rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAuMNRWcqwEaS/K31WDUNvxcNbmwUn39tI8IJI5ESxlxvZbP22 3 | oL1FwYHgdxPGarl472PSkO+4h3RsgDJXidIRbErykFQ7mc0zFP52+HBdbFzpOPfN 4 | 5K+jhqYij/0SoEzqRa2soaIcFaNG/nwD3+vKkuK+M/9UBsGsQk7VkKHA611420o8 5 | C9OlGM1kscIGsP7FmEudCXgM4p+y1Y79UR0n88KxsIeu3CfsKTbd5FKAdlpzD2+m 6 | Zqaz7lESzNSTGXDdjAs3Ijkir1kJcuZbVTzJ7/kQiOK55lIpPUjioGKlo/PtY7qd 7 | 4zgVIY19i8WsccpmucPtIo0hkEcwQEYnMrntowIDAQABAoIBAEdMR/ZIew0z7/mO 8 | Ukin/1fnfVAi+zItYsY84HgF8ioHuy2N8o2wvFxiDAangOfqTrrCYJ1BhInw6XXG 9 | 93TdtY9+lIARoTZGszGkyLAyXDrW18D+D1vyUz5AmhHKbQei5rygun9dGU1YRqsp 10 | nC0qxm9MRG24V+qLjjfASDWZ1eJ89Y7ansj4GoTNST4zOfYhvgeMjwGs/0zaQUbe 11 | zWaIKx06Q7MD35sXuyerNRmxGmoc2YVWiaEZIHMJL1uE8yKA9quaE9LeHGTOzw/6 12 | psdCx+uplY/z7Urdc6FJm7UWieKpe/d5w/rnp//igsHYJ5YnJKzdpJ3cxhONO3dc 13 | 24I3JWECgYEA4fWT6QlbXNPAAFOjM/urQuDO9nakJJ3xYaRKOImNpsW9Wpgah8a9 14 | ljGLMKE2QwH5yVrbHXze+3zhN3TD58c1Yhcm0Tna+c6cnogi0Jp1C9Tg8Bkc/2Cx 15 | dys92tNDIYcE9U64i0SJ0jQhnl11j7/QgBv8seAOVgOaKgL+WGaPbVsCgYEA0VOk 16 | P72L3VNeG+palcwQhIvFjECh5FJukFiQTPSHiBboPRFDkBXjQfN56AuD2otxu7jA 17 | CChe2utBeJ8aLYX6XKhy8zWVYC2nygVUEH2Aq46JgebHULunjb5Bj5QXCzJhJTgT 18 | To8YeoZ4NwA0msyXVstth8qcEfqnkXBlcBmeC1kCgYEAgL6pCQVwzgJEiqsc+Thz 19 | C0cGBT3yJn7rksPGGlKdqCFQ03aI30XordQKx6mDPki45vZilHman1Y3CJ76JGzH 20 | yN0CHAJH9z+200kj9RGckSI5C/RzJjsUwp6bvrvSqx7AP3kcAxYJJQDZCt+bZU1Y 21 | YjYQE9VitbWVuEw+WWYOYLMCgYAtTdT2lqd2t1xe3lHMqeXJShbvS+295LlZNFHG 22 | 1gWfRpXs4Zelz5bn5zIzLorS+esbndix60rcRp5c5NJdl+mftDVsveQedMMjzhNr 23 | nj4C406PdsschgC1hL/bu0lhev3beE91aTL7Ea9i+ABqoG0As/Z4tTkiCwXJTHIn 24 | 2OODwQKBgGY4OFoXV/K9rX5XVrsq4aio4t3xvEo/lBzMJs14TGk9xFV2do0aZsY8 25 | PCUr9A6an7CgOg0xRU9UWoJNJQO+zQQ6OqaLYN0B2YtFugomYOLGVX3JpqacUkyx 26 | 8vzYMzlGWd/TqNz2zBEIqvpu5O8LVUS/he6FmFJYpqO1z5k5uaTP 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: ["*"] 4 | tags: 5 | include: ["*"] 6 | 7 | strategy: 8 | matrix: 9 | stable: 10 | rustup_toolchain: stable 11 | beta: 12 | rustup_toolchain: beta 13 | nightly: 14 | rustup_toolchain: nightly 15 | 16 | variables: 17 | build_name: "$(Build.SourceBranchName)" 18 | is_tag: "${{ startsWith(variables['Build.SourceBranch'], 'refs/tags/') }}" 19 | 20 | pool: 21 | vmImage: "ubuntu-16.04" 22 | 23 | steps: 24 | - template: ".build/install-rust.yml" 25 | - template: ".build/cargo-build-and-test.yml" 26 | - template: ".build/publish-crate.yml" 27 | -------------------------------------------------------------------------------- /katalyst/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "katalyst" 3 | description = "High performance, modular API Gateway" 4 | repository = "https://github.com/proctorlabs/katalyst" 5 | version = "0.2.0" 6 | authors = ["Phil Proctor "] 7 | edition = "2018" 8 | keywords = ["http", "api", "gateway"] 9 | categories = ["network-programming", "web-programming::http-server", "web-programming"] 10 | license = "MIT" 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | # Base 15 | katalyst_macros = { version = "0.2", path = "../katalyst_macros" } 16 | 17 | #Utility 18 | log = "0.4" 19 | lazy_static = "1.3" 20 | rand = "0.6" 21 | derive_more = "0.15" 22 | signal-hook = "0.1" 23 | parking_lot = "0.8" 24 | 25 | # HTTP 26 | hyper = "0.12" 27 | rustls = "0.15" 28 | cookie = { version = "0.12", features = ["secure"] } 29 | hyper-rustls = "0.16" 30 | webpki-roots = "0.16" 31 | tokio = "0.1" 32 | tokio-fs = "0.1" 33 | tokio-io = "0.1" 34 | tokio-rustls = "0.9" 35 | tokio-tcp = "0.1" 36 | http = "0.1" 37 | url = "1.7" 38 | futures = "0.1" 39 | base64 = "0.10" 40 | mime_guess = "1.8" 41 | 42 | #Parsing 43 | unstructured = "0.2.0" 44 | regex = "1.1" 45 | serde = { version = "1.0", features = ["derive"] } 46 | serde_yaml = "0.8" 47 | serde_json = "1.0" 48 | pest = { version = "2.1" } 49 | pest_derive = { version = "2.1" } 50 | 51 | # CLI 52 | simple_logger = { version = "1.3", optional = true } 53 | clap = { version = "2.33", optional = true } 54 | structopt = { version = "0.2", optional = true } 55 | 56 | [features] 57 | default = ["cli"] 58 | cli = ["simple_logger", "clap", "structopt"] 59 | 60 | [dev-dependencies] 61 | simple_logger = "1.3" 62 | 63 | [build-dependencies] 64 | 65 | [lib] 66 | name = "katalyst" 67 | path = "src/lib.rs" 68 | 69 | [[bin]] 70 | name = "katalyst" 71 | path = "src/main.rs" 72 | required-features = ["cli"] 73 | 74 | [badges] 75 | maintenance = { status = "actively-developed" } 76 | -------------------------------------------------------------------------------- /katalyst/Makefile.toml: -------------------------------------------------------------------------------- 1 | [tasks.deb] 2 | description = "Create a debian package" 3 | install_crate = "cargo-deb" 4 | command = "cargo" 5 | args = ["deb"] 6 | 7 | [tasks.run] 8 | description = "Run Katalyst" 9 | command = "cargo" 10 | args = ["run", "--", "-c", "../assets/manual-test.yml"] 11 | 12 | [tasks.watch] 13 | description = "Run Katalyst and watch for changes" 14 | install_crate = "cargo-watch" 15 | command = "cargo" 16 | args = ["watch", "-x", "run -- -c ../assets/manual-test.yml"] 17 | 18 | [tasks.build] 19 | description = "Build the project" 20 | command = "cargo" 21 | args = ["build"] 22 | 23 | [tasks.build-release] 24 | description = "Build the project with optimizations" 25 | command = "cargo" 26 | args = ["build", "--release"] 27 | 28 | [tasks.test] 29 | description = "Run the unit tests" 30 | command = "cargo" 31 | args = ["test"] 32 | -------------------------------------------------------------------------------- /katalyst/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /katalyst/examples/basic.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | use katalyst::Katalyst; 5 | use log::Level; 6 | use std::{env, io, path::PathBuf}; 7 | 8 | fn config_path() -> io::Result { 9 | let mut dir = env::current_exe()?; 10 | dir.pop(); 11 | dir.push("config.yml"); 12 | Ok(dir) 13 | } 14 | 15 | fn main() { 16 | simple_logger::init_with_level(Level::Debug).unwrap(); 17 | let path_buf = config_path().expect("Couldn't create path"); 18 | let path = path_buf.to_string_lossy(); 19 | info!("Loading file from {}", &path); 20 | Katalyst::start(&path).unwrap(); 21 | } 22 | -------------------------------------------------------------------------------- /katalyst/src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::{parsers, Builder}, 3 | instance::Instance, 4 | modules::ModuleRegistry, 5 | prelude::*, 6 | server::*, 7 | }; 8 | use hyper::{ 9 | client::{connect::dns::TokioThreadpoolGaiResolver, HttpConnector}, 10 | rt::Future, 11 | Body, Client, 12 | }; 13 | use hyper_rustls::HttpsConnector; 14 | use parking_lot::RwLock; 15 | use rustls::ClientConfig; 16 | use signal_hook::{iterator::Signals, SIGINT, SIGQUIT, SIGTERM}; 17 | use std::{fmt, sync::Arc}; 18 | use tokio::runtime::Runtime; 19 | 20 | pub type HttpsClient = Client>, Body>; 21 | 22 | /// This is the core structure for the API Gateway. 23 | pub struct Katalyst { 24 | instance: RwLock>, 25 | servers: RwLock>, 26 | client: Arc, 27 | compiler: Arc, 28 | modules: ModuleRegistry, 29 | rt: RwLock, 30 | } 31 | 32 | impl std::fmt::Debug for Katalyst { 33 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 34 | write!(f, "Katalyst {{ instance: {:?} }}", self.instance) 35 | } 36 | } 37 | 38 | impl Default for Katalyst { 39 | fn default() -> Self { 40 | let builder = Client::builder(); 41 | let mut http_connector = HttpConnector::new_with_tokio_threadpool_resolver(); 42 | http_connector.enforce_http(false); 43 | let mut tls = ClientConfig::new(); 44 | tls.root_store.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); 45 | 46 | Katalyst { 47 | instance: RwLock::default(), 48 | servers: RwLock::default(), 49 | client: Arc::new(builder.build(HttpsConnector::from((http_connector, tls)))), 50 | compiler: Arc::new(Compiler::default()), 51 | modules: ModuleRegistry::default(), 52 | rt: RwLock::new(Runtime::new().unwrap()), 53 | } 54 | } 55 | } 56 | 57 | pub trait ArcKatalystImpl { 58 | /// Update the Katalyst instance with the configuration from the specified file. 59 | fn load(&self, config_file: &str) -> Result<()>; 60 | 61 | /// Run the Katalyst instance. This thread will block and run the async runtime. 62 | fn run(&mut self) -> Result<()>; 63 | 64 | fn run_service(&mut self) -> Result<()>; 65 | } 66 | 67 | impl ArcKatalystImpl for Arc { 68 | /// Update the Katalyst instance with the configuration from the specified file. 69 | fn load(&self, config_file: &str) -> Result<()> { 70 | let config = parsers::parse_file(config_file)?; 71 | self.update_instance(config.build(self.clone())?)?; 72 | Ok(()) 73 | } 74 | 75 | /// Run the Katalyst instance. This thread will block and run the async runtime. 76 | #[inline] 77 | fn run(&mut self) -> Result<()> { 78 | self.run_service()?; 79 | self.wait()?; 80 | Ok(()) 81 | } 82 | 83 | fn run_service(&mut self) -> Result<()> { 84 | let instance = self.get_instance()?.clone(); 85 | for interface in instance.service.interfaces.iter() { 86 | let server = Server::new(interface)?; 87 | server.spawn(self)?; 88 | let mut servers = self.servers.write(); 89 | servers.push(server); 90 | } 91 | Ok(()) 92 | } 93 | } 94 | 95 | impl Katalyst { 96 | /// Update the running configuration of the API Gateway. 97 | pub fn update_instance(&self, new_instance: Instance) -> Result<()> { 98 | let mut instance = self.instance.write(); 99 | *instance = Arc::new(new_instance); 100 | Ok(()) 101 | } 102 | 103 | /// Get a copy of the currently running API Gateway configuration. 104 | pub fn get_instance(&self) -> Result> { 105 | let instance = self.instance.read(); 106 | Ok(instance.clone()) 107 | } 108 | 109 | #[inline] 110 | pub(crate) fn get_client(&self) -> Arc { 111 | self.client.clone() 112 | } 113 | 114 | #[inline] 115 | pub(crate) fn get_compiler(&self) -> Arc { 116 | self.compiler.clone() 117 | } 118 | 119 | #[inline] 120 | pub(crate) fn get_module(&self, name: &str) -> Result> { 121 | self.modules.get(name) 122 | } 123 | 124 | /// Spawn a future on the runtime backing Katalyst 125 | pub fn spawn + Send + 'static>(&self, fut: F) -> Result<()> { 126 | let mut rt = self.rt.write(); 127 | rt.spawn(fut); 128 | Ok(()) 129 | } 130 | 131 | /// Register OS signals and respond to them. This method will not return unless 132 | /// a SIGINT, SIGTERM, or SIGQUIT is received. 133 | pub fn wait(&self) -> Result<()> { 134 | let signals = Signals::new(&[SIGINT, SIGTERM, SIGQUIT])?; 135 | for sig in signals.forever() { 136 | match sig { 137 | SIGINT | SIGTERM | SIGQUIT => break, 138 | _ => (), 139 | }; 140 | } 141 | info!("Signal received, shutting down..."); 142 | Ok(()) 143 | } 144 | 145 | /// This is a convenience method to start an instance of Katalyst from a configuration file. 146 | /// This will load the configuration from the specified file and run the gateway until an OS 147 | /// signal is received. 148 | pub fn start(config_file: &str) -> Result> { 149 | let mut app = Arc::new(Katalyst::default()); 150 | app.load(config_file)?; 151 | app.run()?; 152 | Ok(app) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /katalyst/src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | #[derive(StructOpt, Debug)] 4 | #[structopt(name = "Katalyst", rename_all = "kebab_case")] 5 | pub struct Args { 6 | /// Config file 7 | #[structopt(short, long, help = "Config file", default_value = "katalyst.yaml")] 8 | pub config: String, 9 | 10 | /// Filter to apply to input files 11 | #[structopt(short, long, help = "Logging level to use", default_value = "info")] 12 | pub log_level: log::Level, 13 | 14 | /// The command to run 15 | #[structopt(subcommand)] 16 | pub command: Option, 17 | } 18 | 19 | #[derive(Debug, StructOpt)] 20 | #[structopt(name = "command", rename_all = "kebab_case")] 21 | pub enum Command { 22 | /// Start the API Gateway (default) 23 | Run, 24 | } 25 | 26 | impl Args { 27 | pub fn new() -> Self { 28 | Args::from_args() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /katalyst/src/config/builder/mod.rs: -------------------------------------------------------------------------------- 1 | mod module; 2 | mod path; 3 | mod routes; 4 | mod service; 5 | 6 | pub use module::ModuleBuilder; 7 | pub use path::PathBuilder; 8 | pub use routes::RouteBuilder; 9 | pub use service::InterfaceBuilder; 10 | pub use service::ServiceBuilder; 11 | 12 | use crate::{app::Katalyst, error::GatewayError, instance::*, prelude::*}; 13 | use serde::{Deserialize, Serialize}; 14 | use std::{collections::HashMap, sync::Arc}; 15 | 16 | /// A configuration builder for an instance of Katalyst 17 | pub trait Builder { 18 | /// Build an instance configuration using the supplied base Katalyst instance 19 | fn build(&self, engine: Arc) -> Result; 20 | } 21 | 22 | /// The base builder for building a new Katalyst Instance 23 | #[derive(Debug, Serialize, Deserialize, Default)] 24 | #[serde(default)] 25 | pub struct KatalystBuilder { 26 | hosts: HashMap>, 27 | routes: Vec, 28 | service: ServiceBuilder, 29 | } 30 | 31 | impl Builder for KatalystBuilder { 32 | fn build(&self, engine: Arc) -> Result { 33 | //build routes... 34 | let mut all_routes = vec![]; 35 | for route in self.routes.iter() { 36 | all_routes.push(Arc::new(route.build(engine.clone())?)); 37 | } 38 | 39 | //build hosts... 40 | let mut hosts: HashMap = HashMap::new(); 41 | for (k, v) in self.hosts.iter() { 42 | hosts.insert( 43 | k.to_string(), 44 | Hosts { servers: module_unwrap!(LoadBalancer, v.build(engine.clone())?) }, 45 | ); 46 | } 47 | 48 | //final result 49 | Ok(Instance { hosts, routes: all_routes, service: self.service.build(engine.clone())? }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /katalyst/src/config/builder/module.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::app::Katalyst; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{marker::PhantomData, string::String, sync::Arc}; 5 | 6 | /// Builder for any implementation of ModuleProvider 7 | #[derive(Debug, Serialize, Deserialize)] 8 | #[serde(rename_all = "snake_case")] 9 | pub struct ModuleBuilder { 10 | #[serde(skip)] 11 | __module_type: PhantomData, 12 | #[serde(rename = "type")] 13 | module: String, 14 | #[serde(flatten)] 15 | config: unstructured::Document, 16 | } 17 | 18 | impl Default for ModuleBuilder { 19 | fn default() -> Self { 20 | ModuleBuilder { 21 | __module_type: PhantomData::default(), 22 | module: String::default(), 23 | config: unstructured::Document::Unit, 24 | } 25 | } 26 | } 27 | 28 | impl Builder for ModuleBuilder 29 | where 30 | T: ModuleData, 31 | { 32 | fn build(&self, engine: Arc) -> Result { 33 | let module = engine.get_module(&self.module)?; 34 | Ok(module.build(T::MODULE_TYPE, engine.clone(), &self.config)?) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /katalyst/src/config/builder/path.rs: -------------------------------------------------------------------------------- 1 | use super::Builder; 2 | use crate::{app::Katalyst, prelude::*}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::sync::Arc; 5 | 6 | /// A PathBuilder for building path strings from configuration 7 | #[derive(Debug, Serialize, Deserialize)] 8 | #[serde(tag = "type")] 9 | #[serde(rename_all = "snake_case")] 10 | pub enum PathBuilder { 11 | /// A regex path 12 | Regex { 13 | /// The regex match string for this path 14 | pattern: String, 15 | }, 16 | /// An expression templated path 17 | Template { 18 | /// The expression based template for this path 19 | template: String, 20 | }, 21 | /// Exact match only path 22 | Exact { 23 | /// The exact path that should match 24 | path: String, 25 | /// Boolean indicating if the path should be case sensitive 26 | #[serde(default)] 27 | sensitive: bool, 28 | }, 29 | } 30 | 31 | impl Default for PathBuilder { 32 | fn default() -> Self { 33 | PathBuilder::Exact { path: "/".to_string(), sensitive: false } 34 | } 35 | } 36 | 37 | impl Builder for PathBuilder { 38 | fn build(&self, e: Arc) -> Result { 39 | match self { 40 | PathBuilder::Regex { pattern } => Ok(pattern.to_string()), 41 | PathBuilder::Template { template } => Ok({ 42 | let compiler = e.get_compiler(); 43 | let mut result = String::new(); 44 | result.push_str("^"); 45 | let cmp = compiler.compile_template(Some(template))?; 46 | let ctx = RequestContext::default(); 47 | let rnd = cmp.render(&ctx).map_err(|e| { 48 | err!( 49 | ConfigurationFailure, 50 | format!("Unable to parse path template {}", template), 51 | e 52 | ) 53 | })?; 54 | result.push_str(&rnd); 55 | result 56 | }), 57 | PathBuilder::Exact { path, sensitive } => Ok({ 58 | let mut result = String::new(); 59 | result.push_str("^"); 60 | if !*sensitive { 61 | result.push_str("(?i:"); 62 | } 63 | result.push_str("(?P"); 64 | let escaped = regex::escape(path); 65 | result.push_str(&escaped); 66 | if !*sensitive { 67 | result.push_str(")") 68 | } 69 | result.push_str(")$"); 70 | result 71 | }), 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /katalyst/src/config/builder/routes.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::{app::Katalyst, instance::Route}; 3 | use http::Method; 4 | use regex::Regex; 5 | use serde::{Deserialize, Serialize}; 6 | use std::{collections::HashSet, string::String, sync::Arc}; 7 | 8 | /// Builder for instance routes 9 | #[derive(Debug, Serialize, Deserialize, Default)] 10 | pub struct RouteBuilder { 11 | path: PathBuilder, 12 | #[serde(default)] 13 | children: Option>, 14 | handler: ModuleBuilder, 15 | #[serde(default)] 16 | methods: Option>, 17 | #[serde(default)] 18 | plugins: Option>>, 19 | #[serde(default)] 20 | cache: Option>, 21 | #[serde(default)] 22 | authorizers: Option>>, 23 | #[serde(default)] 24 | authenticators: Option>>, 25 | } 26 | 27 | impl Builder for RouteBuilder { 28 | fn build(&self, engine: Arc) -> Result { 29 | let routebuilders: &Option> = &self.children; 30 | let routes = match routebuilders { 31 | Some(b) => { 32 | let mut result = vec![]; 33 | for rb in b { 34 | result.push(Arc::new(rb.build(engine.clone())?)); 35 | } 36 | Some(result) 37 | } 38 | None => None, 39 | }; 40 | let handler = module_unwrap!(RequestHandler, self.handler.build(engine.clone())?); 41 | 42 | //Build method hashset 43 | let methods = match &self.methods { 44 | Some(s) => { 45 | let mut vec_methods: HashSet = HashSet::new(); 46 | for method_string in s { 47 | let method = Method::from_bytes(method_string.to_uppercase().as_bytes())?; 48 | vec_methods.insert(method); 49 | } 50 | Some(vec_methods) 51 | } 52 | None => None, 53 | }; 54 | 55 | let plugins = match &self.plugins { 56 | Some(plugins) => { 57 | let mut vec_plugins: Vec> = vec![]; 58 | for p in plugins { 59 | vec_plugins.push(module_unwrap!(Plugin, p.build(engine.clone())?)); 60 | } 61 | Some(vec_plugins) 62 | } 63 | None => None, 64 | }; 65 | 66 | let authorizers = match &self.authorizers { 67 | Some(auths) => { 68 | let mut vec_auths: Vec> = vec![]; 69 | for a in auths { 70 | vec_auths.push(module_unwrap!(Authorizer, a.build(engine.clone())?)); 71 | } 72 | Some(vec_auths) 73 | } 74 | None => None, 75 | }; 76 | 77 | let authenticators = match &self.authenticators { 78 | Some(auths) => { 79 | let mut vec_auths: Vec> = vec![]; 80 | for a in auths { 81 | vec_auths.push(module_unwrap!(Authenticator, a.build(engine.clone())?)); 82 | } 83 | Some(vec_auths) 84 | } 85 | None => None, 86 | }; 87 | 88 | let cache: Option> = match &self.cache { 89 | Some(c) => Some(module_unwrap!(CacheHandler, c.build(engine.clone())?)), 90 | None => None, 91 | }; 92 | 93 | Ok(Route { 94 | pattern: Regex::new(&self.path.build(engine)?)?, 95 | children: routes, 96 | handler, 97 | plugins, 98 | authorizers, 99 | cache, 100 | methods, 101 | authenticators, 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /katalyst/src/config/builder/service.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::{ 3 | app::Katalyst, 4 | instance::{Interface, Service}, 5 | prelude::*, 6 | }; 7 | use serde::{Deserialize, Serialize}; 8 | use std::sync::Arc; 9 | 10 | /// Builder for an interface attached to this service 11 | #[derive(Debug, Serialize, Deserialize, Default)] 12 | #[serde(default)] 13 | pub struct InterfaceBuilder { 14 | address: String, 15 | ssl: bool, 16 | ssl_cert: String, 17 | ssl_key: String, 18 | } 19 | 20 | impl InterfaceBuilder { 21 | fn make_interface(&self) -> Result { 22 | Ok(if self.ssl { 23 | Interface::Https { 24 | addr: self.address.parse().map_err(|e| { 25 | err!( 26 | ConfigurationFailure, 27 | format!("Failed to parse the listener address {}", self.address), 28 | e 29 | ) 30 | })?, 31 | cert: self.ssl_cert.clone(), 32 | key: self.ssl_key.clone(), 33 | } 34 | } else { 35 | Interface::Http { 36 | addr: self.address.parse().map_err(|e| { 37 | err!( 38 | ConfigurationFailure, 39 | format!("Failed to parse the listener address {}", self.address), 40 | e 41 | ) 42 | })?, 43 | } 44 | }) 45 | } 46 | } 47 | 48 | /// Builder for a Katalyst service instance 49 | #[derive(Debug, Serialize, Deserialize, Default)] 50 | #[serde(default)] 51 | pub struct ServiceBuilder { 52 | interfaces: Vec, 53 | cache: ModuleBuilder, 54 | } 55 | 56 | impl Builder for ServiceBuilder { 57 | fn build(&self, instance: Arc) -> Result { 58 | Ok(Service { 59 | interfaces: self 60 | .interfaces 61 | .iter() 62 | .map(|i| i.make_interface()) 63 | .collect::>>()?, 64 | cache: module_unwrap!(CacheProvider, self.cache.build(instance.clone())?), 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /katalyst/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Configuration of Katalyst is available in YAML and JSON formats. When running katalyst as a binary, 3 | the configuration is generally loaded from a file specified with the -c option. 4 | 5 | # Configuration Format 6 | 7 | Although Katalyst supports both JSON and YAML formats, YAML is generally preferred and works better 8 | with the expression syntax in the configuration files. As such, all examples here will be in the YAML 9 | format. 10 | 11 | A basic config could look something like this: 12 | 13 | ```yaml 14 | service: 15 | interfaces: 16 | - address: "0.0.0.0:8080" 17 | cache: 18 | type: memory_cache 19 | 20 | hosts: 21 | httpbin: 22 | type: round_robin 23 | servers: 24 | - "http://httpbin.org" 25 | 26 | routes: 27 | - path: 28 | type: regex 29 | pattern: "^/$" 30 | handler: 31 | type: host 32 | host: httpbin 33 | path: "/ip" 34 | cache: 35 | type: cache_response 36 | 37 | ``` 38 | 39 | This example configuration is for a server that listens on port 8080, has one downstream host group 40 | configured with one server (httpbin.org), and has one route configured that will match requests to / and 41 | route them to http://httpbin.org/ip 42 | 43 | # Expressions 44 | 45 | Many configuration sections support configuration through "expressions". These expressions allow adding 46 | custom logic and dynamic behavior within the configuration itself. In addition, the expression syntax allows 47 | nesting expressions such that the result of one expression (which itself may have nested expressions) can be used 48 | by another expression. 49 | 50 | As an example, if you needed to add the base64 encoded remote IP address to a query parameter of a downstream host, 51 | this could be accomplished like so: 52 | 53 | ```yaml 54 | handler: 55 | type: host 56 | host: downstream_host_group 57 | path: "/get" 58 | query: 59 | encoded_ip: "{{ encode.base64(http.ip()) }}" 60 | ``` 61 | 62 | While this specific example is a bit contived, this flexibility allows you to do a number of things based off of 63 | the state of the incoming request. 64 | 65 | TODO: Document the built in expressions 66 | 67 | # Modules 68 | 69 | Throughout the configuration, custom modules can be used with specialized configuration. The only universal 70 | configuration option for a module is the 'type' field, all other fields are determined by the module itself. 71 | When a module section of the configuration is parsed, that section of the configuration is kept as an 72 | `unstructured::Document` so that the module can define as simple or complex of a configuration as required. 73 | Documentation for the individual modules should contain information about configuration specific to those modules. 74 | 75 | # Configuration Sections 76 | 77 | As demonstrated in the basic configuration above, these configuration sections are available: 78 | 79 | * **service**: Global service options such as listening addresses, cache store, etc. 80 | * **hosts**: This defines "host groups" which are simply groups of servers that can be referred to by name and are load balanced 81 | * **routes**: The list of routes that exist on this server. Incoming requests are matched to a route by a pattern and sent to a handler. 82 | 83 | # Service Configuration Options 84 | 85 | TODO: **WIP** 86 | 87 | ## interfaces 88 | 89 | ```yaml 90 | interfaces: 91 | # HTTP Configuration 92 | - address: "0.0.0.0:8080" 93 | # HTTPS Configuration 94 | - address: "0.0.0.0:8443" 95 | ssl: true 96 | ssl_cert: "cert.crt" 97 | ssl_key: "key.rsa" 98 | ``` 99 | 100 | ## cache 101 | 102 | ```yaml 103 | cache: # This is a 'cache' module 104 | type: memory_cache 105 | ``` 106 | 107 | # Host Configuration Options 108 | 109 | TODO: **WIP** 110 | 111 | ```yaml 112 | hosts: 113 | httpbin: # This is a 'load balancer' module 114 | type: round_robin 115 | servers: 116 | - "http://httpbin.org" 117 | ``` 118 | 119 | # Route Configuration Options 120 | 121 | TODO: **WIP** 122 | 123 | ```yaml 124 | routes: 125 | - path: 126 | type: regex 127 | pattern: "^/my/profile$" 128 | handler: # this is a 'handler' module 129 | type: host 130 | host: host_group_name 131 | path: "/user/{{ auth.claim('userid') }}/profile" 132 | cache: # this is a 'cache' module 133 | type: cache_response 134 | # Other module types currently supported here: authenticator, authorizer, plugin 135 | ``` 136 | */ 137 | 138 | mod builder; 139 | pub(crate) mod parsers; 140 | 141 | pub use builder::*; 142 | -------------------------------------------------------------------------------- /katalyst/src/config/parsers/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{config::builder::KatalystBuilder, prelude::*}; 2 | use std::{ffi::OsStr, fs::File, io::prelude::*, path::Path}; 3 | 4 | pub(crate) fn parse_file(file_path: &str) -> Result { 5 | let path = Path::new(file_path); 6 | let contents = load_file(path)?; 7 | 8 | Ok(Parser::from_str(&contents, Format::ext(path.extension().and_then(OsStr::to_str)))?) 9 | } 10 | 11 | fn load_file(path: &Path) -> Result { 12 | info!("Loading file from: {}", path.canonicalize()?.display()); 13 | 14 | let mut file = File::open(path)?; 15 | let mut contents = String::new(); 16 | file.read_to_string(&mut contents)?; 17 | Ok(contents) 18 | } 19 | -------------------------------------------------------------------------------- /katalyst/src/context/auth.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | /// Authentication detail for this context 4 | #[derive(Debug, Clone)] 5 | pub enum Authentication { 6 | /// An anonymous request (not authenticated) 7 | Anonymous, 8 | /// An authenticated request 9 | Authenticated { 10 | /// The claims associated with this request 11 | claims: HashMap>, 12 | }, 13 | } 14 | 15 | impl Authentication { 16 | /// Add a claim to this authentication context. This has no effect if the current 17 | /// authentication type is anonymous. 18 | pub fn add_claim(&mut self, claim_type: String, claim_value: String) { 19 | if let Authentication::Authenticated { claims } = self { 20 | if let Some(claim) = claims.get_mut(&claim_type) { 21 | claim.push(claim_value); 22 | } else { 23 | claims.insert(claim_type, vec![claim_value]); 24 | } 25 | } 26 | } 27 | 28 | /// Retrieve a claim from this authentication context. 29 | pub fn get_claim(&self, claim_type: String) -> String { 30 | if let Authentication::Authenticated { claims } = self { 31 | match claims.get(&claim_type) { 32 | Some(c) => c[0].to_string(), 33 | None => String::default(), 34 | } 35 | } else { 36 | String::default() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /katalyst/src/context/data.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | collections::HashMap, 4 | sync::Arc, 5 | }; 6 | 7 | #[derive(Debug)] 8 | struct Container { 9 | data: Arc, 10 | } 11 | 12 | impl Container { 13 | fn new(obj: T) -> Self { 14 | Container { data: Arc::new(obj) } 15 | } 16 | 17 | fn get(&self) -> Arc { 18 | self.data.clone() 19 | } 20 | } 21 | 22 | #[derive(Debug, Default)] 23 | pub struct ContextData { 24 | store: HashMap>, 25 | } 26 | 27 | impl ContextData { 28 | pub fn get(&self) -> Option> { 29 | let id = TypeId::of::(); 30 | let result = self.store.get(&id)?; 31 | let dc = result.downcast_ref::>()?; 32 | Some(dc.get()) 33 | } 34 | 35 | pub fn set(&mut self, item: T) { 36 | let id = TypeId::of::(); 37 | self.store.insert(id, Box::new(Container::new(item))); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /katalyst/src/context/matched.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::instance::Route; 3 | use std::collections::HashMap; 4 | use std::sync::Arc; 5 | 6 | /// Detail on the matched route currently associated with this context 7 | #[derive(Debug)] 8 | pub enum Match { 9 | /// No route has been matched yet 10 | Unmatched, 11 | /// A route has been matched 12 | Matched { 13 | /// The detail of the matched route 14 | route: Arc, 15 | /// Hashmap of variables that have been captured from this route match 16 | captures: HashMap, 17 | }, 18 | } 19 | 20 | impl Match { 21 | /// Returns a boolean indicating if a route has been matched to this request 22 | pub fn is_matched(&self) -> bool { 23 | match self { 24 | Match::Matched { .. } => true, 25 | Match::Unmatched => false, 26 | } 27 | } 28 | 29 | /// Return the details of the route attached to this request. Will return a 30 | /// GatewayError::RequestFailed with a status code of NOT_FOUND if the route 31 | /// has not been matched. 32 | pub fn route(&self) -> Result> { 33 | match self { 34 | Match::Matched { route, .. } => Ok(route.clone()), 35 | _ => fail!(NOT_FOUND), 36 | } 37 | } 38 | 39 | /// Retrieve the captures value for the specified key for the current route. 40 | /// Will return a GatewayError::RequestFailed with a status code of NOT_FOUND 41 | /// if the route has not been matched or if the capture does not exist. 42 | pub fn get_value(&self, key: &str) -> Result { 43 | match self { 44 | Match::Matched { captures, .. } => { 45 | Ok(captures.get(key).ok_or_else(|| fail!(_ NOT_FOUND))?.to_string()) 46 | } 47 | _ => fail!(NOT_FOUND), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /katalyst/src/context/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This module contains the request context information used by modules and the request 3 | processing pipeline. 4 | */ 5 | mod auth; 6 | mod data; 7 | mod matched; 8 | mod requests; 9 | 10 | use crate::{ 11 | app::Katalyst, 12 | instance::Route, 13 | prelude::*, 14 | util::{LockedResource, Resource}, 15 | }; 16 | use data::ContextData; 17 | use hyper::{Body, Request}; 18 | use parking_lot::Mutex; 19 | use std::{any::Any, net::SocketAddr, time::Instant}; 20 | 21 | pub use auth::Authentication; 22 | pub use matched::Match; 23 | pub use requests::*; 24 | 25 | /// The base Katalyst request context supplied to all modules and expressions 26 | #[derive(Debug, Default, Clone)] 27 | pub struct RequestContext { 28 | context: Arc, 29 | } 30 | 31 | impl std::ops::Deref for RequestContext { 32 | type Target = Arc; 33 | 34 | fn deref(&self) -> &Self::Target { 35 | &self.context 36 | } 37 | } 38 | 39 | /// The main request context 40 | #[derive(Debug)] 41 | pub struct Context { 42 | request: LockedResource, 43 | metadata: Arc, 44 | authentication: LockedResource, 45 | matched: LockedResource, 46 | data: Mutex, 47 | katalyst: Arc, 48 | } 49 | 50 | /// Metadata for this request context 51 | #[derive(Debug)] 52 | pub struct Metadata { 53 | /// Requeset processing start time 54 | pub started: Instant, 55 | /// Remote IP address of the client 56 | pub remote_ip: String, 57 | /// The parsed URL for this request 58 | pub url: url::Url, 59 | /// Holds the current load balancer lease for this request 60 | pub balancer_lease: Mutex>>, 61 | } 62 | 63 | impl Default for Context { 64 | fn default() -> Self { 65 | *Box::new(Context { 66 | request: LockedResource::new(HttpRequest::Empty), 67 | metadata: Arc::new(Metadata { 68 | remote_ip: String::default(), 69 | url: url::Url::parse("http://localhost/").unwrap(), 70 | balancer_lease: Mutex::new(None), 71 | started: Instant::now(), 72 | }), 73 | matched: LockedResource::new(Match::Unmatched), 74 | authentication: LockedResource::new(Authentication::Anonymous), 75 | data: Mutex::new(ContextData::default()), 76 | katalyst: Arc::new(Katalyst::default()), 77 | }) 78 | } 79 | } 80 | 81 | impl RequestContext { 82 | /// Create a new RequestContext with the supplied arguments 83 | pub fn new(request: Request, katalyst: Arc, remote_addr: SocketAddr) -> Self { 84 | let uri = request.uri(); 85 | let path = format!( 86 | "{scheme}://{host}{path}", 87 | scheme = &uri.scheme_str().unwrap_or("http"), 88 | host = &uri.host().unwrap_or("localhost"), 89 | path = &uri 90 | ); 91 | RequestContext { 92 | context: Arc::new(Context { 93 | request: LockedResource::new(HttpRequest::new(request)), 94 | metadata: Arc::new(Metadata { 95 | remote_ip: remote_addr.ip().to_string(), 96 | url: url::Url::parse(&path).unwrap(), 97 | balancer_lease: Mutex::new(None), 98 | started: Instant::now(), 99 | }), 100 | matched: LockedResource::new(Match::Unmatched), 101 | authentication: LockedResource::new(Authentication::Anonymous), 102 | data: Mutex::new(ContextData::default()), 103 | katalyst: katalyst.clone(), 104 | }), 105 | } 106 | } 107 | 108 | /// Get the base katalyst instance associated with this request 109 | pub fn katalyst(&self) -> Result> { 110 | Ok(self.katalyst.clone()) 111 | } 112 | 113 | /// This request's metadata 114 | pub fn metadata(&self) -> Result> { 115 | Ok(self.metadata.clone()) 116 | } 117 | 118 | /// Get authentication state of this request 119 | pub fn get_authentication(&self) -> Result> { 120 | Ok(self.authentication.get()) 121 | } 122 | 123 | /// Change the authentication state for this request 124 | pub fn set_authentication(&self, info: Authentication) -> Result<()> { 125 | self.authentication.set(info); 126 | Ok(()) 127 | } 128 | 129 | /// Get the match for this request 130 | pub fn get_match(&self) -> Result> { 131 | Ok(self.matched.get()) 132 | } 133 | 134 | /// Get the matched route for this request 135 | pub fn get_route(&self) -> Result> { 136 | let resource = self.get_match()?; 137 | Ok(resource.route()?.clone()) 138 | } 139 | 140 | /// Set the match for this request 141 | pub fn set_match(&self, matched: Match) -> Result<()> { 142 | self.matched.set(matched); 143 | Ok(()) 144 | } 145 | 146 | /// Get custom extension data of type T 147 | pub fn get_extension(&self) -> Result> { 148 | self.data.lock().get().ok_or_else(|| fail!(_ INTERNAL_SERVER_ERROR, "Attempted to retrieve a module that does not exist!")) 149 | } 150 | 151 | /// Set custom extension data of type T 152 | pub fn set_extension(&self, data: T) -> Result<()> { 153 | self.data.lock().set(data); 154 | Ok(()) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /katalyst/src/context/requests.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::util::*; 3 | use futures::{future::*, stream::Stream, Future}; 4 | use http::request::Parts; 5 | use hyper::{Body, Request, Response}; 6 | use unstructured::Document; 7 | 8 | /// Current details on the request or response currently associated with this context 9 | #[derive(Debug)] 10 | pub enum HttpRequest { 11 | /// No request/response available 12 | Empty, 13 | /// An unprocessed request 14 | RawRequest(Box<(Parts, Body)>), 15 | /// A request with all contents loaded into memory 16 | LoadedRequest(Box<(Parts, Vec)>), 17 | /// A request that is fully loaded and the request contents have been serialized into 18 | /// an unstructured::Document 19 | ParsedRequest(Box<(Parts, Vec, Document)>), 20 | /// An unprocessed response 21 | RawResponse(Box<(http::response::Parts, Body)>), 22 | /// A response with all contents loaded into memory 23 | LoadedResponse(Box<(http::response::Parts, Vec)>), 24 | /// A response that is fully loaded and the request contents have been serialized into 25 | /// an unstructured::Document 26 | ParsedResponse(Box<(http::response::Parts, Vec, Document)>), 27 | } 28 | 29 | impl Default for HttpRequest { 30 | fn default() -> Self { 31 | HttpRequest::Empty 32 | } 33 | } 34 | 35 | impl RequestContext { 36 | /// Load the request into memory 37 | pub fn preload(&self) -> ModuleResult { 38 | let guard = self.clone(); 39 | let req = ensure!(:guard.take_http_request()); 40 | match req { 41 | HttpRequest::RawRequest(r) => { 42 | let (data, body) = (r.0, r.1); 43 | Box::new(body.concat2().then(move |r| match r { 44 | Ok(body) => { 45 | let res = Box::new((data, body.into_iter().collect())); 46 | guard.set_http_request(HttpRequest::LoadedRequest(res))?; 47 | Ok(()) 48 | } 49 | Err(e) => fail!(INTERNAL_SERVER_ERROR, "Error occurred loading request", e), 50 | })) 51 | } 52 | HttpRequest::RawResponse(r) => { 53 | let (data, body) = (r.0, r.1); 54 | Box::new(body.concat2().then(move |r| match r { 55 | Ok(body) => { 56 | let res = Box::new((data, body.into_iter().collect())); 57 | guard.set_http_request(HttpRequest::LoadedResponse(res))?; 58 | Ok(()) 59 | } 60 | Err(e) => fail!(INTERNAL_SERVER_ERROR, "Error occurred loading response", e), 61 | })) 62 | } 63 | _ => { 64 | ensure!(:guard.set_http_request(req)); 65 | Box::new(ok(())) 66 | } 67 | } 68 | } 69 | 70 | /// Parse the request 71 | pub fn parse(&self) -> ModuleResult { 72 | let guard = self.clone(); 73 | Box::new(self.preload().and_then(move |_| { 74 | let hdr = guard.header("Content-Type").unwrap_or_default(); 75 | let format = Format::content_type(Some(&hdr)); 76 | let mut req = guard.take_http_request()?; 77 | if let HttpRequest::LoadedRequest(boxed) = req { 78 | let (data, body) = *boxed; 79 | let doc = match format.parse(&body) { 80 | Ok(d) => d, 81 | Err(_) => Document::Unit, 82 | }; 83 | req = HttpRequest::ParsedRequest(Box::new((data, body, doc))); 84 | } else if let HttpRequest::LoadedResponse(boxed) = req { 85 | let (data, body) = *boxed; 86 | let doc = match format.parse(&body) { 87 | Ok(d) => d, 88 | Err(_) => Document::Unit, 89 | }; 90 | req = HttpRequest::ParsedResponse(Box::new((data, body, doc))); 91 | } 92 | guard.set_http_request(req)?; 93 | Ok(()) 94 | })) 95 | } 96 | 97 | /// Get the HTTP request (Note: This will lock the request) 98 | pub fn get_http_request(&self) -> Result> { 99 | Ok(self.request.get()) 100 | } 101 | 102 | /// Take the request details, leaving behind a HttpRequest::Empty 103 | pub fn take_http_request(&self) -> Result { 104 | Ok(self.request.take()) 105 | } 106 | 107 | /// Set the current HTTP request 108 | pub fn set_http_request(&self, inreq: HttpRequest) -> Result { 109 | Ok(self.request.set(inreq)) 110 | } 111 | 112 | /// Take the HttpRequest and convert it into a hyper Request 113 | pub fn take_request(&self) -> Result> { 114 | let res: HttpRequest = self.take_http_request()?; 115 | Ok(match res { 116 | HttpRequest::RawRequest(data) => Request::from_parts(data.0, data.1), 117 | HttpRequest::LoadedRequest(data) => Request::from_parts(data.0, Body::from(data.1)), 118 | HttpRequest::ParsedRequest(data) => Request::from_parts(data.0, Body::from(data.1)), 119 | _ => Request::default(), 120 | }) 121 | } 122 | 123 | /// Take the HttpRequest and convert it into a hyper Response 124 | pub fn take_response(&self) -> Result> { 125 | let res: HttpRequest = self.take_http_request()?; 126 | Ok(match res { 127 | HttpRequest::RawResponse(data) => Response::from_parts(data.0, data.1), 128 | HttpRequest::LoadedResponse(data) => Response::from_parts(data.0, Body::from(data.1)), 129 | HttpRequest::ParsedResponse(data) => Response::from_parts(data.0, Body::from(data.1)), 130 | _ => Response::default(), 131 | }) 132 | } 133 | 134 | /// The HTTP method associated with this request 135 | pub fn method(&self) -> http::Method { 136 | let req: &HttpRequest = &self.request.get(); 137 | match req { 138 | HttpRequest::RawRequest(r) => r.0.method.clone(), 139 | HttpRequest::LoadedRequest(r) => r.0.method.clone(), 140 | HttpRequest::ParsedRequest(r) => r.0.method.clone(), 141 | _ => http::Method::GET, 142 | } 143 | } 144 | 145 | /// Get an HTTP header associated with this request 146 | pub fn header(&self, key: &str) -> Option { 147 | let req: &HttpRequest = &self.request.get(); 148 | let prts = match req { 149 | HttpRequest::RawRequest(r) => &r.0, 150 | HttpRequest::LoadedRequest(r) => &r.0, 151 | HttpRequest::ParsedRequest(r) => &r.0, 152 | _ => return None, 153 | }; 154 | if let Some(h) = prts.headers.get(key).map(|h| h.to_str().unwrap_or_default()) { 155 | Some(h.to_owned()) 156 | } else { 157 | None 158 | } 159 | } 160 | 161 | /// Set the HttpRequest using the provided hyper Response 162 | pub fn set_response(&self, rsp: Response) -> Result<()> { 163 | self.set_http_request(HttpRequest::RawResponse(Box::new(rsp.into_parts())))?; 164 | Ok(()) 165 | } 166 | 167 | /// Returns true if the HttpRequest currently holds a client request 168 | pub fn is_request(&self) -> Result { 169 | let req: &HttpRequest = &self.request.get(); 170 | Ok(match req { 171 | HttpRequest::RawRequest(_) 172 | | HttpRequest::LoadedRequest(_) 173 | | HttpRequest::ParsedRequest(_) => true, 174 | _ => false, 175 | }) 176 | } 177 | 178 | /// Returns true if the HttpRequest currently holds a service response 179 | pub fn is_response(&self) -> Result { 180 | let req: &HttpRequest = &self.request.get(); 181 | Ok(match req { 182 | HttpRequest::RawResponse(_) 183 | | HttpRequest::LoadedResponse(_) 184 | | HttpRequest::ParsedResponse(_) => true, 185 | _ => false, 186 | }) 187 | } 188 | } 189 | 190 | impl HttpRequest { 191 | /// Create a new HttpRequest using the supplied hyper Request 192 | pub fn new(req: Request) -> Self { 193 | HttpRequest::RawRequest(Box::new(req.into_parts())) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /katalyst/src/error/from.rs: -------------------------------------------------------------------------------- 1 | use super::GatewayError; 2 | use std::net::AddrParseError; 3 | 4 | impl From for GatewayError { 5 | fn from(err: std::io::Error) -> Self { 6 | err!(IoError, "An IO Error occurred", err) 7 | } 8 | } 9 | 10 | impl From for GatewayError { 11 | fn from(err: regex::Error) -> Self { 12 | err!(ConfigurationFailure, "Could not compile regex", err) 13 | } 14 | } 15 | 16 | impl From for GatewayError { 17 | fn from(err: http::method::InvalidMethod) -> Self { 18 | err!(ConfigurationFailure, "Invalid HTTP method", err) 19 | } 20 | } 21 | 22 | impl From for GatewayError { 23 | fn from(err: serde_yaml::Error) -> Self { 24 | err!(ConfigurationFailure, "Configuration parse error", err) 25 | } 26 | } 27 | 28 | impl From for GatewayError { 29 | fn from(err: serde_json::Error) -> Self { 30 | err!(ConfigurationFailure, "Configuration parse error", err) 31 | } 32 | } 33 | 34 | impl From> for GatewayError { 35 | fn from(err: pest::error::Error) -> Self { 36 | err!(ConfigurationFailure, "Invalid expression", err) 37 | } 38 | } 39 | 40 | impl From for GatewayError { 41 | fn from(err: std::num::ParseIntError) -> Self { 42 | err!(ConfigurationFailure, "Invalid number format", err) 43 | } 44 | } 45 | 46 | impl From for GatewayError { 47 | fn from(e: http::uri::InvalidUri) -> Self { 48 | err!(ConfigurationFailure, "Unable to parse URI", e) 49 | } 50 | } 51 | 52 | impl From for GatewayError { 53 | fn from(e: AddrParseError) -> Self { 54 | err!(ConfigurationFailure, "Unable to parse network address", e) 55 | } 56 | } 57 | 58 | impl From<&'static str> for GatewayError { 59 | fn from(err: &'static str) -> Self { 60 | err!(Other, err) 61 | } 62 | } 63 | 64 | impl From for GatewayError { 65 | fn from(err: String) -> Self { 66 | err!(Other, err) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /katalyst/src/error/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This module space contains the enum `GatewayError` which is the error type produced by 3 | the internal Katalyst libraries. 4 | */ 5 | 6 | mod from; 7 | mod result; 8 | 9 | use http::StatusCode; 10 | use std::error::Error; 11 | 12 | pub use result::*; 13 | 14 | type Source = Option>; 15 | #[doc(hidden)] 16 | pub trait SourceError: Error + std::fmt::Display + Send {} 17 | impl SourceError for T {} 18 | 19 | fn add_source(source: &Source) -> String { 20 | if let Some(s) = source { 21 | format!("\nCaused by: {}", s) 22 | } else { 23 | "".into() 24 | } 25 | } 26 | 27 | /// All Katalyst library methods will return a variant of GatewayError 28 | #[derive(Debug, Display)] 29 | pub enum GatewayError { 30 | /// This is the primary type that is returned when there is some error that occurs 31 | /// while processing a request. 32 | #[display( 33 | fmt = "[{} -> {}:{}] <{}> {}{}", 34 | module_path, 35 | line, 36 | col, 37 | status, 38 | message, 39 | "add_source(source)" 40 | )] 41 | RequestFailed { 42 | #[doc(hidden)] 43 | status: StatusCode, 44 | #[doc(hidden)] 45 | message: String, 46 | #[doc(hidden)] 47 | source: Source, 48 | #[doc(hidden)] 49 | module_path: &'static str, 50 | #[doc(hidden)] 51 | line: u32, 52 | #[doc(hidden)] 53 | col: u32, 54 | }, 55 | /// Error that occurs when there is a configuration failure 56 | #[display(fmt = "[{} -> {}:{}] {}{}", module_path, line, col, message, "add_source(source)")] 57 | ConfigurationFailure { 58 | #[doc(hidden)] 59 | message: String, 60 | #[doc(hidden)] 61 | source: Source, 62 | #[doc(hidden)] 63 | module_path: &'static str, 64 | #[doc(hidden)] 65 | line: u32, 66 | #[doc(hidden)] 67 | col: u32, 68 | }, 69 | 70 | /// Catastrophic and fatal errors 71 | #[display(fmt = "[{} -> {}:{}] {}{}", module_path, line, col, message, "add_source(source)")] 72 | Critical { 73 | #[doc(hidden)] 74 | message: String, 75 | #[doc(hidden)] 76 | source: Source, 77 | #[doc(hidden)] 78 | module_path: &'static str, 79 | #[doc(hidden)] 80 | line: u32, 81 | #[doc(hidden)] 82 | col: u32, 83 | }, 84 | 85 | /// A dependency that was expected is not available 86 | #[display( 87 | fmt = "[{} -> {}:{}] Component {}: {}{}", 88 | module_path, 89 | line, 90 | col, 91 | name, 92 | message, 93 | "add_source(source)" 94 | )] 95 | RequiredComponent { 96 | #[doc(hidden)] 97 | name: String, 98 | #[doc(hidden)] 99 | message: String, 100 | #[doc(hidden)] 101 | source: Source, 102 | #[doc(hidden)] 103 | module_path: &'static str, 104 | #[doc(hidden)] 105 | line: u32, 106 | #[doc(hidden)] 107 | col: u32, 108 | }, 109 | 110 | /// An IO Error, check the source error for more detail 111 | #[display(fmt = "[{} -> {}:{}] {}{}", module_path, line, col, message, "add_source(source)")] 112 | IoError { 113 | #[doc(hidden)] 114 | message: String, 115 | #[doc(hidden)] 116 | source: Source, 117 | #[doc(hidden)] 118 | module_path: &'static str, 119 | #[doc(hidden)] 120 | line: u32, 121 | #[doc(hidden)] 122 | col: u32, 123 | }, 124 | 125 | /// Other uncategorized/general error 126 | #[display(fmt = "[{} -> {}:{}] {}{}", module_path, line, col, message, "add_source(source)")] 127 | Other { 128 | #[doc(hidden)] 129 | message: String, 130 | #[doc(hidden)] 131 | source: Source, 132 | #[doc(hidden)] 133 | module_path: &'static str, 134 | #[doc(hidden)] 135 | line: u32, 136 | #[doc(hidden)] 137 | col: u32, 138 | }, 139 | 140 | /// Used in some circumstance to return from the request pipeline early 141 | #[display(fmt = "Request finished early")] 142 | Done, 143 | } 144 | 145 | impl Error for GatewayError {} 146 | 147 | impl GatewayError { 148 | pub(crate) fn status_code(&self) -> StatusCode { 149 | match *self { 150 | GatewayError::RequestFailed { status, .. } => status, 151 | GatewayError::Done => StatusCode::OK, 152 | GatewayError::IoError { .. } => StatusCode::CONFLICT, 153 | _ => StatusCode::INTERNAL_SERVER_ERROR, 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /katalyst/src/error/result.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | pub(crate) use futures::future::{done, err, ok}; 4 | 5 | /// Katalyst result type 6 | pub type Result = std::result::Result; 7 | /// Katalyst async result type 8 | pub type AsyncResult = Box + Send>; 9 | 10 | pub(crate) trait ResultExt { 11 | fn fut(self) -> AsyncResult; 12 | } 13 | 14 | impl ResultExt for Result { 15 | fn fut(self) -> AsyncResult { 16 | Box::new(done(self)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /katalyst/src/expression/bindings/auth.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | #[derive(ExpressionBinding)] 4 | #[expression(name = "auth", bind = claim)] 5 | pub struct Auth; 6 | 7 | impl Auth { 8 | fn claim(guard: &RequestContext, args: &[ExpressionArg]) -> ExpressionResult { 9 | Ok(guard.get_authentication()?.get_claim(args[0].render(guard)?).into()) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /katalyst/src/expression/bindings/content.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use unstructured::Document; 3 | 4 | #[derive(ExpressionBinding)] 5 | #[expression(name = "content", bind = val)] 6 | pub struct Content; 7 | 8 | impl Content { 9 | fn val(guard: &RequestContext, args: &[ExpressionArg]) -> ExpressionResult { 10 | let key = args[0].render(guard)?; 11 | let key = Document::String(key); 12 | let req = guard.get_http_request()?; 13 | let http_req: &HttpRequest = &req; 14 | if let HttpRequest::ParsedRequest(d) = http_req { 15 | let val = &d.2; 16 | let res = match val { 17 | Document::Map(map) => { 18 | Ok(map.get(&key).ok_or_else(|| fail!(_ INTERNAL_SERVER_ERROR, format!("Incoming request does not container key {}", key)))?) 19 | } 20 | _ => fail!(INTERNAL_SERVER_ERROR, "Incoming request malformed"), 21 | }?; 22 | match res { 23 | Document::String(s) => Ok(s.to_owned().into()), 24 | _ => fail!( 25 | INTERNAL_SERVER_ERROR, 26 | format!("Key {}'s value was not able to be displayed", key) 27 | ), 28 | } 29 | } else { 30 | fail!(=> INTERNAL_SERVER_ERROR, "Request contents not loaded") 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /katalyst/src/expression/bindings/encoding.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use base64::{decode, encode}; 3 | use std::str; 4 | 5 | #[derive(ExpressionBinding)] 6 | #[expression(name = "encode", bind = base64)] 7 | pub struct Encode; 8 | 9 | impl Encode { 10 | fn base64(guard: &RequestContext, args: &[ExpressionArg]) -> ExpressionResult { 11 | let to_encode = args[0].render(guard)?; 12 | Ok(encode(&to_encode).into()) 13 | } 14 | } 15 | 16 | #[derive(ExpressionBinding)] 17 | #[expression(name = "decode", bind = base64)] 18 | pub struct Decode; 19 | 20 | impl Decode { 21 | fn base64(guard: &RequestContext, args: &[ExpressionArg]) -> ExpressionResult { 22 | let to_decode = args[0].render(guard)?; 23 | Ok(str::from_utf8( 24 | decode(&to_decode) 25 | .map_err(|e| fail!(_ INTERNAL_SERVER_ERROR, "Could not load data as utf8", e))? 26 | .as_slice(), 27 | ) 28 | .map_err(|e| fail!(_ INTERNAL_SERVER_ERROR, "Could not decode base64 data", e))? 29 | .to_string() 30 | .into()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /katalyst/src/expression/bindings/http.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | #[derive(ExpressionBinding)] 4 | #[expression(name = "http", bind = method)] 5 | #[expression(bind = ip)] 6 | #[expression(bind = path)] 7 | #[expression(bind = query)] 8 | #[expression(bind = query_param)] 9 | #[expression(bind = header)] 10 | #[expression(bind = matched)] 11 | pub struct Http; 12 | 13 | impl Http { 14 | fn method(guard: &RequestContext, _: &[ExpressionArg]) -> ExpressionResult { 15 | Ok(guard.method().as_str().into()) 16 | } 17 | 18 | fn ip(guard: &RequestContext, _: &[ExpressionArg]) -> ExpressionResult { 19 | Ok(guard.metadata()?.remote_ip.to_owned().into()) 20 | } 21 | 22 | fn path(guard: &RequestContext, _: &[ExpressionArg]) -> ExpressionResult { 23 | Ok(guard.metadata()?.url.path().into()) 24 | } 25 | 26 | fn query(guard: &RequestContext, _: &[ExpressionArg]) -> ExpressionResult { 27 | Ok(guard.metadata()?.url.query().unwrap_or_default().into()) 28 | } 29 | 30 | fn query_param(guard: &RequestContext, args: &[ExpressionArg]) -> ExpressionResult { 31 | let metadata = guard.metadata()?; 32 | let name = args[0].render(guard)?; 33 | let res = metadata.url.query_pairs().find(|q| q.0 == name); 34 | res.map_or_else( 35 | || fail!(BAD_REQUEST, format!("Expected query parameter {}", name)), 36 | |v| Ok(v.1.to_string().into()), 37 | ) 38 | } 39 | 40 | fn header(guard: &RequestContext, args: &[ExpressionArg]) -> ExpressionResult { 41 | let arg = &args[0].render(guard)?; 42 | let hdr = guard 43 | .header(arg) 44 | .ok_or_else(|| fail!(_ BAD_REQUEST, format!("Expected header parameter {}", arg)))?; 45 | Ok(hdr.into()) 46 | } 47 | 48 | fn matched(guard: &RequestContext, args: &[ExpressionArg]) -> ExpressionResult { 49 | let value = args[0].render(guard)?; 50 | let result = guard.get_match()?.get_value(&value)?; 51 | Ok(result.into()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /katalyst/src/expression/bindings/mod.rs: -------------------------------------------------------------------------------- 1 | mod auth; 2 | mod content; 3 | mod encoding; 4 | mod http; 5 | mod sys; 6 | mod url; 7 | 8 | pub use self::{ 9 | auth::Auth, 10 | content::Content, 11 | encoding::{Decode, Encode}, 12 | http::Http, 13 | sys::Sys, 14 | url::Url, 15 | }; 16 | -------------------------------------------------------------------------------- /katalyst/src/expression/bindings/sys.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | #[derive(ExpressionBinding)] 4 | #[expression(name = "sys", bind = env)] 5 | pub struct Sys; 6 | 7 | impl Sys { 8 | fn env(guard: &RequestContext, args: &[ExpressionArg]) -> ExpressionResult { 9 | let env_var = args[0].render(guard)?; 10 | Ok(std::env::var_os(&env_var) 11 | .ok_or_else(|| fail!(_ INTERNAL_SERVER_ERROR, format!("Environment var {} not found!", &env_var)))? 12 | .to_str() 13 | .ok_or_else(|| fail!(_ INTERNAL_SERVER_ERROR, format!("Environment var {} is of invalid format!", &env_var)))? 14 | .into()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /katalyst/src/expression/bindings/url.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | #[derive(ExpressionBinding)] 4 | #[expression(name = "url", bind = segment)] 5 | #[expression(bind = all)] 6 | pub struct Url; 7 | 8 | impl Url { 9 | fn segment(guard: &RequestContext, args: &[ExpressionArg]) -> ExpressionResult { 10 | let mut result = String::new(); 11 | result.push_str(r"(?P<"); 12 | result.push_str(&args[0].render(guard)?); 13 | result.push_str(r">[^/]+)"); 14 | Ok(result.into()) 15 | } 16 | 17 | fn all(guard: &RequestContext, args: &[ExpressionArg]) -> ExpressionResult { 18 | let mut result = String::new(); 19 | result.push_str(r"(?P<"); 20 | result.push_str(&args[0].render(guard)?); 21 | result.push_str(r">.*)"); 22 | Ok(result.into()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /katalyst/src/expression/compiler/compiled.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use std::{fmt, sync::Arc}; 3 | use unstructured::Document; 4 | 5 | pub struct CompiledExpressionNode { 6 | pub name: String, 7 | pub result: ExpressionResultType, 8 | pub args: Vec>, 9 | pub render_fn: ExpressionRenderMethod, 10 | } 11 | 12 | impl CompiledExpression for CompiledExpressionNode { 13 | fn render(&self, guard: &RequestContext) -> RenderResult { 14 | Ok(self.result(guard)?.to_string()) 15 | } 16 | 17 | fn result(&self, guard: &RequestContext) -> ExpressionResult { 18 | (self.render_fn)(guard, &&self.args) 19 | } 20 | 21 | fn result_type(&self) -> Document { 22 | "".into() 23 | } 24 | } 25 | 26 | impl CompiledExpression for Document { 27 | fn render(&self, _: &RequestContext) -> RenderResult { 28 | Ok(self.to_string()) 29 | } 30 | 31 | fn result(&self, _: &RequestContext) -> ExpressionResult { 32 | Ok(self.clone()) 33 | } 34 | 35 | fn result_type(&self) -> Document { 36 | self.clone() 37 | } 38 | } 39 | 40 | impl CompiledExpression for String { 41 | fn render(&self, _: &RequestContext) -> RenderResult { 42 | Ok(self.to_string()) 43 | } 44 | 45 | fn result(&self, _: &RequestContext) -> ExpressionResult { 46 | Ok(self.as_str().into()) 47 | } 48 | 49 | fn result_type(&self) -> Document { 50 | "".into() 51 | } 52 | } 53 | 54 | impl CompiledExpression for i64 { 55 | fn render(&self, _: &RequestContext) -> RenderResult { 56 | Ok(self.to_string()) 57 | } 58 | 59 | fn result(&self, _: &RequestContext) -> ExpressionResult { 60 | Ok((*self).into()) 61 | } 62 | 63 | fn result_type(&self) -> Document { 64 | (0 as i64).into() 65 | } 66 | } 67 | 68 | impl CompiledExpression for bool { 69 | fn render(&self, _: &RequestContext) -> RenderResult { 70 | Ok(self.to_string()) 71 | } 72 | 73 | fn result(&self, _: &RequestContext) -> ExpressionResult { 74 | Ok((*self).into()) 75 | } 76 | 77 | fn result_type(&self) -> Document { 78 | true.into() 79 | } 80 | } 81 | 82 | impl fmt::Debug for CompiledExpressionNode { 83 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 84 | write!(f, "{}(", &self.name)?; 85 | self.args.fmt(f)?; 86 | write!(f, ") -> ")?; 87 | match self.result { 88 | ExpressionResultType::Text => write!(f, "str"), 89 | ExpressionResultType::Number => write!(f, "i64"), 90 | ExpressionResultType::Boolean => write!(f, "bool"), 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /katalyst/src/expression/compiler/mod.rs: -------------------------------------------------------------------------------- 1 | mod compiled; 2 | pub(crate) mod nodes; 3 | 4 | use crate::expression::*; 5 | use compiled::*; 6 | use std::collections::HashMap; 7 | 8 | type BuilderDirectory = HashMap<&'static str, Box>; 9 | 10 | /// This struct and directory is used for compiling expression bindings 11 | pub struct Compiler { 12 | builders: BuilderDirectory, 13 | } 14 | 15 | impl Compiler { 16 | /// Register an expression binding 17 | pub fn register(&mut self, provider: Box) { 18 | self.builders.insert(provider.identifier(), provider); 19 | } 20 | 21 | /// Create a compiler with an empty set of expression bindings 22 | pub fn empty() -> Self { 23 | Compiler { builders: HashMap::new() } 24 | } 25 | 26 | pub(crate) fn compile_template_map( 27 | &self, 28 | template: &Option>, 29 | ) -> Result>> { 30 | match template { 31 | Some(m) => Ok(Some({ 32 | let mut result = HashMap::::new(); 33 | for i in m { 34 | result.insert(i.0.to_string(), self.compile_template(Some(i.1))?); 35 | } 36 | result 37 | })), 38 | None => Ok(None), 39 | } 40 | } 41 | 42 | pub(crate) fn compile_template_option( 43 | &self, 44 | template: Option<&str>, 45 | ) -> Result> { 46 | match template { 47 | Some(_) => Ok(Some(self.compile_template(template)?)), 48 | None => Ok(None), 49 | } 50 | } 51 | 52 | /// Compile a template string into a prepared expression 53 | pub fn compile_template(&self, template: Option<&str>) -> Result { 54 | let tmpl = req!(template); 55 | Ok(nodes::parse_template(tmpl, &self.builders)?) 56 | } 57 | } 58 | 59 | impl Default for Compiler { 60 | fn default() -> Self { 61 | let mut providers = Compiler::empty(); 62 | providers.register(Sys.into()); 63 | providers.register(Http.into()); 64 | providers.register(Auth.into()); 65 | providers.register(Url.into()); 66 | providers.register(Content.into()); 67 | providers.register(Encode.into()); 68 | providers.register(Decode.into()); 69 | providers 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /katalyst/src/expression/compiler/nodes.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(clippy::eval_order_dependence)] 3 | use super::*; 4 | use crate::prelude::*; 5 | use pest::Parser; 6 | use pest_derive::*; 7 | 8 | #[derive(Parser)] 9 | #[grammar = "expression/expr.pest"] 10 | #[allow(dead_code)] 11 | struct TemplateParser; 12 | 13 | #[derive(Debug)] 14 | pub enum ExpressionMetadata { 15 | Raw(String), 16 | Number(i64), 17 | Bool(bool), 18 | Text(String), 19 | Expression { module: String, method: String, args: Vec }, 20 | } 21 | 22 | impl ExpressionMetadata { 23 | pub fn compile( 24 | self, 25 | directory: &BuilderDirectory, 26 | ) -> std::result::Result, GatewayError> { 27 | match self { 28 | ExpressionMetadata::Expression { module, method, args } => { 29 | let builder = directory.get(&module.as_str()); 30 | match builder { 31 | Some(b) => { 32 | let mut c_args: Vec> = vec![]; 33 | for arg in args.into_iter() { 34 | c_args.push(arg.compile(directory)?); 35 | } 36 | Ok(Arc::new(CompiledExpressionNode { 37 | name: module.to_owned(), 38 | render_fn: b.make_fn(&method, &c_args)?, 39 | args: c_args, 40 | result: ExpressionResultType::Text, 41 | })) 42 | } 43 | None => Err(err!( 44 | ConfigurationFailure, 45 | format!("Could not find expression module named {}", module) 46 | )), 47 | } 48 | } 49 | ExpressionMetadata::Raw(text) | ExpressionMetadata::Text(text) => Ok(Arc::new(text)), 50 | ExpressionMetadata::Number(number) => Ok(Arc::new(number)), 51 | ExpressionMetadata::Bool(cnd) => Ok(Arc::new(cnd)), 52 | } 53 | } 54 | } 55 | 56 | pub fn parse_template( 57 | input: &str, 58 | directory: &BuilderDirectory, 59 | ) -> Result>> { 60 | let tokens = TemplateParser::parse(Rule::template, input)?; 61 | let metadata = parse_tokens(tokens)?; 62 | let mut result = vec![]; 63 | for item in metadata.into_iter() { 64 | result.push(item.compile(directory)?); 65 | } 66 | Ok(result) 67 | } 68 | 69 | fn parse_tokens( 70 | pairs: pest::iterators::Pairs<'_, Rule>, 71 | ) -> std::result::Result, GatewayError> { 72 | let mut result = vec![]; 73 | for pair in pairs { 74 | match pair.as_rule() { 75 | Rule::raw_block => result.push(ExpressionMetadata::Raw(pair.as_str().into())), 76 | Rule::number_lit => result.push(ExpressionMetadata::Number(pair.as_str().parse()?)), 77 | Rule::true_lit => result.push(ExpressionMetadata::Bool(true)), 78 | Rule::false_lit => result.push(ExpressionMetadata::Bool(false)), 79 | Rule::string_lit => result 80 | .push(ExpressionMetadata::Text(pair.into_inner().as_str().replace("\\'", "'"))), 81 | Rule::object_call => result.push(parse_object(pair.into_inner())?), 82 | Rule::EOI => return Ok(result), 83 | _ => { 84 | return Err(err!( 85 | ConfigurationFailure, 86 | format!("Unexpected element when parsing expression {}", pair) 87 | )) 88 | } 89 | } 90 | } 91 | Ok(result) 92 | } 93 | 94 | fn parse_object( 95 | mut pairs: pest::iterators::Pairs<'_, Rule>, 96 | ) -> std::result::Result { 97 | let module = pairs.next().unwrap().as_str().to_string(); 98 | let method = pairs.next().unwrap().as_str().to_string(); 99 | Ok(ExpressionMetadata::Expression { module, method, args: parse_tokens(pairs)? }) 100 | } 101 | 102 | #[cfg(test)] 103 | mod test { 104 | use super::*; 105 | 106 | lazy_static! { 107 | static ref BUILDERS: BuilderDirectory = BuilderDirectory::default(); 108 | } 109 | 110 | #[test] 111 | fn test_parser() -> std::result::Result<(), GatewayError> { 112 | let result = parse_template("/this/is/a/ {{ 'path' }} /to/something", &BUILDERS)?; 113 | println!("{:?}", result); 114 | Ok(()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /katalyst/src/expression/expr.pest: -------------------------------------------------------------------------------- 1 | WHITESPACE = _{ " " | "\t" | "\n" | "\r" } 2 | 3 | template = _{ SOI ~ (raw_block | template_block)+ ~ EOI } 4 | raw_block = @{ (!"{{" ~ ANY)+ } 5 | template_block = _{ "{{" ~ (!"}}" ~ expression) ~ "}}" } 6 | 7 | expression = _{ object_call | bool_lit | number_lit | string_lit } 8 | expression_args = _{ (expression ~ ("," ~ expression)*)? } 9 | 10 | object_call = { ident ~ "." ~ ident ~ "(" ~ expression_args ~ ")" } 11 | 12 | bool_lit = _{ true_lit | false_lit } 13 | true_lit = { "true" } 14 | false_lit = { "false" } 15 | number_lit = @{ "-"? ~ ASCII_DIGIT+ } 16 | string_lit = ${ "'" ~ string_lit_chars ~ "'" } 17 | string_lit_chars = @{ (!"'" ~ ("\\'" | ANY))* } 18 | ident = @{ ASCII_ALPHA ~ ASCII_ALPHANUMERIC*? } 19 | -------------------------------------------------------------------------------- /katalyst/src/expression/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Katalyst configuration is based around expressions. Expressions are a syntax for templating and 3 | customization from within the configuration. 4 | */ 5 | 6 | mod bindings; 7 | pub(crate) mod compiler; 8 | mod traits; 9 | 10 | use crate::prelude::*; 11 | use bindings::*; 12 | pub use compiler::Compiler; 13 | use std::sync::Arc; 14 | pub use traits::*; 15 | use unstructured::Document; 16 | 17 | /// Arguments passed to an expression 18 | pub type ExpressionArgs = Vec>; 19 | /// The base expression 20 | pub type Expression = Vec>; 21 | 22 | impl CompiledExpression for Expression { 23 | fn render(&self, guard: &RequestContext) -> RenderResult { 24 | let mut result = String::new(); 25 | for part in self.iter() { 26 | result.push_str(&part.render(guard)?); 27 | } 28 | Ok(result) 29 | } 30 | 31 | fn result(&self, guard: &RequestContext) -> ExpressionResult { 32 | let mut res = vec![]; 33 | for exp in self.iter() { 34 | res.push(exp.result(guard)?); 35 | } 36 | Ok(res.into()) 37 | } 38 | 39 | fn result_type(&self) -> Document { 40 | Document::Seq(vec![]) 41 | } 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use super::*; 47 | 48 | #[test] 49 | fn compile_template() { 50 | let compiler = Compiler::default(); 51 | compiler.compile_template(Some("/testing/the/parser/{{http.ip()}}/test")).unwrap(); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /katalyst/src/expression/traits/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use std::{fmt::Debug, sync::Arc}; 3 | use unstructured::Document; 4 | 5 | lazy_static! { 6 | static ref DEF_STRING: String = String::default(); 7 | } 8 | 9 | /// The result type of rendering an expression 10 | pub type RenderResult = Result; 11 | /// The result type of calling an expression 12 | pub type ExpressionResult = Result; 13 | /// A single expression argument 14 | pub type ExpressionArg = Arc; 15 | /// The method on an expression used to render the result 16 | pub type ExpressionRenderMethod = 17 | Arc ExpressionResult + Send + Sync>; 18 | 19 | /// Metadata indicating the type of result from this expression 20 | #[derive(Clone)] 21 | pub enum ExpressionResultType { 22 | /// String result 23 | Text, 24 | /// Numeric result 25 | Number, 26 | /// Boolean result 27 | Boolean, 28 | } 29 | 30 | /// This is the trait used by Katalyst for building the placeholders used in a downstream URL template 31 | pub trait ExpressionBinding: Send + Sync { 32 | /// The identifier in this template to locate that this provider should be used 33 | fn identifier(&self) -> &'static str; 34 | /// This returns the render function for this expression 35 | fn make_fn(&self, name: &str, args: &[ExpressionArg]) -> Result; 36 | } 37 | 38 | /// This is the trait that must be implemented by any expression that can be compiled from config 39 | pub trait CompiledExpression: Send + Sync + Debug { 40 | /// Render processes the compiled expression and returns a string rendering of the contents regardless of underlying types 41 | fn render(&self, guard: &RequestContext) -> RenderResult; 42 | /// Get the direct result of evaluating the expression 43 | fn result(&self, guard: &RequestContext) -> ExpressionResult; 44 | /// Return a document shell indicating the type 45 | fn result_type(&self) -> Document; 46 | } 47 | -------------------------------------------------------------------------------- /katalyst/src/instance/hosts.rs: -------------------------------------------------------------------------------- 1 | use crate::modules::{balancer::default_balancer, *}; 2 | use std::sync::Arc; 3 | 4 | /// This is the directory of hosts/load balancers attacyed to this instance 5 | #[derive(Debug)] 6 | pub struct Hosts { 7 | /// The actual directory 8 | pub servers: Arc, 9 | } 10 | 11 | impl Default for Hosts { 12 | fn default() -> Self { 13 | Hosts { servers: default_balancer() } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /katalyst/src/instance/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Instance provides details for the current running state of Katalyst. 3 | */ 4 | 5 | mod hosts; 6 | mod route; 7 | mod service; 8 | 9 | pub use hosts::Hosts; 10 | pub use route::Route; 11 | pub use service::{Interface, Service}; 12 | use std::{collections::HashMap, sync::Arc}; 13 | 14 | /// The primary Katalyst instance configuration 15 | #[derive(Debug, Default)] 16 | pub struct Instance { 17 | /// This is the directory of hosts/load balancers attacyed to this instance 18 | pub hosts: HashMap, 19 | /// The routes associated with this instance 20 | pub routes: Vec>, 21 | /// Base service metadata 22 | pub service: Service, 23 | } 24 | -------------------------------------------------------------------------------- /katalyst/src/instance/route.rs: -------------------------------------------------------------------------------- 1 | use crate::modules::*; 2 | use http::Method; 3 | use regex::Regex; 4 | use std::{collections::HashSet, sync::Arc}; 5 | 6 | /// Modules and other data associated with a specific route 7 | #[derive(Debug)] 8 | pub struct Route { 9 | /// The URI pattern used to match a request to this route 10 | pub pattern: Regex, 11 | /// Child routes 12 | pub children: Option>>, 13 | /// The request handler for this route 14 | pub handler: Arc, 15 | /// Plugin modules for this route 16 | pub plugins: Option>>, 17 | /// Authorization modules for this route 18 | pub authorizers: Option>>, 19 | /// Cache handler for this route 20 | pub cache: Option>, 21 | /// Valid methods for this route. If `None` then any method is allowed 22 | pub methods: Option>, 23 | /// Authenticator modules for this route 24 | pub authenticators: Option>>, 25 | } 26 | -------------------------------------------------------------------------------- /katalyst/src/instance/service.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use std::{net::SocketAddr, sync::Arc}; 3 | 4 | /// This instance's interface description 5 | #[derive(Debug)] 6 | pub enum Interface { 7 | /// An HTTP interface 8 | Http { 9 | /// The binding address for this interface 10 | addr: SocketAddr, 11 | }, 12 | /// An HTTPS interface 13 | Https { 14 | /// The binding address for this interface 15 | addr: SocketAddr, 16 | /// The certifacte path 17 | cert: String, 18 | /// The certificate key path 19 | key: String, 20 | }, 21 | } 22 | 23 | impl Default for Interface { 24 | fn default() -> Self { 25 | Interface::Http { addr: "0.0.0.0:8080".parse().unwrap() } 26 | } 27 | } 28 | 29 | /// The API Gateway service metadata 30 | #[derive(Debug)] 31 | pub struct Service { 32 | /// Array of interfaces for this service 33 | pub interfaces: Vec, 34 | /// The cache provider for this service 35 | pub cache: Arc, 36 | } 37 | 38 | impl Default for Service { 39 | fn default() -> Self { 40 | Service { interfaces: vec![], cache: cache::default_cache() } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /katalyst/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This is the base module for the Katalyst API Gateway library. 3 | */ 4 | 5 | #![warn(missing_docs)] 6 | #![recursion_limit = "128"] 7 | 8 | #[macro_use] 9 | extern crate log; 10 | 11 | #[macro_use] 12 | extern crate lazy_static; 13 | 14 | #[macro_use] 15 | extern crate derive_more; 16 | 17 | #[macro_use] 18 | pub mod prelude; 19 | 20 | mod app; 21 | mod instance; 22 | mod server; 23 | mod util; 24 | 25 | pub(crate) mod parser; 26 | 27 | pub mod config; 28 | pub mod context; 29 | pub mod error; 30 | pub mod expression; 31 | pub mod modules; 32 | pub use app::Katalyst; 33 | pub use katalyst_macros::ExpressionBinding; 34 | -------------------------------------------------------------------------------- /katalyst/src/main.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Katalyst is a high performance and low memory API Gateway. It can be used as either an 3 | appliance through Docker or it can be used as a library. This project is currently under 4 | heavy development and will likely experience many changes and issues as we work towards the 5 | 1.0 release. 6 | 7 | # Features 8 | 9 | * Configuration via YAML files 10 | * Configuration design done using templating and modular 'expressions' for dynamic route handling 11 | * Request routing with either regex or custom route builders 12 | * Modular design for customization of the gateway, internal modules can be overridden 13 | * Load balance hosts using Round Robin, Least Connection, or Random algorithms 14 | * SSL/TLS Termination 15 | * Highly performant, with at least some use cases and loads outperforming nginx 16 | * Built on the tokio runtime with Hyper, leveraging async I/O where possible 17 | * Does not require rust nightly, despite the heavy async I/O 18 | * Usable as a rust library, standalone application, or lightweight docker container 19 | 20 | # Library usage 21 | 22 | For library usage, refer to the official [rust documentation](https://docs.rs/katalyst/). 23 | 24 | # Install 25 | 26 | Current installation of the binary requires Cargo, though other package formats may be coming soon. 27 | 28 | ```bash 29 | # Add --force if you need to overwrite an already installed version 30 | cargo install katalyst 31 | ``` 32 | 33 | # Usage 34 | 35 | Once installed, starting Katalyst is easy. Use the -c option to specify the config file. 36 | {{version}} 37 | 38 | ```bash 39 | ➤ katalyst -c config.yml 40 | 2019-06-25 19:44:03,103 INFO [katalyst::config::parsers] Loading file from: config.yml 41 | 2019-06-25 19:44:03,105 INFO [katalyst::server] Listening on http://0.0.0.0:8080 42 | 2019-06-25 19:44:03,105 INFO [katalyst::server] Listening on https://0.0.0.0:8443 43 | ... 44 | ``` 45 | 46 | Run with the help command or flags to get all CLI options 47 | 48 | ```bash 49 | ➤ katalyst help 50 | katalyst 0.2.0 51 | Phil Proctor 52 | High performance, modular API Gateway 53 | 54 | USAGE: 55 | katalyst [OPTIONS] [SUBCOMMAND] 56 | 57 | FLAGS: 58 | -h, --help Prints help information 59 | -V, --version Prints version information 60 | 61 | OPTIONS: 62 | -c, --config Config file [default: katalyst.yaml] 63 | -l, --log-level Logging level to use [default: info] 64 | 65 | SUBCOMMANDS: 66 | help Prints this message or the help of the given subcommand(s) 67 | run Start the API Gateway (default) 68 | ``` 69 | 70 | # Configuration 71 | 72 | Refer to the documentation [here](CONFIG.md) 73 | */ 74 | 75 | #[macro_use] 76 | extern crate log; 77 | 78 | mod cli; 79 | 80 | use cli::{Args, Command}; 81 | use katalyst::Katalyst; 82 | 83 | fn main() { 84 | ::std::process::exit(match start() { 85 | Err(e) => { 86 | error!("Could not start services. {}", e); 87 | 1 88 | } 89 | Ok(_) => 0, 90 | }) 91 | } 92 | 93 | fn start() -> Result<(), String> { 94 | let args = Args::new(); 95 | simple_logger::init_with_level(args.log_level).map_err(|e| format!("{}", e))?; 96 | match args.command.as_ref().unwrap_or(&Command::Run) { 97 | Command::Run => { 98 | Katalyst::start(&args.config).map_err(|e| format!("{}", e))?; 99 | Ok(()) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /katalyst/src/modules/authentication/always.rs: -------------------------------------------------------------------------------- 1 | use crate::{app::Katalyst, modules::*, prelude::*}; 2 | 3 | #[derive(Default, Debug)] 4 | pub struct AlwaysAuthenticator; 5 | 6 | impl ModuleProvider for AlwaysAuthenticator { 7 | fn name(&self) -> &'static str { 8 | "always" 9 | } 10 | 11 | fn build(&self, _: ModuleType, _: Arc, _: &unstructured::Document) -> Result { 12 | Ok(AlwaysAuthenticator.into_module()) 13 | } 14 | } 15 | 16 | impl AuthenticatorModule for AlwaysAuthenticator { 17 | fn authenticate(&self, guard: RequestContext) -> AsyncResult<()> { 18 | let mut result = Authentication::Authenticated { claims: HashMap::default() }; 19 | result.add_claim("KatalystAuthenticator".to_string(), "always".to_string()); 20 | guard.set_authentication(result).fut() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /katalyst/src/modules/authentication/http.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | app::Katalyst, 3 | config::Builder, 4 | modules::*, 5 | util::{ClientRequestBuilder, CompiledClientRequest}, 6 | }; 7 | use futures::Future; 8 | 9 | #[derive(Default, Debug)] 10 | pub struct HttpAuthenticatorBuilder; 11 | 12 | impl ModuleProvider for HttpAuthenticatorBuilder { 13 | fn name(&self) -> &'static str { 14 | "http" 15 | } 16 | 17 | fn build( 18 | &self, 19 | _: ModuleType, 20 | kat: Arc, 21 | config: &unstructured::Document, 22 | ) -> Result { 23 | let request: ClientRequestBuilder = config.clone().try_into().map_err(|e| { 24 | err!( 25 | ConfigurationFailure, 26 | "Failed to parse HTTP authentication module configuration", 27 | e 28 | ) 29 | })?; 30 | Ok(HttpAuthenticator { request: request.build(kat)? }.into_module()) 31 | } 32 | } 33 | 34 | #[derive(Debug)] 35 | pub struct HttpAuthenticator { 36 | request: CompiledClientRequest, 37 | } 38 | 39 | impl AuthenticatorModule for HttpAuthenticator { 40 | fn authenticate(&self, guard: RequestContext) -> AsyncResult<()> { 41 | let request = ensure!(:self.request.prepare_request(&guard)); 42 | let client = ensure!(:guard.katalyst()).get_client(); 43 | Box::new(request.send_parse(&client).then(move |response| match response { 44 | Ok(_) => { 45 | let mut auth = Authentication::Authenticated { claims: HashMap::default() }; 46 | auth.add_claim("KatalystAuthenticator".to_string(), "http".to_string()); 47 | guard.set_authentication(auth) 48 | } 49 | Err(e) => fail!(FORBIDDEN, "Access rejected due to downstream authenticator error", e), 50 | })) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /katalyst/src/modules/authentication/mod.rs: -------------------------------------------------------------------------------- 1 | mod always; 2 | mod http; 3 | mod never; 4 | mod whitelist; 5 | 6 | pub use self::http::HttpAuthenticatorBuilder; 7 | pub use always::AlwaysAuthenticator; 8 | pub use never::NeverAuthenticator; 9 | pub use whitelist::WhitelistBuilder; 10 | -------------------------------------------------------------------------------- /katalyst/src/modules/authentication/never.rs: -------------------------------------------------------------------------------- 1 | use crate::{app::Katalyst, modules::*}; 2 | use futures::future::err; 3 | 4 | #[derive(Default, Debug)] 5 | pub struct NeverAuthenticator; 6 | 7 | impl ModuleProvider for NeverAuthenticator { 8 | fn name(&self) -> &'static str { 9 | "never" 10 | } 11 | 12 | fn build(&self, _: ModuleType, _: Arc, _: &unstructured::Document) -> Result { 13 | Ok(NeverAuthenticator.into_module()) 14 | } 15 | } 16 | 17 | impl AuthenticatorModule for NeverAuthenticator { 18 | fn authenticate(&self, _: RequestContext) -> ModuleResult { 19 | fail!(:FORBIDDEN, "This module always rejects requests") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /katalyst/src/modules/authentication/whitelist.rs: -------------------------------------------------------------------------------- 1 | use crate::{app::Katalyst, modules::*}; 2 | use futures::future::*; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Clone, Debug, Serialize, Deserialize)] 6 | #[serde(rename_all = "snake_case")] 7 | struct WhitelistConfig { 8 | ips: Vec, 9 | } 10 | 11 | #[derive(Default, Debug)] 12 | pub struct WhitelistBuilder; 13 | 14 | impl ModuleProvider for WhitelistBuilder { 15 | fn name(&self) -> &'static str { 16 | "whitelist" 17 | } 18 | 19 | fn build( 20 | &self, 21 | _: ModuleType, 22 | _: Arc, 23 | config: &unstructured::Document, 24 | ) -> Result { 25 | let c: WhitelistConfig = config.clone().try_into().map_err(|e| { 26 | err!( 27 | ConfigurationFailure, 28 | "Failed to parse Whitelist authentication module configuration", 29 | e 30 | ) 31 | })?; 32 | Ok(Whitelist { ips: c.ips }.into_module()) 33 | } 34 | } 35 | 36 | #[derive(Default, Debug)] 37 | pub struct Whitelist { 38 | ips: Vec, 39 | } 40 | 41 | impl AuthenticatorModule for Whitelist { 42 | fn authenticate(&self, guard: RequestContext) -> ModuleResult { 43 | let metadata = ensure!(:guard.metadata()); 44 | if self.ips.contains(&metadata.remote_ip) { 45 | Box::new(ok(())) 46 | } else { 47 | fail!(:FORBIDDEN) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /katalyst/src/modules/authorization/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /katalyst/src/modules/balancer/least_connection.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Default, Debug)] 4 | pub struct LeastConnectionBalancerBuilder; 5 | 6 | impl ModuleProvider for LeastConnectionBalancerBuilder { 7 | fn name(&self) -> &'static str { 8 | "least_connection" 9 | } 10 | 11 | fn build( 12 | &self, 13 | _: ModuleType, 14 | _: Arc, 15 | doc: &unstructured::Document, 16 | ) -> Result { 17 | let hosts: Vec = doc["servers"].clone().try_into().unwrap_or_default(); 18 | let mut arc_hosts = vec![]; 19 | for new_host in hosts.iter() { 20 | arc_hosts.push(Arc::new(new_host.to_string())); 21 | } 22 | Ok(LeastConnectionBalancer { hosts: arc_hosts }.into_module()) 23 | } 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct LeastConnectionBalancer { 28 | hosts: Vec>, 29 | } 30 | 31 | impl LoadBalancerModule for LeastConnectionBalancer { 32 | fn lease(&self) -> BalancerLease { 33 | let element = self.hosts.iter().fold(&self.hosts[0], |last, current| { 34 | if Arc::strong_count(current) < Arc::strong_count(last) { 35 | current 36 | } else { 37 | last 38 | } 39 | }); 40 | Ok(Arc::clone(element)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /katalyst/src/modules/balancer/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This module provides all of the built in load balancers for Katalyst. 3 | 4 | There are three major load balancer types for Katalyst: 5 | 6 | - **least_connection**: Route requests to the service with the least amount of connections currently leased 7 | - **random**: Simply route requests to hosts at random 8 | - **round_robin**: Route one request to one host at a time 9 | 10 | For most cases, least_connection is the preferred balancer type. 11 | */ 12 | 13 | mod least_connection; 14 | mod random; 15 | mod round_robin; 16 | 17 | use crate::prelude::*; 18 | use std::sync::Arc; 19 | 20 | pub use least_connection::LeastConnectionBalancerBuilder; 21 | pub use random::RandomBalancerBuilder; 22 | pub use round_robin::RoundRobinBalancerBuilder; 23 | 24 | pub(crate) fn default_balancer() -> Arc { 25 | Arc::new(round_robin::RoundRobinBalancer::default()) 26 | } 27 | -------------------------------------------------------------------------------- /katalyst/src/modules/balancer/random.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use rand::Rng; 3 | 4 | #[derive(Default, Debug)] 5 | pub struct RandomBalancerBuilder; 6 | 7 | impl ModuleProvider for RandomBalancerBuilder { 8 | fn name(&self) -> &'static str { 9 | "random" 10 | } 11 | 12 | fn build( 13 | &self, 14 | _: ModuleType, 15 | _: Arc, 16 | doc: &unstructured::Document, 17 | ) -> Result { 18 | let hosts: Vec = doc["servers"].clone().try_into().unwrap_or_default(); 19 | let mut arc_hosts = vec![]; 20 | for new_host in hosts.iter() { 21 | arc_hosts.push(Arc::new(new_host.to_string())); 22 | } 23 | Ok(RandomBalancer { hosts: arc_hosts }.into_module()) 24 | } 25 | } 26 | 27 | #[derive(Debug)] 28 | pub struct RandomBalancer { 29 | hosts: Vec>, 30 | } 31 | 32 | impl LoadBalancerModule for RandomBalancer { 33 | fn lease(&self) -> BalancerLease { 34 | Ok(self.hosts[rand::thread_rng().gen_range(0, self.hosts.len())].clone()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /katalyst/src/modules/balancer/round_robin.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use parking_lot::Mutex; 3 | 4 | #[derive(Default, Debug)] 5 | pub struct RoundRobinBalancerBuilder; 6 | 7 | impl ModuleProvider for RoundRobinBalancerBuilder { 8 | fn name(&self) -> &'static str { 9 | "round_robin" 10 | } 11 | 12 | fn build( 13 | &self, 14 | _: ModuleType, 15 | _: Arc, 16 | doc: &unstructured::Document, 17 | ) -> Result { 18 | let hosts: Vec = doc["servers"].clone().try_into().unwrap_or_default(); 19 | let mut arc_hosts = vec![]; 20 | for new_host in hosts.iter() { 21 | arc_hosts.push(Arc::new(new_host.to_string())); 22 | } 23 | Ok(RoundRobinBalancer { hosts: arc_hosts, host_index: Mutex::new(0) }.into_module()) 24 | } 25 | } 26 | 27 | #[derive(Default, Debug)] 28 | pub struct RoundRobinBalancer { 29 | hosts: Vec>, 30 | host_index: Mutex, 31 | } 32 | 33 | impl RoundRobinBalancer { 34 | fn get_next_index(&self) -> usize { 35 | let len = self.hosts.len(); 36 | let mut index = self.host_index.lock(); 37 | *index = (*index + 1) % len; 38 | *index 39 | } 40 | } 41 | 42 | impl LoadBalancerModule for RoundRobinBalancer { 43 | fn lease(&self) -> BalancerLease { 44 | Ok(self.hosts[self.get_next_index()].clone()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /katalyst/src/modules/cache/cache_handler.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use futures::Future; 3 | use std::sync::Arc; 4 | 5 | #[derive(Debug)] 6 | pub struct DefaultCacheHandler; 7 | 8 | impl ModuleProvider for DefaultCacheHandler { 9 | fn name(&self) -> &'static str { 10 | "cache_response" 11 | } 12 | 13 | fn build(&self, _: ModuleType, _: Arc, _: &unstructured::Document) -> Result { 14 | Ok(DefaultCacheHandler.into_module()) 15 | } 16 | } 17 | 18 | impl CacheHandlerModule for DefaultCacheHandler { 19 | fn check_cache(&self, guard: RequestContext) -> ModuleResult { 20 | let katalyst = ensure!(:guard.katalyst()); 21 | let metadata = ensure!(:guard.metadata()); 22 | if let Ok(instance) = katalyst.get_instance() { 23 | let cache = instance.service.cache.clone(); 24 | Box::new(cache.get_key(metadata.url.as_str()).then(move |r| match r { 25 | Ok(r) => { 26 | guard.set_http_request(r.as_ref().clone().into_response())?; 27 | Err(GatewayError::Done) 28 | } 29 | Err(_) => Ok(()), 30 | })) 31 | } else { 32 | Ok(()).fut() 33 | } 34 | } 35 | 36 | fn update_cache(&self, guard: RequestContext) -> ModuleResult { 37 | if !ensure!(:guard.is_response()) { 38 | return Ok(()).fut(); 39 | } 40 | let instance = ensure!(:ensure!(:guard.katalyst()).get_instance()); 41 | let cache = instance.service.cache.clone(); 42 | Box::new(guard.preload().and_then(move |_| { 43 | let req = guard.take_http_request()?; 44 | guard.set_http_request(match req { 45 | HttpRequest::LoadedResponse(_) => { 46 | cache.set_key( 47 | guard.metadata()?.url.as_str(), 48 | CachedObject::from_response(&req).unwrap(), 49 | ); 50 | req 51 | } 52 | _ => req, 53 | })?; 54 | Ok(()) 55 | })) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /katalyst/src/modules/cache/memory.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use futures::future::*; 3 | use parking_lot::Mutex; 4 | use std::{collections::HashMap, sync::Arc}; 5 | 6 | #[derive(Default, Debug)] 7 | pub struct MemoryCacheBuilder; 8 | 9 | impl ModuleProvider for MemoryCacheBuilder { 10 | fn name(&self) -> &'static str { 11 | "memory_cache" 12 | } 13 | 14 | fn build(&self, _: ModuleType, _: Arc, _: &unstructured::Document) -> Result { 15 | Ok(MemoryCache::default().into_module()) 16 | } 17 | } 18 | 19 | #[derive(Default, Debug)] 20 | pub struct MemoryCache { 21 | cache: Mutex>>, 22 | } 23 | 24 | impl CacheProviderModule for MemoryCache { 25 | fn get_key( 26 | &self, 27 | key: &str, 28 | ) -> Box, Error = GatewayError> + Send> { 29 | let cache = &self.cache.lock(); 30 | match cache.get(key) { 31 | Some(r) => Box::new(ok(r.clone())), 32 | None => fail!(:NOT_FOUND), 33 | } 34 | } 35 | 36 | fn set_key( 37 | &self, 38 | key: &str, 39 | val: CachedObject, 40 | ) -> Box + Send> { 41 | let cache = &mut self.cache.lock(); 42 | cache.insert(key.to_owned(), Arc::new(val)); 43 | Box::new(ok(())) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /katalyst/src/modules/cache/mod.rs: -------------------------------------------------------------------------------- 1 | mod cache_handler; 2 | mod memory; 3 | 4 | use crate::modules::*; 5 | use hyper::Response; 6 | use serde::{Deserialize, Serialize}; 7 | use std::{collections::HashMap, sync::Arc}; 8 | 9 | pub use cache_handler::DefaultCacheHandler; 10 | pub use memory::MemoryCacheBuilder; 11 | 12 | pub fn default_cache() -> Arc { 13 | Arc::new(memory::MemoryCache::default()) 14 | } 15 | 16 | /// Container for a cached response object 17 | #[derive(Debug, Clone, Deserialize, Serialize)] 18 | pub struct CachedObject { 19 | /// Raw content of a cached response 20 | pub content: Vec, 21 | /// Headers of a cached response 22 | pub headers: HashMap, 23 | } 24 | 25 | impl CachedObject { 26 | /// Generate a cached object from a response object 27 | pub fn from_response(req: &HttpRequest) -> Result { 28 | match req { 29 | HttpRequest::LoadedResponse(r) => Ok(CachedObject { 30 | content: r.1.clone(), 31 | headers: r 32 | .0 33 | .headers 34 | .iter() 35 | .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) 36 | .collect(), 37 | }), 38 | HttpRequest::ParsedResponse(r) => Ok(CachedObject { 39 | content: r.1.clone(), 40 | headers: r 41 | .0 42 | .headers 43 | .iter() 44 | .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) 45 | .collect(), 46 | }), 47 | _ => fail!(NOT_FOUND), 48 | } 49 | } 50 | 51 | /// Generate a response from a cached object 52 | pub fn into_response(self) -> HttpRequest { 53 | let mut builder = Response::builder(); 54 | for (k, v) in self.headers.into_iter() { 55 | builder.header(k.as_str(), v.as_str()); 56 | } 57 | let p = builder.body(hyper::Body::empty()).unwrap().into_parts().0; 58 | HttpRequest::LoadedResponse(Box::new((p, self.content))) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /katalyst/src/modules/def.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use std::{fmt::Debug, sync::Arc}; 3 | use unstructured::Document; 4 | 5 | #[doc(hidden)] 6 | pub trait ModuleData { 7 | const MODULE_TYPE: ModuleType; 8 | type RUST_TYPE; 9 | } 10 | 11 | /// Required trait for any modules to be registered 12 | pub trait ModuleProvider: Send + Sync + Debug { 13 | /// The name of the module, matched to the "type" field in configuration 14 | fn name(&self) -> &'static str; 15 | /// The method used to build a module. 16 | fn build(&self, _: ModuleType, _: Arc, _: &Document) -> Result; 17 | } 18 | 19 | macro_rules! impl_module { 20 | ($($sname:expr, $name:ident, $trait:ident { $( $ret:ty: $method:ident => ( $($argname:ident : $argtype:ty),* ) )* })+) => { 21 | /// Variants corresponding to each module type. 22 | #[derive(PartialEq, Debug)] 23 | pub enum ModuleType { 24 | $( 25 | #[doc = $sname] 26 | #[doc = " module type."] 27 | $name, 28 | )* 29 | } 30 | 31 | /// This enum is a container for all module types. 32 | #[derive(Debug)] 33 | pub enum Module { 34 | $( 35 | #[doc = "Variant containing "] 36 | #[doc = $sname] 37 | #[doc = " modules."] 38 | $name($name), 39 | )* 40 | } 41 | 42 | $( 43 | impl From<$name> for Module { 44 | fn from(module: $name) -> Self { 45 | Module::$name(module) 46 | } 47 | } 48 | 49 | impl ModuleData for $name { 50 | const MODULE_TYPE: ModuleType = ModuleType::$name; 51 | type RUST_TYPE = $name; 52 | } 53 | )* 54 | 55 | $( 56 | #[doc = $sname] 57 | #[doc = " container."] 58 | #[derive(Debug)] 59 | pub struct $name(pub Box); 60 | 61 | #[doc = "Implement this trait when building "] 62 | #[doc = $sname] 63 | #[doc = " modules."] 64 | pub trait $trait: Send + Sync + Debug { 65 | $( 66 | #[doc = "Method implementation for "] 67 | #[doc = $sname] 68 | #[doc = " modules."] 69 | fn $method(&self, $($argname: $argtype , )*) -> $ret; 70 | )* 71 | 72 | /// Box this module into the Module enum. 73 | fn into_module(self) -> Module where Self: 'static + Sized { 74 | Module::$name($name(Box::new(self))) 75 | } 76 | } 77 | 78 | impl From> for $name { 79 | fn from(module: Box<$trait + Send>) -> Self { 80 | $name(module) 81 | } 82 | } 83 | 84 | impl $trait for $name { 85 | $( 86 | #[inline] 87 | fn $method(&self, $($argname: $argtype , )*) -> $ret { 88 | self. 0 .$method($($argname,)*) 89 | } 90 | )* 91 | } 92 | )* 93 | }; 94 | } 95 | 96 | /// A lease from a load balancer 97 | pub type BalancerLease = Result>; 98 | 99 | impl_module! { 100 | "Authenticator", Authenticator, AuthenticatorModule { 101 | AsyncResult<()>: authenticate => (guard: RequestContext) 102 | } 103 | 104 | "Authorizer", Authorizer, AuthorizerModule { 105 | AsyncResult<()>: authorize => (guard: RequestContext) 106 | } 107 | 108 | "CacheHandler", CacheHandler, CacheHandlerModule { 109 | AsyncResult<()>: check_cache => (guard: RequestContext) 110 | AsyncResult<()>: update_cache => (guard: RequestContext) 111 | } 112 | 113 | "CacheProvider", CacheProvider, CacheProviderModule { 114 | AsyncResult>: get_key => (key: &str) 115 | AsyncResult<()>: set_key => (key: &str, val: CachedObject) 116 | } 117 | 118 | "Plugin", Plugin, PluginModule { 119 | AsyncResult<()>: run => (guard: RequestContext) 120 | } 121 | 122 | "RequestHandler", RequestHandler, RequestHandlerModule { 123 | AsyncResult<()>: dispatch => (guard: RequestContext) 124 | } 125 | 126 | "LoadBalancer", LoadBalancer, LoadBalancerModule { 127 | BalancerLease: lease => () 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /katalyst/src/modules/handlers/files/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{app::Katalyst, expression::*, modules::*}; 2 | use futures::future::*; 3 | use http::header::HeaderValue; 4 | use hyper::{Body, Response}; 5 | use std::path::PathBuf; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[derive(Clone, Debug, Serialize, Deserialize)] 10 | #[serde(rename_all = "snake_case")] 11 | struct FileServerConfig { 12 | root_path: String, 13 | selector: String, 14 | } 15 | 16 | #[derive(Debug)] 17 | pub struct FileServerModule; 18 | 19 | impl ModuleProvider for FileServerModule { 20 | fn name(&self) -> &'static str { 21 | "file_server" 22 | } 23 | 24 | fn build( 25 | &self, 26 | _: ModuleType, 27 | engine: Arc, 28 | config: &unstructured::Document, 29 | ) -> Result { 30 | let c: FileServerConfig = config.clone().try_into().map_err(|e| { 31 | err!(ConfigurationFailure, "Failed to parse File Server module configuration", e) 32 | })?; 33 | Ok(FileServerDispatcher { 34 | root_path: c.root_path, 35 | selector: engine.get_compiler().compile_template(Some(&c.selector))?, 36 | } 37 | .into_module()) 38 | } 39 | } 40 | 41 | #[derive(Debug)] 42 | pub struct FileServerDispatcher { 43 | pub root_path: String, 44 | pub selector: Expression, 45 | } 46 | 47 | impl RequestHandlerModule for FileServerDispatcher { 48 | fn dispatch(&self, guard: RequestContext) -> ModuleResult { 49 | let path = ensure!(:self.selector.render(&guard)); 50 | let mut full_path = PathBuf::from(&self.root_path); 51 | full_path.push(&path); 52 | send_file(guard.clone(), full_path) 53 | } 54 | } 55 | 56 | fn send_file(guard: RequestContext, file: PathBuf) -> ModuleResult { 57 | let result = Box::new( 58 | tokio_fs::file::File::open(file.to_str().unwrap_or_default().to_string()).and_then( 59 | |file| { 60 | let buf: Vec = Vec::new(); 61 | tokio_io::io::read_to_end(file, buf) 62 | .and_then(|item| Ok(Response::::new(item.1.into()))) 63 | }, 64 | ), 65 | ); 66 | Box::new(result.then(move |result| match result { 67 | Ok(mut r) => { 68 | let mime = mime_guess::get_mime_type_str( 69 | file.extension().unwrap_or_default().to_str().unwrap_or_default(), 70 | ) 71 | .unwrap_or("application/octet-stream"); 72 | let hdrs = r.headers_mut(); 73 | let hdr_val = HeaderValue::from_str(mime).unwrap(); 74 | hdrs.append("Content-Type", hdr_val); 75 | guard.set_response(r).unwrap_or_default(); 76 | Ok(()) 77 | } 78 | Err(_) => fail!(NOT_FOUND), 79 | })) 80 | } 81 | -------------------------------------------------------------------------------- /katalyst/src/modules/handlers/host/dispatcher.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use futures::{future::*, Future}; 3 | 4 | impl HostDispatcher { 5 | pub fn prepare(&self, guard: RequestContext) -> ModuleResultSync { 6 | let config = guard.katalyst()?; 7 | let metadata = guard.metadata()?; 8 | 9 | let balancer_lease = match config.get_instance()?.hosts.get(&self.host) { 10 | Some(s) => s.servers.lease()?, 11 | None => fail!(=> NOT_FOUND), 12 | }; 13 | 14 | let transformer = self.transformer(guard.clone(), balancer_lease.to_string())?; 15 | let lease_lock: &mut Option> = &mut metadata.balancer_lease.lock(); 16 | *lease_lock = Some(balancer_lease); 17 | 18 | let request = guard.take_request()?; 19 | 20 | let mut client_req = transformer.transform(request)?; 21 | add_forwarding_headers(&mut client_req.headers_mut(), &guard.metadata()?.remote_ip); 22 | strip_hop_headers(&mut client_req.headers_mut()); 23 | guard.set_http_request(HttpRequest::new(client_req))?; 24 | Ok(()) 25 | } 26 | 27 | pub fn send(guard: RequestContext) -> ModuleResult { 28 | let dsr = ensure!(:guard.take_request()); 29 | let client = ensure!(:guard.katalyst()).get_client(); 30 | let res = client.request(dsr); 31 | Box::new(res.then(move |response| match response { 32 | Ok(r) => { 33 | guard.set_response(r).unwrap_or_default(); 34 | ok(()) 35 | } 36 | Err(e) => err(fail!(_ GATEWAY_TIMEOUT, "Downstream request failed!", e)), 37 | })) 38 | } 39 | 40 | pub fn clean_response(guard: RequestContext) -> Result<()> { 41 | let mut req = guard.take_http_request()?; 42 | if let HttpRequest::RawResponse(res) = &mut req { 43 | strip_hop_headers(&mut res.0.headers); 44 | } 45 | guard.set_http_request(req)?; 46 | Ok(()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /katalyst/src/modules/handlers/host/mod.rs: -------------------------------------------------------------------------------- 1 | mod dispatcher; 2 | mod transformers; 3 | mod util; 4 | 5 | use crate::{app::Katalyst, expression::*, modules::*}; 6 | use futures::{future::*, Future}; 7 | use http::Method; 8 | use std::collections::HashMap; 9 | use transformers::DownstreamTransformer; 10 | pub use util::*; 11 | 12 | use serde::{Deserialize, Serialize}; 13 | 14 | #[derive(Clone, Debug, Serialize, Deserialize)] 15 | #[serde(rename_all = "snake_case")] 16 | struct HostConfig { 17 | host: String, 18 | path: String, 19 | #[serde(default)] 20 | method: Option, 21 | #[serde(default)] 22 | query: Option>, 23 | #[serde(default)] 24 | headers: Option>, 25 | #[serde(default)] 26 | body: Option, 27 | } 28 | 29 | #[derive(Debug)] 30 | pub struct HostDispatcher { 31 | pub host: String, 32 | pub path: Expression, 33 | pub method: Option, 34 | pub query: Option>, 35 | pub headers: Option>, 36 | pub body: Option, 37 | } 38 | 39 | #[derive(Debug)] 40 | pub struct HostModule; 41 | 42 | impl ModuleProvider for HostModule { 43 | fn name(&self) -> &'static str { 44 | "host" 45 | } 46 | 47 | fn build( 48 | &self, 49 | _: ModuleType, 50 | engine: Arc, 51 | config: &unstructured::Document, 52 | ) -> Result { 53 | let c: HostConfig = config.clone().try_into().map_err(|e| { 54 | err!(ConfigurationFailure, "Failed to parse host proxy module configuration", e) 55 | })?; 56 | let providers = engine.get_compiler(); 57 | let method = match c.method { 58 | Some(m) => Some(Method::from_bytes(m.to_uppercase().as_bytes())?), 59 | None => None, 60 | }; 61 | let temp; 62 | let body = match c.body { 63 | Some(bod) => { 64 | temp = bod; 65 | Some(temp.as_str()) 66 | } 67 | None => None, 68 | }; 69 | Ok(HostDispatcher { 70 | host: c.host.to_owned(), 71 | path: providers.compile_template(Some(c.path.as_str()))?, 72 | method, 73 | query: providers.compile_template_map(&c.query)?, 74 | headers: providers.compile_template_map(&c.headers)?, 75 | body: providers.compile_template_option(body)?, 76 | } 77 | .into_module()) 78 | } 79 | } 80 | 81 | impl RequestHandlerModule for HostDispatcher { 82 | fn dispatch(&self, guard: RequestContext) -> ModuleResult { 83 | let guard2 = guard.clone(); 84 | Box::new( 85 | result(self.prepare(guard.clone())) 86 | .and_then(move |_| HostDispatcher::send(guard)) 87 | .then(move |_| HostDispatcher::clean_response(guard2)), 88 | ) 89 | } 90 | } 91 | 92 | impl HostDispatcher { 93 | pub fn transformer( 94 | &self, 95 | guard: RequestContext, 96 | lease_str: String, 97 | ) -> Result { 98 | let mut uri = lease_str; 99 | uri.push_str(&self.path.render(&guard)?); 100 | if let Some(query) = &self.query { 101 | uri.push_str("?"); 102 | for (key, val) in query.iter() { 103 | uri.push_str(&key); 104 | uri.push_str("="); 105 | uri.push_str(&val.render(&guard)?); 106 | uri.push_str("&"); 107 | } 108 | uri.truncate(uri.len() - 1); 109 | }; 110 | 111 | let method = self.method.clone(); 112 | 113 | let headers = match &self.headers { 114 | Some(h) => Some( 115 | h.iter() 116 | .map(|(key, val)| Ok((key.to_string(), val.render(&guard)?))) 117 | .collect::>>()?, 118 | ), 119 | None => None, 120 | }; 121 | 122 | let body = match &self.body { 123 | Some(b) => Some(b.render(&guard)?), 124 | None => None, 125 | }; 126 | 127 | Ok(DownstreamTransformer { uri, method, headers, body }) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /katalyst/src/modules/handlers/host/transformers.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use http::{ 3 | header::{HeaderName, HeaderValue}, 4 | Method, 5 | }; 6 | use hyper::{Body, Request}; 7 | use std::{collections::HashMap, str::FromStr}; 8 | 9 | #[derive(Debug)] 10 | pub struct DownstreamTransformer { 11 | pub uri: String, 12 | pub method: Option, 13 | pub headers: Option>, 14 | pub body: Option, 15 | } 16 | 17 | impl DownstreamTransformer { 18 | pub fn transform(self, req: Request) -> Result> { 19 | let (mut parts, mut body) = req.into_parts(); 20 | parts.uri = self.uri.parse()?; 21 | 22 | if let Some(method) = self.method { 23 | parts.method = method; 24 | } 25 | 26 | if let Some(body_str) = self.body { 27 | while parts.headers.contains_key("Content-Length") { 28 | parts.headers.remove("Content-Length"); 29 | } 30 | body = hyper::Body::from(body_str); 31 | } 32 | 33 | if let Some(headers) = self.headers { 34 | for (key_str, val_str) in headers.iter() { 35 | if let (Ok(key), Ok(val)) = 36 | (HeaderName::from_str(&key_str), HeaderValue::from_str(val_str)) 37 | { 38 | while parts.headers.contains_key(key_str) { 39 | parts.headers.remove(key_str); 40 | } 41 | parts.headers.append(key, val); 42 | } 43 | } 44 | } 45 | 46 | Ok(Request::from_parts(parts, body)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /katalyst/src/modules/handlers/host/util.rs: -------------------------------------------------------------------------------- 1 | use http::{header::HeaderValue, HeaderMap}; 2 | 3 | lazy_static! { 4 | static ref HOP_HEADERS: Vec<&'static str> = vec![ 5 | "Connection", 6 | "Keep-Alive", 7 | "Proxy-Authenticate", 8 | "Proxy-Authorization", 9 | "Te", 10 | "Trailers", 11 | "Transfer-Encoding", 12 | "Upgrade", 13 | ]; 14 | } 15 | 16 | pub fn strip_hop_headers(headers: &mut HeaderMap) { 17 | for header in HOP_HEADERS.iter() { 18 | headers.remove(*header); 19 | } 20 | } 21 | 22 | pub fn add_forwarding_headers(headers: &mut HeaderMap, remote_ip: &str) { 23 | headers.remove("X-Forwarded-For"); 24 | headers.remove("X-Forwarded-Proto"); 25 | headers.remove("X-Forwarded-Port"); 26 | if let Ok(header) = HeaderValue::from_str(remote_ip) { 27 | headers.append("X-Forwarded-For", header); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /katalyst/src/modules/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | mod files; 2 | mod host; 3 | 4 | pub use files::FileServerModule; 5 | pub use host::HostModule; 6 | -------------------------------------------------------------------------------- /katalyst/src/modules/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Module traits and built in modules 3 | */ 4 | 5 | mod authentication; 6 | mod authorization; 7 | mod def; 8 | mod handlers; 9 | mod plugins; 10 | mod registry; 11 | mod result; 12 | 13 | use crate::prelude::*; 14 | use std::{collections::HashMap, sync::Arc}; 15 | 16 | pub(crate) mod balancer; 17 | pub(crate) mod cache; 18 | pub use cache::CachedObject; 19 | pub use def::*; 20 | pub use registry::ModuleRegistry; 21 | pub(crate) use result::*; 22 | -------------------------------------------------------------------------------- /katalyst/src/modules/plugins/content_plugin.rs: -------------------------------------------------------------------------------- 1 | use crate::{app::Katalyst, modules::*}; 2 | 3 | #[derive(Debug)] 4 | pub struct ContentPlugin; 5 | 6 | impl ModuleProvider for ContentPlugin { 7 | fn name(&self) -> &'static str { 8 | "parse-content" 9 | } 10 | 11 | fn build(&self, _: ModuleType, _: Arc, _: &unstructured::Document) -> Result { 12 | Ok(ContentPlugin.into_module()) 13 | } 14 | } 15 | 16 | impl PluginModule for ContentPlugin { 17 | fn run(&self, guard: RequestContext) -> ModuleResult { 18 | guard.parse() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /katalyst/src/modules/plugins/mod.rs: -------------------------------------------------------------------------------- 1 | mod content_plugin; 2 | 3 | pub use content_plugin::ContentPlugin; 4 | -------------------------------------------------------------------------------- /katalyst/src/modules/registry.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | /// The ModuleRegistry holds all of the modules that are registered with this 4 | /// instance of Katalyst. 5 | #[derive(Debug)] 6 | pub struct ModuleRegistry { 7 | modules: HashMap>, 8 | } 9 | 10 | impl ModuleRegistry { 11 | /// Register a new module to this instance 12 | pub fn register(&mut self, module: Arc) { 13 | self.modules.insert(module.name().to_string(), module); 14 | } 15 | 16 | pub(crate) fn get(&self, name: &str) -> Result> { 17 | Ok(self 18 | .modules 19 | .get(name) 20 | .ok_or_else(|| { 21 | err!(RequiredComponent, 22 | format!("Required module {} not found!", name), 23 | name: name.to_string() 24 | ) 25 | })? 26 | .clone()) 27 | } 28 | } 29 | 30 | macro_rules! register_modules { 31 | ($($toreg:expr);*) => { 32 | impl Default for ModuleRegistry { 33 | fn default() -> Self { 34 | let mut result = ModuleRegistry { 35 | modules: HashMap::default(), 36 | }; 37 | $( 38 | result.register(Arc::new($toreg)); 39 | )* 40 | result 41 | } 42 | } 43 | }; 44 | } 45 | 46 | register_modules! { 47 | handlers::FileServerModule; 48 | handlers::HostModule; 49 | authentication::AlwaysAuthenticator; 50 | authentication::NeverAuthenticator; 51 | authentication::HttpAuthenticatorBuilder; 52 | authentication::WhitelistBuilder; 53 | plugins::ContentPlugin; 54 | cache::DefaultCacheHandler; 55 | cache::MemoryCacheBuilder; 56 | balancer::LeastConnectionBalancerBuilder; 57 | balancer::RandomBalancerBuilder; 58 | balancer::RoundRobinBalancerBuilder 59 | } 60 | -------------------------------------------------------------------------------- /katalyst/src/modules/result.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | /// Module result type 4 | pub(crate) type ModuleResultSync = Result<()>; 5 | /// Async module result type 6 | pub(crate) type ModuleResult = AsyncResult<()>; 7 | 8 | pub(crate) struct ModuleError { 9 | pub error: GatewayError, 10 | pub context: RequestContext, 11 | } 12 | -------------------------------------------------------------------------------- /katalyst/src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use unstructured::Document; 3 | 4 | pub enum Format { 5 | Default, 6 | Json, 7 | Yaml, 8 | } 9 | 10 | impl Format { 11 | pub fn ext(ext: Option<&str>) -> Format { 12 | match ext { 13 | Some("yml") | Some("yaml") => Format::Yaml, 14 | Some("json") | Some("js") => Format::Json, 15 | _ => Format::Default, 16 | } 17 | } 18 | 19 | pub fn content_type(content_type: Option<&str>) -> Format { 20 | if let Some(ct) = content_type { 21 | match ct { 22 | "application/json" | "application/javascript" => Format::Json, 23 | "application/x-yaml" | "text/vnd.yaml" | "text/yaml" | "text/x-yaml" => { 24 | Format::Yaml 25 | } 26 | _ => Format::Default, 27 | } 28 | } else { 29 | Format::Default 30 | } 31 | } 32 | 33 | pub fn parse(&self, data: &[u8]) -> Result { 34 | match self { 35 | Format::Json => Ok(serde_json::from_slice(data)?), 36 | Format::Yaml => Ok(serde_yaml::from_slice(data)?), 37 | _ => Ok(serde_json::from_slice(data).unwrap_or_else(|_| Document::default())), 38 | } 39 | } 40 | } 41 | 42 | pub struct Parser; 43 | 44 | impl Parser { 45 | pub fn from_str(ser: &str, f: Format) -> Result { 46 | match f { 47 | Format::Json | Format::Default => serde_json::from_str(ser).map_err(|e| { 48 | err!(ConfigurationFailure, "Failed to parse JSON configuration file", e) 49 | }), 50 | Format::Yaml => serde_yaml::from_str(ser).map_err(|e| { 51 | err!(ConfigurationFailure, "Failed to parse YAML configuration file", e) 52 | }), 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /katalyst/src/prelude.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This module provides a "prelude" useful for extending Katalyst functionality 3 | */ 4 | 5 | pub use crate::{ 6 | app::Katalyst, 7 | context::*, 8 | error::{GatewayError::*, *}, 9 | expression::*, 10 | modules::*, 11 | }; 12 | pub(crate) use crate::{parser::*, *}; 13 | pub(crate) use futures::prelude::*; 14 | pub(crate) use std::sync::Arc; 15 | 16 | macro_rules! req { 17 | ($item:expr, $enum:ident :: $name:ident) => { 18 | match $item { 19 | $enum::$name(mtch) => mtch, 20 | _ => return Err(RequiredComponent { name: stringify!($enum::$name).to_string() }), 21 | } 22 | }; 23 | (:$item:expr, $enum:ident :: $name:ident) => { 24 | match $item { 25 | $enum::$name(mtch) => mtch, 26 | _ => { 27 | return Box::new(err(RequiredComponent { 28 | name: stringify!($enum::$name).to_string(), 29 | })) 30 | } 31 | } 32 | }; 33 | ($name:expr) => { 34 | if let Some(mtch) = $name { 35 | mtch 36 | } else { 37 | return Err(err!( 38 | RequiredComponent, 39 | format!("Required component {} not found!", stringify!($name)), 40 | name: stringify!($name).to_string() 41 | )); 42 | } 43 | }; 44 | (:$name:expr) => { 45 | if let Some(mtch) = $name { 46 | mtch 47 | } else { 48 | return Box::new(err(err!( 49 | RequiredComponent, 50 | format!("Required component {} not found!", stringify!($name)), 51 | name: stringify!($name).to_string() 52 | ))); 53 | } 54 | }; 55 | } 56 | 57 | macro_rules! err { 58 | ($name:ident, $message:expr $(, $arg_name:ident : $val:expr),*) => { 59 | err!(_ $name, $message, None $(, $arg_name : $val )* ) 60 | }; 61 | ($name:ident, $message:expr, $e:expr $(, $arg_name:ident : $val:expr),*) => { 62 | err!(_ $name, $message, Some(Box::new($e)) $(, $arg_name : $val )* ) 63 | }; 64 | (_ $name:ident, $message:expr, $e:expr $(, $arg_name:ident : $val:expr),*) => { 65 | GatewayError::$name { 66 | message: $message.into(), 67 | module_path: module_path!(), 68 | line: line!(), 69 | col: column!(), 70 | source: $e, 71 | $($arg_name: $val,)* 72 | } 73 | }; 74 | } 75 | 76 | macro_rules! fail { 77 | (_$code:ident) => { 78 | fail!(_$code, "Request failed") 79 | }; 80 | (_$code:ident, $message:expr) => { 81 | err!(RequestFailed, $message, status: http::StatusCode::$code) 82 | }; 83 | (_$code:ident, $message:expr, $source:ident) => { 84 | err!(RequestFailed, $message, $source, status: http::StatusCode::$code) 85 | }; 86 | ($code:ident) => { 87 | Err(fail!(_$code)) 88 | }; 89 | ($code:ident, $message:expr) => { 90 | Err(fail!(_$code, $message)) 91 | }; 92 | ($code:ident, $message:expr, $source:ident) => { 93 | Err(fail!(_$code, $message, $source)) 94 | }; 95 | (:$code:ident) => { 96 | Box::new(err(fail!(_$code))) 97 | }; 98 | (:$code:ident, $message:expr) => { 99 | Box::new(err(fail!(_$code, $message))) 100 | }; 101 | (:$code:ident, $message:expr, $source:ident) => { 102 | Box::new(err(fail!(_$code, $message, $source))) 103 | }; 104 | (=> $code:ident) => { 105 | return fail!($code); 106 | }; 107 | (=> $code:ident, $message:expr) => { 108 | return fail!($code, $message); 109 | }; 110 | (=> $code:ident, $message:expr, $source:ident) => { 111 | return fail!($code, $message, $source); 112 | }; 113 | (=> :$code:ident) => { 114 | return fail!(:$code); 115 | }; 116 | (=> :$code:ident, $message:expr) => { 117 | return fail!(:$code, $message); 118 | }; 119 | (=> :$code:ident, $message:expr, $source:ident) => { 120 | return fail!(:$code, $message, $source); 121 | }; 122 | } 123 | 124 | macro_rules! ensure { 125 | (:$res:expr) => { 126 | match $res { 127 | Ok(res) => res, 128 | Err(e) => return Box::new(err(e)), 129 | } 130 | }; 131 | } 132 | 133 | macro_rules! module_unwrap { 134 | ($name:ident, $mt:expr) => { 135 | Arc::new(match $mt { 136 | Module::$name(mtch) => mtch, 137 | _ => { 138 | return Err(err!( 139 | RequiredComponent, 140 | format!("No module with the name {} is registered", stringify!(Module::$name)), 141 | name: stringify!(Module::$name).to_string() 142 | )) 143 | } 144 | }) 145 | }; 146 | } 147 | -------------------------------------------------------------------------------- /katalyst/src/server/mod.rs: -------------------------------------------------------------------------------- 1 | mod pipeline; 2 | 3 | use crate::{instance::Interface, prelude::*}; 4 | use futures::{stream::Stream, Future}; 5 | use hyper::{ 6 | server::conn::AddrStream, 7 | service::{make_service_fn, service_fn}, 8 | Body, Request, 9 | }; 10 | use pipeline::{run, HyperResult}; 11 | use rustls::internal::pemfile; 12 | use std::{fs, io, net::SocketAddr, sync::Arc}; 13 | use tokio_rustls::TlsAcceptor; 14 | 15 | pub(crate) enum Server { 16 | Http(HttpServer), 17 | Https(HttpsServer), 18 | } 19 | 20 | pub(crate) trait Service { 21 | fn spawn(&self, _: &mut Arc) -> Result<()>; 22 | } 23 | 24 | impl Server { 25 | pub fn new(iface: &Interface) -> Result { 26 | Ok(match iface { 27 | Interface::Http { addr } => Server::Http(HttpServer { addr: *addr }), 28 | Interface::Https { addr, cert, key } => Server::Https(HttpsServer { 29 | http: HttpServer { addr: *addr }, 30 | cert: cert.to_owned(), 31 | key: key.to_owned(), 32 | }), 33 | }) 34 | } 35 | } 36 | 37 | impl Service for Server { 38 | fn spawn(&self, katalyst: &mut Arc) -> Result<()> { 39 | match self { 40 | Server::Http(s) => s.spawn(katalyst), 41 | Server::Https(s) => s.spawn(katalyst), 42 | } 43 | } 44 | } 45 | 46 | pub(crate) struct HttpServer { 47 | addr: SocketAddr, 48 | } 49 | 50 | impl Service for HttpServer { 51 | fn spawn(&self, instance: &mut Arc) -> Result<()> { 52 | let engine = instance.clone(); 53 | let server = hyper::Server::bind(&self.addr) 54 | .serve(make_service_fn(move |conn: &AddrStream| { 55 | let engine = engine.clone(); 56 | let remote_addr = conn.remote_addr(); 57 | service_fn(move |req: Request| -> HyperResult { 58 | run(remote_addr, req, engine.clone()) 59 | }) 60 | })) 61 | .map_err(|e| error!("server error: {}", e)); 62 | 63 | info!("Listening on http://{}", self.addr); 64 | instance.spawn(server)?; 65 | Ok(()) 66 | } 67 | } 68 | 69 | pub(crate) struct HttpsServer { 70 | http: HttpServer, 71 | cert: String, 72 | key: String, 73 | } 74 | 75 | impl Service for HttpsServer { 76 | fn spawn(&self, instance: &mut Arc) -> Result<()> { 77 | let engine = instance.clone(); 78 | let tls_cfg = { 79 | let certs = self.load_certs(&self.cert)?; 80 | let key = self.load_private_key(&self.key)?; 81 | let mut cfg = rustls::ServerConfig::new(rustls::NoClientAuth::new()); 82 | cfg.set_single_cert(certs, key).unwrap(); 83 | Arc::new(cfg) 84 | }; 85 | 86 | let tcp = tokio_tcp::TcpListener::bind(&self.http.addr)?; 87 | let tls_acceptor = TlsAcceptor::from(tls_cfg); 88 | let tls = tcp 89 | .incoming() 90 | .and_then(move |s| tls_acceptor.accept(s)) 91 | .then(|r| match r { 92 | Ok(x) => Ok::<_, io::Error>(Some(x)), 93 | Err(_e) => Ok(None), 94 | }) 95 | .filter_map(|x| x); 96 | let server = hyper::Server::builder(tls) 97 | .serve(make_service_fn( 98 | move |conn: &tokio_rustls::TlsStream< 99 | tokio_tcp::TcpStream, 100 | rustls::ServerSession, 101 | >| { 102 | let remote_addr = conn.get_ref().0.peer_addr().unwrap(); 103 | let engine = engine.clone(); 104 | service_fn(move |req: Request| -> HyperResult { 105 | run(remote_addr, req, engine.clone()) 106 | }) 107 | }, 108 | )) 109 | .map_err(|e| error!("server error: {}", e)); 110 | 111 | info!("Listening on https://{}", self.http.addr); 112 | instance.spawn(server)?; 113 | Ok(()) 114 | } 115 | } 116 | 117 | impl HttpsServer { 118 | fn load_certs(&self, filename: &str) -> Result> { 119 | let certfile = fs::File::open(filename)?; 120 | let mut reader = io::BufReader::new(certfile); 121 | pemfile::certs(&mut reader).map_err(|_| err!(Critical, "Failed to load certificate")) 122 | } 123 | 124 | fn load_private_key(&self, filename: &str) -> Result { 125 | let keyfile = fs::File::open(filename)?; 126 | let mut reader = io::BufReader::new(keyfile); 127 | let mut keys = pemfile::rsa_private_keys(&mut reader) 128 | .map_err(|_| err!(Critical, "Failed to load private key"))?; 129 | if keys.len() != 1 { 130 | return Err(err!(Critical, "Expected a single private key")); 131 | } 132 | Ok(keys.pop().unwrap()) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /katalyst/src/server/pipeline/auth.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use futures::future::*; 3 | 4 | pub(crate) fn authenticate(guard: RequestContext) -> AsyncResult<()> { 5 | let route = ensure!(:guard.get_route()); 6 | match &route.authenticators { 7 | Some(state_authenticators) => { 8 | let authenticators = state_authenticators.clone(); 9 | let mut result: AsyncResult<()> = Ok(()).fut(); 10 | for authenticator in authenticators.iter() { 11 | let module_guard = guard.clone(); 12 | result = Box::new(result.and_then({ 13 | let r = authenticator.clone(); 14 | move |_| r.authenticate(module_guard) 15 | })); 16 | } 17 | result 18 | } 19 | None => Ok(()).fut(), 20 | } 21 | } 22 | 23 | pub(crate) fn authorize(guard: RequestContext) -> AsyncResult<()> { 24 | let route = ensure!(:guard.get_route()); 25 | let mut result: AsyncResult<()> = Ok(()).fut(); 26 | if let Some(authorizers) = &route.authorizers { 27 | for auth in authorizers.iter() { 28 | let a = auth.clone(); 29 | let module_guard = guard.clone(); 30 | result = Box::new(result.and_then(move |_| a.authorize(module_guard))); 31 | } 32 | } 33 | result 34 | } 35 | -------------------------------------------------------------------------------- /katalyst/src/server/pipeline/cache.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | pub(crate) fn check_cache(guard: RequestContext) -> ModuleResult { 4 | let route = ensure!(:guard.get_route()); 5 | if let Some(cache) = &route.cache { 6 | Box::new(cache.check_cache(guard.clone())) 7 | } else { 8 | Ok(()).fut() 9 | } 10 | } 11 | 12 | pub(crate) fn update_cache(guard: RequestContext) -> ModuleResult { 13 | let route = ensure!(:guard.get_route()); 14 | if let Some(cache) = &route.cache { 15 | Box::new(cache.update_cache(guard.clone())) 16 | } else { 17 | Ok(()).fut() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /katalyst/src/server/pipeline/dispatcher.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use futures::future::*; 3 | 4 | pub(crate) fn run_plugins(guard: RequestContext) -> ModuleResult { 5 | let mut result: ModuleResult = Ok(()).fut(); 6 | let route = ensure!(:guard.get_route()); 7 | if let Some(plugins) = &route.plugins { 8 | for plugin in plugins.iter() { 9 | let p = plugin.clone(); 10 | let module_guard = guard.clone(); 11 | result = Box::new(result.and_then(move |_| p.run(module_guard))); 12 | } 13 | } 14 | result 15 | } 16 | 17 | pub(crate) fn run_handler(guard: RequestContext) -> ModuleResult { 18 | let route = ensure!(:guard.get_route()); 19 | route.handler.dispatch(guard.clone()) 20 | } 21 | -------------------------------------------------------------------------------- /katalyst/src/server/pipeline/logger.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use std::time::Instant; 3 | 4 | pub(crate) fn log_request(guard: RequestContext) -> AsyncResult<()> { 5 | let ctx = ensure!(:guard.metadata()); 6 | info!("Request started to {:?}", ctx.url); 7 | Ok(()).fut() 8 | } 9 | 10 | pub(crate) fn log_result(guard: RequestContext) -> RequestContext { 11 | if let Ok(ctx) = guard.metadata() { 12 | let duration = Instant::now().duration_since(ctx.started); 13 | let total_ms = u64::from(duration.subsec_millis()) + (duration.as_secs() * 1000); 14 | info!("Request processed in {:?}ms", total_ms); 15 | } 16 | guard 17 | } 18 | 19 | pub(crate) fn log_error(err: ModuleError) -> ModuleError { 20 | if let Ok(ctx) = err.context.metadata() { 21 | let duration = Instant::now().duration_since(ctx.started); 22 | let total_ms = u64::from(duration.subsec_millis()) + (duration.as_secs() * 1000); 23 | warn!("{} after {}ms", err.error, total_ms); 24 | } 25 | err 26 | } 27 | -------------------------------------------------------------------------------- /katalyst/src/server/pipeline/mapper.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use futures::future::*; 3 | use hyper::{Body, Error, Response}; 4 | 5 | pub(crate) type HyperResult = Box, Error = Error> + Send>; 6 | 7 | pub(crate) fn map_result_to_hyper(res: PipelineResultSync) -> HyperResult { 8 | Box::new(match res { 9 | Ok(ctx) => ok::, Error>(ctx.take_response().unwrap()), 10 | Err(e) => ok::, Error>({ 11 | let mut resp = Response::default(); 12 | *resp.status_mut() = e.error.status_code(); 13 | resp 14 | }), 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /katalyst/src/server/pipeline/matcher.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::prelude::*; 3 | 4 | pub(crate) fn matcher(guard: RequestContext) -> AsyncResult<()> { 5 | match_int(guard).fut() 6 | } 7 | 8 | fn match_int(guard: RequestContext) -> Result<()> { 9 | let metadata = guard.metadata()?; 10 | let config = guard.katalyst()?.get_instance()?; 11 | let method = guard.method(); 12 | for route in config.routes.iter() { 13 | let method_match = match &route.methods { 14 | Some(methods) => methods.contains(&method), 15 | None => true, 16 | }; 17 | let path = metadata.url.path(); 18 | if method_match && route.pattern.is_match(path) { 19 | let mut cap_map = HashMap::new(); 20 | let caps = route.pattern.captures(path).ok_or_else( 21 | || fail!(_ INTERNAL_SERVER_ERROR, format!("Captures not found for path {}", path)), 22 | )?; 23 | for name_option in route.pattern.capture_names() { 24 | if name_option.is_some() { 25 | let name = name_option.ok_or_else(|| fail!(_ INTERNAL_SERVER_ERROR))?; 26 | cap_map.insert( 27 | name.to_string(), 28 | caps.name(name) 29 | .ok_or_else(|| fail!(_ INTERNAL_SERVER_ERROR, format!("Route {} has no placeholder for {}", path, name)))? 30 | .as_str() 31 | .to_string(), 32 | ); 33 | } 34 | } 35 | guard.set_match(Match::Matched { route: route.clone(), captures: cap_map })?; 36 | debug!("Request has been matched to route!"); 37 | return Ok(()); 38 | } 39 | } 40 | fail!(NOT_FOUND) 41 | } 42 | -------------------------------------------------------------------------------- /katalyst/src/server/pipeline/mod.rs: -------------------------------------------------------------------------------- 1 | mod auth; 2 | mod cache; 3 | mod dispatcher; 4 | mod logger; 5 | mod mapper; 6 | mod matcher; 7 | 8 | use crate::{app::Katalyst, prelude::*}; 9 | use futures::Future; 10 | use hyper::{Body, Request}; 11 | use std::{collections::HashMap, net::SocketAddr, sync::Arc}; 12 | 13 | pub(crate) use mapper::HyperResult; 14 | 15 | pub(crate) type PipelineResultSync = std::result::Result; 16 | pub(crate) type PipelineResult = Box + Send>; 17 | 18 | macro_rules! pipe { 19 | ($ty:path) => { 20 | |ctx: RequestContext| { 21 | $ty(ctx.clone()).then(|res| match res { 22 | Ok(_) => Ok(ctx), 23 | Err(e) => Err(ModuleError { error: e, context: ctx }), 24 | }) 25 | } 26 | }; 27 | } 28 | 29 | pub(crate) fn run( 30 | remote_addr: SocketAddr, 31 | request: Request, 32 | engine: Arc, 33 | ) -> HyperResult { 34 | Box::new( 35 | ok(RequestContext::new(request, engine, remote_addr)) 36 | .and_then(pipe!(logger::log_request)) 37 | .and_then(pipe!(matcher::matcher)) 38 | .and_then(pipe!(auth::authenticate)) 39 | .and_then(pipe!(auth::authorize)) 40 | .and_then(pipe!(cache::check_cache)) 41 | .and_then(pipe!(dispatcher::run_plugins)) 42 | .and_then(pipe!(dispatcher::run_handler)) 43 | .and_then(pipe!(cache::update_cache)) 44 | .then(map_early_finish) 45 | .map(logger::log_result) 46 | .map_err(logger::log_error) 47 | .then(mapper::map_result_to_hyper), 48 | ) 49 | } 50 | 51 | pub(crate) fn map_early_finish(res: PipelineResultSync) -> PipelineResult { 52 | match res { 53 | Err(ModuleError { error: GatewayError::Done, context }) => { 54 | Box::new(ok::(context)) 55 | } 56 | other => Box::new(futures::future::result(other)), 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /katalyst/src/util/client_requests.rs: -------------------------------------------------------------------------------- 1 | use crate::{app::HttpsClient, config::Builder, expression::*, prelude::*}; 2 | use futures::{future::*, Future}; 3 | use http::{ 4 | header::{HeaderMap, HeaderName, HeaderValue}, 5 | request::Builder as RequestBuilder, 6 | Method, Request, Response, StatusCode, 7 | }; 8 | use hyper::Body; 9 | use serde::{Deserialize, Serialize}; 10 | use std::{collections::HashMap, str::FromStr, sync::Arc}; 11 | use unstructured::Document; 12 | 13 | #[derive(Clone, Debug, Serialize, Deserialize, Default)] 14 | #[serde(default, rename_all = "snake_case")] 15 | pub struct ClientRequestBuilder { 16 | pub host: String, 17 | pub path: String, 18 | pub method: Option, 19 | pub query: Option>, 20 | pub headers: Option>, 21 | pub body: Option, 22 | } 23 | 24 | impl Builder for ClientRequestBuilder { 25 | fn build(&self, katalyst: Arc) -> Result { 26 | let compiler = katalyst.get_compiler(); 27 | 28 | let method = match &self.method { 29 | Some(m) => Some(Method::from_bytes(m.to_uppercase().as_bytes())?), 30 | None => None, 31 | }; 32 | 33 | let temp; 34 | let body = match &self.body { 35 | Some(bod) => { 36 | temp = bod; 37 | Some(temp.as_str()) 38 | } 39 | None => None, 40 | }; 41 | 42 | Ok(CompiledClientRequest { 43 | host: self.host.to_owned(), 44 | path: compiler.compile_template(Some(self.path.as_str()))?, 45 | method, 46 | query: compiler.compile_template_map(&self.query)?, 47 | headers: compiler.compile_template_map(&self.headers)?, 48 | body: compiler.compile_template_option(body)?, 49 | }) 50 | } 51 | } 52 | 53 | #[derive(Debug)] 54 | pub struct CompiledClientRequest { 55 | host: String, 56 | path: Expression, 57 | method: Option, 58 | query: Option>, 59 | headers: Option>, 60 | body: Option, 61 | } 62 | 63 | impl CompiledClientRequest { 64 | pub fn prepare_request(&self, ctx: &RequestContext) -> Result { 65 | let mut path = self.path.render(ctx)?; 66 | 67 | if let Some(query) = &self.query { 68 | let raw_query = query 69 | .iter() 70 | .map(|(k, v)| Ok(format!("{}={}", k, v.render(ctx)?))) 71 | .collect::>>()? 72 | .join("&"); 73 | path = format!("{}?{}", path, raw_query); 74 | } 75 | 76 | let headers = if let Some(hdrs) = &self.headers { 77 | hdrs.iter() 78 | .map(|(k, v)| { 79 | Ok(( 80 | HeaderName::from_str(k).unwrap(), 81 | HeaderValue::from_str(&v.render(ctx)?).unwrap(), 82 | )) 83 | }) 84 | .collect::>>()? 85 | } else { 86 | HeaderMap::default() 87 | }; 88 | 89 | let host = ctx 90 | .katalyst()? 91 | .get_instance()? 92 | .hosts 93 | .get(&self.host) 94 | .ok_or_else(|| fail!(_ NOT_FOUND))? 95 | .servers 96 | .lease()?; 97 | 98 | let body = 99 | if let Some(body) = &self.body { Body::from(body.render(ctx)?) } else { Body::empty() }; 100 | 101 | Ok(HttpData { 102 | request_type: HttpAction::Request( 103 | format!("{}{}", host, path), 104 | self.method.as_ref().cloned().unwrap_or(Method::GET), 105 | ), 106 | headers, 107 | body: HttpContent::Raw(body), 108 | }) 109 | } 110 | } 111 | 112 | #[derive(Debug)] 113 | pub enum HttpContent { 114 | Raw(Body), 115 | Bytes(Vec), 116 | Parsed(Document), 117 | } 118 | 119 | impl HttpContent { 120 | pub fn into_body(self) -> Body { 121 | match self { 122 | HttpContent::Raw(body) => body, 123 | HttpContent::Bytes(bytes) => Body::from(bytes), 124 | HttpContent::Parsed(doc) => Body::from(serde_json::to_vec(&doc).unwrap()), 125 | } 126 | } 127 | } 128 | 129 | #[derive(Debug)] 130 | pub enum HttpAction { 131 | Request(String, Method), 132 | Response(StatusCode), 133 | } 134 | 135 | #[derive(Debug)] 136 | pub struct HttpData { 137 | pub request_type: HttpAction, 138 | pub headers: HeaderMap, 139 | pub body: HttpContent, 140 | } 141 | 142 | impl From> for HttpData { 143 | fn from(req: Request) -> HttpData { 144 | let (parts, body) = req.into_parts(); 145 | HttpData { 146 | request_type: HttpAction::Request(parts.uri.to_string(), parts.method), 147 | headers: parts.headers, 148 | body: HttpContent::Raw(body), 149 | } 150 | } 151 | } 152 | 153 | impl From> for HttpData { 154 | fn from(req: Response) -> HttpData { 155 | let (parts, body) = req.into_parts(); 156 | HttpData { 157 | request_type: HttpAction::Response(parts.status), 158 | headers: parts.headers, 159 | body: HttpContent::Raw(body), 160 | } 161 | } 162 | } 163 | 164 | impl HttpData { 165 | pub fn send(self, client: &HttpsClient) -> AsyncResult { 166 | if let HttpAction::Request(uri, method) = self.request_type { 167 | let mut request = RequestBuilder::new(); 168 | request.method(method); 169 | request.uri(&uri); 170 | *request.headers_mut().unwrap() = self.headers; 171 | let req = request.body(self.body.into_body()).unwrap(); 172 | let res = client.request(req); 173 | Box::new(res.then(move |response| match response { 174 | Ok(r) => ok::(HttpData::from(r)), 175 | Err(e) => { 176 | err(fail!(_ GATEWAY_TIMEOUT, format!("Error sending request to {}", uri), e)) 177 | } 178 | })) 179 | } else { 180 | Box::new(err::(err!(Other, "Response type cannot be sent"))) 181 | } 182 | } 183 | 184 | pub fn send_parse(self, client: &HttpsClient) -> AsyncResult { 185 | Box::new(self.send(client).and_then(|mut resp| { 186 | let content_type = resp.headers.get("Content-Type").map(|m| m.to_str().unwrap()); 187 | let format = Format::content_type(content_type); 188 | if let HttpContent::Raw(body) = resp.body { 189 | resp.body = HttpContent::Bytes(vec![]); 190 | return Box::new(Either::A(body.concat2().then(move |r| { 191 | let doc = match r { 192 | Ok(d) => format.parse(&d).unwrap_or_default(), 193 | Err(_) => Document::Unit, 194 | }; 195 | resp.body = HttpContent::Parsed(doc); 196 | Box::new(ok::(resp)) 197 | }))); 198 | } 199 | Box::new(Either::B(ok(resp))) 200 | })) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /katalyst/src/util/locked_resource.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::{Mutex, MutexGuard}; 2 | use std::sync::Arc; 3 | 4 | #[derive(Debug)] 5 | pub struct LockedResource(Arc>); 6 | 7 | #[derive(Debug)] 8 | pub struct Resource<'a, T>(MutexGuard<'a, T>); 9 | 10 | impl<'a, T> std::ops::Deref for Resource<'a, T> { 11 | type Target = T; 12 | 13 | fn deref(&self) -> &Self::Target { 14 | &self.0 15 | } 16 | } 17 | 18 | impl Clone for LockedResource { 19 | fn clone(&self) -> Self { 20 | LockedResource(self.0.clone()) 21 | } 22 | } 23 | 24 | impl LockedResource { 25 | pub fn new(res: T) -> LockedResource { 26 | LockedResource(Arc::new(Mutex::new(res))) 27 | } 28 | 29 | pub fn set(&self, new: T) -> T { 30 | let res: &mut T = &mut self.0.lock(); 31 | std::mem::replace(res, new) 32 | } 33 | 34 | pub fn take(&self) -> T 35 | where 36 | T: Default, 37 | { 38 | self.set(T::default()) 39 | } 40 | 41 | pub fn get(&self) -> Resource { 42 | Resource(self.0.lock()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /katalyst/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | mod client_requests; 2 | mod locked_resource; 3 | 4 | pub use client_requests::*; 5 | pub use locked_resource::{LockedResource, Resource}; 6 | -------------------------------------------------------------------------------- /katalyst_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "katalyst_macros" 3 | version = "0.2.0" 4 | authors = ["Phil Proctor "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | proc-macro2 = "0.4" 9 | quote = "0.6" 10 | syn = { version = "0.15", features = ["derive"] } 11 | 12 | [lib] 13 | name = "katalyst_macros" 14 | proc-macro = true -------------------------------------------------------------------------------- /katalyst_macros/src/attr.rs: -------------------------------------------------------------------------------- 1 | use syn::{ 2 | parse::{Parse, ParseStream}, 3 | punctuated::Punctuated, 4 | }; 5 | 6 | pub(crate) struct BindingAttrValues { 7 | pub ident: syn::Ident, 8 | pub equal: Token![=], 9 | pub val: syn::Expr, 10 | } 11 | 12 | impl Parse for BindingAttrValues { 13 | fn parse(input: ParseStream) -> syn::Result { 14 | Ok(BindingAttrValues { ident: input.parse()?, equal: input.parse()?, val: input.parse()? }) 15 | } 16 | } 17 | 18 | pub(crate) struct BindingAttrParens { 19 | pub parens: syn::token::Paren, 20 | pub contents: Punctuated, 21 | } 22 | 23 | impl Parse for BindingAttrParens { 24 | fn parse(input: ParseStream) -> syn::Result { 25 | let content; 26 | let parens = parenthesized!(content in input); 27 | let contents = content.parse_terminated(BindingAttrValues::parse)?; 28 | Ok(BindingAttrParens { parens, contents }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /katalyst_macros/src/binding_derive.rs: -------------------------------------------------------------------------------- 1 | use crate::attr::*; 2 | use proc_macro::TokenStream; 3 | use quote::*; 4 | 5 | type BindingTuple = (Option>, Option>); 6 | 7 | #[derive(Default)] 8 | struct BindingMetadata { 9 | object_name: Option>, 10 | bindings: Vec, 11 | } 12 | 13 | fn read_attributes(attrs: &[syn::Attribute]) -> BindingMetadata { 14 | let mut result = BindingMetadata::default(); 15 | for attr in attrs 16 | .iter() 17 | .filter(|a| !a.path.segments.is_empty() && a.path.segments[0].ident == "expression") 18 | { 19 | let container: BindingAttrParens = syn::parse2(attr.tts.clone()).unwrap(); 20 | let mut binding: BindingTuple = (None, None); 21 | let mut def_binding = String::default(); 22 | for item in container.contents.into_iter() { 23 | match item.ident.to_string().as_str() { 24 | "bind" => { 25 | let mut tokens = proc_macro2::TokenStream::default(); 26 | item.val.to_tokens(&mut tokens); 27 | def_binding = tokens.to_string(); 28 | binding.0 = Some(Box::new(item.val)); 29 | } 30 | "call_name" => binding.1 = Some(Box::new(item.val)), 31 | "name" => result.object_name = Some(Box::new(item.val)), 32 | _ => panic!("Unknown!"), 33 | } 34 | } 35 | if binding.0.is_some() { 36 | if binding.1.is_none() { 37 | binding.1 = Some(Box::new(def_binding)); 38 | } 39 | result.bindings.push(binding); 40 | } 41 | } 42 | result 43 | } 44 | 45 | pub fn impl_derive_expression_binding(ast: &syn::DeriveInput) -> TokenStream { 46 | let ident = &ast.ident; 47 | let mut metadata: BindingMetadata = read_attributes(&ast.attrs); 48 | if metadata.object_name.is_none() { 49 | metadata.object_name = Some(Box::new(ident.to_string().to_ascii_lowercase())); 50 | } 51 | 52 | let mut match_options = vec![]; 53 | for binding in metadata.bindings.iter() { 54 | let check = &binding.1; 55 | let method = &binding.0; 56 | match_options.push(quote! { 57 | #check => { 58 | Ok(std::sync::Arc::new(#ident::#method)) 59 | }, 60 | }); 61 | } 62 | let name = &metadata.object_name; 63 | 64 | let gen = quote! { 65 | impl From<#ident> for Box { 66 | fn from(item: #ident) -> Self { 67 | Box::new(item) 68 | } 69 | } 70 | 71 | impl ExpressionBinding for #ident { 72 | fn identifier(&self) -> &'static str { 73 | #name 74 | } 75 | 76 | fn make_fn(&self, name: &str, args: &[ExpressionArg]) -> Result { 77 | match name { 78 | #(#match_options)* 79 | _ => Err(err!(ConfigurationFailure, format!("Expression {} has no member {}", #name, name))), 80 | } 81 | } 82 | } 83 | }; 84 | 85 | gen.into() 86 | } 87 | -------------------------------------------------------------------------------- /katalyst_macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "128"] 2 | 3 | extern crate quote; 4 | #[macro_use] 5 | extern crate syn; 6 | extern crate proc_macro; 7 | extern crate proc_macro2; 8 | 9 | pub(crate) mod attr; 10 | mod binding_derive; 11 | 12 | use proc_macro::TokenStream; 13 | 14 | #[proc_macro_derive(ExpressionBinding, attributes(expression))] 15 | pub fn expression_binding_derive(input: TokenStream) -> TokenStream { 16 | let ast = syn::parse(input).unwrap(); 17 | binding_derive::impl_derive_expression_binding(&ast) 18 | } 19 | --------------------------------------------------------------------------------