├── .github ├── NSIS │ ├── kapow.ico │ └── windows.nsi ├── dependabot.yml ├── go │ └── Dockerfile ├── revokers │ ├── 44AB0A1631645936 │ ├── 679F65C4563CF5BA │ └── A06E2A1A3F63AA6D └── workflows │ ├── codeql-analysis.yml │ └── test_and_release.yml ├── .gitignore ├── .goreleaser.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── docs ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── make.bat ├── release-notes │ ├── RELEASE-v0.3.0.md │ ├── RELEASE-v0.4.0.md │ ├── RELEASE-v0.5.0.md │ ├── RELEASE-v0.5.1.md │ ├── RELEASE-v0.5.2.md │ ├── RELEASE-v0.5.3.md │ └── RELEASE-v0.5.4.md └── source │ ├── _static │ ├── browser.png │ ├── kapow-quick-overview.png │ ├── logo-200px.png │ ├── logo.png │ ├── network.png │ ├── request_life_cycle.png │ ├── request_life_cycle.xcf │ └── sequence.png │ ├── concepts │ ├── interfaces.rst │ ├── philosophy.rst │ ├── request_life_cycle.rst │ ├── resource_tree.rst │ ├── route_matching.rst │ ├── routes.rst │ └── toc.rst │ ├── conf.py │ ├── examples │ ├── handling_http_requests.rst │ ├── https_mtls.rst │ ├── managing_routes.rst │ ├── shell_tricks.rst │ ├── toc.rst │ ├── using_json.rst │ └── working_with_init_programs.rst │ ├── index.rst │ ├── latextoc.rst │ ├── the_project │ ├── install_and_configure.rst │ ├── quickstart.rst │ ├── security.rst │ └── toc.rst │ └── tutorial │ ├── index.rst │ ├── materials │ └── backup_db.sh │ ├── toc.rst │ ├── tutorial00.rst │ ├── tutorial01.rst │ ├── tutorial02.rst │ ├── tutorial03.rst │ ├── tutorial04.rst │ ├── tutorial05.rst │ └── tutorial06.rst ├── examples ├── advanced │ ├── 01_DocumentConverter │ │ ├── DocumentConverter │ │ ├── README.md │ │ └── index.html │ ├── 02_NetworkScanner │ │ ├── NetworkScanner │ │ └── README.md │ └── 03_NetworkSniffer │ │ ├── NetworkSniffer │ │ └── README.md ├── basic │ ├── 01_HelloWorld │ │ ├── HelloWorld │ │ └── README.md │ ├── 02_FixLogGrep │ │ ├── FixLogGrep │ │ └── README.md │ ├── 03_DynamicLogGrep │ │ ├── DynamicLogGrep │ │ └── README.md │ └── 04_SystemMonitor │ │ ├── README.md │ │ └── SystemMonitor ├── docker │ └── awscli │ │ └── Dockerfile └── mTLS │ └── README.md ├── go.mod ├── go.sum ├── internal ├── certs │ └── certs.go ├── client │ ├── client_test.go │ ├── get.go │ ├── get_test.go │ ├── route_add.go │ ├── route_add_test.go │ ├── route_list.go │ ├── route_list_test.go │ ├── route_remove.go │ ├── route_remove_test.go │ ├── set.go │ └── set_test.go ├── cmd │ ├── get.go │ ├── route.go │ ├── runner.go │ ├── runner_windows.go │ ├── server.go │ ├── set.go │ └── validations.go ├── http │ ├── reason.go │ ├── reason_test.go │ ├── request.go │ └── request_test.go ├── logger │ └── logger.go └── server │ ├── control │ ├── control.go │ ├── control_test.go │ ├── entrypoint.go │ ├── entrypoint_windows.go │ └── server.go │ ├── data │ ├── decorator.go │ ├── decorator_test.go │ ├── resource.go │ ├── resource_test.go │ ├── server.go │ ├── server_test.go │ ├── state.go │ ├── state_test.go │ └── testdata │ │ └── client_chain.crt │ ├── httperror │ ├── error.go │ └── error_test.go │ ├── model │ ├── handler.go │ └── route.go │ ├── server.go │ └── user │ ├── mux │ ├── gorillize.go │ ├── gorillize_test.go │ ├── handlerbuilder.go │ ├── handlerbuilder_test.go │ ├── mux.go │ └── mux_test.go │ ├── server.go │ ├── spawn │ ├── spawn.go │ └── spawn_test.go │ ├── state.go │ └── state_test.go ├── main.go ├── release-key.gpg ├── spec ├── README.md └── test │ ├── .envrc │ ├── .gherkin-lintrc │ ├── Dockerfile │ ├── Makefile │ ├── README.rst │ ├── features │ ├── control │ │ ├── append │ │ │ ├── error_malformed.feature │ │ │ ├── error_unprocessable.feature │ │ │ └── success.feature │ │ ├── delete │ │ │ ├── error_notfound.feature │ │ │ ├── list_order.feature │ │ │ └── success.feature │ │ ├── get │ │ │ ├── error_notfound.feature │ │ │ └── success.feature │ │ ├── insert │ │ │ ├── error_malformed.feature │ │ │ ├── error_unprocessable.feature │ │ │ ├── list_order.feature │ │ │ └── success.feature │ │ ├── list │ │ │ └── success.feature │ │ └── mtls.feature │ ├── data │ │ ├── handler │ │ │ ├── error_handleridnotfound.feature │ │ │ ├── error_invalidresource.feature │ │ │ ├── error_itemnotfound.feature │ │ │ └── success.feature │ │ ├── request │ │ │ └── success.feature │ │ └── response │ │ │ └── success.feature │ ├── environment.py │ └── steps │ │ ├── comparedict.py │ │ ├── get_environment.py │ │ ├── jsonexample.py │ │ ├── steps.py │ │ └── testinghandler.py │ ├── kapow │ └── Dockerfile │ ├── node-dependencies.nix │ ├── node-env.nix │ ├── node-packages.json │ ├── node-packages.nix │ └── shell.nix ├── test └── runwindowsrun.go ├── testutils ├── jaillover │ └── main.go └── poc │ ├── .gitignore │ ├── README.rst │ ├── kapow │ └── requirements.txt └── tools └── validsslclient /.github/NSIS/kapow.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BBVA/kapow/3cef9f0cd25ab41f683a883d3ae2ce042e162d59/.github/NSIS/kapow.ico -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Hi, Dependabot! 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | 9 | - package-ecosystem: "gomod" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | 14 | - package-ecosystem: "docker" 15 | directory: "/.github/go" 16 | schedule: 17 | interval: "daily" 18 | 19 | - package-ecosystem: "pip" 20 | directory: "/" 21 | schedule: 22 | interval: "daily" 23 | ignore: 24 | - dependency-name: "*" 25 | -------------------------------------------------------------------------------- /.github/go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22.5 2 | -------------------------------------------------------------------------------- /.github/revokers/44AB0A1631645936: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQINBF7ZESABEADst4ZbF14ya72DsPgYinoF9XVwJllAdiLWt+3dEABjSe9VN+IX 4 | pBdcEkrvSbv/rba6D4pHYQHWL0O2ZIqjHf0GxelUT6thvLeAZ32RHtFIykHgUuVg 5 | g3VgTpTE8J/Ciuicl8kjIdCyezRaMJtUlEp4fkAcziZBUQ2INshvLaVVk/xFeako 6 | U8WZchHIRlxjX7FMCO+6jRAOqPjTNiIyWy1gCrfAEmHHdTalBhQfB3VBrOlb/Zvh 7 | msB4+Au34p2OG71FU/1z7a/bWD6cKzusS5eryE19vu+7fdplxHAJulNbrTQsmNYv 8 | MhZUholB46ChULx+rckqBK32kHoEBFxQM/i/YOQ4WwyAdKxHHYWtbRl2guWiq6vs 9 | EakLRIW+61hW/wmlbGpPrOsDq/DW8BZq/9nCy7w8luk/fXOZc6OU8Z7eHgGz4byY 10 | NvxXo2RCgYAyL2ZL9IK74iXp3KAkIlHblOPlVGcA8Tlu9Cnd22CRqraFQYnSzLby 11 | eLf6xbTuAquE1N7CyVE6j5RnW960pyhZnzKmGmTHTP/IfsomMDSA6OUHTduQ47No 12 | pBQtAr4unEyw2DRb2oZYYuuTgVeICpWRLutspsbxFamPKK/4oRVT61pO+X3BHsIE 13 | sykeAEgXRB7pG/avwRF16gTIkeC/VGhnhrzgN+TW7UHlNw5lbB7s3TrgUwARAQAB 14 | tEJIZWN0b3IgSHVydGFkbyBSdWVzZ2EgPDQxMzI5MzY2K2hodXJ0YWRvQHVzZXJz 15 | Lm5vcmVwbHkuZ2l0aHViLmNvbT6JAk4EEwEIADgWIQTJFt6xfHLn3RQ/0UJEqwoW 16 | MWRZNgUCXtkRIAIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBEqwoWMWRZ 17 | Nn24EADZXcCxGjoWDysGTlcSsONKhj8Ij9d8uL6wa2vI5nT6sRQjT73dEWko3cQ/ 18 | eILlaxH8VqUtWDOR7PV4DFJdeeSW98fnBSK4drYzV5ORFkePDt2RssMlxAHsuVCv 19 | EbGehmYsoP87IwDLyRO9XEGdUrLD++quhUyiW4Q1rFkQtFX+Ur+0ElU98ZnApH17 20 | VD3dyby/DZRu1674zhvVHuxk5EIwkamkxCEH+ta3aPr6j8MByedTaddTTSwWuLEZ 21 | 4XviQvqCLoNyG14gKydIpGDfSwWUTJgrsZakilAcAT7Mqf3zSmViW3+jtbntF/1f 22 | dQ8Wtq6oKM/HyFW8PwoLrzhJ0b7ErUBPEmjC1bil49fw/sw0omtSuktkFJNtnIZd 23 | WBzZOy/+ZYs+f1evgmbz0c8f6y/ytCBoVezzumvFmqI0X85LMjIFpPD2x+g/LG7s 24 | 4A/dWl2eAiEJ5w8DRumBt8jnG83t0AewNeSBFX6QZfYxkDhJm/990p6Kn3B8qEzJ 25 | yv3u4fhcPJbWAHsrzgMNQrsn0jl2EAy91NQnv1S+9RZXzRCm0NaKjhOPv0oh/0V3 26 | hNiLLKrOe9jV5/UtjcoGKqF70P7ikI2Z2XjRNw+yOjz+Mw+/CIXwU1AXSAeGpzMb 27 | xzu5aqp51oQXWu7wYR5BOFIDrjyjpZweRA52CfIPxaEIizQKMrkCDQRe2REgARAA 28 | vl9ljLmcRwyLGbGbGLpoROyOwftMnaoY/kbKtqTM6SaRW2Ro0SdsJID99uvFWRVp 29 | AbfdWzQ9F9zLdfuttxakbYAVYD0ie/X+XcYvVJ0rDETw0IFOE0M4TOUnO3BBdPz4 30 | eTf5RG2nVdo0aoB/3IhbxKkmJ0maEmBL5jr7EdQO5DgdYtUi7HHs/cFAgqPYxTy5 31 | /XNIiaOI3aekQjwFaT4xycta/yudxnRa/6eUDb8FLcWyc6PBYJryuS+rctusBZgv 32 | L3RvcPooO9GUWioOxA1Lmt5sVBpAE7r/Ef3uWdpmnBMMbgq2MfxdxlMyES6J6JhF 33 | 68tnsoc5BMCDvyPsQ9tcLaHjxxohXgAnVXbppKLnK7iGdL7zYPOyOM5Nhkc6fPLW 34 | 1YnF7G3+B7uVY4LBuBHHz81HC0RvDcKwhidJk9bfW2a4lyEy4ZjYGYQ6wHPVu4ts 35 | qWh0F/b3gIbAB3YWKaoB+IEdSo8UNM+7VmBjySPDgOx58NKva3X4LzQ1SXvI2wSV 36 | vw7I81s6Qea39IuPmrOoSEUiEVW96IdEUsDtwbtIuigOk+EYl/KOLL1UKtYC+mh8 37 | 35A4W7uFz5SECv/UekYL9VQjOrG0+VNcHI9Fjfnd7FTeksACl6oWf3RRdHV7X5fK 38 | axjWPpQ6MlVvC5O9m/vVW4zP8np09QN7za3g8jfw2ekAEQEAAYkCNgQYAQgAIBYh 39 | BMkW3rF8cufdFD/RQkSrChYxZFk2BQJe2REgAhsMAAoJEESrChYxZFk2IyAP/i7c 40 | Si21sL298n5CXec8sGA0N/pvhRzZ/Vhef9idxldbgZlZV7TXfNc7rXXjfpIiQ6NW 41 | YsexIeRcM6oLBNvUTdIqKaTlIJ9cv9jo2uOnthfbNpA0IQtJrnYUCHmQ1LN560wv 42 | mzJd8gRa03Llh73d6Rnm7bDVb9alde91cOD13OILoo/p6N4fiFvXTC+1CgxGQ8mg 43 | spm+oWJhJQEm4ldhAZvI0kLkKZZmIz6FHs0dsWrrgR6ULEY01iLH394XT/0HKLaf 44 | beusC4l5bMEIGURbfF5x9zvCoHV+cNrsFMD5EpA4HMYm0hLmhwKoK5i1VjoMiyr1 45 | qUB6V+7iArKnVdaOZcqKQ+QoV4cECaZnP+y/oOVxHWv2PtSR22dr3I3YRlp2pPkr 46 | O2BCIsDN2izYZ/xUo5FlOttrOgZCVlIFCg6+vxZ5Q0mmDrRYenbDmO37pCWGVzvC 47 | a1QQKg696xTlfwUpnUg6CE5H0s+zaSgZU4Oyla0AVbun/hvMABlt6GjOveGdmXEq 48 | uyiipgoC7SXqsHcT3cNKS5fwhde0OZEOBLVQFqMuIrSZJaCagE8mbEBVIlP9bNJn 49 | rIanYoWCfqMIwPz2K17lmjN/m8vmD11AZ7OKL2sas8p/lSvNprh5QN7DayduJQLv 50 | CUrXmHAcCI5KKXRk/H0BOzxUQVQnEIRI/Ls1/JTx 51 | =Z27G 52 | -----END PGP PUBLIC KEY BLOCK----- 53 | -------------------------------------------------------------------------------- /.github/revokers/679F65C4563CF5BA: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 65 | =6uBS 66 | -----END PGP PUBLIC KEY BLOCK----- 67 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '28 19 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4.1.7 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v3 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | 3 | output 4 | build 5 | .vscode 6 | 7 | docs/build 8 | 9 | node_modules 10 | 11 | *.swp 12 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | 5 | project_name: kapow 6 | 7 | builds: 8 | - id: kapow 9 | flags: 10 | - -trimpath 11 | env: 12 | - CGO_ENABLED=0 13 | goos: 14 | - darwin 15 | - linux 16 | - windows 17 | - freebsd 18 | goarch: 19 | - 386 20 | - amd64 21 | - arm 22 | - arm64 23 | ignore: 24 | - goos: darwin 25 | goarch: 386 26 | archives: 27 | - format: binary 28 | checksum: 29 | name_template: '{{ .ProjectName }}-{{ .Version }}-SHA512SUMS' 30 | algorithm: sha512 31 | snapshot: 32 | name_template: "{{ .Tag }}-next" 33 | changelog: 34 | filters: 35 | exclude: 36 | - '^(chore|docs?|fix|refactor|style|test|typo|wip)(\([^)]+\))?:' 37 | 38 | dockers: 39 | - goos: linux 40 | goarch: amd64 41 | goarm: '' 42 | 43 | image_templates: 44 | - "bbvalabsci/kapow:latest" 45 | - "bbvalabsci/kapow:{{ .Tag }}" 46 | - "bbvalabsci/kapow:v{{ .Major }}" 47 | 48 | skip_push: true 49 | 50 | release: 51 | draft: false 52 | prerelease: auto 53 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Sorted by ``awk '{ print length(), $0 | "sort -n" }'``: 2 | 3 | - Luis Saiz 4 | - Emilio Belmar 5 | - César Gallego 6 | - Hector Hurtado 7 | - Sara de Pablos 8 | - pancho horrillo 9 | - Daniel García (cr0hn) 10 | - Roberto Abdelkader Martínez Pérez 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Contributing 3 | ************ 4 | 5 | Want to hack on ``Kapow!``? Awesome! Here are a few instructions to get you 6 | started. They are probably not perfect, please let us know if anything feels 7 | wrong or incomplete. 8 | 9 | In general, we follow the "fork-and-pull" Git workflow. 10 | 11 | 1. **Fork** this repo 12 | 2. **Clone** the project to your own machine 13 | 3. **Commit** changes to your own branch 14 | 4. **Push** your work back up to your fork 15 | 5. Submit a **Pull request** so that we can review your changes 16 | 17 | NOTE: Be sure to merge the latest commits from **upstream** before making a pull 18 | request! 19 | 20 | If your pull request is not accepted on the first try, don't be discouraged! If 21 | there's a problem with the implementation, hopefully you'll receive feedback on 22 | what to improve. 23 | 24 | Submit unit tests for your changes. Go has a great testing framework built 25 | in; use it! Take a look at existing tests for inspiration. Run the full test 26 | suite on your branch before submitting a pull request. 27 | 28 | Make sure you include relevant updates or additions to documentation 29 | when creating or modifying features. 30 | 31 | Before creating a Pull Request 32 | ------------------------------ 33 | 34 | Please, run the tests :) 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY kapow / 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint build test jaillover race coverage install acceptance deps docker clean 2 | 3 | GOCMD=go 4 | GOBUILD=$(GOCMD) build -trimpath 5 | GOINSTALL=$(GOCMD) install -trimpath 6 | GOGET=$(GOCMD) get 7 | GOTEST=$(GOCMD) test 8 | GOTOOL=$(GOCMD) tool 9 | GOLANGLINT=golangci-lint 10 | PROJECTREPO=github.com/BBVA/kapow 11 | 12 | BUILD_DIR=./build 13 | OUTPUT_DIR=./output 14 | TMP_DIR=/tmp 15 | DOCS_DIR=./doc 16 | DOCKER_DIR=./docker 17 | 18 | BINARY_NAME=kapow 19 | 20 | all: lint test race build 21 | 22 | lint: 23 | $(GOLANGLINT) run 24 | 25 | build: deps 26 | mkdir -p $(BUILD_DIR) 27 | CGO_ENABLED=0 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) -v 28 | 29 | test: build jaillover 30 | $(GOTEST) -v -coverprofile=$(TMP_DIR)/c.out ./... 31 | 32 | jaillover: 33 | $(GOINSTALL) -v $(PROJECTREPO)/testutils/$@ 34 | 35 | race: build 36 | $(GOTEST) -race -v ./... 37 | 38 | coverage: test race 39 | mkdir -p $(OUTPUT_DIR) 40 | $(GOTOOL) cover -html=$(TMP_DIR)/c.out -o $(OUTPUT_DIR)/coverage.html 41 | 42 | install: build 43 | CGO_ENABLED=0 $(GOINSTALL) ./... 44 | 45 | acceptance: build 46 | cd ./spec/test && PATH=$(PWD)/build:$$PATH nix-shell --command make 47 | 48 | deps: 49 | @echo "deps here" 50 | 51 | clean: 52 | rm -rf $(BUILD_DIR) $(OUTPUT_DIR) $(DOCKER_DIR)/* 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Kapow! Logo 3 |

If you can script it, you can HTTP it.

4 |

5 | Test status 6 | Go Report 7 | Open Issues 8 | Documentation 9 | Current Version 10 |

11 |

12 | 13 | --- 14 | 15 | ## What's *Kapow!* 16 | 17 | Say we have a nice cozy **shell command** that solves our problem. *Kapow!* lets 18 | us easily **turn that into an HTTP API**. 19 | 20 | 21 | ### Let's see this with an example 22 | 23 | We want to expose **log entries** for files not found on our **Apache Web 24 | Server**, as an HTTP API. With *Kapow!* we just need to write this 25 | *executable* script: 26 | 27 | ``` console 28 | [apache-host]$ cat search-apache-errors 29 | #!/usr/bin/env sh 30 | kapow route add /apache-errors - <<-'EOF' 31 | cat /var/log/apache2/access.log | grep 'File does not exist' | kapow set /response/body 32 | EOF 33 | [apache-host]$ chmod +x search-apache-errors 34 | ``` 35 | 36 | and then, run it using *Kapow!* 37 | 38 | ```bash 39 | [apache-host]$ kapow server search-apache-errors 40 | ``` 41 | 42 | finally, we can read from the just-defined endpoint: 43 | 44 | ```bash 45 | [another-host]$ curl http://apache-host:8080/apache-errors 46 | [Fri Feb 01 22:07:57.154391 2019] [core:info] [pid 7:tid 140284200093440] [client 172.17.0.1:50756] AH00128: File does not exist: /usr/var/www/mysite/favicon.ico 47 | [Fri Feb 01 22:07:57.808291 2019] [core:info] [pid 8:tid 140284216878848] [client 172.17.0.1:50758] AH00128: File does not exist: /usr/var/www/mysite/favicon.ico 48 | [Fri Feb 01 22:07:57.878149 2019] [core:info] [pid 8:tid 140284208486144] [client 172.17.0.1:50758] AH00128: File does not exist: /usr/var/www/mysite/favicon.ico 49 | ... 50 | ``` 51 | 52 | ### Why *Kapow!* shines in these cases 53 | 54 | - We can share information **without having to grant SSH access** to anybody. 55 | - We can share information easily **over HTTP**. 56 | - We can effectively **limit** what gets executed. 57 | 58 | 59 | ## Documentation 60 | 61 | You can find the complete documentation and examples [here](https://kapow.readthedocs.io). 62 | 63 | ## Security 64 | 65 | Please consider the following 66 | [Security Concerns](https://kapow.readthedocs.io/en/stable/the_project/security.html#security-concerns) 67 | **before** using *Kapow!* 68 | 69 | If you are not 100% sure about what you are doing we recommend not using *Kapow!* 70 | 71 | ## Authors 72 | 73 | *Kapow!* is being developed by [BBVA-Labs Security team members](https://github.com/BBVA/kapow/blob/master/AUTHORS.rst). 74 | 75 | *Kapow!* is Open Source Software and available under the [Apache 2 76 | license](https://raw.githubusercontent.com/BBVA/kapow/master/LICENSE). 77 | 78 | 79 | ## Contributions 80 | 81 | Contributions are of course welcome. See 82 | [CONTRIBUTING](https://raw.githubusercontent.com/BBVA/kapow/master/CONTRIBUTING.rst) 83 | or skim existing tickets to see where you could help out. 84 | 85 | --- 86 | 87 | * [Did you spot the UUoC?](https://github.com/BBVA/kapow/issues/118) Congrats! We are hoping [to win](http://porkmail.org/era/unix/award.html) this year :) 88 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | sphinx = "*" 10 | sphinx-rtd-theme = "*" 11 | 12 | [requires] 13 | python_version = "3.10" 14 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | This directory contains the documentation _source code_. To see the rendered docs go to http://kapow.readthedocs.io 2 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/release-notes/RELEASE-v0.3.0.md: -------------------------------------------------------------------------------- 1 | Kapow! v0.3.0 2 | 3 | ## Features 4 | 5 | * Feature parity with the original PoC (written in Python), with some exceptions 6 | explained in the next section. 7 | 8 | * Built and tested against Go 1.13.5. 9 | 10 | * (Almost) no bytes were harmed during the development of this release. 11 | 12 | 13 | ## Known issues and limitations 14 | 15 | ### User server 16 | 17 | * [#76][i76] Only plain `http` is supported for now, since `https` support is 18 | not yet complete. The `kapow server` flags `--certfile` and `--keyfile` are 19 | present but non-functional yet. 20 | 21 | 22 | ### Data API 23 | 24 | * [#73][i73] `/response/body` and `/response/stream` behave identically for now. 25 | 26 | * [#92][i92] `KAPOW_DATA_URL` is always set to its default value of 27 | `http://localhost:8082`, and not to the actual value, as specified by the 28 | `kapow server --data-bind "host:port"` invocation. 29 | 30 | 31 | ### Windows® 32 | 33 | * [#83][i83] `kapow server file.pow` will try to run `file.pow` through `bash`, not `cmd` 34 | or `powershell`. 35 | * `kapow route` default entrypoint is `/bin/sh`, so in order to use `cmd` or 36 | `powershell`, it must be explicitly set. 37 | 38 | 39 | [i73]: https://github.com/BBVA/kapow/issues/73 40 | [i76]: https://github.com/BBVA/kapow/issues/76 41 | [i83]: https://github.com/BBVA/kapow/issues/83 42 | [i92]: https://github.com/BBVA/kapow/issues/92 43 | -------------------------------------------------------------------------------- /docs/release-notes/RELEASE-v0.4.0.md: -------------------------------------------------------------------------------- 1 | Kapow! v0.4.0 2 | 3 | ## Features 4 | 5 | * [#76][i76] Implement `https` support in the user server. 6 | 7 | * [#104][i104] Implement TLS mutual auth with x509 certs. 8 | 9 | * Built and tested against Go 1.14. 10 | 11 | 12 | ## Known issues and limitations 13 | 14 | ### Data API 15 | 16 | * [#73][i73] `/response/body` and `/response/stream` behave identically for now. 17 | 18 | * [#92][i92] `KAPOW_DATA_URL` is always set to its default value of 19 | `http://localhost:8082`, and not to the actual value, as specified by the 20 | `kapow server --data-bind "host:port"` invocation. 21 | 22 | 23 | ### Windows® 24 | 25 | * [#83][i83] `kapow server file.pow` will try to run `file.pow` through `bash`, not `cmd` 26 | or `powershell`. 27 | * `kapow route` default entrypoint is `/bin/sh`, so in order to use `cmd` or 28 | `powershell`, it must be explicitly set. 29 | 30 | 31 | [i73]: https://github.com/BBVA/kapow/issues/73 32 | [i76]: https://github.com/BBVA/kapow/issues/76 33 | [i83]: https://github.com/BBVA/kapow/issues/83 34 | [i92]: https://github.com/BBVA/kapow/issues/92 35 | [i104]: https://github.com/BBVA/kapow/issues/104 36 | -------------------------------------------------------------------------------- /docs/release-notes/RELEASE-v0.5.0.md: -------------------------------------------------------------------------------- 1 | Kapow! v0.5.0 2 | 3 | ## Features 4 | 5 | * [#89][i89] Wrong environment variables exported to process 6 | 7 | * [#98][i98] Implement a proper logging system (Partially solved. Added a logging feature for scripts in User-server for aid in debugging) 8 | 9 | * [#92][i92] Spawn package uses static default value for KAPOW_DATA_URL 10 | 11 | * [#102][i102] Handle race condition between Control API and pow files 12 | 13 | * [#105][i105] Handle unexpected exit of servers 14 | 15 | 16 | ## Known issues and limitations 17 | 18 | ### Data API 19 | 20 | * [#73][i73] `/response/body` and `/response/stream` behave identically for now. 21 | 22 | 23 | ### Windows® 24 | 25 | * [#83][i83] `kapow server file.pow` will try to run `file.pow` through `bash`, not `cmd` 26 | or `powershell`. 27 | * `kapow route` default entrypoint is `/bin/sh`, so in order to use `cmd` or 28 | `powershell`, it must be explicitly set. 29 | 30 | 31 | [i73]: https://github.com/BBVA/kapow/issues/73 32 | [i83]: https://github.com/BBVA/kapow/issues/83 33 | [i89]: https://github.com/BBVA/kapow/issues/89 34 | [i98]: https://github.com/BBVA/kapow/issues/98 35 | [i92]: https://github.com/BBVA/kapow/issues/92 36 | [i102]: https://github.com/BBVA/kapow/issues/102 37 | [i105]: https://github.com/BBVA/kapow/issues/105 38 | -------------------------------------------------------------------------------- /docs/release-notes/RELEASE-v0.5.1.md: -------------------------------------------------------------------------------- 1 | Kapow! v0.5.1 2 | 3 | ## Features 4 | 5 | * Re-release of v0.5.0 with missing release-key.gpg added, as well as debugging doc example 6 | 7 | * [#89][i89] Wrong environment variables exported to process 8 | 9 | * [#98][i98] Implement a proper logging system (Partially solved. Added a logging feature for scripts in User-server for aid in debugging) 10 | 11 | * [#92][i92] Spawn package uses static default value for KAPOW_DATA_URL 12 | 13 | * [#102][i102] Handle race condition between Control API and pow files 14 | 15 | * [#105][i105] Handle unexpected exit of servers 16 | 17 | 18 | ## Known issues and limitations 19 | 20 | ### Data API 21 | 22 | * [#73][i73] `/response/body` and `/response/stream` behave identically for now. 23 | 24 | 25 | ### Windows® 26 | 27 | * [#83][i83] `kapow server file.pow` will try to run `file.pow` through `bash`, not `cmd` 28 | or `powershell`. 29 | * `kapow route` default entrypoint is `/bin/sh`, so in order to use `cmd` or 30 | `powershell`, it must be explicitly set. 31 | 32 | 33 | [i73]: https://github.com/BBVA/kapow/issues/73 34 | [i83]: https://github.com/BBVA/kapow/issues/83 35 | [i89]: https://github.com/BBVA/kapow/issues/89 36 | [i98]: https://github.com/BBVA/kapow/issues/98 37 | [i92]: https://github.com/BBVA/kapow/issues/92 38 | [i102]: https://github.com/BBVA/kapow/issues/102 39 | [i105]: https://github.com/BBVA/kapow/issues/105 40 | -------------------------------------------------------------------------------- /docs/release-notes/RELEASE-v0.5.2.md: -------------------------------------------------------------------------------- 1 | Kapow! v0.5.2 2 | 3 | ## Features 4 | 5 | * Fix handling of misbehaving http clients 6 | 7 | ## Known issues and limitations 8 | 9 | ### Data API 10 | 11 | * [#73][i73] `/response/body` and `/response/stream` behave identically for now. 12 | 13 | 14 | ### Windows® 15 | 16 | * [#83][i83] `kapow server file.pow` will try to run `file.pow` through `bash`, not `cmd` 17 | or `powershell`. 18 | * `kapow route` default entrypoint is `/bin/sh`, so in order to use `cmd` or 19 | `powershell`, it must be explicitly set. 20 | 21 | 22 | [i73]: https://github.com/BBVA/kapow/issues/73 23 | [i83]: https://github.com/BBVA/kapow/issues/83 24 | -------------------------------------------------------------------------------- /docs/release-notes/RELEASE-v0.5.3.md: -------------------------------------------------------------------------------- 1 | Kapow! v0.5.3 2 | 3 | ## Features 4 | 5 | * Rebuild againts Go 1.4.3 6 | 7 | ## Known issues and limitations 8 | 9 | ### Data API 10 | 11 | * [#73][i73] `/response/body` and `/response/stream` behave identically for now. 12 | 13 | 14 | ### Windows® 15 | 16 | * [#83][i83] `kapow server file.pow` will try to run `file.pow` through `bash`, not `cmd` 17 | or `powershell`. 18 | * `kapow route` default entrypoint is `/bin/sh`, so in order to use `cmd` or 19 | `powershell`, it must be explicitly set. 20 | 21 | 22 | [i73]: https://github.com/BBVA/kapow/issues/73 23 | [i83]: https://github.com/BBVA/kapow/issues/83 24 | -------------------------------------------------------------------------------- /docs/release-notes/RELEASE-v0.5.4.md: -------------------------------------------------------------------------------- 1 | Kapow! v0.5.4 2 | 3 | ## Features 4 | 5 | * Rebuild againts Go 1.14.4 6 | 7 | ## Known issues and limitations 8 | 9 | ### Data API 10 | 11 | * [#73][i73] `/response/body` and `/response/stream` behave identically for now. 12 | 13 | 14 | ### Windows® 15 | 16 | * [#83][i83] `kapow server file.pow` will try to run `file.pow` through `bash`, not `cmd` 17 | or `powershell`. 18 | * `kapow route` default entrypoint is `/bin/sh`, so in order to use `cmd` or 19 | `powershell`, it must be explicitly set. 20 | 21 | 22 | [i73]: https://github.com/BBVA/kapow/issues/73 23 | [i83]: https://github.com/BBVA/kapow/issues/83 24 | -------------------------------------------------------------------------------- /docs/source/_static/browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BBVA/kapow/3cef9f0cd25ab41f683a883d3ae2ce042e162d59/docs/source/_static/browser.png -------------------------------------------------------------------------------- /docs/source/_static/kapow-quick-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BBVA/kapow/3cef9f0cd25ab41f683a883d3ae2ce042e162d59/docs/source/_static/kapow-quick-overview.png -------------------------------------------------------------------------------- /docs/source/_static/logo-200px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BBVA/kapow/3cef9f0cd25ab41f683a883d3ae2ce042e162d59/docs/source/_static/logo-200px.png -------------------------------------------------------------------------------- /docs/source/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BBVA/kapow/3cef9f0cd25ab41f683a883d3ae2ce042e162d59/docs/source/_static/logo.png -------------------------------------------------------------------------------- /docs/source/_static/network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BBVA/kapow/3cef9f0cd25ab41f683a883d3ae2ce042e162d59/docs/source/_static/network.png -------------------------------------------------------------------------------- /docs/source/_static/request_life_cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BBVA/kapow/3cef9f0cd25ab41f683a883d3ae2ce042e162d59/docs/source/_static/request_life_cycle.png -------------------------------------------------------------------------------- /docs/source/_static/request_life_cycle.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BBVA/kapow/3cef9f0cd25ab41f683a883d3ae2ce042e162d59/docs/source/_static/request_life_cycle.xcf -------------------------------------------------------------------------------- /docs/source/_static/sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BBVA/kapow/3cef9f0cd25ab41f683a883d3ae2ce042e162d59/docs/source/_static/sequence.png -------------------------------------------------------------------------------- /docs/source/concepts/interfaces.rst: -------------------------------------------------------------------------------- 1 | *Kapow!* HTTP Interfaces 2 | ======================== 3 | 4 | ``kapow server`` sets up three HTTP server interfaces, each with a distinct and 5 | clear purpose. 6 | 7 | 8 | .. _http-user-interface: 9 | 10 | HTTP User Interface 11 | ------------------- 12 | 13 | The `HTTP User Interface` is used to serve final user requests. 14 | 15 | By default it binds to address ``0.0.0.0`` and port ``8080``, but that can be 16 | changed via the ``--bind`` flag. 17 | 18 | 19 | .. _https-control-interface: 20 | 21 | HTTPS Control Interface 22 | ----------------------- 23 | 24 | The `HTTPS Control Interface` is used by the command ``kapow route`` to 25 | administer the list of system routes. 26 | 27 | This interface uses mTLS by default (double-pinned autogenerated certs). 28 | 29 | By default it binds to address ``127.0.0.1`` and port ``8081``, but that can be 30 | changed via the ``--control-bind`` flag. If this is the case, consider 31 | also ``--control-reachable-addr`` which will configure the autogenerated 32 | certificate to match that address. 33 | 34 | 35 | .. _http-data-interface: 36 | 37 | HTTP Data Interface 38 | ------------------- 39 | 40 | The `HTTP Data Interface` is used by the commands ``kapow get`` and ``kapow 41 | set`` to exchange the data for a particular request. 42 | 43 | By default it binds to address ``127.0.0.1`` and port ``8082``, but that can be 44 | changed via the ``--data-bind`` flag. 45 | -------------------------------------------------------------------------------- /docs/source/concepts/philosophy.rst: -------------------------------------------------------------------------------- 1 | Philosophy 2 | ========== 3 | 4 | 5 | Single Static Binary 6 | -------------------- 7 | 8 | - Deployment is then as simple as it gets. 9 | 10 | - `Docker`-friendly. 11 | 12 | 13 | Shell Agnostic 14 | -------------- 15 | 16 | - *Kapow!*, like John Snow, knows nothing, and makes no assumptions about the 17 | shell you are using. It only spawns executables. 18 | 19 | - You are free to implement a client to the Data API directly if you are so 20 | inclined. The spec provides all the necessary details. 21 | 22 | 23 | Not a Silver Bullet 24 | ------------------- 25 | 26 | You should not use *Kapow!* if your project requires complex business logic. 27 | 28 | If you try to encode business logic in a shell script, you will **deeply** 29 | regret it soon enough. 30 | 31 | *Kapow!* is designed for automating simple stuff. 32 | 33 | 34 | Interoperability over Performance 35 | --------------------------------- 36 | 37 | We want *Kapow!* to be as performant as possible, but not at the cost of 38 | flexibility. This is the reason why our :ref:`Data API 39 | ` leverages HTTP 40 | instead of a lighter protocol for example. 41 | 42 | When we have to choose between making things faster or more 43 | interoperable the latter usually wins. 44 | -------------------------------------------------------------------------------- /docs/source/concepts/request_life_cycle.rst: -------------------------------------------------------------------------------- 1 | Request Life Cycle 2 | ================== 3 | 4 | This section describes the sequence of events happening for each request 5 | answered by the :ref:`http-user-interface`. 6 | 7 | .. image:: ../_static/request_life_cycle.png 8 | 9 | 10 | 1. request 11 | ---------- 12 | 13 | The user makes a request to the :ref:`http-user-interface`. 14 | 15 | - The request is matched against the route table. 16 | 17 | - :program:`kapow` provides a `HANDLER_ID` to identify this request and don't 18 | mix it with other requests that could be running concurrently. 19 | 20 | 21 | 2. spawn 22 | -------- 23 | 24 | :program:`kapow` spawns the executable specified as entrypoint in the matching 25 | route. 26 | 27 | The default entrypoint is :command:`/bin/sh`; let's focus on this workflow. 28 | 29 | The spawned entrypoint is run with the following variables added to its 30 | environment: 31 | 32 | - :envvar:`KAPOW_HANDLER_ID`: Containing the `HANDLER_ID` 33 | - :envvar:`KAPOW_DATA_URL`: With the URL of the :ref:`http-data-interface` 34 | - :envvar:`KAPOW_CONTROL_URL`: With the URL of the :ref:`https-control-interface` 35 | 36 | 37 | 3. ``kapow set /response/body banana`` 38 | -------------------------------------- 39 | 40 | During the lifetime of the shell, the :ref:`request and response resources 41 | ` are available via these commands: 42 | 43 | - ``kapow get /request/...`` 44 | 45 | - ``kapow set /response/...`` 46 | 47 | These commands use the aforementioned environment variables to read data 48 | from the user request and to write the response. They accept data either as 49 | arguments or from `stdin`. 50 | 51 | 52 | 4. exit 53 | ------- 54 | 55 | The shell dies. Long live the shell! 56 | 57 | 58 | 5. response 59 | ----------- 60 | 61 | :program:`kapow` finalizes the original request. Enjoy your banana now. 62 | -------------------------------------------------------------------------------- /docs/source/concepts/route_matching.rst: -------------------------------------------------------------------------------- 1 | Route Matching 2 | ============== 3 | 4 | *Kapow!* maintains a :ref:`route ` table with a list of routes as provided by the user, 5 | and uses it to determine which handler an incoming request should be dispatched 6 | to. 7 | 8 | Each incoming request is matched against the routes in the route table in 9 | strict order. For each route in the route table, the criteria are checked. 10 | If the request does not match, the next route in the route list is examined. 11 | -------------------------------------------------------------------------------- /docs/source/concepts/routes.rst: -------------------------------------------------------------------------------- 1 | .. _routes: 2 | 3 | Routes 4 | ====== 5 | 6 | A *Kapow!* route specifies the matching criteria for an incoming request on 7 | the :ref:`http-user-interface`, and the details to handle it. 8 | 9 | *Kapow!* implements a *route table* where all routes reside. 10 | 11 | A route can be set like this: 12 | 13 | .. code-block:: console 14 | 15 | $ kapow route add \ 16 | -X POST \ 17 | '/register/{username}' \ 18 | -e '/bin/bash -c' \ 19 | -c 'touch /var/lib/mydb/"$(kapow get /request/matches/username)"' \ 20 | | jq 21 | { 22 | "id": "deadbeef-0d09-11ea-b18e-106530610c4d", 23 | "method": "POST", 24 | "url_pattern": "/register/{username}", 25 | "entrypoint": "/bin/bash -c", 26 | "command": "touch /var/lib/mydb/\"$(kapow get /request/matches/username)\"" 27 | } 28 | 29 | Let's use this example to discuss its elements. 30 | 31 | 32 | Elements 33 | -------- 34 | 35 | ``id`` Route Element 36 | ~~~~~~~~~~~~~~~~~~~~ 37 | 38 | Uniquely identifies each route. It is used for instance by ``kapow route remove 39 | ``. 40 | 41 | .. note:: 42 | 43 | The current implementation of *Kapow!* autogenerates a `UUID` for this field. 44 | In the future the user will be able to specify a custom value. 45 | 46 | 47 | ``method`` Route Element 48 | ~~~~~~~~~~~~~~~~~~~~~~~~ 49 | 50 | Specifies the HTTP method for the route to match the incoming request. 51 | 52 | Note that the route shown above will only match a ``POST`` request. 53 | 54 | 55 | ``url_pattern`` Route Element 56 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 57 | 58 | It matches the `path` component of the `URL` of the incoming request. 59 | 60 | It can contain regex placeholders for easily capturing fragments of the path. 61 | 62 | In the route shown above, a request with a URL ``/register/joe`` would match, 63 | assigning `joe` to the placeholder ``username``. 64 | 65 | *Kapow!* leverages `Gorilla Mux`_ for managing routes. For the full story, see 66 | https://github.com/gorilla/mux 67 | 68 | 69 | .. _entrypoint-route-element: 70 | 71 | ``entrypoint`` Route Element 72 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 73 | 74 | This sets the executable to be spawned, along with any arguments required. 75 | 76 | In the route shown above, the entrypoint that will be run is ``/bin/bash -c``, 77 | which is an incomplete recipe. It is then completed by the :ref:`command 78 | element `. 79 | 80 | .. note:: 81 | 82 | The semantics of this element closely match the `Dockerfile`'s `ENTRYPOINT`_ 83 | directive. 84 | 85 | 86 | .. _command-route-element: 87 | 88 | ``command`` Route Element 89 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 90 | 91 | This is an optional last argument to be passed to the 92 | :ref:`entrypoint `. 93 | 94 | In the route shown above, it completes the ``entrypoint`` to form the final 95 | incantation to be executed: 96 | 97 | .. code-block:: bash 98 | 99 | /bin/bash -c 'touch /var/lib/mydb/"$(kapow get /request/matches/username)"' 100 | 101 | .. note:: 102 | 103 | The semantics of this element closely match the `Dockerfile`'s `CMD`_ 104 | directive. 105 | 106 | 107 | Matching Algorithm 108 | ------------------ 109 | 110 | *Kapow!* leverages `Gorilla Mux`_ for this task. Check their documentation for 111 | the gory details. 112 | 113 | 114 | .. _ENTRYPOINT: https://docs.docker.com/engine/reference/builder/#entrypoint 115 | .. _CMD: https://docs.docker.com/engine/reference/builder/#cmd 116 | .. _Gorilla Mux: https://www.gorillatoolkit.org/pkg/mux 117 | -------------------------------------------------------------------------------- /docs/source/concepts/toc.rst: -------------------------------------------------------------------------------- 1 | Concepts 2 | ======== 3 | 4 | This section is a contains a reference of all the important *Kapow!* 5 | concepts that you should know. 6 | 7 | .. toctree:: 8 | :hidden: 9 | 10 | interfaces 11 | philosophy 12 | request_life_cycle 13 | resource_tree 14 | route_matching 15 | routes 16 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | import os 17 | import re 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'Kapow!' 22 | copyright = '2019, BBVA Innovation Labs' 23 | author = 'BBVA Innovation Labs' 24 | 25 | # The full version, including alpha/beta/rc tags. 26 | try: 27 | release = re.sub('^v', '', os.popen('git describe --tags').read().strip()) 28 | except: 29 | release = 'unknown' 30 | 31 | try: 32 | # The short X.Y.Z-rcN version. 33 | version = re.search('^(\d+\.\d+\.\d+(:?-rc\d+)?)', release).group(0) 34 | except: 35 | version = release 36 | 37 | 38 | # -- General configuration --------------------------------------------------- 39 | 40 | # Add any Sphinx extension module names here, as strings. They can be 41 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 42 | # ones. 43 | extensions = [ 44 | 'sphinx.ext.todo', 45 | 'sphinx.ext.imgconverter' 46 | ] 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # List of patterns, relative to source directory, that match files and 52 | # directories to ignore when looking for source files. 53 | # This pattern also affects html_static_path and html_extra_path. 54 | exclude_patterns = [] 55 | 56 | rst_prolog = """ 57 | .. role:: tech(code) 58 | :class: xref 59 | 60 | .. role:: nref-option(code) 61 | :class: xref 62 | 63 | .. default-role:: tech 64 | 65 | """ 66 | 67 | # -- Options for HTML output ------------------------------------------------- 68 | 69 | # The theme to use for HTML and HTML Help pages. See the documentation for 70 | # a list of builtin themes. 71 | # 72 | html_theme = "sphinx_rtd_theme" 73 | html_logo = "_static/logo-200px.png" 74 | html_theme_options = { 75 | 'logo_only': True, 76 | 'collapse_navigation': False, 77 | 'navigation_depth': 3, 78 | 'includehidden': True, 79 | 'titles_only': False 80 | 81 | } 82 | 83 | # Add any paths that contain custom static files (such as style sheets) here, 84 | # relative to this directory. They are copied after the builtin static files, 85 | # so a file named "default.css" will overwrite the builtin "default.css". 86 | html_static_path = ['_static'] 87 | 88 | # https://stackoverflow.com/a/56448499 89 | master_doc = 'index' 90 | 91 | latex_logo = '_static/logo.png' 92 | latex_documents = [ 93 | ('latextoc', 94 | 'kapow.tex', 95 | 'Kapow! Documentation', 96 | 'BBVA Innovation Labs', 97 | 'manual', 98 | True) 99 | ] 100 | 101 | man_pages = [ 102 | ('concepts/resource_tree', 103 | 'kapow-resources', 104 | 'Kapow! Resource Tree Reference', 105 | 'BBVA Innovation Labs', 106 | 1), 107 | ('examples/examples', 108 | 'kapow-examples', 109 | 'Kapow! Usage Examples', 110 | 'BBVA Innovation Labs', 111 | 1), 112 | ] 113 | -------------------------------------------------------------------------------- /docs/source/examples/managing_routes.rst: -------------------------------------------------------------------------------- 1 | Managing Routes 2 | =============== 3 | 4 | Adding New Routes 5 | ----------------- 6 | 7 | .. warning:: 8 | 9 | Be aware that if you register more than one route with exactly the 10 | same path, only the first route added will be used. 11 | 12 | GET route 13 | +++++++++ 14 | 15 | Defining a route: 16 | 17 | .. code-block:: console 18 | :linenos: 19 | 20 | $ kapow route add /my/route -c 'echo hello world | kapow set /response/body' 21 | 22 | 23 | Calling route: 24 | 25 | .. code-block:: console 26 | :linenos: 27 | 28 | $ curl http://localhost:8080/my/route 29 | hello world 30 | 31 | POST route 32 | ++++++++++ 33 | 34 | Defining a route: 35 | 36 | .. code-block:: console 37 | :linenos: 38 | 39 | $ kapow route add -X POST /echo -c 'kapow get /request/body | kapow set /response/body' 40 | 41 | 42 | Calling a route: 43 | 44 | .. code-block:: console 45 | :linenos: 46 | 47 | $ curl -d 'hello world' -X POST http://localhost:8080/echo 48 | hello world 49 | 50 | 51 | Capturing Parts of the URL 52 | ++++++++++++++++++++++++++ 53 | 54 | Defining a route: 55 | 56 | .. code-block:: console 57 | :linenos: 58 | 59 | $ kapow route add '/echo/{message}' -c 'kapow get /request/matches/message | kapow set /response/body' 60 | 61 | 62 | Calling a route: 63 | 64 | .. code-block:: console 65 | :linenos: 66 | 67 | $ curl http://localhost:8080/echo/hello%20world 68 | hello world 69 | 70 | 71 | Listing Routes 72 | -------------- 73 | 74 | You can list the active routes in the *Kapow!* server. 75 | 76 | .. _listing-routes-example: 77 | 78 | .. code-block:: console 79 | :linenos: 80 | 81 | $ kapow route list 82 | [{"id":"20c98328-0b82-11ea-90a8-784f434dfbe2","method":"GET","url_pattern":"/echo/{message}","entrypoint":"/bin/sh -c","command":"kapow get /request/matches/message | kapow set /response/body"}] 83 | 84 | Or, if you want human-readable output, you can use :program:`jq`: 85 | 86 | .. code-block:: console 87 | :linenos: 88 | 89 | $ kapow route list | jq 90 | [ 91 | { 92 | "id": "20c98328-0b82-11ea-90a8-784f434dfbe2", 93 | "method": "GET", 94 | "url_pattern": "/echo/{message}", 95 | "entrypoint": "/bin/sh -c", 96 | "command": "kapow get /request/matches/message | kapow set /response/body", 97 | } 98 | ] 99 | 100 | 101 | .. note:: 102 | 103 | *Kapow!* has a :ref:`https-control-interface`, bound by default to 104 | ``localhost:8081``. 105 | 106 | 107 | Deleting Routes 108 | --------------- 109 | 110 | You need the ID of a route to delete it. 111 | Running the command used in the :ref:`listing routes example 112 | `, you can obtain the ID of the route, and then delete 113 | it by typing: 114 | 115 | .. code-block:: console 116 | :linenos: 117 | 118 | $ kapow route remove 20c98328-0b82-11ea-90a8-784f434dfbe2 119 | 120 | 121 | -------------------------------------------------------------------------------- /docs/source/examples/shell_tricks.rst: -------------------------------------------------------------------------------- 1 | Shell Tricks 2 | ============ 3 | 4 | How to Execute Two Processes in Parallel 5 | ---------------------------------------- 6 | 7 | We want to :command:`ping` two machines parallel. *Kapow!* can get IP addresses 8 | from query params: 9 | 10 | .. code-block:: console 11 | :linenos: 12 | 13 | $ cat parallel-route 14 | #!/usr/bin/env sh 15 | kapow route add '/parallel/{ip1}/{ip2}' - <<-'EOF' 16 | ping -c 1 -- "$(kapow get /request/matches/ip1)" | kapow set /response/body & 17 | ping -c 1 -- "$(kapow get /request/matches/ip2)" | kapow set /response/body & 18 | wait 19 | EOF 20 | 21 | Calling with :program:`curl`: 22 | 23 | .. code-block:: console 24 | :linenos: 25 | 26 | $ curl -v http://localhost:8080/parallel/10.0.0.1/10.10.10.1 27 | 28 | Script debugging 29 | ---------------- 30 | 31 | Bash provides the ``set -x`` builtin command that "After expanding each simple command, 32 | for command, case command, select command, or arithmetic for command, display the 33 | expanded value of PS4, followed by the command and its expanded arguments or associated 34 | word list". This feature can be used to help debugging the init programs and, 35 | together the ``--debug`` option in the server sub-command, the scripts executed 36 | in user requests. 37 | -------------------------------------------------------------------------------- /docs/source/examples/toc.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | .. toctree:: 5 | 6 | working_with_init_programs 7 | managing_routes 8 | handling_http_requests 9 | using_json 10 | shell_tricks 11 | https_mtls 12 | -------------------------------------------------------------------------------- /docs/source/examples/using_json.rst: -------------------------------------------------------------------------------- 1 | Using JSON 2 | ========== 3 | 4 | Modify JSON by Using Shell Commands 5 | ----------------------------------- 6 | 7 | .. note:: 8 | 9 | Nowadays Web services are `JSON`-based, so making your script `JSON` aware is 10 | probably a good choice. In order to be able to extract data from a `JSON` 11 | document as well as composing `JSON` documents from a script, you can leverage 12 | `jq `_. 13 | 14 | 15 | Example #1 16 | ++++++++++ 17 | 18 | In this example our *Kapow!* service will receive a `JSON` value with an incorrect 19 | date, then our init program will fix it and return the correct value to the user. 20 | 21 | .. code-block:: console 22 | :linenos: 23 | 24 | $ cat fix_date 25 | #!/usr/bin/env sh 26 | kapow route add -X POST /fix-date - <<-'EOF' 27 | kapow set /response/headers/Content-Type application/json 28 | kapow get /request/body | jq --arg newdate "$(date +'%Y-%m-%d_%H-%M-%S')" '.incorrectDate=$newdate' | kapow set /response/body 29 | EOF 30 | 31 | Call the service with :program:`curl`: 32 | 33 | .. code-block:: console 34 | :linenos: 35 | 36 | $ curl -X POST http://localhost:8080/fix-date -H 'Content-Type: application/json' -d '{"incorrectDate": "no way, Jose"}' 37 | { 38 | "incorrectDate": "2019-11-22_10-42-06" 39 | } 40 | 41 | 42 | Example #2 43 | ++++++++++ 44 | 45 | In this example we extract the ``name`` field from the incoming `JSON` document in 46 | order to generate a two-attribute `JSON` response. 47 | 48 | .. code-block:: console 49 | 50 | $ cat echo-attribute 51 | #!/usr/bin/env sh 52 | kapow route add -X POST /echo-attribute - <<-'EOF' 53 | JSON_WHO=$(kapow get /request/body | jq -r .name) 54 | 55 | kapow set /response/headers/Content-Type application/json 56 | kapow set /response/status 200 57 | 58 | jq --arg greet Hello --arg value "${JSON_WHO:-World}" --null-input '{ greet: $greet, to: $value }' | kapow set /response/body 59 | EOF 60 | 61 | Call the service with :program:`curl`: 62 | 63 | .. code-block:: console 64 | :linenos: 65 | :emphasize-lines: 4 66 | 67 | $ curl -X POST http://localhost:8080/echo-attribute -H 'Content-Type: application/json' -d '{"name": "MyName"}' 68 | { 69 | "greet": "Hello", 70 | "to": "MyName" 71 | } 72 | -------------------------------------------------------------------------------- /docs/source/examples/working_with_init_programs.rst: -------------------------------------------------------------------------------- 1 | Working with Init Scripts 2 | ========================= 3 | 4 | Starting *Kapow!* using an init script 5 | -------------------------------------- 6 | 7 | An init program, which can be just a shell script, allows you to make calls to 8 | the ``kapow route`` command. 9 | 10 | .. code-block:: console 11 | :linenos: 12 | 13 | $ kapow server example-init-program 14 | 15 | With the :file:`example-init-program`: 16 | 17 | .. code-block:: console 18 | :linenos: 19 | 20 | $ cat example-init-program 21 | #!/usr/bin/env sh 22 | # 23 | # This is a simple example of an init program 24 | # 25 | echo '[*] Starting my init program' 26 | 27 | # We add 2 Kapow! routes 28 | kapow route add /my/route -c 'echo hello world | kapow set /response/body' 29 | kapow route add -X POST /echo -c 'kapow get /request/body | kapow set /response/body' 30 | 31 | .. note:: 32 | 33 | *Kapow!* can be fully configured using just init scripts 34 | 35 | 36 | Writing Multiline Routes 37 | ------------------------ 38 | 39 | If you need to write more complex actions, you can leverage multiline routes: 40 | 41 | .. code-block:: console 42 | :linenos: 43 | 44 | $ cat multiline-route 45 | #!/usr/bin/env sh 46 | kapow route add /log_and_stuff - <<-'EOF' 47 | echo this is a quite long sentence and other stuff | tee log.txt | kapow set /response/body 48 | cat log.txt | kapow set /response/body 49 | EOF 50 | 51 | .. warning:: 52 | 53 | Be aware of the **"-"** at the end of the ``kapow route add`` command. 54 | It tells ``kapow route add`` to read commands from `stdin`. 55 | 56 | .. warning:: 57 | 58 | If you want to learn more about multiline usage, see: `Here Doc 59 | `_ 60 | 61 | 62 | Keeping Things Tidy 63 | ------------------- 64 | 65 | Sometimes things grow, and keeping things tidy is the only way to mantain the 66 | whole thing. 67 | 68 | You can distribute your endpoints in several init programs. And you can keep 69 | the whole thing documented in one html file, served with *Kapow!*. 70 | 71 | .. code-block:: console 72 | :linenos: 73 | 74 | $ cat index-route 75 | #!/usr/bin/env sh 76 | kapow route add / - <<-'EOF' 77 | cat howto.html | kapow set /response/body 78 | EOF 79 | 80 | source ./info_stuff 81 | source ./other_endpoints 82 | 83 | You can import other shell script libraries with `source`. 84 | 85 | 86 | Debugging Init Programs/Scripts 87 | ------------------------------- 88 | 89 | Since *Kapow!* redirects the standard output and the standard error of the init 90 | program given on server startup to its own, you can leverage ``set -x`` to see 91 | the commands that are being executed, and use that for debugging. 92 | 93 | To support debugging user request executions, the server subcommand has a 94 | ``--debug`` option flag that prompts *Kapow!* to redirect both the script's 95 | standard output and standard error to *Kapow!*'s standard output, so you can 96 | leverage ``set -x`` the same way as with init programs. 97 | 98 | 99 | .. code-block:: console 100 | 101 | $ cat withdebug-route 102 | #!/usr/bin/env sh 103 | kapow route add / - <<-'EOF' 104 | set -x 105 | echo "This will be seen in the log" 106 | echo "Hi HTTP" | kapow set /response/body 107 | EOF 108 | 109 | $ kapow server --debug withdebug-route 110 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to *Kapow!* 2 | =================== 3 | 4 | .. image:: https://github.com/BBVA/kapow/actions/workflows/test_and_release.yml/badge.svg 5 | :target: https://github.com/BBVA/kapow/actions/workflows/test_and_release.yml 6 | .. image:: https://goreportcard.com/badge/github.com/bbva/kapow 7 | :target: https://goreportcard.com/report/github.com/bbva/kapow 8 | .. image:: https://img.shields.io/github/issues/BBVA/kapow 9 | :target: https://github.com/BBVA/kapow/issues/ 10 | .. image:: https://img.shields.io/github/v/release/BBVA/kapow?include_prereleases 11 | :target: https://github.com/BBVA/kapow/releases 12 | 13 | **If you can script it, you can HTTP it** 14 | 15 | 16 | What's *Kapow!* 17 | --------------- 18 | 19 | Think of that software that you need but **only runs in the command 20 | line**. *Kapow!* lets you wrap it into an `HTTP API` **real easy**. 21 | 22 | .. image:: _static/kapow-quick-overview.png 23 | :width: 80% 24 | :align: center 25 | 26 | Want to know more? 27 | Check the :ref:`Quick Start Guide ` section for a longer 28 | explanation of what *Kapow!* does. 29 | 30 | 31 | Authors 32 | ------- 33 | 34 | *Kapow!* is being developed by the `BBVA-Labs Security team`_. 35 | 36 | 37 | License 38 | ------- 39 | 40 | *Kapow!* is Open Source Software and available under the `Apache 2 license`_. 41 | 42 | 43 | Contributions 44 | ------------- 45 | 46 | Contributions are of course welcome. See `CONTRIBUTING`_ or skim 47 | `existing tickets`_ to see where you could help out. 48 | 49 | 50 | Contents 51 | ======== 52 | 53 | .. toctree:: 54 | :maxdepth: 2 55 | :caption: The Project 56 | 57 | the_project/quickstart 58 | the_project/security 59 | the_project/install_and_configure 60 | 61 | .. toctree:: 62 | :maxdepth: 2 63 | :caption: Tutorial 64 | 65 | tutorial/index 66 | tutorial/tutorial00 67 | tutorial/tutorial01 68 | tutorial/tutorial02 69 | tutorial/tutorial03 70 | tutorial/tutorial04 71 | tutorial/tutorial05 72 | tutorial/tutorial06 73 | 74 | .. toctree:: 75 | :maxdepth: 2 76 | :caption: Usage Examples 77 | 78 | examples/working_with_init_programs 79 | examples/managing_routes 80 | examples/handling_http_requests 81 | examples/using_json 82 | examples/shell_tricks 83 | examples/https_mtls 84 | 85 | 86 | .. toctree:: 87 | :maxdepth: 2 88 | :caption: Concepts 89 | 90 | concepts/interfaces 91 | concepts/philosophy 92 | concepts/request_life_cycle 93 | concepts/resource_tree 94 | concepts/route_matching 95 | concepts/routes 96 | 97 | Indices and Tables 98 | ================== 99 | 100 | * :ref:`genindex` 101 | * :ref:`modindex` 102 | * :ref:`search` 103 | 104 | .. _BBVA-Labs Security team: https://github.com/BBVA/kapow/blob/master/AUTHORS.rst 105 | .. _Apache 2 license: https://raw.githubusercontent.com/BBVA/kapow/master/LICENSE 106 | .. _CONTRIBUTING: https://github.com/BBVA/kapow/blob/master/CONTRIBUTING.rst 107 | .. _existing tickets: https://github.com/BBVA/kapow/labels/good%20first%20issue 108 | -------------------------------------------------------------------------------- /docs/source/latextoc.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. toctree:: 4 | :maxdepth: 3 5 | 6 | the_project/toc 7 | tutorial/toc 8 | examples/toc 9 | concepts/toc 10 | -------------------------------------------------------------------------------- /docs/source/the_project/toc.rst: -------------------------------------------------------------------------------- 1 | The Project 2 | =========== 3 | 4 | This section will introduce you to *Kapow!* basics. 5 | 6 | .. toctree:: 7 | 8 | quickstart 9 | security 10 | install_and_configure 11 | -------------------------------------------------------------------------------- /docs/source/tutorial/index.rst: -------------------------------------------------------------------------------- 1 | *Kapow!* Tutorial 2 | ================= 3 | 4 | This tutorial will help you get more familiar with *Kapow!*. 5 | 6 | It tells you the story of a junior ops person landing a new job in a small 7 | company. Teaming up with an experienced senior, they'll face many challenges 8 | of increasing difficulty. With *Kapow!* at their side, they will be able to 9 | pass all the hurdles. 10 | 11 | You just need to follow the steps and execute the code shown in the tutorial 12 | to learn *the Kapow! way*. 13 | 14 | Enjoy the ride! 15 | -------------------------------------------------------------------------------- /docs/source/tutorial/materials/backup_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | { 4 | echo -------------------------------------------------------------------------------- 5 | echo "[$(date)] Starting backup procedure" 6 | for i in $(seq 1 10) 7 | do 8 | echo "[$(date)] Backing up table ${i}..." 9 | sleep .2 10 | done 11 | echo "[$(date)] Backup finished" 12 | } >> /tmp/backup_db.log 13 | 14 | echo 'Backup done!' 15 | -------------------------------------------------------------------------------- /docs/source/tutorial/toc.rst: -------------------------------------------------------------------------------- 1 | .. include:: index.rst 2 | 3 | .. toctree:: 4 | :caption: Chapters 5 | 6 | tutorial00 7 | tutorial01 8 | tutorial02 9 | tutorial03 10 | tutorial04 11 | tutorial05 12 | tutorial06 13 | -------------------------------------------------------------------------------- /docs/source/tutorial/tutorial00.rst: -------------------------------------------------------------------------------- 1 | Your First Day at Work 2 | ====================== 3 | 4 | **Senior** 5 | 6 | Welcome to *ACME Inc.* This is your first day here, right? 7 | 8 | **Junior** 9 | 10 | Hi! Yes! And I am eager to start working. What will be my first task? 11 | 12 | **Senior** 13 | 14 | First, let me help you get acquainted with our infrastructure. 15 | 16 | **Junior** 17 | 18 | OK. 19 | 20 | **Senior** 21 | 22 | We have two Linux boxes that provide services to our employees. 23 | 24 | #. The Corporate Server: Provides email, database and web services. 25 | 26 | #. The Backup Server: It is used to store backup of the important 27 | company data. 28 | 29 | **Junior** 30 | 31 | That's it? OK, just like Google, then. 32 | 33 | **Senior** 34 | 35 | Smartass... 36 | 37 | **Junior** 38 | 39 | (chuckles nervously). 40 | 41 | **Senior** 42 | 43 | Well, I think is time for you to start with your first task. It just so 44 | happens that we received another request to backup the database from the 45 | projects team. 46 | -------------------------------------------------------------------------------- /docs/source/tutorial/tutorial02.rst: -------------------------------------------------------------------------------- 1 | What have we done? 2 | ================== 3 | 4 | **Senior** 5 | 6 | Hey, I come from seeing our project team mates. They're delighted with their 7 | new toy, but they miss something. 8 | 9 | I forgot to tell you that after the backup is run they need to review the log 10 | file to check that everything went OK. 11 | 12 | **Junior** 13 | 14 | Makes sense. Do you think that *Kapow!* can help with this? I have the 15 | feeling that this is the right way to go about it... 16 | 17 | **Senior** 18 | 19 | Sure! Let's take a look at the documentation to see how we can tweak the 20 | logic of the request. 21 | 22 | **Junior** 23 | 24 | Got it! There're a :ref:`lot of resources to work with `. 25 | I see that we can write to the response. Do you think this will work for us? 26 | 27 | **Senior** 28 | 29 | Yeah, the team is used to :command:`cat` the log file contents to see what 30 | happened in the last execution: 31 | 32 | .. code-block:: console 33 | 34 | $ cat /tmp/backup_db.log 35 | 36 | I've made it easy for you. Are you up to it? 37 | 38 | **Junior** 39 | 40 | Let me try add this to our init program: 41 | 42 | .. code-block:: console 43 | 44 | #!/usr/bin/env sh 45 | kapow route add /db/backup_logs -c 'cat /tmp/backup_db.log | kapow set /response/body' 46 | 47 | **Senior** 48 | 49 | Looks good to me, clean and simple, and it is a very good idea to use ``GET`` 50 | here as it won't change anything in the server. Let's restart *Kapow!* and try it. 51 | 52 | **Junior** 53 | 54 | Wooow! I get back the content of the file. If they liked the first one 55 | they're going to loooove this. 56 | 57 | **Senior** 58 | 59 | Agreed. And with this, I think we are done for the day... 60 | -------------------------------------------------------------------------------- /docs/source/tutorial/tutorial03.rst: -------------------------------------------------------------------------------- 1 | We need to filter 2 | ================= 3 | 4 | **Senior** 5 | 6 | Hiya! How're you doing this morning? I've got a new challenge from our 7 | grateful mates. 8 | 9 | As time goes on from the last log rotation, the size of the log file gets 10 | bigger and bigger. Furthermore, they want to limit the output of the file to 11 | pick only some records, and only from the end of the file. We need to do 12 | something to help them as they are wasting a lot of time reviewing the output. 13 | 14 | **Junior** 15 | 16 | I have a feeling that this is going to entail some serious *bash-foo*. What 17 | do you think? 18 | 19 | **Senior** 20 | 21 | Sure! But in addition to some good shell plumbing we're going to squeeze 22 | *Kapow!*'s superpowers a litle bit more to get a really good solution. 23 | 24 | Can you take a look at *Kapow!*'s documentation to see if something can be 25 | done? 26 | 27 | **Junior** 28 | 29 | I've read in the documentation that there is a way to get access to the data 30 | coming in the request. Do you think we can use this to let them choose how 31 | to do the filtering? 32 | 33 | **Senior** 34 | 35 | Sounds great! How have we lived without *Kapow!* all this time? 36 | 37 | As they requested, we can offer them a parameter to filter the registers 38 | they want to pick, and another parameter to limit the output size in lines. 39 | 40 | **Junior** 41 | 42 | Sounds about right. Now we have to make some modifications to our last 43 | endpoint definition to add this new feature. Let's get cracking! 44 | 45 | **Senior** 46 | 47 | Well, we got it again, this is exactly what they need: 48 | 49 | .. code-block:: bash 50 | 51 | kapow route add /db/backup_logs -c 'grep -- "$(kapow get /request/params/filter)" /tmp/backup_db.log \ 52 | | tail -n "$(kapow get /request/params/lines)" \ 53 | | kapow set /response/body' 54 | 55 | It looks a bit weird, but we'll have time to revise the style later. Please 56 | make some tests on your laptop before we publish it on the **Corporate Server**. 57 | Remember to send them an example URL with the parameters they can use to 58 | filter and limit the amount of lines they get. 59 | 60 | **Junior** 61 | 62 | OK, should look like this, doesn't it? 63 | 64 | .. code-block:: console 65 | 66 | $ curl 'http://localhost:8080/db/backup_logs?filter=rows%20inserted&lines=200' 67 | 68 | **Senior** 69 | 70 | Exactly. Another great day helping the company advance. Let's go grab a 71 | beer to celebrate! 72 | -------------------------------------------------------------------------------- /docs/source/tutorial/tutorial06.rst: -------------------------------------------------------------------------------- 1 | Securing the server 2 | =================== 3 | 4 | **Senior** 5 | 6 | Hi... I hope you rested last night! 7 | 8 | Come on, I need your help here! 9 | 10 | **Junior** 11 | 12 | Good morning! What's the matter? Sounds worrying 13 | 14 | **Senior** 15 | 16 | We forgot to take the most basic security measures when deploying our services. 17 | Every body at the company can access the services and the information is 18 | transferred in clear text. 19 | 20 | **Junior** 21 | 22 | Oh! Damn, you're right! You think we can do anything to solve this mess? 23 | 24 | **Senior** 25 | 26 | Yes, I'm pretty sure that those smart guys have thought on that when building 27 | Kapow! Have a look at the :ref:`documentation `. 28 | 29 | **Junior** 30 | 31 | Got it! They did it, here're the instictions to start a server with HTTPS support. 32 | 33 | It's amazing! It says we can even use mTLS to control access, really promising. 34 | 35 | **Senior** 36 | 37 | Ok, ok... First thigs first. We need to get a server certificate to start 38 | working with HTTPS. Fortunately we can ask for one to the CA we use for the 39 | other servers. Let's pick up one for development, they're quick to get. 40 | 41 | **Junior** 42 | 43 | Yeah! I'll change the startup script to configure HTTPS: 44 | 45 | .. code-block:: console 46 | 47 | $ kapow server --keyfile /etc/kapow/tls/keyfile \ 48 | --certfile /etc/kapow/tls/certfile \ 49 | /etc/kapow/awesome-route 50 | 51 | It's easy, please copy the private key file and certificate chain to `/etc/kapow/tls` and we can restart. 52 | 53 | **Senior** 54 | 55 | Great! it's working, communications are secured. Let's say everybody to change 56 | from http to https. 57 | 58 | **Junior** 59 | 60 | Ok, did it. What are the steps to follow to limit access by using mTLS? 61 | 62 | **Senior** 63 | 64 | Besides configuring the server we need to provide the users with their own 65 | client certificates and private keys so they can configure their browsers and 66 | the application server. 67 | 68 | **Junior** 69 | 70 | Yes, please give me the CA certificate that will issue our client certificates 71 | and I'll change the startup script again 72 | 73 | .. code-block:: console 74 | 75 | $ kapow server --keyfile /etc/kapow/tls/keyfile \ 76 | --certfile /etc/kapow/tls/certfile \ 77 | --clientauth true \ 78 | --clientcafile /etc/kapow/tls/clientCAfile \ 79 | /etc/kapow/awesome-route 80 | 81 | Done! 82 | 83 | **Senior** 84 | 85 | Ok, let's communicate the changes to all the affected teams before we restart 86 | 87 | **Junior** 88 | 89 | Oh God, After all we're starting to look like Google 90 | 91 | (chuckles) 92 | -------------------------------------------------------------------------------- /examples/advanced/01_DocumentConverter/DocumentConverter: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | kapow route add '/format/input' - <<-'EOF' 4 | kapow set /response/headers/Content-Type application/json 5 | pandoc --list-input-formats \ 6 | | jq --raw-input --slurp 'split("\n") | .[0:-1]' \ 7 | | kapow set /response/body 8 | EOF 9 | 10 | kapow route add '/format/output' - <<-'EOF' 11 | kapow set /response/headers/Content-Type application/json 12 | pandoc --list-output-formats \ 13 | | jq --raw-input --slurp 'split("\n") | .[0:-1]' \ 14 | | kapow set /response/body 15 | EOF 16 | 17 | kapow route add -X POST --entrypoint '/bin/zsh -c' '/convert' - <<-'EOF' 18 | kapow set /response/headers/Content-Type application/octet-stream 19 | kapow set /response/headers/Content-Disposition "attachment; filename=$(kapow get /request/files/inputfile/filename).$(kapow get /request/form/to)" 20 | pandoc --from=$(kapow get /request/form/from) \ 21 | --to=$(kapow get /request/form/to) \ 22 | --output=>(kapow set /response/body) \ 23 | =(kapow get /request/files/inputfile/content) 24 | EOF 25 | 26 | kapow route add / - <<-'EOF' 27 | kapow set /response/headers/Location /index.html 28 | kapow set /response/status 301 29 | EOF 30 | 31 | kapow route add /index.html - <<-'EOF' 32 | kapow set /response/headers/Content-Type text/html 33 | kapow set /response/body < index.html 34 | EOF 35 | -------------------------------------------------------------------------------- /examples/advanced/01_DocumentConverter/README.md: -------------------------------------------------------------------------------- 1 | # Document Converter (pandoc) as a Service 2 | 3 | A small web gui for [pandoc](https://pandoc.org) that allows to convert between text formats. 4 | 5 | ## How to run it 6 | 7 | ``` 8 | $ kapow server DocumentConverter 9 | ``` 10 | 11 | 12 | ## How to consume it 13 | 14 | 1. Visit http://localhost:8080/ from your web browser. 15 | 1. Select and input format. E.g. Markdown 16 | 1. Select and output format. E.g. HTML 17 | 1. Select a file from your disk. E.g. README.md 18 | 1. Press "Convert!" 19 | 1. Enjoy! 20 | -------------------------------------------------------------------------------- /examples/advanced/01_DocumentConverter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46 | 47 | 48 |
49 | 50 | 51 | 52 | 53 |
54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/advanced/02_NetworkScanner/NetworkScanner: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | kapow route add -X POST '/scan' - <<-'EOF' 4 | PORTS=$(kapow get /request/form/ports) 5 | IP=$(kapow get /request/form/ip) 6 | WEBHOOK=$(kapow get /request/form/webhook) 7 | JOBID=$(uuidgen | tr -d '\n') 8 | 9 | (nmap -Pn -n -p "${PORTS:-8080}" -oX "${JOBID}.running.xml" -- "${IP:-127.0.0.1}"; 10 | mv "${JOBID}.running.xml" "${JOBID}.done.xml"; 11 | [ ! -z "$WEBHOOK" ] && curl -s -F "data=@${JOBID}.done.xml" -F "jobid=${JOBID}" "$WEBHOOK"; 12 | ) & 13 | 14 | kapow set /response/headers/Content-Type application/json 15 | jq -n --arg jobid "$JOBID" '{"job": $jobid}' | kapow set /response/body 16 | EOF 17 | 18 | kapow route add -X GET '/scan/{jobid}' - <<-'EOF' 19 | JOBID=$(kapow get /request/matches/jobid) 20 | [ -f "${JOBID}.running.xml" ] && kapow set /response/status 202 && exit 0 21 | if [ -f "${JOBID}.done.xml" ]; then 22 | kapow set /response/headers/Content-Type application/xml 23 | kapow set /response/body < "${JOBID}.done.xml" 24 | else 25 | kapow set /response/status 404 26 | kapow set /response/body "Scan $JOBID not found" 27 | fi 28 | EOF 29 | 30 | -------------------------------------------------------------------------------- /examples/advanced/02_NetworkScanner/README.md: -------------------------------------------------------------------------------- 1 | # Network Scanner (nmap) as a Service 2 | 3 | Run a long network scan in background with support for webhook on completion. 4 | 5 | * The user can define the destination IP and port(s). 6 | * The service answers immediately with a `jobid`. 7 | * If a webhook url is defined it will be called on completion with the result and the jobid. 8 | * At any moment the user can request the status of the scan at /scan/{jobid} 9 | 10 | ## How to run it 11 | 12 | ``` 13 | $ kapow server NetworkScanner 14 | ``` 15 | 16 | 17 | ## How to consume it 18 | 19 | * Scan your own host 20 | ``` 21 | $ curl --data 'ports=1-65535&ip=127.0.0.1' http://localhost:8080/scan 22 | { 23 | "job": "dba2edbc-527d-453a-9c25-0608bb8f06da" 24 | } 25 | ``` 26 | 27 | * Grab the result 28 | 29 | ``` 30 | $ curl -v http://localhost:8080/scan/dba2edbc-527d-453a-9c25-0608bb8f06da 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ``` 58 | 59 | *If you receive a 202 it means the scan is still on progress* 60 | -------------------------------------------------------------------------------- /examples/advanced/03_NetworkSniffer/NetworkSniffer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | kapow route add /sniff -c 'tcpdump -i any -U -s0 -w - "not portrange 8080-8082" | kapow set /response/body' 4 | -------------------------------------------------------------------------------- /examples/advanced/03_NetworkSniffer/README.md: -------------------------------------------------------------------------------- 1 | # Network Sniffer (tcpdump) as a Service 2 | 3 | Provides an HTTP service that allows the user to sniff the network in real time. The packet capture data is served as an HTTP stream that can be injected to a packet analysis tool on the fly. 4 | 5 | 6 | ## How to run it 7 | 8 | For the sake of simplicity, run: 9 | 10 | ``` 11 | $ sudo -E kapow server NetworkSniffer 12 | ``` 13 | 14 | In a production environment, tcpdump should be run with the appropiate 15 | permissions, but kapow can (and should) run as an unprivileged user. 16 | 17 | 18 | ## How to consume it 19 | 20 | ``` 21 | $ curl http://localhost:8080/sniff | sudo -E wireshark -k -i - 22 | ``` 23 | -------------------------------------------------------------------------------- /examples/basic/01_HelloWorld/HelloWorld: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | kapow route add /helloworld -c "echo 'Hello World!' | kapow set /response/body" 4 | -------------------------------------------------------------------------------- /examples/basic/01_HelloWorld/README.md: -------------------------------------------------------------------------------- 1 | # Hello World! 2 | 3 | A simple "Hello World!" type example. 4 | 5 | 6 | ## How to run it 7 | 8 | ``` 9 | $ kapow server HelloWorld 10 | ``` 11 | 12 | 13 | ## How to consume it 14 | 15 | ``` 16 | $ curl http://localhost:8080/helloworld 17 | Hello World! 18 | ``` 19 | -------------------------------------------------------------------------------- /examples/basic/02_FixLogGrep/FixLogGrep: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | kapow route add /apache-errors - <<-'EOF' 4 | cat /var/log/apache2/access.log | grep 'File does not exist' | kapow set /response/body 5 | EOF 6 | -------------------------------------------------------------------------------- /examples/basic/02_FixLogGrep/README.md: -------------------------------------------------------------------------------- 1 | # Fix Log Grep as a Service 2 | 3 | A simple service that exposes log entries for files not found on our Apache Web Server. 4 | 5 | ## How to run it 6 | 7 | ``` 8 | $ kapow server FixLogGrep 9 | ``` 10 | 11 | 12 | ## How to consume it 13 | 14 | ``` 15 | $ curl http://localhost:8080/apache-errors 16 | [Fri Feb 01 22:07:57.154391 2019] [core:info] [pid 7:tid 140284200093440] [client 172.17.0.1:50756] AH00128: File does not exist: /usr/var/www/mysite/favicon.ico 17 | [Fri Feb 01 22:07:57.808291 2019] [core:info] [pid 8:tid 140284216878848] [client 172.17.0.1:50758] AH00128: File does not exist: /usr/var/www/mysite/favicon.ico 18 | [Fri Feb 01 22:07:57.878149 2019] [core:info] [pid 8:tid 140284208486144] [client 172.17.0.1:50758] AH00128: File does not exist: /usr/var/www/mysite/favicon.ico 19 | ``` 20 | -------------------------------------------------------------------------------- /examples/basic/03_DynamicLogGrep/DynamicLogGrep: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | kapow route add /apache-logs - <<-'EOF' 4 | cat /var/log/apache2/access.log | grep -- "$(kapow get /request/params/pattern)" | kapow set /response/body 5 | EOF 6 | -------------------------------------------------------------------------------- /examples/basic/03_DynamicLogGrep/README.md: -------------------------------------------------------------------------------- 1 | # Dynamic Log Grep as a Service 2 | 3 | A simple service that exposes log entries that matches **with a given text** on our Apache Web Server. 4 | 5 | ## How to run it 6 | 7 | ``` 8 | $ kapow server DynamicLogGrep 9 | ``` 10 | 11 | 12 | ## How to consume it 13 | 14 | ``` 15 | $ curl http://localhost:8080/apache-logs?pattern=Chrome 16 | 70.127.254.161 - - [17/May/2015:22:05:19 +0000] "GET /reset.css HTTP/1.1" 200 1015 "http://www.semicomplete.com/projects/xdotool/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36" 17 | 70.127.254.161 - - [17/May/2015:22:05:27 +0000] "GET /style2.css HTTP/1.1" 200 4877 "http://www.semicomplete.com/projects/xdotool/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36" 18 | 70.127.254.161 - - [17/May/2015:22:05:19 +0000] "GET /images/jordan-80.png HTTP/1.1" 200 6146 "http://www.semicomplete.com/projects/xdotool/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36" 19 | 70.127.254.161 - - [17/May/2015:22:05:25 +0000] "GET /images/web/2009/banner.png HTTP/1.1" 200 52315 "http://www.semicomplete.com/projects/xdotool/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36" 20 | 70.127.254.161 - - [17/May/2015:22:05:56 +0000] "GET /favicon.ico HTTP/1.1" 200 3638 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36" 21 | ``` 22 | -------------------------------------------------------------------------------- /examples/basic/04_SystemMonitor/SystemMonitor: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | kapow route add '/file/{path:.*}' -c 'ls -la -- "/$(kapow get /request/matches/path)" | kapow set /response/body' 4 | 5 | kapow route add /process -c 'ps -aux | kapow set /response/body' 6 | 7 | kapow route add /cpu -c 'kapow set /response/body < /proc/cpuinfo' 8 | 9 | kapow route add /memory -c 'free -m | kapow set /response/body' 10 | 11 | kapow route add /disk/usage -c 'df -h | kapow set /response/body' 12 | 13 | kapow route add /disk/mounts -c 'mount | kapow set /response/body' 14 | 15 | kapow route add /socket -c 'ss -pluton | kapow set /response/body' 16 | 17 | kapow route add /kernel/messages - <<-'EOF' 18 | kapow set /response/headers/X-Content-Type-Options nosniff 19 | kapow set /response/headers/Content-Type text/plain 20 | dmesg | kapow set /response/body 21 | EOF 22 | 23 | kapow route add /systemd/journal - <<-'EOF' 24 | kapow set /response/headers/X-Content-Type-Options nosniff 25 | kapow set /response/headers/Content-Type text/plain 26 | journalctl -n1000 | kapow set /response/body 27 | EOF 28 | -------------------------------------------------------------------------------- /examples/docker/awscli/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM bbvalabsci/kapow:latest as kp 2 | FROM amazon/aws-cli:latest 3 | 4 | COPY --from=kp /kapow /usr/bin/kapow 5 | 6 | ENTRYPOINT kapow 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/BBVA/kapow 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf 7 | github.com/google/uuid v1.6.0 8 | github.com/gorilla/mux v1.8.1 9 | github.com/spf13/cobra v1.8.1 10 | gopkg.in/h2non/gock.v1 v1.1.2 11 | ) 12 | 13 | require ( 14 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect 15 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 16 | github.com/spf13/pflag v1.0.5 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg= 3 | github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= 4 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 5 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 6 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 7 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 8 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= 9 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= 10 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 11 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 12 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= 13 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= 14 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 15 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 16 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 17 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 18 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= 21 | gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= 22 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | -------------------------------------------------------------------------------- /internal/certs/certs.go: -------------------------------------------------------------------------------- 1 | package certs 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/pem" 11 | "math/big" 12 | "net" 13 | "time" 14 | 15 | "github.com/BBVA/kapow/internal/logger" 16 | ) 17 | 18 | type Cert struct { 19 | X509Cert *x509.Certificate 20 | PrivKey crypto.PrivateKey 21 | SignedCert []byte 22 | } 23 | 24 | func (c Cert) SignedCertPEMBytes() []byte { 25 | 26 | PEM := new(bytes.Buffer) 27 | err := pem.Encode(PEM, &pem.Block{ 28 | Type: "CERTIFICATE", 29 | Bytes: c.SignedCert, 30 | }) 31 | if err != nil { 32 | logger.L.Fatal(err) 33 | } 34 | 35 | return PEM.Bytes() 36 | } 37 | 38 | func (c Cert) PrivateKeyPEMBytes() []byte { 39 | PEM := new(bytes.Buffer) 40 | err := pem.Encode(PEM, &pem.Block{ 41 | Type: "RSA PRIVATE KEY", 42 | Bytes: x509.MarshalPKCS1PrivateKey(c.PrivKey.(*rsa.PrivateKey)), 43 | }) 44 | if err != nil { 45 | logger.L.Fatal(err) 46 | } 47 | 48 | return PEM.Bytes() 49 | } 50 | 51 | func GenCert(name, altName string, isServer bool) Cert { 52 | 53 | usage := x509.ExtKeyUsageClientAuth 54 | if isServer { 55 | usage = x509.ExtKeyUsageServerAuth 56 | } 57 | 58 | var dnsNames []string 59 | var ipAddresses []net.IP 60 | if altName != "" { 61 | if ipAddr := net.ParseIP(altName); ipAddr != nil { 62 | ipAddresses = []net.IP{ipAddr} 63 | } else { 64 | dnsNames = []string{altName} 65 | } 66 | } 67 | 68 | cert := &x509.Certificate{ 69 | SerialNumber: big.NewInt(1), 70 | DNSNames: dnsNames, 71 | IPAddresses: ipAddresses, 72 | Subject: pkix.Name{ 73 | CommonName: name, 74 | }, 75 | NotBefore: time.Now(), 76 | NotAfter: time.Now().AddDate(10, 0, 0), 77 | IsCA: false, 78 | BasicConstraintsValid: true, 79 | ExtKeyUsage: []x509.ExtKeyUsage{ 80 | usage, 81 | }, 82 | } 83 | 84 | certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) 85 | if err != nil { 86 | logger.L.Fatal(err) 87 | } 88 | 89 | certBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, &certPrivKey.PublicKey, certPrivKey) 90 | if err != nil { 91 | logger.L.Fatal(err) 92 | } 93 | 94 | return Cert{ 95 | X509Cert: cert, 96 | PrivKey: certPrivKey, 97 | SignedCert: certBytes, 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/client/client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package client 18 | 19 | import ( 20 | "os" 21 | "testing" 22 | 23 | "github.com/BBVA/kapow/internal/http" 24 | ) 25 | 26 | func TestMain(m *testing.M) { 27 | http.ControlClientGenerator = nil 28 | os.Exit(m.Run()) 29 | } 30 | -------------------------------------------------------------------------------- /internal/client/get.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package client 18 | 19 | import ( 20 | "io" 21 | 22 | "github.com/BBVA/kapow/internal/http" 23 | ) 24 | 25 | // GetData will perform the request and write the results on the provided writer 26 | func GetData(host, id, path string, w io.Writer) error { 27 | url := host + "/handlers/" + id + path 28 | return http.Get(url, nil, w, nil) 29 | } 30 | -------------------------------------------------------------------------------- /internal/client/get_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package client 18 | 19 | import ( 20 | "bytes" 21 | "net/http" 22 | "testing" 23 | 24 | gock "gopkg.in/h2non/gock.v1" 25 | ) 26 | 27 | func TestWriteContentToWriter(t *testing.T) { 28 | defer gock.Off() 29 | gock.New("http://localhost"). 30 | Get("/handlers/HANDLER_BAR/request/body"). 31 | Reply(http.StatusOK). 32 | BodyString("FOO") 33 | 34 | var b bytes.Buffer 35 | err := GetData("http://localhost", "HANDLER_BAR", "/request/body", &b) 36 | 37 | if err != nil { 38 | t.Errorf("Unexpected error: %q", err) 39 | } 40 | 41 | if !bytes.Equal(b.Bytes(), []byte("FOO")) { 42 | t.Errorf("Received content mismatch: %q != %q", b.Bytes(), []byte("FOO")) 43 | } 44 | 45 | if !gock.IsDone() { 46 | t.Error("No expected endpoint called") 47 | } 48 | } 49 | 50 | func TestPropagateHTTPError(t *testing.T) { 51 | defer gock.Off() 52 | gock.New("http://localhost"). 53 | Get("/handlers/HANDLER_BAR/request/body"). 54 | Reply(http.StatusTeapot) 55 | 56 | err := GetData( 57 | "http://localhost", "HANDLER_BAR", "/request/body", nil) 58 | 59 | if err == nil { 60 | t.Errorf("Expected error not returned") 61 | } 62 | 63 | if !gock.IsDone() { 64 | t.Error("No expected endpoint called") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/client/route_add.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package client 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "io" 23 | 24 | "github.com/BBVA/kapow/internal/http" 25 | ) 26 | 27 | // AddRoute will add a new route in kapow 28 | func AddRoute(host, path, method, entrypoint, command string, w io.Writer) error { 29 | url := host + "/routes" 30 | payload := map[string]string{ 31 | "method": method, 32 | "url_pattern": path, 33 | "command": command, 34 | } 35 | if entrypoint != "" { 36 | payload["entrypoint"] = entrypoint 37 | } 38 | body, _ := json.Marshal(payload) 39 | return http.Post(url, bytes.NewReader(body), w, http.ControlClientGenerator, http.AsJSON) 40 | } 41 | -------------------------------------------------------------------------------- /internal/client/route_add_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package client 18 | 19 | import ( 20 | "net/http" 21 | "testing" 22 | 23 | gock "gopkg.in/h2non/gock.v1" 24 | ) 25 | 26 | func TestSuccessOnCorrectRoute(t *testing.T) { 27 | defer gock.Off() 28 | gock.New("http://localhost"). 29 | Post("/routes"). 30 | MatchType("json"). 31 | JSON(map[string]string{ 32 | "method": "GET", 33 | "url_pattern": "/hello", 34 | "command": "echo Hello World | kapow set /response/body", 35 | }). 36 | Reply(http.StatusCreated). 37 | JSON(map[string]string{}) 38 | 39 | err := AddRoute( 40 | "http://localhost", 41 | "/hello", "GET", "", "echo Hello World | kapow set /response/body", nil) 42 | if err != nil { 43 | t.Errorf("Unexpected error: %s", err) 44 | } 45 | 46 | if !gock.IsDone() { 47 | t.Error("Expected endpoint call not made") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/client/route_list.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package client 18 | 19 | import ( 20 | "io" 21 | 22 | "github.com/BBVA/kapow/internal/http" 23 | ) 24 | 25 | // ListRoutes queries the kapow! instance for the routes that are registered 26 | func ListRoutes(host string, w io.Writer) error { 27 | url := host + "/routes" 28 | return http.Get(url, nil, w, http.ControlClientGenerator) 29 | } 30 | -------------------------------------------------------------------------------- /internal/client/route_list_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package client 18 | 19 | import ( 20 | "bytes" 21 | "net/http" 22 | "testing" 23 | 24 | gock "gopkg.in/h2non/gock.v1" 25 | ) 26 | 27 | func TestListRoutesOKEmpty(t *testing.T) { 28 | defer gock.Off() 29 | gock.New("http://localhost:8080"). 30 | Get("/routes"). 31 | Reply(http.StatusOK) 32 | 33 | err := ListRoutes("http://localhost:8080", nil) 34 | if err != nil { 35 | t.Errorf("Unexpected error %q", err) 36 | } 37 | 38 | if !gock.IsDone() { 39 | t.Errorf("No endpoint called") 40 | } 41 | } 42 | 43 | func TestListRoutesOKSome(t *testing.T) { 44 | defer gock.Off() 45 | gock.New("http://localhost:8080"). 46 | Get("/routes"). 47 | Reply(http.StatusOK). 48 | JSON([]map[string]string{ 49 | {"foo": "bar"}, 50 | {"bar": "foo"}, 51 | }) 52 | 53 | var b bytes.Buffer 54 | err := ListRoutes("http://localhost:8080", &b) 55 | if err != nil { 56 | t.Errorf("Unexpected error: %q", err) 57 | } else if !bytes.Equal( 58 | b.Bytes(), []byte(`[{"foo":"bar"},{"bar":"foo"}]`+"\n")) { 59 | t.Errorf("Unexpected error: got %q, want %q", 60 | b.String(), `[{"foo":"bar"},{"bar":"foo"}]`+"\n") 61 | } 62 | 63 | if !gock.IsDone() { 64 | t.Errorf("No endpoint called") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/client/route_remove.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package client 18 | 19 | import ( 20 | "github.com/BBVA/kapow/internal/http" 21 | ) 22 | 23 | // RemoveRoute removes a registered route in Kapow! server 24 | func RemoveRoute(host, id string) error { 25 | url := host + "/routes/" + id 26 | return http.Delete(url, nil, nil, http.ControlClientGenerator) 27 | } 28 | -------------------------------------------------------------------------------- /internal/client/route_remove_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package client 18 | 19 | import ( 20 | "net/http" 21 | "testing" 22 | 23 | gock "gopkg.in/h2non/gock.v1" 24 | ) 25 | 26 | func TestRemoveRouteOKExistent(t *testing.T) { 27 | defer gock.Off() 28 | gock.New("http://localhost:8080"). 29 | Delete("/routes/ROUTE_FOO"). 30 | Reply(http.StatusNoContent) 31 | 32 | err := RemoveRoute("http://localhost:8080", "ROUTE_FOO") 33 | if err != nil { 34 | t.Errorf("unexpected error: %q", err) 35 | } 36 | 37 | if !gock.IsDone() { 38 | t.Errorf("No endpoint called") 39 | } 40 | } 41 | 42 | func TestRemoveRouteErrorNonExistent(t *testing.T) { 43 | defer gock.Off() 44 | gock.New("http://localhost:8080"). 45 | Delete("/routes/ROUTE_BAD"). 46 | Reply(http.StatusNotFound). 47 | BodyString(`{"reason": "Route Not Found"}`) 48 | 49 | err := RemoveRoute("http://localhost:8080", "ROUTE_BAD") 50 | if err == nil { 51 | t.Errorf("Error not reported for nonexistent route") 52 | } else if err.Error() != "Route Not Found" { 53 | t.Errorf(`Error mismatch: got %q, want "Not Found"`, err) 54 | } 55 | 56 | if !gock.IsDone() { 57 | t.Errorf("No endpoint called") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/client/set.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package client 18 | 19 | import ( 20 | "io" 21 | 22 | "github.com/BBVA/kapow/internal/http" 23 | ) 24 | 25 | func SetData(host, handlerID, path string, r io.Reader) error { 26 | url := host + "/handlers/" + handlerID + path 27 | return http.Put(url, r, nil, nil) 28 | } 29 | -------------------------------------------------------------------------------- /internal/client/set_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package client_test 18 | 19 | import ( 20 | "net/http" 21 | "strings" 22 | "testing" 23 | 24 | gock "gopkg.in/h2non/gock.v1" 25 | 26 | "github.com/BBVA/kapow/internal/client" 27 | ) 28 | 29 | // Test an HTTP OK request 30 | func TestSetDataSuccessOnCorrectRequest(t *testing.T) { 31 | defer gock.Off() 32 | gock.New("http://localhost:8080"). 33 | Put("/HANDLER_FOO/response/status/code"). 34 | Reply(http.StatusOK) 35 | 36 | if err := client.SetData( 37 | "http://localhost:8080", 38 | "HANDLER_FOO", 39 | "/response/status/code", 40 | strings.NewReader("200"), 41 | ); err != nil { 42 | t.Error("Unexpected error") 43 | } 44 | 45 | if !gock.IsDone() { 46 | t.Errorf("No endpoint called") 47 | } 48 | } 49 | 50 | // Test that Not Found errors are detected when an invalid handler id is sent 51 | func TestSetDataErrIfBadHandlerID(t *testing.T) { 52 | defer gock.Off() 53 | gock.New("http://localhost:8080"). 54 | Put("/HANDLER_BAD/response/status/code"). 55 | Reply(http.StatusNotFound). 56 | BodyString(`{"reason": "Handler ID Not Found"}`) 57 | 58 | if err := client.SetData( 59 | "http://localhost:8080", 60 | "HANDLER_BAD", 61 | "/response/status/code", 62 | strings.NewReader("200"), 63 | ); err == nil { 64 | t.Error("Expected error not present") 65 | } else if err.Error() != "Handler ID Not Found" { 66 | t.Errorf(`Error mismatch: expected "Handler ID Not Found", got %q`, err) 67 | } 68 | 69 | if !gock.IsDone() { 70 | t.Errorf("No endpoint called") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/cmd/get.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "os" 21 | 22 | "github.com/spf13/cobra" 23 | 24 | "github.com/BBVA/kapow/internal/client" 25 | "github.com/BBVA/kapow/internal/logger" 26 | ) 27 | 28 | // GetCmd is the command line interface for get kapow data operation 29 | var GetCmd = &cobra.Command{ 30 | Use: "get [flags] resource", 31 | Short: "Retrieve a Kapow! resource", 32 | Long: "Retrieve a Kapow! resource for the current request", 33 | Args: cobra.ExactArgs(1), 34 | PreRunE: handlerIDRequired, 35 | Run: func(cmd *cobra.Command, args []string) { 36 | dataURL, _ := cmd.Flags().GetString("data-url") 37 | handler, _ := cmd.Flags().GetString("handler") 38 | 39 | err := client.GetData(dataURL, handler, args[0], os.Stdout) 40 | if err != nil { 41 | logger.L.Fatal(err) 42 | } 43 | }, 44 | } 45 | 46 | func init() { 47 | GetCmd.Flags().String("data-url", getEnv("KAPOW_DATA_URL", "http://localhost:8082"), "Kapow! data interface URL") 48 | GetCmd.Flags().String("handler", getEnv("KAPOW_HANDLER_ID", ""), "Kapow! handler ID") 49 | } 50 | -------------------------------------------------------------------------------- /internal/cmd/route.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "io" 21 | "os" 22 | 23 | "github.com/BBVA/kapow/internal/client" 24 | "github.com/BBVA/kapow/internal/logger" 25 | 26 | "github.com/spf13/cobra" 27 | ) 28 | 29 | // RouteCmd is the command line interface for kapow route handling 30 | var RouteCmd = &cobra.Command{ 31 | Use: "route [action]", 32 | } 33 | 34 | func init() { 35 | var routeListCmd = &cobra.Command{ 36 | Use: "list [flags]", 37 | Short: "List the current Kapow! routes", 38 | Run: func(cmd *cobra.Command, args []string) { 39 | controlURL, _ := cmd.Flags().GetString("control-url") 40 | 41 | if err := client.ListRoutes(controlURL, os.Stdout); err != nil { 42 | logger.L.Fatal(err) 43 | } 44 | }, 45 | } 46 | routeListCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "https://localhost:8081"), "Kapow! control interface URL") 47 | 48 | // TODO: Manage args for url_pattern and command_file (2 exact args) 49 | var routeAddCmd = &cobra.Command{ 50 | Use: "add [flags] url_pattern [command_file]", 51 | Short: "Add a route", 52 | Args: cobra.RangeArgs(1, 2), 53 | Run: func(cmd *cobra.Command, args []string) { 54 | controlURL, _ := cmd.Flags().GetString("control-url") 55 | method, _ := cmd.Flags().GetString("method") 56 | command, _ := cmd.Flags().GetString("command") 57 | entrypoint, _ := cmd.Flags().GetString("entrypoint") 58 | urlPattern := args[0] 59 | 60 | if len(args) > 1 && command == "" { 61 | commandFile := args[1] 62 | var buf []byte 63 | var err error 64 | if commandFile == "-" { 65 | buf, err = io.ReadAll(os.Stdin) 66 | } else { 67 | buf, err = os.ReadFile(commandFile) 68 | } 69 | if err != nil { 70 | logger.L.Fatal(err) 71 | } 72 | command = string(buf) 73 | } 74 | 75 | if err := client.AddRoute(controlURL, urlPattern, method, entrypoint, command, os.Stdout); err != nil { 76 | logger.L.Fatal(err) 77 | } 78 | }, 79 | } 80 | // TODO: Add default values for flags and remove path flag 81 | routeAddCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "https://localhost:8081"), "Kapow! control interface URL") 82 | routeAddCmd.Flags().StringP("method", "X", "GET", "HTTP method to accept") 83 | routeAddCmd.Flags().StringP("entrypoint", "e", "", "Command to execute") 84 | routeAddCmd.Flags().StringP("command", "c", "", "Command to pass to the shell") 85 | 86 | var routeRemoveCmd = &cobra.Command{ 87 | Use: "remove [flags] route_id", 88 | Short: "Remove the given route", 89 | Args: cobra.ExactArgs(1), 90 | Run: func(cmd *cobra.Command, args []string) { 91 | controlURL, _ := cmd.Flags().GetString("control-url") 92 | 93 | if err := client.RemoveRoute(controlURL, args[0]); err != nil { 94 | logger.L.Fatal(err) 95 | } 96 | }, 97 | } 98 | routeRemoveCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "https://localhost:8081"), "Kapow! control interface URL") 99 | 100 | RouteCmd.AddCommand(routeListCmd) 101 | RouteCmd.AddCommand(routeAddCmd) 102 | RouteCmd.AddCommand(routeRemoveCmd) 103 | } 104 | -------------------------------------------------------------------------------- /internal/cmd/runner.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package cmd 4 | 5 | import ( 6 | "os/exec" 7 | ) 8 | 9 | func BuildCmd(path string) *exec.Cmd { 10 | return exec.Command(path) 11 | } 12 | -------------------------------------------------------------------------------- /internal/cmd/runner_windows.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os/exec" 5 | ) 6 | 7 | func BuildCmd(path string) *exec.Cmd { 8 | return exec.Command("cmd.exe", "/c", path) 9 | } 10 | -------------------------------------------------------------------------------- /internal/cmd/set.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "io" 21 | "os" 22 | "strings" 23 | 24 | "github.com/BBVA/kapow/internal/client" 25 | "github.com/BBVA/kapow/internal/logger" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | // SetCmd is the command line interface for set kapow data operation 31 | var SetCmd = &cobra.Command{ 32 | Use: "set [flags] resource [value]", 33 | Short: "Set a Kapow! resource value", 34 | Long: "Set a Kapow! resource value for the current request", 35 | Args: cobra.RangeArgs(1, 2), 36 | PreRunE: handlerIDRequired, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | var r io.Reader 39 | dataURL, _ := cmd.Flags().GetString("data-url") 40 | handler, _ := cmd.Flags().GetString("handler") 41 | path, args := args[0], args[1:] 42 | 43 | if len(args) == 1 { 44 | r = strings.NewReader(args[0]) 45 | } else { 46 | r = os.Stdin 47 | } 48 | 49 | if err := client.SetData(dataURL, handler, path, r); err != nil { 50 | logger.L.Fatal(err) 51 | } 52 | }, 53 | } 54 | 55 | func init() { 56 | SetCmd.Flags().String("data-url", getEnv("KAPOW_DATA_URL", "http://localhost:8082"), "Kapow! data interface URL") 57 | SetCmd.Flags().String("handler", getEnv("KAPOW_HANDLER_ID", ""), "Kapow! handler ID") 58 | } 59 | -------------------------------------------------------------------------------- /internal/cmd/validations.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "errors" 21 | "os" 22 | 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | func getEnv(key, fallback string) string { 27 | value, exists := os.LookupEnv(key) 28 | if !exists { 29 | return fallback 30 | } 31 | return value 32 | } 33 | 34 | func handlerIDRequired(cmd *cobra.Command, args []string) error { 35 | handler, _ := cmd.Flags().GetString("handler") 36 | if handler == "" { 37 | return errors.New("--handler or KAPOW_HANDLER_ID is mandatory") 38 | } 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/http/reason.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package http 18 | 19 | import ( 20 | "encoding/json" 21 | "errors" 22 | "io" 23 | "net/http" 24 | 25 | "github.com/BBVA/kapow/internal/server/httperror" 26 | ) 27 | 28 | // Reason returns the reason phrase embedded within the JSON error 29 | // body, or an error if no reason can be extracted 30 | func Reason(r *http.Response) (string, error) { 31 | body, err := io.ReadAll(r.Body) 32 | if err != nil { 33 | return "", errors.New("error reading response's body") 34 | } 35 | 36 | reason := &httperror.ServerErrMessage{} 37 | err = json.Unmarshal(body, reason) 38 | if err != nil { 39 | return "", errors.New("error unmarshaling JSON") 40 | } 41 | 42 | if reason.Reason == "" { 43 | return "", errors.New("no reason") 44 | } 45 | 46 | return reason.Reason, nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/http/reason_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package http 18 | 19 | import ( 20 | "io" 21 | nethttp "net/http" 22 | "strings" 23 | "testing" 24 | ) 25 | 26 | func TestReasonExtractsReasonFromJSON(t *testing.T) { 27 | r := &nethttp.Response{ 28 | Status: "200 OK", 29 | Body: io.NopCloser( 30 | strings.NewReader( 31 | `{"reason": "Because reasons", "foo": "bar"}`, 32 | ), 33 | ), 34 | } 35 | 36 | reason, _ := Reason(r) 37 | 38 | if reason != "Because reasons" { 39 | t.Errorf(`reason mismatch, want "Because reasons", got %q`, reason) 40 | } 41 | } 42 | 43 | func TestReasonErrorsOnJSONWithNoReason(t *testing.T) { 44 | r := &nethttp.Response{ 45 | Status: "200 OK", 46 | Body: io.NopCloser( 47 | strings.NewReader( 48 | `{"madness": "Because madness", "foo": "bar"}`, 49 | ), 50 | ), 51 | } 52 | 53 | _, err := Reason(r) 54 | 55 | if err == nil { 56 | t.Error("error not reported") 57 | } 58 | } 59 | 60 | func TestReasonErrorsOnJSONWithEmptyReason(t *testing.T) { 61 | r := &nethttp.Response{ 62 | Body: io.NopCloser( 63 | strings.NewReader( 64 | `{"reason": "", "foo": "bar"}`, 65 | ), 66 | ), 67 | } 68 | 69 | _, err := Reason(r) 70 | 71 | if err == nil { 72 | t.Error("error not reported") 73 | } 74 | } 75 | 76 | func TestReasonErrorsOnNoJSON(t *testing.T) { 77 | r := &nethttp.Response{ 78 | Body: io.NopCloser( 79 | strings.NewReader(""), 80 | ), 81 | } 82 | 83 | _, err := Reason(r) 84 | 85 | if err == nil { 86 | t.Error("error not reported") 87 | } 88 | } 89 | 90 | func TestReasonErrorsOnInvalidJSON(t *testing.T) { 91 | r := &nethttp.Response{ 92 | Body: io.NopCloser( 93 | strings.NewReader( 94 | `{"reason": "Because reasons", "cliffhanger...`, 95 | ), 96 | ), 97 | } 98 | 99 | _, err := Reason(r) 100 | 101 | if err == nil { 102 | t.Error("error not reported") 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | ) 9 | 10 | var A *log.Logger 11 | var L *log.Logger 12 | 13 | func init() { 14 | A = log.New(os.Stdout, "", 0) 15 | L = log.New(os.Stderr, "", log.LstdFlags|log.Lmicroseconds|log.LUTC) 16 | } 17 | 18 | // See https://en.wikipedia.org/wiki/Common_Log_Format 19 | // Tested with love against CLFParser https://pypi.org/project/clfparser/ 20 | func LogAccess(clientAddr, handlerId, user, method, resource, version string, status int, bytesSent int64, referer, userAgent string) { 21 | clientAddr = dashIfEmpty(clientAddr) 22 | handlerId = dashIfEmpty(handlerId) 23 | user = dashIfEmpty(user) 24 | referer = dashIfEmpty(referer) 25 | userAgent = dashIfEmpty(userAgent) 26 | firstLine := fmt.Sprintf("%s %s %s", method, resource, version) 27 | ts := time.Now().UTC().Format("02/Jan/2006:15:04:05 -0700") // Amazing date format layout! Not. 28 | A.Printf("%s %s %s [%s] %q %d %d %q %q\n", 29 | clientAddr, 30 | handlerId, 31 | user, 32 | ts, 33 | firstLine, 34 | status, 35 | bytesSent, 36 | referer, 37 | userAgent) 38 | } 39 | 40 | func dashIfEmpty(value string) string { 41 | if value == "" { 42 | return "-" 43 | } 44 | return value 45 | } 46 | -------------------------------------------------------------------------------- /internal/server/control/entrypoint.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package control 4 | 5 | var defaultEntrypoint = "/bin/sh -c" 6 | -------------------------------------------------------------------------------- /internal/server/control/entrypoint_windows.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | var defaultEntrypoint = "cmd.exe /c" 4 | -------------------------------------------------------------------------------- /internal/server/control/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package control 18 | 19 | import ( 20 | "crypto/tls" 21 | "crypto/x509" 22 | "net" 23 | "net/http" 24 | "sync" 25 | 26 | "github.com/BBVA/kapow/internal/certs" 27 | "github.com/BBVA/kapow/internal/logger" 28 | ) 29 | 30 | // Run Starts the control server listening in bindAddr 31 | func Run(bindAddr string, wg *sync.WaitGroup, serverCert, clientCert certs.Cert) { 32 | 33 | caCertPool := x509.NewCertPool() 34 | caCertPool.AppendCertsFromPEM(clientCert.SignedCertPEMBytes()) 35 | 36 | ln, err := net.Listen("tcp", bindAddr) 37 | if err != nil { 38 | logger.L.Fatal(err) 39 | } 40 | 41 | server := &http.Server{ 42 | Addr: bindAddr, 43 | TLSConfig: &tls.Config{ 44 | Certificates: []tls.Certificate{ 45 | tls.Certificate{ 46 | Certificate: [][]byte{serverCert.SignedCert}, 47 | PrivateKey: serverCert.PrivKey, 48 | Leaf: serverCert.X509Cert, 49 | }, 50 | }, 51 | ClientAuth: tls.RequireAndVerifyClientCert, 52 | ClientCAs: caCertPool, 53 | }, 54 | Handler: configRouter(), 55 | } 56 | 57 | // Signal startup 58 | logger.L.Printf("ControlServer listening at %s\n", bindAddr) 59 | wg.Done() 60 | 61 | // Listen to HTTPS connections with the server certificate and wait 62 | logger.L.Fatal(server.ServeTLS(ln, "", "")) 63 | } 64 | -------------------------------------------------------------------------------- /internal/server/data/decorator.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package data 18 | 19 | import ( 20 | "net/http" 21 | 22 | "github.com/BBVA/kapow/internal/server/httperror" 23 | "github.com/BBVA/kapow/internal/server/model" 24 | "github.com/gorilla/mux" 25 | ) 26 | 27 | type resourceHandler func(http.ResponseWriter, *http.Request, *model.Handler) 28 | 29 | func lockResponseWriter(fn resourceHandler) resourceHandler { 30 | return func(w http.ResponseWriter, r *http.Request, h *model.Handler) { 31 | h.Writing.Lock() 32 | defer h.Writing.Unlock() 33 | fn(w, r, h) 34 | } 35 | } 36 | 37 | func checkHandler(fn resourceHandler) func(http.ResponseWriter, *http.Request) { 38 | return func(w http.ResponseWriter, r *http.Request) { 39 | handlerID := mux.Vars(r)["handlerID"] 40 | if h, ok := Handlers.Get(handlerID); ok { 41 | fn(w, r, h) 42 | } else { 43 | httperror.ErrorJSON(w, "Handler ID Not Found", http.StatusNotFound) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/server/data/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package data 18 | 19 | import ( 20 | "net" 21 | "net/http" 22 | "sync" 23 | 24 | "github.com/BBVA/kapow/internal/logger" 25 | "github.com/BBVA/kapow/internal/server/httperror" 26 | "github.com/gorilla/mux" 27 | ) 28 | 29 | type routeSpec struct { 30 | route string 31 | method string 32 | rh resourceHandler 33 | } 34 | 35 | func configRouter(rs []routeSpec) (r *mux.Router) { 36 | r = mux.NewRouter() 37 | for _, s := range rs { 38 | r.HandleFunc(s.route, checkHandler(s.rh)).Methods(s.method) 39 | } 40 | r.HandleFunc( 41 | "/handlers/{handlerID}/{resource:.*}", 42 | func(w http.ResponseWriter, r *http.Request) { 43 | httperror.ErrorJSON(w, "Invalid Resource Path", http.StatusBadRequest) 44 | }) 45 | 46 | r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, h *http.Request) { 47 | httperror.ErrorJSON(w, "Data server: Not found", http.StatusNotFound) 48 | }) 49 | 50 | r.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, h *http.Request) { 51 | httperror.ErrorJSON(w, "Data server: Method not allowed", http.StatusMethodNotAllowed) 52 | }) 53 | 54 | return r 55 | } 56 | 57 | func Run(bindAddr string, wg *sync.WaitGroup) { 58 | rs := []routeSpec{ 59 | // request 60 | {"/handlers/{handlerID}/request/method", "GET", getRequestMethod}, 61 | {"/handlers/{handlerID}/request/host", "GET", getRequestHost}, 62 | {"/handlers/{handlerID}/request/version", "GET", getRequestVersion}, 63 | {"/handlers/{handlerID}/request/path", "GET", getRequestPath}, 64 | {"/handlers/{handlerID}/request/remote", "GET", getRequestRemote}, 65 | {"/handlers/{handlerID}/request/matches/{name}", "GET", getRequestMatches}, 66 | {"/handlers/{handlerID}/request/params/{name}", "GET", getRequestParams}, 67 | {"/handlers/{handlerID}/request/headers/{name}", "GET", getRequestHeaders}, 68 | {"/handlers/{handlerID}/request/cookies/{name}", "GET", getRequestCookies}, 69 | {"/handlers/{handlerID}/request/form/{name}", "GET", getRequestForm}, 70 | {"/handlers/{handlerID}/request/files/{name}/filename", "GET", getRequestFileName}, 71 | {"/handlers/{handlerID}/request/files/{name}/content", "GET", getRequestFileContent}, 72 | {"/handlers/{handlerID}/request/body", "GET", getRequestBody}, 73 | 74 | // route 75 | {"/handlers/{handlerID}/route/id", "GET", getRouteId}, 76 | 77 | // SSL stuff 78 | {"/handlers/{handlerID}/ssl/client/i/dn", "GET", getSSLClietnDN}, 79 | 80 | // response 81 | {"/handlers/{handlerID}/response/status", "PUT", lockResponseWriter(setResponseStatus)}, 82 | {"/handlers/{handlerID}/response/headers/{name}", "PUT", lockResponseWriter(setResponseHeaders)}, 83 | {"/handlers/{handlerID}/response/cookies/{name}", "PUT", lockResponseWriter(setResponseCookies)}, 84 | {"/handlers/{handlerID}/response/body", "PUT", lockResponseWriter(setResponseBody)}, 85 | {"/handlers/{handlerID}/response/stream", "PUT", lockResponseWriter(setResponseBody)}, 86 | 87 | // logging 88 | {"/handlers/{handlerID}/server/log/{prefix}", "PUT", setServerLog}, 89 | {"/handlers/{handlerID}/server/log", "PUT", setServerLog}, 90 | } 91 | 92 | listener, err := net.Listen("tcp", bindAddr) 93 | if err != nil { 94 | logger.L.Fatal(err) 95 | } 96 | 97 | // Signal startup 98 | logger.L.Printf("DataServer listening at %s\n", bindAddr) 99 | wg.Done() 100 | 101 | logger.L.Fatal(http.Serve(listener, configRouter(rs))) 102 | } 103 | -------------------------------------------------------------------------------- /internal/server/data/server_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package data 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "io" 23 | "net/http" 24 | "net/http/httptest" 25 | "testing" 26 | 27 | "github.com/BBVA/kapow/internal/server/httperror" 28 | "github.com/BBVA/kapow/internal/server/model" 29 | ) 30 | 31 | func checkErrorResponse(r *http.Response, expectedErrcode int, expectedReason string) []error { 32 | errList := make([]error, 0) 33 | 34 | if r.StatusCode != expectedErrcode { 35 | errList = append(errList, fmt.Errorf("HTTP status mismatch. Expected: %d, got: %d", expectedErrcode, r.StatusCode)) 36 | } 37 | 38 | if v := r.Header.Get("Content-Type"); v != "application/json; charset=utf-8" { 39 | errList = append(errList, fmt.Errorf("Content-Type header mismatch. Expected: %q, got: %q", "application/json; charset=utf-8", v)) 40 | } 41 | 42 | errMsg := httperror.ServerErrMessage{} 43 | if bodyBytes, err := io.ReadAll(r.Body); err != nil { 44 | errList = append(errList, fmt.Errorf("Unexpected error reading response body: %v", err)) 45 | } else if err := json.Unmarshal(bodyBytes, &errMsg); err != nil { 46 | errList = append(errList, fmt.Errorf("Response body contains invalid JSON entity: %v", err)) 47 | } else if errMsg.Reason != expectedReason { 48 | errList = append(errList, fmt.Errorf("Unexpected reason in response. Expected: %q, got: %q", expectedReason, errMsg.Reason)) 49 | } 50 | 51 | return errList 52 | } 53 | 54 | func TestConfigRouterReturnsRouterWithDecoratedRoutes(t *testing.T) { 55 | var handlerID string 56 | rs := []routeSpec{ 57 | { 58 | "/handlers/{handlerID}/dummy", 59 | "GET", 60 | func(w http.ResponseWriter, r *http.Request, h *model.Handler) { handlerID = h.ID }, 61 | }, 62 | } 63 | Handlers = New() 64 | Handlers.Add(&model.Handler{ID: "FOO"}) 65 | m := configRouter(rs) 66 | 67 | m.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/handlers/FOO/dummy", nil)) 68 | 69 | if handlerID != "FOO" { 70 | t.Errorf(`Handler ID mismatch. Expected "FOO". Got %q`, handlerID) 71 | } 72 | } 73 | 74 | func TestConfigRouterReturnsRouterThat400sOnUnconfiguredResources(t *testing.T) { 75 | m := configRouter([]routeSpec{}) 76 | w := httptest.NewRecorder() 77 | 78 | m.ServeHTTP(w, httptest.NewRequest("GET", "/handlers/FOO/dummy", nil)) 79 | 80 | for _, e := range checkErrorResponse(w.Result(), http.StatusBadRequest, "Invalid Resource Path") { 81 | t.Error(e) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /internal/server/data/state.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package data 18 | 19 | import ( 20 | "sync" 21 | 22 | "github.com/BBVA/kapow/internal/server/model" 23 | ) 24 | 25 | type safeHandlerMap struct { 26 | hs map[string]*model.Handler 27 | m *sync.RWMutex 28 | } 29 | 30 | // Singleton containing all current handlers as a safeHandlerMap 31 | var Handlers = New() 32 | 33 | // New creates a read-to-use safeHandlerMap 34 | func New() safeHandlerMap { 35 | return safeHandlerMap{ 36 | hs: make(map[string]*model.Handler), 37 | m: &sync.RWMutex{}, 38 | } 39 | } 40 | 41 | func (shm *safeHandlerMap) Add(h *model.Handler) { 42 | shm.m.Lock() 43 | shm.hs[h.ID] = h 44 | shm.m.Unlock() 45 | } 46 | 47 | func (shm *safeHandlerMap) Remove(id string) { 48 | shm.m.Lock() 49 | delete(shm.hs, id) 50 | shm.m.Unlock() 51 | } 52 | 53 | func (shm *safeHandlerMap) Get(id string) (*model.Handler, bool) { 54 | shm.m.RLock() 55 | h, ok := shm.hs[id] 56 | shm.m.RUnlock() 57 | return h, ok 58 | } 59 | 60 | func (shm *safeHandlerMap) ListIDs() (ids []string) { 61 | shm.m.RLock() 62 | defer shm.m.RUnlock() 63 | for id := range shm.hs { 64 | ids = append(ids, id) 65 | } 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /internal/server/data/testdata/client_chain.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDdTCCAl0CAQIwDQYJKoZIhvcNAQELBQAwgZ8xCzAJBgNVBAYTAkVTMQ8wDQYD 3 | VQQIDAZNYWRyaWQxDzANBgNVBAcMBk1hZHJpZDENMAsGA1UECgwEQkJWQTEYMBYG 4 | A1UECwwPSW5ub3ZhdGlvbiBMYWJzMR0wGwYDVQQDDBRTZWN1cml0eS1DQS5iYnZh 5 | LmNvbTEmMCQGCSqGSIb3DQEJARYXc2VjdXJpdHkuZ3JvdXBAYmJ2YS5jb20wHhcN 6 | MjAwMTIzMTQwODUxWhcNMjEwMTIyMTQwODUxWjBhMQswCQYDVQQGEwJFUzEPMA0G 7 | A1UECAwGTWFkcmlkMQ0wCwYDVQQKDARCQlZBMRgwFgYDVQQLDA9Jbm5vdmF0aW9u 8 | IExhYnMxGDAWBgNVBAMMD0thcG93ISBjbGllbnQgMTCCASIwDQYJKoZIhvcNAQEB 9 | BQADggEPADCCAQoCggEBAJKXoqOe0S1i8c0bDLGsvibSpDmWkb/2oXn4qn8XtlLF 10 | PSY69qeqkeLZov0nVV6zenag9Vh99uy7M4kw/pFqYv1eViDvV6I0wyKEzXNyeQmL 11 | O11YUWfP38T+Usw0JY0Pau+ewSQkurtHmGRWC5fAgPxiyi03hD3V3eHPR60V5yE6 12 | 1wYoLqz6xJ7nkXVyVLdg6wekrMkpos3ciA3Roco4m5fbXbVGYrx8E97byT9yyKzI 13 | kvFjQ0T4+67dPuWW+juoD0lOwfNu1WtY4PPnkUuzjUftKzD1t/a4zsLcFLafuuVR 14 | f4qb21o2pqDOWxMMZGrceS5uEF0nI1wtdw+XMtFHdFcCAwEAATANBgkqhkiG9w0B 15 | AQsFAAOCAQEAsnPW8pCIiejBwjQ4TTNPo5wiRNOib69ANj2lHE1gidO8HA29/ssF 16 | U7jbcxCQf0/flv+JddnSJzmeFhrt15CL6nOZ1whSqVA1W1dAno0RYNiPUILofq50 17 | zKNUVF+eYz24nksdI87d9j1Zri2H91p+gA1pBnIxBE8zgXZ5+u7FUrA41HOuVyAy 18 | 55EwUDloVg4WBeddb8Y/mPgXNHS7ZB0Z13+bLHeSkSWWV3Gw1OtLHJEv/j+/9K5O 19 | KoJCyO4xSkKP7/nYYKCed4grIfOAu7iHqN/Ok9yuAOm0tbgwKmzw9EGga82YCH0M 20 | jfd8wfVCqiZvW9SUa11fM/Np9/+04QtlNA== 21 | -----END CERTIFICATE----- 22 | -----BEGIN CERTIFICATE----- 23 | MIIEITCCAwmgAwIBAgIUMAqlEXi1gcpy97bWApqtBwgqEvgwDQYJKoZIhvcNAQEL 24 | BQAwgZ8xCzAJBgNVBAYTAkVTMQ8wDQYDVQQIDAZNYWRyaWQxDzANBgNVBAcMBk1h 25 | ZHJpZDENMAsGA1UECgwEQkJWQTEYMBYGA1UECwwPSW5ub3ZhdGlvbiBMYWJzMR0w 26 | GwYDVQQDDBRTZWN1cml0eS1DQS5iYnZhLmNvbTEmMCQGCSqGSIb3DQEJARYXc2Vj 27 | dXJpdHkuZ3JvdXBAYmJ2YS5jb20wHhcNMjAwMTIyMTcxNzUwWhcNMjAwMjIxMTcx 28 | NzUwWjCBnzELMAkGA1UEBhMCRVMxDzANBgNVBAgMBk1hZHJpZDEPMA0GA1UEBwwG 29 | TWFkcmlkMQ0wCwYDVQQKDARCQlZBMRgwFgYDVQQLDA9Jbm5vdmF0aW9uIExhYnMx 30 | HTAbBgNVBAMMFFNlY3VyaXR5LUNBLmJidmEuY29tMSYwJAYJKoZIhvcNAQkBFhdz 31 | ZWN1cml0eS5ncm91cEBiYnZhLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 32 | AQoCggEBAMM8xmTpUg2PXV6zPohGOKLiXHP3nQMfkoYLRwFYcKq0WnYmaKmIK5T0 33 | gCLmiQMCNv/NV8+aIkd2HuTPBnobFLbyUCN9yf2Gj81wxeRydBwbjaj0dpB1Jx9W 34 | 5OdMBFxGIKmnqVN/z784Ma9cj+tv0t5LYpWIxnEgnBaiuMkQnwJFyv6aL1VNRmW7 35 | zFMAqOiMishMKb/0UaSW53ZBFjCqmhquZ7CZYLsaB+mMDv1fOXf8jSX/WrcCCkvQ 36 | HX04/9HxNVIe3a0Zl8CPfyUOd9njVl1VRDgjljUyMeMM4zo31enpwWOuhpfI6jU5 37 | AB7If6xufHyN8FCvKpDy9Z9Sp5Ww1o8CAwEAAaNTMFEwHQYDVR0OBBYEFOfc0yly 38 | jUPuoy54Ods9KKt5CITsMB8GA1UdIwQYMBaAFOfc0ylyjUPuoy54Ods9KKt5CITs 39 | MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBALvzf14HF78rowNA 40 | XuczbkgKLbNJam0YC3ZxoB/pxcwfSZCf4E7+FAWKfmwrNqZv2PweOvDfP4Rx4T1x 41 | VLWDr0qtcsol7gOPga8HD/1zTgK096rYs5pCxQabgIpmHhzDUnjBrQyds8U4sakP 42 | 3xzuy/eZ/ozDzCGZn8HzqYHDTcEfypMSdZUUDgN4vVE3Il3AZRSEG9X/Ov4W8tLF 43 | dofY/JDVObXV0DAms9xcux8BfAslh8NNytP3q+uneIeuJT/eRQu+Z5GRB+mbL8X6 44 | DmXeSSkMiVeFgD1qg9VZVSBsihBpWpJakcxvXOOj5fY2t5ovW6TR2jDjW5XjHs82 45 | idlvJUw= 46 | -----END CERTIFICATE----- 47 | -------------------------------------------------------------------------------- /internal/server/httperror/error.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package httperror 18 | 19 | import ( 20 | "encoding/json" 21 | "net/http" 22 | ) 23 | 24 | // A ServerErrMessage represents the reason why the error happened 25 | type ServerErrMessage struct { 26 | Reason string `json:"reason"` 27 | } 28 | 29 | // ErrorJSON writes the provided error as a JSON body to the provided 30 | // http.ResponseWriter, after setting the appropriate Content-Type header 31 | func ErrorJSON(w http.ResponseWriter, error string, code int) { 32 | body, _ := json.Marshal( 33 | ServerErrMessage{ 34 | Reason: error, 35 | }, 36 | ) 37 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 38 | w.WriteHeader(code) 39 | _, _ = w.Write(body) 40 | } 41 | -------------------------------------------------------------------------------- /internal/server/httperror/error_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package httperror_test 18 | 19 | import ( 20 | "encoding/json" 21 | "io" 22 | "net/http" 23 | "net/http/httptest" 24 | "testing" 25 | 26 | "github.com/BBVA/kapow/internal/server/httperror" 27 | ) 28 | 29 | func TestErrorJSONSetsAppJsonContentType(t *testing.T) { 30 | w := httptest.NewRecorder() 31 | 32 | httperror.ErrorJSON(w, "Not Important Here", 500) 33 | 34 | if v := w.Result().Header.Get("Content-Type"); v != "application/json; charset=utf-8" { 35 | t.Errorf("Content-Type header mismatch. Expected: %q, got: %q", "application/json; charset=utf-8", v) 36 | } 37 | } 38 | 39 | func TestErrorJSONSetsRequestedStatusCode(t *testing.T) { 40 | w := httptest.NewRecorder() 41 | 42 | httperror.ErrorJSON(w, "Not Important Here", http.StatusGone) 43 | 44 | if v := w.Result().StatusCode; v != http.StatusGone { 45 | t.Errorf("Status code mismatch. Expected: %d, got: %d", http.StatusGone, v) 46 | } 47 | } 48 | 49 | func TestErrorJSONSetsBodyCorrectly(t *testing.T) { 50 | expectedReason := "Something Not Found" 51 | w := httptest.NewRecorder() 52 | 53 | httperror.ErrorJSON(w, expectedReason, http.StatusNotFound) 54 | 55 | errMsg := httperror.ServerErrMessage{} 56 | if bodyBytes, err := io.ReadAll(w.Result().Body); err != nil { 57 | t.Errorf("Unexpected error reading response body: %v", err) 58 | } else if err := json.Unmarshal(bodyBytes, &errMsg); err != nil { 59 | t.Errorf("Response body contains invalid JSON entity: %v", err) 60 | } else if errMsg.Reason != expectedReason { 61 | t.Errorf("Unexpected reason in response. Expected: %q, got: %q", expectedReason, errMsg.Reason) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/server/model/handler.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package model 18 | 19 | import ( 20 | "net/http" 21 | "sync" 22 | ) 23 | 24 | // Handler represents an open HTTP connection in the User Server. 25 | // 26 | // This struct contains the connection Writer and Request to be managed 27 | // by endpoints of the Data Server. 28 | type Handler struct { 29 | // ID is unique identifier of the request. 30 | ID string 31 | 32 | // Route is the original route that matched this request. 33 | Route 34 | 35 | // Writing is a mutex that prevents two goroutines from writing at 36 | // the same time in the response. 37 | Writing sync.Mutex 38 | 39 | // Request is a pointer to the in-progress request. 40 | Request *http.Request 41 | 42 | // Writer is the original http.ResponseWriter of the request. 43 | Writer http.ResponseWriter 44 | 45 | // Status is the returned status code 46 | Status int 47 | 48 | // SentBytes is the number of sent bytes 49 | SentBytes int64 50 | 51 | // The transfer of the body has started 52 | BodyOut bool 53 | } 54 | 55 | // TODO: add a Handler smart constructor to initialize Status to 200 56 | -------------------------------------------------------------------------------- /internal/server/model/route.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package model 18 | 19 | // Route contains the data needed to represent a Kapow! user route. 20 | type Route struct { 21 | // ID is the unique identifier of the Route. 22 | ID string `json:"id,omitempty"` 23 | 24 | // Method is the HTTP method that will match this Route. 25 | Method string `json:"method"` 26 | 27 | // Pattern is the gorilla/mux path pattern that will match this 28 | // Route. 29 | Pattern string `json:"url_pattern"` 30 | 31 | // Entrypoint is the string that will be executed when the Route 32 | // match. 33 | // 34 | // This string will be split according to the shell parsing rules to 35 | // be passed as a list to exec.Command. 36 | Entrypoint string `json:"entrypoint,omitempty"` 37 | 38 | // Command is the last argument to be passed to exec.Command when 39 | // executing the Entrypoint 40 | Command string `json:"command"` 41 | 42 | // Index is this route position in the server's routes list. 43 | // It is an output field, its value is ignored as input. 44 | Index int `json:"index"` 45 | 46 | Debug bool `json:"debug,omitempty"` 47 | } 48 | -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package server 18 | 19 | import ( 20 | "sync" 21 | 22 | "github.com/BBVA/kapow/internal/certs" 23 | "github.com/BBVA/kapow/internal/server/control" 24 | "github.com/BBVA/kapow/internal/server/data" 25 | "github.com/BBVA/kapow/internal/server/user" 26 | ) 27 | 28 | type ServerConfig struct { 29 | ControlBindAddr, 30 | DataBindAddr, 31 | UserBindAddr, 32 | KeyFile, 33 | CertFile, 34 | ClientCaFile string 35 | 36 | ClientAuth, 37 | Debug bool 38 | 39 | ControlServerCert certs.Cert 40 | ControlClientCert certs.Cert 41 | } 42 | 43 | // StartServer Starts one instance of each server in a goroutine and remains listening on a channel for trace events generated by them 44 | func StartServer(config ServerConfig) { 45 | var wg = sync.WaitGroup{} 46 | wg.Add(3) 47 | go control.Run(config.ControlBindAddr, &wg, config.ControlServerCert, config.ControlClientCert) 48 | go data.Run(config.DataBindAddr, &wg) 49 | go user.Run(config.UserBindAddr, &wg, config.CertFile, config.KeyFile, config.ClientCaFile, config.ClientAuth, config.Debug) 50 | 51 | // Wait for servers signals in order to return 52 | wg.Wait() 53 | } 54 | -------------------------------------------------------------------------------- /internal/server/user/mux/gorillize.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package mux 18 | 19 | import ( 20 | "net/http" 21 | 22 | "github.com/gorilla/mux" 23 | 24 | "github.com/BBVA/kapow/internal/logger" 25 | "github.com/BBVA/kapow/internal/server/model" 26 | ) 27 | 28 | func gorillize(rs []model.Route, buildHandler func(model.Route) http.Handler) *mux.Router { 29 | m := mux.NewRouter() 30 | 31 | for _, r := range rs { 32 | m.Handle(r.Pattern, buildHandler(r)).Methods(r.Method) 33 | } 34 | m.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 35 | w.WriteHeader(http.StatusNotFound) 36 | logger.LogAccess( 37 | r.RemoteAddr, 38 | "-", 39 | "-", 40 | r.Method, 41 | r.RequestURI, 42 | r.Proto, 43 | http.StatusNotFound, 44 | 0, 45 | r.Header.Get("Referer"), 46 | r.Header.Get("User-Agent"), 47 | ) 48 | }) 49 | m.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 50 | w.WriteHeader(http.StatusMethodNotAllowed) 51 | logger.LogAccess( 52 | r.RemoteAddr, 53 | "-", 54 | "-", 55 | r.Method, 56 | r.RequestURI, 57 | r.Proto, 58 | http.StatusMethodNotAllowed, 59 | 0, 60 | r.Header.Get("Referer"), 61 | r.Header.Get("User-Agent"), 62 | ) 63 | }) 64 | return m 65 | } 66 | -------------------------------------------------------------------------------- /internal/server/user/mux/handlerbuilder.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package mux 18 | 19 | import ( 20 | "bufio" 21 | "net/http" 22 | "os" 23 | 24 | "github.com/google/uuid" 25 | 26 | "github.com/BBVA/kapow/internal/logger" 27 | "github.com/BBVA/kapow/internal/server/data" 28 | "github.com/BBVA/kapow/internal/server/model" 29 | "github.com/BBVA/kapow/internal/server/user/spawn" 30 | ) 31 | 32 | var spawner = spawn.Spawn 33 | var idGenerator = uuid.NewUUID 34 | 35 | func handlerBuilder(route model.Route) http.Handler { 36 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 37 | id, err := idGenerator() 38 | if err != nil { 39 | w.WriteHeader(http.StatusInternalServerError) 40 | return 41 | } 42 | 43 | h := &model.Handler{ 44 | ID: id.String(), 45 | Route: route, 46 | Request: r, 47 | Writer: w, 48 | Status: 200, 49 | } 50 | 51 | data.Handlers.Add(h) 52 | defer data.Handlers.Remove(h.ID) 53 | 54 | if route.Debug { 55 | var stdOutR, stdOutW *os.File 56 | stdOutR, stdOutW, err = os.Pipe() 57 | defer stdOutW.Close() 58 | if err != nil { 59 | logger.L.Println(err) 60 | return 61 | } 62 | var stdErrR, stdErrW *os.File 63 | stdErrR, stdErrW, err = os.Pipe() 64 | defer stdErrW.Close() 65 | if err != nil { 66 | logger.L.Println(err) 67 | return 68 | } 69 | 70 | go logStream(h.ID, "stdout", stdOutR) 71 | go logStream(h.ID, "stderr", stdErrR) 72 | 73 | err = spawner(h, stdOutW, stdErrW) 74 | } else { 75 | err = spawner(h, nil, nil) 76 | } 77 | 78 | // In case of the user not setting /request/body 79 | if !h.BodyOut { 80 | if h.Status != 0 { 81 | h.Writer.WriteHeader(h.Status) 82 | } 83 | h.BodyOut = true 84 | } 85 | 86 | if err != nil { 87 | logger.L.Println(err) 88 | } 89 | 90 | if r != nil { 91 | logger.LogAccess( 92 | r.RemoteAddr, 93 | h.ID, 94 | "-", 95 | r.Method, 96 | r.RequestURI, 97 | r.Proto, 98 | h.Status, 99 | h.SentBytes, 100 | r.Header.Get("Referer"), 101 | r.Header.Get("User-Agent"), 102 | ) 103 | } 104 | }) 105 | } 106 | 107 | func logStream(handlerId string, streamName string, stream *os.File) { 108 | defer stream.Close() 109 | scanner := bufio.NewScanner(stream) 110 | for scanner.Scan() { 111 | logger.L.Printf("%s %s: %s", handlerId, streamName, scanner.Text()) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /internal/server/user/mux/mux.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package mux 18 | 19 | import ( 20 | "net/http" 21 | "sync" 22 | 23 | "github.com/gorilla/mux" 24 | 25 | "github.com/BBVA/kapow/internal/server/model" 26 | ) 27 | 28 | type SwappableMux struct { 29 | m sync.RWMutex 30 | root *mux.Router 31 | } 32 | 33 | func New() *SwappableMux { 34 | return &SwappableMux{ 35 | root: gorillize([]model.Route{}, handlerBuilder), 36 | } 37 | } 38 | 39 | func (sm *SwappableMux) get() *mux.Router { 40 | sm.m.RLock() 41 | defer sm.m.RUnlock() 42 | 43 | return sm.root 44 | } 45 | 46 | func (sm *SwappableMux) set(mux *mux.Router) { 47 | sm.m.Lock() 48 | sm.root = mux 49 | sm.m.Unlock() 50 | } 51 | 52 | func (sm *SwappableMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { 53 | sm.get().ServeHTTP(w, r) 54 | } 55 | 56 | func (sm *SwappableMux) Update(rs []model.Route) { 57 | sm.set(gorillize(rs, handlerBuilder)) 58 | } 59 | -------------------------------------------------------------------------------- /internal/server/user/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package user 18 | 19 | import ( 20 | "crypto/tls" 21 | "crypto/x509" 22 | "fmt" 23 | "net" 24 | "net/http" 25 | "os" 26 | "sync" 27 | 28 | "github.com/BBVA/kapow/internal/logger" 29 | "github.com/BBVA/kapow/internal/server/user/mux" 30 | ) 31 | 32 | // Server is a singleton that stores the http.Server for the user package 33 | var Server = http.Server{ 34 | Handler: mux.New(), 35 | } 36 | 37 | var DebugEndpoints bool 38 | 39 | // Run finishes configuring Server and runs ListenAndServe on it 40 | func Run(bindAddr string, wg *sync.WaitGroup, certFile, keyFile, cliCaFile string, cliAuth, debug bool) { 41 | 42 | Server = http.Server{ 43 | Addr: bindAddr, 44 | Handler: mux.New(), 45 | } 46 | 47 | if debug { 48 | Routes.SetDebug() 49 | } 50 | 51 | listener, err := net.Listen("tcp", bindAddr) 52 | if err != nil { 53 | logger.L.Fatal(err) 54 | } 55 | 56 | if (certFile != "") && (keyFile != "") { 57 | if cliAuth { 58 | if Server.TLSConfig == nil { 59 | Server.TLSConfig = &tls.Config{} 60 | } 61 | 62 | var err error 63 | Server.TLSConfig.ClientCAs, err = loadCertificatesFromFile(cliCaFile) 64 | if err != nil { 65 | logger.L.Fatalf("UserServer failed to load CA certs: %s\n", err) 66 | } else { 67 | CAStore := "System store" 68 | if Server.TLSConfig.ClientCAs != nil { 69 | CAStore = cliCaFile 70 | } 71 | logger.L.Printf("UserServer using CA certs from %s\n", CAStore) 72 | Server.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert 73 | } 74 | } 75 | 76 | // Signal startup 77 | logger.L.Printf("UserServer listening at %s\n", bindAddr) 78 | wg.Done() 79 | 80 | logger.L.Fatal(Server.ServeTLS(listener, certFile, keyFile)) 81 | } else { 82 | // Signal startup 83 | logger.L.Printf("UserServer listening at %s\n", bindAddr) 84 | wg.Done() 85 | 86 | logger.L.Fatal(Server.Serve(listener)) 87 | } 88 | } 89 | 90 | func loadCertificatesFromFile(certFile string) (pool *x509.CertPool, err error) { 91 | if certFile != "" { 92 | var caCerts []byte 93 | caCerts, err = os.ReadFile(certFile) 94 | if err == nil { 95 | pool = x509.NewCertPool() 96 | if !pool.AppendCertsFromPEM(caCerts) { 97 | err = fmt.Errorf("Invalid certificate file %s", certFile) 98 | } 99 | } 100 | } 101 | 102 | return 103 | } 104 | -------------------------------------------------------------------------------- /internal/server/user/spawn/spawn.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package spawn 18 | 19 | import ( 20 | "errors" 21 | "io" 22 | "os" 23 | "os/exec" 24 | 25 | "github.com/google/shlex" 26 | 27 | "github.com/BBVA/kapow/internal/server/model" 28 | ) 29 | 30 | func Spawn(h *model.Handler, stdout io.Writer, stderr io.Writer) error { 31 | if h.Route.Entrypoint == "" { 32 | return errors.New("Entrypoint cannot be empty") 33 | } 34 | args, err := shlex.Split(h.Route.Entrypoint) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | if h.Route.Command != "" { 40 | args = append(args, h.Route.Command) 41 | } 42 | 43 | cmd := exec.Command(args[0], args[1:]...) 44 | if stdout != nil { 45 | cmd.Stdout = stdout 46 | } 47 | if stderr != nil { 48 | cmd.Stderr = stderr 49 | } 50 | cmd.Env = os.Environ() 51 | cmd.Env = append(cmd.Env, "KAPOW_HANDLER_ID="+h.ID) 52 | 53 | err = cmd.Run() 54 | 55 | return err 56 | } 57 | -------------------------------------------------------------------------------- /internal/server/user/state.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package user 18 | 19 | import ( 20 | "errors" 21 | "sync" 22 | 23 | "github.com/BBVA/kapow/internal/server/model" 24 | "github.com/BBVA/kapow/internal/server/user/mux" 25 | ) 26 | 27 | type safeRouteList struct { 28 | rs []model.Route 29 | m *sync.RWMutex 30 | globalDebug bool 31 | } 32 | 33 | var Routes safeRouteList = New() 34 | 35 | func New() safeRouteList { 36 | return safeRouteList{ 37 | rs: []model.Route{}, 38 | m: &sync.RWMutex{}, 39 | } 40 | } 41 | 42 | func (srl *safeRouteList) SetDebug() { 43 | srl.globalDebug = true 44 | } 45 | 46 | func (srl *safeRouteList) Append(r model.Route) model.Route { 47 | srl.m.Lock() 48 | r.Index = len(srl.rs) 49 | r.Debug = srl.globalDebug || r.Debug 50 | srl.rs = append(srl.rs, r) 51 | srl.m.Unlock() 52 | 53 | Server.Handler.(*mux.SwappableMux).Update(srl.Snapshot()) 54 | 55 | return r 56 | } 57 | 58 | func (srl *safeRouteList) Snapshot() []model.Route { 59 | srl.m.RLock() 60 | defer srl.m.RUnlock() 61 | 62 | rs := make([]model.Route, len(srl.rs)) 63 | copy(rs, srl.rs) 64 | return rs 65 | } 66 | 67 | func (srl *safeRouteList) List() []model.Route { 68 | rs := srl.Snapshot() 69 | for i := 0; i < len(rs); i++ { 70 | rs[i].Index = i 71 | } 72 | return rs 73 | } 74 | 75 | func (srl *safeRouteList) Delete(ID string) error { 76 | // TODO: Refactor with `defer` if applicable 77 | srl.m.Lock() 78 | for i := 0; i < len(srl.rs); i++ { 79 | if srl.rs[i].ID == ID { 80 | srl.rs = append(srl.rs[:i], srl.rs[i+1:]...) 81 | srl.m.Unlock() 82 | Server.Handler.(*mux.SwappableMux).Update(srl.Snapshot()) 83 | return nil 84 | 85 | } 86 | } 87 | srl.m.Unlock() 88 | return errors.New("Route not found") 89 | } 90 | 91 | func (srl *safeRouteList) Get(ID string) (r model.Route, err error) { 92 | srl.m.RLock() 93 | defer srl.m.RUnlock() 94 | for _, r = range srl.rs { 95 | if r.ID == ID { 96 | return 97 | } 98 | } 99 | 100 | err = errors.New("Route not found") 101 | return 102 | } 103 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "github.com/spf13/cobra" 21 | 22 | "github.com/BBVA/kapow/internal/cmd" 23 | "github.com/BBVA/kapow/internal/logger" 24 | ) 25 | 26 | func main() { 27 | var kapowCmd = &cobra.Command{Use: "kapow [action]"} 28 | 29 | kapowCmd.AddCommand(cmd.ServerCmd) 30 | kapowCmd.AddCommand(cmd.GetCmd) 31 | kapowCmd.AddCommand(cmd.SetCmd) 32 | kapowCmd.AddCommand(cmd.RouteCmd) 33 | 34 | err := kapowCmd.Execute() 35 | if err != nil { 36 | logger.L.Fatalln(err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /release-key.gpg: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 64 | 9r7Wg1ayT+mq8kzKV0xf61j6aVIGX+4HUiNE 65 | =/glm 66 | -----END PGP PUBLIC KEY BLOCK----- 67 | -------------------------------------------------------------------------------- /spec/test/.envrc: -------------------------------------------------------------------------------- 1 | use_nix 2 | -------------------------------------------------------------------------------- /spec/test/.gherkin-lintrc: -------------------------------------------------------------------------------- 1 | { 2 | "no-empty-file": "off", 3 | "no-files-without-scenarios" : "off", 4 | "no-unnamed-features": "off", 5 | 6 | "no-unnamed-scenarios": "on", 7 | "no-dupe-scenario-names": ["on", "in-feature"], 8 | "no-dupe-feature-names": "on", 9 | "no-partially-commented-tag-lines": "on", 10 | "indentation": [ 11 | "on", { 12 | "Scenario": 2, 13 | "Background": 2, 14 | "given": 4, 15 | "when": 4, 16 | "then": 4, 17 | "and": 6, 18 | "but": 6, 19 | "example": 6, 20 | "Examples": 4, 21 | "scenario tag": 2} 22 | ], 23 | "no-trailing-spaces": "on", 24 | "new-line-at-eof": ["on", "yes"], 25 | "no-multiple-empty-lines": "on", 26 | "no-scenario-outlines-without-examples": "on", 27 | "name-length": ["on", {"Feature": 80, "Step": 120, "Scenario": 80}], 28 | "no-restricted-tags": ["on", {"tags": ["@watch", "@wip"]}], 29 | "use-and": "on", 30 | "no-duplicate-tags": "on", 31 | "no-superfluous-tags": "on", 32 | "no-homogenous-tags": "on", 33 | "one-space-between-tags": "on", 34 | "no-unused-variables": "on", 35 | "no-background-only-scenario": "on", 36 | "no-empty-background": "on", 37 | "scenario-size": ["on", { "steps-length": {"Background": 15, "Scenario": 15}}] 38 | } 39 | -------------------------------------------------------------------------------- /spec/test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nixos/nix:2.3.6 2 | 3 | # Install CircleCI requirements for base images 4 | # https://circleci.com/docs/2.0/custom-images/ 5 | # RUN apk upgrade --update-cache \ 6 | # && apk add git openssh-server tar gzip ca-certificates 7 | 8 | # Install Kapow! Spec Test Suite 9 | RUN mkdir -p /usr/src/ksts 10 | WORKDIR /usr/src/ksts 11 | COPY features /usr/src/ksts/features 12 | # COPY Pipfile Pipfile.lock /usr/src/ksts/ 13 | # RUN pip install --upgrade pip \ 14 | # && pip install pipenv \ 15 | # && pipenv install --deploy --system \ 16 | # && rm -f Pipfile Pipfile.lock 17 | COPY ./*.nix ./ 18 | ENTRYPOINT [ "nix-shell", "--command" ] 19 | -------------------------------------------------------------------------------- /spec/test/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all lint wip test fix catalog 2 | 3 | all: checkbin test 4 | 5 | lint: 6 | gherkin-lint 7 | wip: 8 | KAPOW_DEBUG_TESTS=1 behave --stop --wip -k 9 | test: lint 10 | behave --no-capture --tags=~@skip 11 | fix: lint 12 | KAPOW_DEBUG_TESTS=1 behave --stop --no-capture --tags=~@skip 13 | catalog: 14 | behave --format steps.usage --dry-run --no-summary -q 15 | checkbin: 16 | @which kapow >/dev/null || (echo "ERROR: Your kapow binary is not present in PATH" && exit 1) 17 | testpoc: 18 | PATH=../../testutils/poc:$$PATH behave --no-capture --tags=~@skip 19 | wippoc: 20 | PATH=../../testutils/poc:$$PATH behave --no-capture --tags=@wip -k 21 | -------------------------------------------------------------------------------- /spec/test/README.rst: -------------------------------------------------------------------------------- 1 | Kapow Spec Test Suite 2 | ===================== 3 | 4 | This is a generic test suite to be run against ANY Kapow! 5 | implementation. 6 | 7 | 8 | Prerequisites 9 | ------------- 10 | 11 | First of all you need a working ``kapow server`` installed to run the 12 | tests against it: 13 | 14 | Check that your installation is correct by typing this in a shell: 15 | 16 | .. code-block:: bash 17 | 18 | kapow --help 19 | 20 | You should see a help screen. If not fix it before continuing. 21 | 22 | You also need ``gherkin-lint``. Run: 23 | 24 | .. code-block:: bash 25 | 26 | npm install -g gherkin-lint 27 | 28 | 29 | How to run the test suite? 30 | ========================== 31 | 32 | With the above requisites in place and with your command shell in this 33 | very directory run: 34 | 35 | .. code-block:: bash 36 | 37 | make 38 | 39 | This should test the current installed kapow implementation and output 40 | something like: 41 | 42 | .. code-block:: none 43 | 44 | 13 features passed, 0 failed, 0 skipped 45 | 23 scenarios passed, 0 failed, 0 skipped 46 | 99 steps passed, 0 failed, 0 skipped, 0 undefined 47 | Took 0m23.553s 48 | 49 | 50 | Troubleshooting 51 | --------------- 52 | 53 | Environment customization 54 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 55 | 56 | You can customize some of the test behavior with the following 57 | environment variables: 58 | 59 | * ``KAPOW_BOOT_TIMEOUT``: Timeout in ms for the server to be ready 60 | * ``KAPOW_SERVER_CMD``: The full command line to start a non-interactive 61 | listening kapow server. By default: ``kapow server`` 62 | * ``KAPOW_CONTROL_URL``: URL of the Control API. By default: ``http://localhost:8081`` 63 | * ``KAPOW_DATA_URL``: URL of the Data API. By default: ``http://localhost:8082`` 64 | * ``KAPOW_USER_URL``: URL of the User Server. By default: ``http://localhost:8080`` 65 | 66 | 67 | Fixing tests one by one 68 | ~~~~~~~~~~~~~~~~~~~~~~~ 69 | 70 | If you use ``make fix`` instead of ``make`` the first failing test will stop 71 | the execution, giving you a chance of inspecting the result. Also the 72 | tests will be in DEBUG mode displaying all requests made to the server. 73 | 74 | 75 | How to develop new tests? 76 | ========================= 77 | 78 | Developing new steps 79 | -------------------- 80 | 81 | First of all execute ``make catalog`` this will display all the existing 82 | steps and associates features. If none of the steps fits your needs 83 | write a new one in your feature and run ``make catalog`` again. 84 | The detected new step will trigger the output of a new section with a 85 | template for the step definition to be implemented (you can copy and 86 | paste it removing the ``u'`` unicode symbol of the strings). 87 | 88 | 89 | Developing new step definitions 90 | ------------------------------- 91 | 92 | To make you life easier, mark the feature or scenario you are working on 93 | with the tag ``@wip`` and use ``make wip`` to run only your 94 | scenario/feature. 95 | 96 | 1. Paste the step definition template you just copied at the end of the 97 | file ``steps/steps.py``. 98 | 2. Run ``make wip`` to test that the step is triggered. You should see a 99 | ``NotImplementedError`` exception. 100 | 3. Implement your step. 101 | 4. Run ``make wip`` again. 102 | 103 | When you finish implementing your step definitions remove the ``@wip`` of 104 | your feature/scenario and run ``make`` to test everything together. 105 | -------------------------------------------------------------------------------- /spec/test/features/control/append/error_malformed.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | Feature: Kapow! server reject append requests with malformed JSON bodies. 17 | Kapow! server will reject to append a route when 18 | it receives a malformed json document in the 19 | request body. 20 | 21 | Scenario: Error because a malformed JSON document. 22 | If a request comes with an invalid JSON document 23 | the server will respond with a bad request error. 24 | 25 | Given I have a running Kapow! server 26 | When I try to append with this malformed JSON document: 27 | """ 28 | Hi! I am an invalid JSON document. 29 | """ 30 | Then I get 400 as response code 31 | And the response header "Content-Type" contains "application/json" 32 | And I get the following response body: 33 | """ 34 | { 35 | "reason": "Malformed JSON" 36 | } 37 | """ 38 | -------------------------------------------------------------------------------- /spec/test/features/control/append/error_unprocessable.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | Feature: Kapow! server rejects requests with semantic errors. 17 | Kapow! server will refuse to append routes when 18 | it receives a valid json document not conforming 19 | to the specification. 20 | 21 | Scenario: Error because lacking mandatory fields. 22 | If a request lacks any mandatory field the server 23 | responds with an error. 24 | 25 | Given I have a running Kapow! server 26 | When I append the route: 27 | """ 28 | { 29 | "entrypoint": "/bin/sh -c", 30 | "command": "ls -la / | kapow set /response/body" 31 | } 32 | """ 33 | Then I get 422 as response code 34 | And the response header "Content-Type" contains "application/json" 35 | And I get the following response body: 36 | """ 37 | { 38 | "reason": "Invalid Route" 39 | } 40 | """ 41 | 42 | Scenario: Error because bad route format. 43 | If a request contains an invalid expression in the 44 | field url_pattern the server responds with an error. 45 | 46 | Given I have a running Kapow! server 47 | When I append the route: 48 | """ 49 | { 50 | "method": "GET", 51 | "url_pattern": "+123--", 52 | "entrypoint": "/bin/sh -c", 53 | "command": "ls -la / | kapow set /response/body" 54 | } 55 | """ 56 | Then I get 422 as response code 57 | And the response header "Content-Type" contains "application/json" 58 | And I get the following response body: 59 | """ 60 | { 61 | "reason": "Invalid Route" 62 | } 63 | """ 64 | -------------------------------------------------------------------------------- /spec/test/features/control/append/success.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | Feature: Append new routes in Kapow! server. 17 | Appending routes allows users to configure the server. New 18 | routes are added at the end of the list of existing routes. 19 | 20 | Scenario: Append the first route. 21 | A just started server or one with all routes removed, 22 | will create a new list of routes. The newly created rule 23 | will be at index 0. 24 | 25 | Given I have a just started Kapow! server 26 | When I append the route: 27 | """ 28 | { 29 | "method": "GET", 30 | "url_pattern": "/foo", 31 | "entrypoint": "/bin/sh -c", 32 | "command": "ls -la / | kapow set /response/body" 33 | } 34 | """ 35 | Then I get 201 as response code 36 | And I get "Created" as response reason phrase 37 | And I get the following response body: 38 | """ 39 | { 40 | "method": "GET", 41 | "url_pattern": "/foo", 42 | "entrypoint": "/bin/sh -c", 43 | "command": "ls -la / | kapow set /response/body", 44 | "index": 0, 45 | "id": ANY 46 | } 47 | """ 48 | 49 | Scenario: Append another route. 50 | Appending routes on a non empty list will create new routes 51 | at the end of the list. 52 | 53 | Given I have a Kapow! server with the following routes: 54 | | method | url_pattern | entrypoint | command | 55 | | GET | /foo | /bin/sh -c | ls -la / \| kapow set /response/body | 56 | | GET | /qux/{dirname} | /bin/sh -c | ls -la /request/params/dirname \| kapow set /response/body | 57 | When I append the route: 58 | """ 59 | { 60 | "method": "GET", 61 | "url_pattern": "/baz", 62 | "entrypoint": "/bin/sh -c", 63 | "command": "ls -la /etc | kapow set /response/body" 64 | } 65 | """ 66 | Then I get 201 as response code 67 | And I get "Created" as response reason phrase 68 | And I get the following response body: 69 | """ 70 | { 71 | "method": "GET", 72 | "url_pattern": "/baz", 73 | "entrypoint": "/bin/sh -c", 74 | "command": "ls -la /etc | kapow set /response/body", 75 | "index": 2, 76 | "id": ANY 77 | } 78 | """ 79 | -------------------------------------------------------------------------------- /spec/test/features/control/delete/error_notfound.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | Feature: Fail to delete a route in Kapow! server. 17 | When trying to delete a route that not exists in the server 18 | the server respons with an error. 19 | 20 | Scenario: Delete a nonexistent route. 21 | A request of removing a nonexistent route 22 | will trigger a not found error. 23 | 24 | Given I have a just started Kapow! server 25 | When I delete the route with id "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx" 26 | Then I get 404 as response code 27 | And the response header "Content-Type" contains "application/json" 28 | And I get the following response body: 29 | """ 30 | { 31 | "reason": "Route Not Found" 32 | } 33 | """ 34 | -------------------------------------------------------------------------------- /spec/test/features/control/delete/success.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | Feature: Delete routes in Kapow! server. 17 | Deleting routes allows users to remove undesired 18 | routes from the server. 19 | 20 | Scenario: Delete a route. 21 | Routes are removed from the sever by specifying their id. 22 | 23 | Given I have a Kapow! server with the following routes: 24 | | method | url_pattern | entrypoint | command | 25 | | GET | /foo | /bin/sh -c | ls -la / \| kapow set /response/body | 26 | | GET | /qux/{dirname} | /bin/sh -c | ls -la /request/params/dirname \| kapow set /response/body | 27 | When I delete the first route 28 | Then I get 204 as response code 29 | And I get "No Content" as response reason phrase 30 | -------------------------------------------------------------------------------- /spec/test/features/control/get/error_notfound.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | Feature: Fail to retrieve route details in Kapow! server. 17 | When trying to get route details for a route that 18 | does no exist the server responds with an error. 19 | 20 | Scenario: Try to get details for a nonexistent route. 21 | A request for retrieving details for a nonexistent 22 | route will trigger a not found error. 23 | 24 | Given I have a just started Kapow! server 25 | When I get the route with id "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx" 26 | Then I get 404 as response code 27 | And the response header "Content-Type" contains "application/json" 28 | And I get the following response body: 29 | """ 30 | { 31 | "reason": "Route Not Found" 32 | } 33 | """ 34 | -------------------------------------------------------------------------------- /spec/test/features/control/get/success.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | Feature: Retrieve route details in Kapow! server. 17 | Users can retrieve route details from the server 18 | by specifying its id. 19 | 20 | Scenario: Retrieve route details. 21 | Get route details by id. 22 | 23 | Given I have a Kapow! server with the following routes: 24 | | method | url_pattern | entrypoint | command | 25 | | GET | /foo | /bin/sh -c | ls -la / \| kapow set /response/body | 26 | | GET | /qux/{dirname} | /bin/sh -c | ls -la /request/params/dirname \| kapow set /response/body | 27 | When I get the first route 28 | Then I get 200 as response code 29 | And I get "OK" as response reason phrase 30 | And I get the following response body: 31 | """ 32 | { 33 | "method": "GET", 34 | "url_pattern": "/foo", 35 | "entrypoint": "/bin/sh -c", 36 | "command": "ls -la / | kapow set /response/body", 37 | "index": 0, 38 | "id": ANY 39 | } 40 | """ 41 | -------------------------------------------------------------------------------- /spec/test/features/control/insert/error_malformed.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | @skip 17 | Feature: Kapow! server rejects insertion requests with malformed JSON bodies. 18 | Kapow! server will reject to insert a route when 19 | it receives a malformed JSON document in the 20 | request body. 21 | 22 | Scenario: Error because of malformed JSON document. 23 | If a request comes with an invalid JSON document 24 | the server will respond with a bad request error. 25 | 26 | Given I have a running Kapow! server 27 | When I try to insert with this JSON document: 28 | """ 29 | { 30 | "method" "GET", 31 | "url_pattern": /hello, 32 | "entrypoint": null 33 | "command": "echo Hello 34 | World | kapow set /response/body", 35 | "index": 0, 36 | "id": "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx" 37 | } 38 | """ 39 | Then I get 400 as response code 40 | And the response header "Content-Type" contains "application/json" 41 | And I get the following response body: 42 | """ 43 | { 44 | "reason": "Malformed JSON" 45 | } 46 | """ 47 | -------------------------------------------------------------------------------- /spec/test/features/control/insert/error_unprocessable.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | @skip 17 | Feature: Kapow! server rejects insertion requests with semantic errors. 18 | Kapow! server will refuse to insert routes when 19 | it receives a valid JSON but not conforming document. 20 | 21 | Scenario: Error because lacking mandatory fields. 22 | If a request lacks any mandatory fields the server 23 | responds with an error. 24 | 25 | Given I have a running Kapow! server 26 | When I insert the route: 27 | """ 28 | { 29 | "entrypoint": "/bin/sh -c", 30 | "command": "ls -la / | kapow set /response/body" 31 | } 32 | """ 33 | Then I get 422 as response code 34 | And I get "Invalid Route" as response reason phrase 35 | 36 | Scenario: Error because wrong route specification. 37 | If a request contains an invalid expression in the 38 | url_pattern field the server responds with an error. 39 | 40 | Given I have a running Kapow! server 41 | When I insert the route: 42 | """ 43 | { 44 | "method": "GET", 45 | "url_pattern": "+123--", 46 | "entrypoint": "/bin/sh -c", 47 | "command": "ls -la / | kapow set /response/body", 48 | "index": 0 49 | } 50 | """ 51 | Then I get 422 as response code 52 | And I get "Invalid Route" as response reason phrase 53 | 54 | Scenario: Error because negative index specified. 55 | If a request contains a negative number in the 56 | index field the server responds with an error. 57 | 58 | Given I have a running Kapow! server 59 | When I insert the route: 60 | """ 61 | { 62 | "method": "GET", 63 | "url_pattern": "+123--", 64 | "entrypoint": "/bin/sh -c", 65 | "command": "ls -la / | kapow set /response/body", 66 | "index": -1 67 | } 68 | """ 69 | Then I get 422 as response code 70 | And the response header "Content-Type" contains "application/json" 71 | And I get the following response body: 72 | """ 73 | { 74 | "reason": "Invalid Route" 75 | } 76 | """ 77 | -------------------------------------------------------------------------------- /spec/test/features/control/insert/success.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | @skip 17 | Feature: Insert new routes in Kapow! server. 18 | Inserting routes allows users to configure the server. New 19 | routes could be inserted at the beginning or before any 20 | existing route of the routes list. 21 | 22 | Background: 23 | Given I have a Kapow! server with the following routes: 24 | | method | url_pattern | entrypoint | command | 25 | | GET | /foo | /bin/sh -c | ls -la / \| kapow set /response/body | 26 | | GET | /qux/{dirname} | /bin/sh -c | ls -la /request/params/dirname \| kapow set /response/body | 27 | 28 | Scenario: Insert a route at the beginning. 29 | A route can be inserted at the beginning of the list 30 | by specifying an index 0 in the request. 31 | 32 | When I insert the route: 33 | """ 34 | { 35 | "method": "GET", 36 | "url_pattern": "/bar", 37 | "entrypoint": "/bin/sh -c", 38 | "command": "ls -la /var | kapow set /response/body", 39 | "index": 0 40 | } 41 | """ 42 | Then I get 201 as response code 43 | And I get "Created" as response reason phrase 44 | And I get the following response body: 45 | """ 46 | { 47 | "method": "GET", 48 | "url_pattern": "/bar", 49 | "entrypoint": "/bin/sh -c", 50 | "command": "ls -la /var | kapow set /response/body", 51 | "index": 0, 52 | "id": ANY 53 | } 54 | """ 55 | 56 | Scenario: Insert a route in the middle. 57 | A route can be inserted in the middle of the list 58 | by specifying an index less or equal to the last 59 | index in the request. 60 | 61 | When I insert the route: 62 | """ 63 | { 64 | "method": "GET", 65 | "url_pattern": "/bar", 66 | "entrypoint": "/bin/sh -c", 67 | "command": "ls -la /var | kapow set /response/body", 68 | "index": 1 69 | } 70 | """ 71 | Then I get 201 as response code 72 | And I get "Created" as response reason phrase 73 | And I get the following response body: 74 | """ 75 | { 76 | "method": "GET", 77 | "url_pattern": "/bar", 78 | "entrypoint": "/bin/sh -c", 79 | "command": "ls -la /var | kapow set /response/body", 80 | "index": 1, 81 | "id": ANY 82 | } 83 | """ 84 | -------------------------------------------------------------------------------- /spec/test/features/control/list/success.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | @server 17 | Feature: Listing routes in a Kapow! server. 18 | Listing routes allows users to know what URLs are 19 | available on a Kapow! server. The List endpoint returns 20 | a list of the routes the server has configured. 21 | 22 | Scenario: List routes on a fresh started server. 23 | A just started or with all routes removed Kapow! server, 24 | will show an empty list of routes. 25 | 26 | Given I have a just started Kapow! server 27 | When I request a routes listing 28 | Then I get 200 as response code 29 | And I get "OK" as response reason phrase 30 | And I get the following response body: 31 | """ 32 | [] 33 | """ 34 | 35 | Scenario: List routes on a server with routes loaded. 36 | After some route creation/insertion operations the server 37 | must return an ordered list of routes stored. 38 | 39 | Given I have a Kapow! server with the following routes: 40 | | method | url_pattern | entrypoint | command | 41 | | GET | /foo | /bin/sh -c | ls -la / \| kapow set /response/body | 42 | | GET | /qux/{dirname} | /bin/sh -c | ls -la /request/params/dirname \| kapow set /response/body | 43 | When I request a routes listing 44 | Then I get 200 as response code 45 | And I get "OK" as response reason phrase 46 | And I get the following response body: 47 | """ 48 | [ 49 | { 50 | "method": "GET", 51 | "url_pattern": "/foo", 52 | "entrypoint": "/bin/sh -c", 53 | "command": "ls -la / | kapow set /response/body", 54 | "index": 0, 55 | "id": ANY 56 | }, 57 | { 58 | "method": "GET", 59 | "url_pattern": "/qux/{dirname}", 60 | "entrypoint": "/bin/sh -c", 61 | "command": "ls -la /request/params/dirname | kapow set /response/body", 62 | "index": 1, 63 | "id": ANY 64 | } 65 | ] 66 | """ 67 | -------------------------------------------------------------------------------- /spec/test/features/control/mtls.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | Feature: Communications with the control interface are secured with mTLS. 18 | Trust is anchored via certificate pinning. 19 | The Kapow! server only allows connections from trusted clients. 20 | The Kapow! clients only establish connections to trusted servers. 21 | 22 | @server 23 | Scenario: Reject clients not providing a certificate. 24 | 25 | Given I have a running Kapow! server 26 | When I try to connect to the control API without providing a certificate 27 | Then I get a connection error 28 | 29 | @server 30 | Scenario: Reject clients providing an invalid certificate. 31 | 32 | Given I have a running Kapow! server 33 | When I try to connect to the control API providing an invalid certificate 34 | Then I get a connection error 35 | 36 | @client 37 | Scenario: Connect to servers providing a valid certificate. 38 | A valid certificate is the one provided via envvars. 39 | 40 | Given a test HTTPS server on the control port 41 | When I run the following command 42 | """ 43 | $ kapow route list 44 | """ 45 | And the HTTPS server receives a "GET" request to "/routes" 46 | And the server responds with 47 | | field | value | 48 | | status | 200 | 49 | | headers.Content-Type | application/json | 50 | | body | [] | 51 | Then the command exits with "0" 52 | 53 | @client 54 | Scenario: Reject servers providing an invalid certificate. 55 | 56 | Given a test HTTPS server on the control port 57 | When I run the following command (with invalid certs) 58 | """ 59 | $ kapow route list 60 | """ 61 | Then the command exits immediately with "1" 62 | 63 | @server 64 | Scenario Outline: The control server is accessible through an alternative address 65 | The automatically generated certificated contains the Alternate Name 66 | provided via the `--control-reachable-addr` parameter. 67 | 68 | Given I launch the server with the following extra arguments 69 | """ 70 | --control-reachable-addr "" 71 | """ 72 | When I inspect the automatically generated control server certificate 73 | Then the extension "Subject Alternative Name" contains "" of type "" 74 | 75 | Examples: 76 | | reachable_addr | value | type | 77 | | localhost:8081 | localhost | DNSName | 78 | | 127.0.0.1:8081 | 127.0.0.1 | IPAddress | 79 | | foo.bar:8081 | foo.bar | DNSName | 80 | | 4.2.2.4:8081 | 4.2.2.4 | IPAddress | 81 | | [2600::]:8081 | 2600:: | IPAddress | 82 | 83 | 84 | @e2e 85 | Scenario: Control server dialog using mTLS 86 | If the user provides the corresponding certificates to the 87 | `kapow route` subcommand, the communication should be possible. 88 | 89 | Given I have a just started Kapow! server 90 | When I run the following command (setting the control certs environment variables) 91 | """ 92 | $ kapow route list 93 | 94 | """ 95 | Then the command exits with "0" 96 | -------------------------------------------------------------------------------- /spec/test/features/data/handler/error_handleridnotfound.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | Feature: Fail to retrieve resources from nonexistent handler in Kapow! server. 17 | If trying to access a nonexistent handler then the 18 | server responds with a not found error. 19 | 20 | Scenario: Try to get a valid resource path from a nonexistent handler. 21 | A request to retrieve a resource from a 22 | nonexistent handler will trigger 23 | a handler ID not found error. 24 | 25 | Given I have a running Kapow! server 26 | When I get the resource "/request/path" for the handler with id "XXXXXXXXXX" 27 | Then I get 404 as response code 28 | And the response header "Content-Type" contains "application/json" 29 | And I get the following response body: 30 | """ 31 | { 32 | "reason": "Handler ID Not Found" 33 | } 34 | """ 35 | -------------------------------------------------------------------------------- /spec/test/features/data/handler/error_invalidresource.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | Feature: Fail to retrieve an invalid resource for a handler in Kapow! server. 17 | If trying to access an invalid resource for a handler 18 | then the server responds with an error. 19 | 20 | Scenario: Try to get an inexistent resource from a handler. 21 | A request to retrieve an invalid resource 22 | from a handler will trigger a invalid resource error. 23 | 24 | Given I have a Kapow! server with the following testing routes: 25 | | method | url_pattern | 26 | | GET | /foo | 27 | When I send a request to the testing route "/foo" 28 | And I get the resource "/invented/path" 29 | Then I get 400 as response code 30 | And the response header "Content-Type" contains "application/json" 31 | And I get the following response body: 32 | """ 33 | { 34 | "reason": "Invalid Resource Path" 35 | } 36 | """ 37 | -------------------------------------------------------------------------------- /spec/test/features/data/handler/error_itemnotfound.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | Feature: Fail to retrieve nonexistent resource items in Kapow! server. 17 | If trying to access a nonexistent resource item 18 | then the server responds with a no content error. 19 | 20 | Scenario: Try to get a nonexistent resource item from a handler. 21 | A request to retrieve a nonexistent resource 22 | item from a handler will trigger a no content 23 | error. 24 | 25 | Given I have a Kapow! server with the following testing routes: 26 | | method | url_pattern | 27 | | GET | /foo | 28 | When I send a request to the testing route "/foo" 29 | And I get the resource "/request/params/meloinvento" 30 | Then I get 404 as response code 31 | And the response header "Content-Type" contains "application/json" 32 | And I get the following response body: 33 | """ 34 | { 35 | "reason": "Resource Item Not Found" 36 | } 37 | """ 38 | -------------------------------------------------------------------------------- /spec/test/features/data/handler/success.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | Feature: Retrieve a resource from a handler in Kapow! server. 17 | Users can retrieve request handler resources 18 | from the server by specifying the handler id 19 | and the resource path. 20 | 21 | Scenario: Retrieve a resource. 22 | Get the "request/path" resource for the current 23 | request through the handler id. 24 | 25 | Given I have a Kapow! server with the following testing routes: 26 | | method | url_pattern | 27 | | GET | /foo | 28 | When I send a request to the testing route "/foo" 29 | And I get the resource "/request/path" 30 | Then I get 200 as response code 31 | And I get "OK" as response reason phrase 32 | And I get the following response raw body: 33 | """ 34 | /foo 35 | """ 36 | 37 | Scenario: Retrieve a resource item. 38 | Get the "request/params/name" item resource for 39 | the current request through the handler id. 40 | 41 | Given I have a Kapow! server with the following testing routes: 42 | | method | url_pattern | 43 | | GET | /foo | 44 | When I send a request to the testing route "/foo?name=bar" 45 | And I get the resource "/request/params/name" 46 | Then I get 200 as response code 47 | And I get "OK" as response reason phrase 48 | And I get the following response raw body: 49 | """ 50 | bar 51 | """ 52 | -------------------------------------------------------------------------------- /spec/test/features/data/request/success.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | Feature: Retrieve request resources from a handler in Kapow! server. 17 | Users can retrieve request resources by 18 | specifying the handler id and the 19 | resource path. 20 | 21 | Scenario Outline: Retrieve different resources for the current request. 22 | Get the following resources for the 23 | current request through the current 24 | handler. 25 | 26 | Given I have a Kapow! server with the following testing routes: 27 | | method | url_pattern | 28 | | GET | /foo/{path} | 29 | When I send a request to the testing route "/foo/matchVal1" adding: 30 | | fieldType | name | value | 31 | | parameter | par1 | paramVal1 | 32 | | header | head1 | headVal1 | 33 | | cookie | cook1 | cookieVal1 | 34 | | body | | bodyVal1 | 35 | And I get the resource "" 36 | Then I get 200 as response code 37 | And I get "OK" as response reason phrase 38 | And I get the following response raw body: 39 | """ 40 | 41 | """ 42 | 43 | Examples: 44 | | resourcePath | value | 45 | | /request/method | GET | 46 | | /request/path | /foo/matchVal1 | 47 | | /request/host | localhost:8080 | 48 | | /request/matches/path | matchVal1 | 49 | | /request/params/par1 | paramVal1 | 50 | | /request/headers/head1 | headVal1 | 51 | | /request/cookies/cook1 | cookieVal1 | 52 | | /request/body | bodyVal1 | 53 | -------------------------------------------------------------------------------- /spec/test/features/data/response/success.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | Feature: Setting values for handler response resources in Kapow! server. 17 | Users can set the values in the response 18 | resources by specifying the handler id 19 | and the resource path. 20 | 21 | Scenario: Set status code for the current response. 22 | Set the status code through the current 23 | handler. 24 | 25 | Given I have a Kapow! server with the following testing routes: 26 | | method | url_pattern | 27 | | GET | /foo | 28 | When I send a request to the testing route "/foo" 29 | And I set the resource "/response/status" with value "418" 30 | And I release the testing request 31 | Then I get 418 as response code in the testing request 32 | 33 | Scenario Outline: Set different resources for the current response. 34 | Set the following resources for the current 35 | response through the current handler. 36 | 37 | Given I have a Kapow! server with the following testing routes: 38 | | method | url_pattern | 39 | | GET | /foo | 40 | When I send a request to the testing route "/foo" 41 | And I set the resource "" with value "" 42 | And I release the testing request 43 | Then I get 200 as response code 44 | And I get "OK" as response reason phrase 45 | And I get the value "" for the response "" named "" in the testing request 46 | 47 | Examples: 48 | | resourcePath | value | fieldType | elementName | 49 | | /response/headers/head1 | headVal1 | header | head1 | 50 | | /response/cookies/cook1 | cookVal1 | cookie | cook1 | 51 | | /response/body | bodyValue1 | body | - | 52 | | /response/stream | bodyValue2 | body | - | 53 | 54 | Scenario: Overwrite a resource for the current response. 55 | Write twice a on a resource, such as a gzip middleware would require: 56 | kapow get /response/body | gzip -c | kapow set /response/body 57 | although for simplicity, we'll just try overwriting the status code. 58 | 59 | Given I have a Kapow! server with the following testing routes: 60 | | method | url_pattern | 61 | | GET | /foo | 62 | When I send a request to the testing route "/foo" 63 | And I set the resource "/response/status" with value "418" 64 | And I set the resource "/response/status" with value "200" 65 | And I release the testing request 66 | Then I get 200 as response code in the testing request 67 | -------------------------------------------------------------------------------- /spec/test/features/environment.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | import tempfile 17 | import os 18 | import signal 19 | from contextlib import suppress 20 | 21 | def tmpfifo(): 22 | while True: 23 | fifo_path = tempfile.mktemp() # The usage mkfifo make this safe 24 | try: 25 | os.mkfifo(fifo_path) 26 | except OSError: 27 | # The file already exist 28 | pass 29 | else: 30 | break 31 | 32 | return fifo_path 33 | 34 | 35 | def before_scenario(context, scenario): 36 | context.handler_fifo_path = tmpfifo() 37 | context.init_script_fifo_path = tmpfifo() 38 | 39 | 40 | def after_scenario(context, scenario): 41 | # Real Kapow! server being tested 42 | if hasattr(context, 'server'): 43 | context.server.terminate() 44 | context.server.wait() 45 | 46 | os.unlink(context.handler_fifo_path) 47 | os.unlink(context.init_script_fifo_path) 48 | 49 | # Mock HTTP server for testing 50 | if hasattr(context, 'httpserver'): 51 | context.response_ready.set() 52 | context.httpserver.shutdown() 53 | context.httpserver_thread.join() 54 | 55 | if getattr(context, 'testing_handler_pid', None) is not None: 56 | with suppress(ProcessLookupError): 57 | os.kill(int(context.testing_handler_pid), signal.SIGTERM) 58 | -------------------------------------------------------------------------------- /spec/test/features/steps/comparedict.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | from functools import singledispatch 17 | from itertools import zip_longest 18 | from jsonexample import ANY 19 | 20 | 21 | def assert_same_type(f): 22 | def wrapper(a, b): 23 | if type(a) != type(b): 24 | raise TypeError(f"Non-matching types {a!r} != {b!r}") 25 | return f(a, b) 26 | return wrapper 27 | 28 | 29 | @singledispatch 30 | @assert_same_type 31 | def is_subset(model, obj): 32 | if model == obj: 33 | return True 34 | else: 35 | raise ValueError(f"Non-matching values {model!r} != {obj!r}") 36 | 37 | 38 | @is_subset.register(dict) 39 | @assert_same_type 40 | def _(model, obj): 41 | for key, value in model.items(): 42 | if key not in obj or not is_subset(value, obj[key]): 43 | raise ValueError(f"Non-matching dicts {model!r} != {obj!r}") 44 | return True 45 | 46 | 47 | @is_subset.register(list) 48 | @assert_same_type 49 | def _(model, obj): 50 | for a, b in zip_longest(model, obj): 51 | if not is_subset(a, b): 52 | raise ValueError(f"Non-matching list member {a!r} in {b!r}") 53 | return True 54 | 55 | 56 | @is_subset.register(ANY) 57 | def _(model, obj): 58 | return True 59 | -------------------------------------------------------------------------------- /spec/test/features/steps/get_environment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import os 5 | import sys 6 | 7 | if __name__ == '__main__': 8 | with open(os.environ['SPECTEST_FIFO'], 'w') as fifo: 9 | json.dump(dict(os.environ), fifo) 10 | -------------------------------------------------------------------------------- /spec/test/features/steps/jsonexample.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | import json 17 | from functools import partial 18 | import re 19 | 20 | 21 | class ANY: 22 | pass 23 | 24 | 25 | class ExampleDecoder(json.JSONDecoder): 26 | def __init__(self, *args, **kwargs): 27 | super().__init__(*args, object_hook=self.object_hook, **kwargs) 28 | 29 | def decode(self, s, *args, **kwargs): 30 | s = re.sub(r'(\W)(ANY)(\W)', r'\1{"_type": "ANY"}\3', s) 31 | return super().decode(s, *args, **kwargs) 32 | 33 | def object_hook(self, dct): 34 | if dct.get('_type', None) == 'ANY': 35 | return ANY() 36 | else: 37 | return dct 38 | 39 | loads = partial(json.loads, cls=ExampleDecoder) 40 | -------------------------------------------------------------------------------- /spec/test/features/steps/testinghandler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import signal 20 | import sys 21 | import os 22 | 23 | 24 | if __name__ == '__main__': 25 | test_runner_fifo = sys.argv[1] 26 | with open(test_runner_fifo, 'w') as other_side: 27 | other_side.write(f"{os.getpid()};{os.environ['KAPOW_HANDLER_ID']}\n") 28 | 29 | signal.pause() 30 | -------------------------------------------------------------------------------- /spec/test/kapow/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kapow-spec-test 2 | ADD https://github.com/BBVA/kapow/releases/download/v0.5.4/kapow_0.5.4_linux_amd64 /usr/bin/kapow 3 | RUN chmod 755 /usr/bin/kapow 4 | -------------------------------------------------------------------------------- /spec/test/node-dependencies.nix: -------------------------------------------------------------------------------- 1 | # This file has been generated by node2nix 1.8.0. Do not edit! 2 | 3 | {pkgs ? import { 4 | inherit system; 5 | }, system ? builtins.currentSystem, nodejs ? pkgs."nodejs-12_x"}: 6 | 7 | let 8 | nodeEnv = import ./node-env.nix { 9 | inherit (pkgs) stdenv python2 utillinux runCommand writeTextFile; 10 | inherit nodejs; 11 | libtool = if pkgs.stdenv.isDarwin then pkgs.darwin.cctools else null; 12 | }; 13 | in 14 | import ./node-packages.nix { 15 | inherit (pkgs) fetchurl fetchgit; 16 | inherit nodeEnv; 17 | } -------------------------------------------------------------------------------- /spec/test/node-packages.json: -------------------------------------------------------------------------------- 1 | [ 2 | "gherkin-lint" 3 | ] 4 | -------------------------------------------------------------------------------- /spec/test/shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import (builtins.fetchTarball { 2 | name = "nixos-20.09-2021-01-15"; 3 | url = "https://github.com/nixos/nixpkgs/archive/cd63096d6d887d689543a0b97743d28995bc9bc3.tar.gz"; 4 | sha256 = "1wg61h4gndm3vcprdcg7rc4s1v3jkm5xd7lw8r2f67w502y94gcy"; 5 | }) {} }: 6 | let 7 | environconfig = pkgs.python38Packages.buildPythonPackage rec { 8 | pname = "environconfig"; 9 | version = "1.7.0"; 10 | 11 | src = pkgs.python38Packages.fetchPypi { 12 | inherit pname version; 13 | sha256 = "087amqnqsx7d816adszd1424kma1kx9lfnzffr140wvy7a50vi86"; 14 | }; 15 | meta = { 16 | homepage = "https://github.com/buguroo/environconfig"; 17 | description = "Environment variables made easy"; 18 | }; 19 | }; 20 | 21 | pythonDependencies = [ 22 | pkgs.python38Packages.behave 23 | pkgs.python38Packages.requests 24 | environconfig 25 | ]; 26 | 27 | nodeDependencies = (pkgs.callPackage ./node-dependencies.nix {}); 28 | in 29 | pkgs.mkShell { 30 | buildInputs = [ 31 | pkgs.python38 32 | pythonDependencies 33 | pkgs.gnumake 34 | pkgs.which 35 | nodeDependencies.gherkin-lint 36 | ]; 37 | } 38 | -------------------------------------------------------------------------------- /test/runwindowsrun.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | func main() { 10 | cmd := exec.Command("rundll32.exe", "url.dll,FileProtocolHandler", os.Args[1]) 11 | err := cmd.Start() 12 | if err != nil { 13 | fmt.Println(err) 14 | } 15 | err = cmd.Wait() 16 | if err != nil { 17 | fmt.Println(err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testutils/jaillover/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // An Output represents the execution context, 12 | // meaning the command line and the environment 13 | type Output struct { 14 | Cmdline []string `json:"cmdline"` 15 | Env map[string]string `json:"env"` 16 | } 17 | 18 | func getEnvMap() map[string]string { 19 | env := make(map[string]string) 20 | for _, e := range os.Environ() { 21 | s := strings.SplitN(e, "=", 2) 22 | env[s[0]] = s[1] 23 | } 24 | return env 25 | } 26 | 27 | func main() { 28 | o := Output{ 29 | Cmdline: os.Args, 30 | Env: getEnvMap(), 31 | } 32 | res, err := json.Marshal(o) 33 | if err != nil { 34 | log.Fatalf("JSON marshal failed %+v", err) 35 | } 36 | fmt.Println(string(res)) 37 | if len(os.Args) > 1 && os.Args[1] == "--miserably-fail" { 38 | fmt.Fprintln(os.Stderr, "jailover miserably failed") 39 | os.Exit(1) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /testutils/poc/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | *.egg-info 3 | dist 4 | -------------------------------------------------------------------------------- /testutils/poc/README.rst: -------------------------------------------------------------------------------- 1 | Kapow! PoC 2 | ========== 3 | 4 | This directory contains a somewhat working Kapow! implementation written 5 | in Python. 6 | 7 | The purpose of this implementation is to be an experimentation playfield 8 | for the developers, allowing us to discuss and test new features 9 | quickly. 10 | 11 | Try the software within this directory with caution, anything can break 12 | at any moment. 13 | 14 | 15 | .. code-block:: none 16 | 17 | _,-._ 18 | ; ___ : ,------------------------------. 19 | ,--' (. .) '--.__ | | 20 | _; ||| \ | Arrr!! Ye be warned! | 21 | '._,-----''';=.____," | | 22 | /// < o> |##| | | 23 | (o \`--' //`-----------------------------' 24 | ///\ >>>> _\ <<<< // 25 | --._>>>>>>>><<<<<<<< / 26 | ___() >>>[||||]<<<< 27 | `--'>>>>>>>><<<<<<< 28 | >>>>>>><<<<<< 29 | >>>>><<<<< 30 | >>ctr<< 31 | 32 | 33 | Running the Spec Test Suite 34 | --------------------------- 35 | 36 | Sitting in this directory run ``make`` to run the Kapow! Python PoC 37 | against the Spec Test Suite. 38 | -------------------------------------------------------------------------------- /testutils/poc/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.9.4 2 | requests==2.31.0 3 | click==7.0 4 | -------------------------------------------------------------------------------- /tools/validsslclient: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | user=$(kapow get /ssl/client/i/dn) 6 | 7 | while read -r dn 8 | do 9 | [ -z "$dn" ] && continue 10 | if [ "$user" = "$dn" ]; then 11 | kapow set /server/log/validsslclient "Found valid user: '$user'" 12 | exit 0 13 | fi 14 | done 15 | 16 | kapow set /response/status 403 # Forbidden 17 | kapow set /server/log/validsslclient "Invalid user: '$user'" 18 | exit 127 19 | --------------------------------------------------------------------------------