Merge remote-tracking branch 'origin/master' into feature-new-doc

This commit is contained in:
Roberto Abdelkader Martínez Pérez
2019-11-19 13:02:48 +01:00
87 changed files with 487 additions and 801 deletions
+4 -2
View File
@@ -1,6 +1,10 @@
Welcome to *Kapow!* Welcome to *Kapow!*
=================== ===================
.. image:: https://goreportcard.com/badge/github.com/bbva/kapow
:target: https://goreportcard.com/report/github.com/bbva/kapow
With *Kapow!* you can publish simple **shell scripts** as **HTTP services** easily. With *Kapow!* you can publish simple **shell scripts** as **HTTP services** easily.
*Kapow!* with an example *Kapow!* with an example
@@ -38,5 +42,3 @@ This is the only line you'll need:
Mention license and contributing Mention license and contributing
-226
View File
@@ -1,226 +0,0 @@
# Installing Kapow!
Kapow! has a reference implementation in Go that is under active development right
now. If you want to start using Kapow! you can:
* Download a binary (linux, at this moment) from our
[releases](https://github.com/BBVA/kapow/releases) section
* Install the package with the `get` command (you need the Go runtime installed
and [configured](https://golang.org/cmd/go/))
```sh
go get -u github.com/BBVA/kapow
```
# Examples
Below are some examples on how to define and invoke routes in Kapow!
As you will see `kapow` binary is both a server and a CLI that you can use to configure
a running server. The server exposes an [API](/spec#http-control-api) that you
can use directly if you want.
In order to get information from the request that fired the script execution and
to help you compose the response, the server exposes
some [resources](/spec#handlers) to interact with from the script.
## The mandatory Hello World (for WWW fans)
First, you create a pow file named `greet.pow` with the following contents:
```sh
kapow route add /greet -c 'name=$(kapow get /request/params/name); echo Hello ${name:-World} | kapow set /response/body'
```
note that you have to escape it as the command will run on a shell itself. Then, you
execute:
```sh
kapow server greet.pow
```
to start a Kapow! server exposing your service. Now you can check that it works
as intended with good ole `curl`:
```sh
curl localhost:8080/greet
Hello World
curl localhost:8080/greet?name=friend
Hello friend
```
If you want to work with JSON you can use this version of the pow
`greet-json.pow`
```sh
kapow route add -X POST /greet -c 'who=$(kapow get /request/body | jq -r .name); kapow set /response/status 201; jq --arg value "${who:-World}" -n \{name:\$value\} | kapow set /response/body'
```
that uses [jq](https://stedolan.github.io/jq/) to allow you to work with JSON
from the command line. Check that it works with
```sh
curl -X POST -H 'Content-Type: application/json' -d '{"name": "friend"}' localhost:8080/greet
{"name": "friend" }
curl -X POST -H 'Content-Type: application/json' -d '' localhost:8080/greet
{"name": "World"}
```
## The mandatory Echo (for UNIX fans)
First, you create a pow file named `echo.pow` with the following contents:
```sh
kapow route add -X POST /echo -c 'kapow get /request/body | kapow set /response/body'
```
then, you execute:
```sh
kapow server echo.pow
```
and you can check that it works as intended with good ole `curl`:
```sh
curl -X POST -d '1,2,3... testing' localhost:8080/echo
1, 2, 3, 4, 5, 6, 7, 8, 9, testing
```
If you send a big file and want to see the content back as a real-time stream
you can use this version `echo-stream.pow`
```sh
kapow route add -X POST /echo -c 'kapow get /request/body | kapow set /response/stream'
```
## The multiline fun
Unless you're a hardcore Perl golfer, you'll probably need to write your stuff
over more than one line in order to avoid the mess we saw on our JSON greet
version.
Don't worry, we need to write several lines, too. Bash, in its magnificent
UNIX® style, provides us with the
[here-documents](https://www.gnu.org/software/bash/manual/bash.html#Here-Documents)
mechanism that we can leverage precisely for this purpose.
Imagine that we want to return both the standard output and a generated file from a
command execution. Let's write a `log-and-stuff.pow` file with the following content:
```sh
kapow route add /log_and_stuff - <<-'EOF'
echo this is a quite long sentence and other stuff | tee log.txt | kapow set /response/body
cat log.txt | kapow set /response/body
EOF
```
then we serve it with `kapow`:
```sh
kapow server log-and-stuff.pow
```
Yup. As simple as that. You can check it.
```sh
curl localhost:8080/log_and_stuff
this is a quite long sentence and other stuff
this is a quite long sentence and other stuff
```
## Interact with other systems
You can leverage all the power of the shell in your scripts and interact with
other systems by using all the available tools. Write a
`log-and-stuff-callback.pow` file with the following content:
```sh
kapow route add /log_and_stuff - <<-'EOF'
callback_url="$(kapow get /request/params/callback)"
echo this is a quite long sentence and other stuff | tee log.txt | kapow set /response/body
echo sending to $callback_url | kapow set /response/body
curl -X POST --data-binary @log.txt $callback_url | kapow set /response/body
EOF
```
serve it with `kapow`:
```sh
kapow server log-and-stuff-callback.pow
```
and finally check it.
```sh
curl localhost:8080/log_and_stuff?callback=nowhere.com
this is a quite long sentence and other stuff
sending to nowhere.com
<html>
<head><title>405 Not Allowed</title></head>
<body>
<center><h1>405 Not Allowed</h1></center>
<hr><center>nginx</center>
</body>
</html>
```
You must be aware that you must have all the dependencies you use in your
scripts installed in the host that will run the Kapow! server.
In addition, a pow file can contain as many routes as you like, so you can start
a server with several routes configured in one shot.
# Sample Docker usage
## Clone the project
```sh
git clone https://github.com/BBVA/kapow.git
```
## Build the kapow! docker image
```sh
make docker
```
Now you have a container image with all the above pow files copied in /tmp so
you can start each example by running
```sh
docker run --rm -p 8080:8080 docker server example.pow
```
## Build a docker image for running the nmap example
```sh
cd /path/to/kapow/poc/examples/nmap; docker build -t kapow-nmap .
```
## Run kapow
```sh
docker run \
-d \
-p 8080:8080 \
kapow-nmap
```
which will output something like this:
```sh
e7da20c7d9a39624b5c56157176764671e5d2d8f1bf306b3ede898d66fe3f4bf
```
## Test /list endpoint
In another terminal, try running:
```sh
curl http://localhost:8080/list/github.com
```
which will respond something like:
```sh
Starting Nmap 7.70 ( https://nmap.org ) at 2019-05-10 14:01 UTC
Nmap scan report for github.com (140.82.118.3)
rDNS record for 140.82.118.3: lb-140-82-118-3-ams.github.com
Nmap done: 1 IP address (0 hosts up) scanned in 0.04 seconds
```
et voilà !
# License
This project is distributed under the [Apache License 2.0](/LICENSE).
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package client package client
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package client package client
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package client package client
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package client package client
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package client package client
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package client package client
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package client package client
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package client package client
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package client package client
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package client_test package client_test
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package cmd package cmd
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package cmd package cmd
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package cmd package cmd
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package cmd package cmd
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package cmd package cmd
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package http package http
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package http package http
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package http package http
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package http package http
import ( import (
+32 -14
View File
@@ -13,26 +13,24 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package control package control
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/BBVA/kapow/internal/server/model" "github.com/BBVA/kapow/internal/server/model"
"github.com/BBVA/kapow/internal/server/srverrors"
"github.com/BBVA/kapow/internal/server/user" "github.com/BBVA/kapow/internal/server/user"
) )
// Run must start the control server in a specific address // configRouter Populates the server mux with all the supported routes. The
func Run(bindAddr string) { // server exposes list, get, delete and add route endpoints.
log.Fatal(http.ListenAndServe(bindAddr, configRouter()))
}
func configRouter() *mux.Router { func configRouter() *mux.Router {
r := mux.NewRouter() r := mux.NewRouter()
r.HandleFunc("/routes/{id}", removeRoute). r.HandleFunc("/routes/{id}", removeRoute).
@@ -46,20 +44,27 @@ func configRouter() *mux.Router {
return r return r
} }
// funcRemove Method used to ask the route model module to delete a route
var funcRemove func(id string) error = user.Routes.Delete var funcRemove func(id string) error = user.Routes.Delete
// removeRoute Handler that removes the requested route. If it doesn't exist,
// returns 404 and an error entity
func removeRoute(res http.ResponseWriter, req *http.Request) { func removeRoute(res http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req) vars := mux.Vars(req)
id := vars["id"] id := vars["id"]
if err := funcRemove(id); err != nil { if err := funcRemove(id); err != nil {
res.WriteHeader(http.StatusNotFound) srverrors.WriteErrorResponse(http.StatusNotFound, "Route Not Found", res)
return return
} }
res.WriteHeader(http.StatusNoContent) res.WriteHeader(http.StatusNoContent)
} }
// funcList Method used to ask the route model module for the list of routes
var funcList func() []model.Route = user.Routes.List var funcList func() []model.Route = user.Routes.List
// listRoutes Handler that retrieves a list of the existing routes. An empty
// list is returned when no routes exist
func listRoutes(res http.ResponseWriter, req *http.Request) { func listRoutes(res http.ResponseWriter, req *http.Request) {
list := funcList() list := funcList()
@@ -69,58 +74,71 @@ func listRoutes(res http.ResponseWriter, req *http.Request) {
_, _ = res.Write(listBytes) _, _ = res.Write(listBytes)
} }
// funcAdd Method used to ask the route model module to append a new route
var funcAdd func(model.Route) model.Route = user.Routes.Append var funcAdd func(model.Route) model.Route = user.Routes.Append
// idGenerator UUID generator for new routes
var idGenerator = uuid.NewUUID var idGenerator = uuid.NewUUID
// pathValidator Validates that a path complies with the gorilla mux
// requirements
var pathValidator func(string) error = func(path string) error { var pathValidator func(string) error = func(path string) error {
return mux.NewRouter().NewRoute().BuildOnly().Path(path).GetError() return mux.NewRouter().NewRoute().BuildOnly().Path(path).GetError()
} }
// addRoute Handler that adds a new route. Makes all parameter validation and
// creates the a new is for the route
func addRoute(res http.ResponseWriter, req *http.Request) { func addRoute(res http.ResponseWriter, req *http.Request) {
var route model.Route var route model.Route
payload, _ := ioutil.ReadAll(req.Body) payload, _ := ioutil.ReadAll(req.Body)
err := json.Unmarshal(payload, &route) err := json.Unmarshal(payload, &route)
if err != nil { if err != nil {
res.WriteHeader(http.StatusBadRequest) srverrors.WriteErrorResponse(http.StatusBadRequest, "Malformed JSON", res)
return return
} }
if route.Method == "" { if route.Method == "" {
res.WriteHeader(http.StatusUnprocessableEntity) srverrors.WriteErrorResponse(http.StatusUnprocessableEntity, "Invalid Route", res)
return return
} }
if route.Pattern == "" { if route.Pattern == "" {
res.WriteHeader(http.StatusUnprocessableEntity) srverrors.WriteErrorResponse(http.StatusUnprocessableEntity, "Invalid Route", res)
return return
} }
err = pathValidator(route.Pattern) err = pathValidator(route.Pattern)
if err != nil { if err != nil {
res.WriteHeader(http.StatusUnprocessableEntity) srverrors.WriteErrorResponse(http.StatusUnprocessableEntity, "Invalid Route", res)
return return
} }
id, err := idGenerator() id, err := idGenerator()
if err != nil { if err != nil {
res.WriteHeader(http.StatusInternalServerError) srverrors.WriteErrorResponse(http.StatusInternalServerError, "Internal Server Error", res)
return return
} }
route.ID = id.String() route.ID = id.String()
created := funcAdd(route) created := funcAdd(route)
createdBytes, _ := json.Marshal(created) createdBytes, _ := json.Marshal(created)
res.WriteHeader(http.StatusCreated)
res.Header().Set("Content-Type", "application/json") res.Header().Set("Content-Type", "application/json")
res.WriteHeader(http.StatusCreated)
_, _ = res.Write(createdBytes) _, _ = res.Write(createdBytes)
} }
// funcGet Method used to ask the route model module for the details of a route
var funcGet func(string) (model.Route, error) = user.Routes.Get var funcGet func(string) (model.Route, error) = user.Routes.Get
// getRoute Handler that retrieves the details of a route. If the route doesn't
// exists returns 404 and an error entity
func getRoute(res http.ResponseWriter, req *http.Request) { func getRoute(res http.ResponseWriter, req *http.Request) {
id := mux.Vars(req)["id"] id := mux.Vars(req)["id"]
if r, err := funcGet(id); err != nil { if r, err := funcGet(id); err != nil {
res.WriteHeader(http.StatusNotFound) srverrors.WriteErrorResponse(http.StatusNotFound, "Route Not Found", res)
} else { } else {
res.Header().Set("Content-Type", "application/json") res.Header().Set("Content-Type", "application/json")
rBytes, _ := json.Marshal(r) rBytes, _ := json.Marshal(r)
+54 -34
View File
@@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package control package control
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@@ -29,9 +31,33 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/BBVA/kapow/internal/server/model" "github.com/BBVA/kapow/internal/server/model"
"github.com/BBVA/kapow/internal/server/srverrors"
"github.com/BBVA/kapow/internal/server/user" "github.com/BBVA/kapow/internal/server/user"
) )
func checkErrorResponse(r *http.Response, expectedErrcode int, expectedReason string) []error {
errList := make([]error, 0)
if r.StatusCode != expectedErrcode {
errList = append(errList, fmt.Errorf("HTTP status mismatch. Expected: %d, got: %d", expectedErrcode, r.StatusCode))
}
if v := r.Header.Get("Content-Type"); v != "application/json; charset=utf-8" {
errList = append(errList, fmt.Errorf("Content-Type header mismatch. Expected: %q, got: %q", "application/json; charset=utf-8", v))
}
errMsg := srverrors.ServerErrMessage{}
if bodyBytes, err := ioutil.ReadAll(r.Body); err != nil {
errList = append(errList, fmt.Errorf("Unexpected error reading response body: %v", err))
} else if err := json.Unmarshal(bodyBytes, &errMsg); err != nil {
errList = append(errList, fmt.Errorf("Response body contains invalid JSON entity: %v", err))
} else if errMsg.Reason != expectedReason {
errList = append(errList, fmt.Errorf("Unexpected reason in response. Expected: %q, got: %q", expectedReason, errMsg.Reason))
}
return errList
}
func TestConfigRouterHasRoutesWellConfigured(t *testing.T) { func TestConfigRouterHasRoutesWellConfigured(t *testing.T) {
testCases := []struct { testCases := []struct {
pattern, method string pattern, method string
@@ -100,17 +126,15 @@ func TestAddRouteReturnsBadRequestWhenMalformedJSONBody(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload)) req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload))
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
handler := http.HandlerFunc(addRoute)
handler.ServeHTTP(resp, req) addRoute(resp, req)
if resp.Code != http.StatusBadRequest {
t.Errorf("HTTP status mismatch. Expected: %d, got: %d", http.StatusBadRequest, resp.Code) for _, e := range checkErrorResponse(resp.Result(), http.StatusBadRequest, "Malformed JSON") {
t.Error(e)
} }
} }
func TestAddRouteReturns422ErrorWhenMandatoryFieldsMissing(t *testing.T) { func TestAddRouteReturns422ErrorWhenMandatoryFieldsMissing(t *testing.T) {
handler := http.HandlerFunc(addRoute)
tc := []struct { tc := []struct {
payload, testCase string payload, testCase string
testMustFail bool testMustFail bool
@@ -166,18 +190,19 @@ func TestAddRouteReturns422ErrorWhenMandatoryFieldsMissing(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(test.payload)) req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(test.payload))
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
handler.ServeHTTP(resp, req) addRoute(resp, req)
r := resp.Result()
if test.testMustFail { if test.testMustFail {
if resp.Code != http.StatusUnprocessableEntity { for _, e := range checkErrorResponse(r, http.StatusUnprocessableEntity, "Invalid Route") {
t.Errorf("HTTP status mismatch in case %s. Expected: %d, got: %d", test.testCase, http.StatusUnprocessableEntity, resp.Code) t.Error(e)
} }
} else if !test.testMustFail { } else if !test.testMustFail {
if resp.Code != http.StatusCreated { if r.StatusCode != http.StatusCreated {
t.Errorf("HTTP status mismatch in case %s. Expected: %d, got: %d", test.testCase, http.StatusUnprocessableEntity, resp.Code) t.Errorf("HTTP status mismatch in case %s. Expected: %d, got: %d", test.testCase, http.StatusUnprocessableEntity, r.StatusCode)
} }
if ct := resp.Header().Get("Content-Type"); ct != "application/json" { if ct := r.Header.Get("Content-Type"); ct != "application/json" {
t.Errorf("Incorrect content type in response. Expected: application/json, got: %s", ct) t.Errorf("Incorrect content type in response. Expected: application/json, got: %q", ct)
} }
} }
} }
@@ -192,7 +217,6 @@ func TestAddRouteGeneratesRouteID(t *testing.T) {
}` }`
req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload)) req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload))
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
handler := http.HandlerFunc(addRoute)
var genID string var genID string
funcAdd = func(input model.Route) model.Route { funcAdd = func(input model.Route) model.Route {
genID = input.ID genID = input.ID
@@ -203,7 +227,7 @@ func TestAddRouteGeneratesRouteID(t *testing.T) {
defer func() { pathValidator = origPathValidator }() defer func() { pathValidator = origPathValidator }()
pathValidator = func(path string) error { return nil } pathValidator = func(path string) error { return nil }
handler.ServeHTTP(resp, req) addRoute(resp, req)
if _, err := uuid.Parse(genID); err != nil { if _, err := uuid.Parse(genID); err != nil {
t.Error("ID not generated properly") t.Error("ID not generated properly")
@@ -219,7 +243,6 @@ func TestAddRoute500sWhenIDGeneratorFails(t *testing.T) {
}` }`
req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload)) req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload))
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
handler := http.HandlerFunc(addRoute)
origPathValidator := pathValidator origPathValidator := pathValidator
defer func() { pathValidator = origPathValidator }() defer func() { pathValidator = origPathValidator }()
@@ -229,14 +252,13 @@ func TestAddRoute500sWhenIDGeneratorFails(t *testing.T) {
defer func() { idGenerator = idGenOrig }() defer func() { idGenerator = idGenOrig }()
idGenerator = func() (uuid.UUID, error) { idGenerator = func() (uuid.UUID, error) {
var uuid uuid.UUID var uuid uuid.UUID
return uuid, errors.New( return uuid, errors.New("End of Time reached; Try again before, or in the next Big Bang cycle")
"End of Time reached; Try again before, or in the next Big Bang cycle")
} }
handler.ServeHTTP(resp, req) addRoute(resp, req)
if resp.Result().StatusCode != http.StatusInternalServerError { for _, e := range checkErrorResponse(resp.Result(), http.StatusInternalServerError, "Internal Server Error") {
t.Errorf("HTTP status mismatch. Expected: %d, got: %d", http.StatusInternalServerError, resp.Result().StatusCode) t.Error(e)
} }
} }
@@ -250,7 +272,6 @@ func TestAddRouteReturnsCreated(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload)) req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload))
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
handler := http.HandlerFunc(addRoute)
var genID string var genID string
funcAdd = func(input model.Route) model.Route { funcAdd = func(input model.Route) model.Route {
expected := model.Route{ID: input.ID, Method: "GET", Pattern: "/hello", Entrypoint: "/bin/sh -c", Command: "echo Hello World | kapow set /response/body"} expected := model.Route{ID: input.ID, Method: "GET", Pattern: "/hello", Entrypoint: "/bin/sh -c", Command: "echo Hello World | kapow set /response/body"}
@@ -266,7 +287,7 @@ func TestAddRouteReturnsCreated(t *testing.T) {
defer func() { pathValidator = origPathValidator }() defer func() { pathValidator = origPathValidator }()
pathValidator = func(path string) error { return nil } pathValidator = func(path string) error { return nil }
handler.ServeHTTP(resp, req) addRoute(resp, req)
if resp.Code != http.StatusCreated { if resp.Code != http.StatusCreated {
t.Errorf("HTTP status mismatch. Expected: %d, got: %d", http.StatusCreated, resp.Code) t.Errorf("HTTP status mismatch. Expected: %d, got: %d", http.StatusCreated, resp.Code)
@@ -296,15 +317,14 @@ func TestAddRoute422sWhenInvalidRoute(t *testing.T) {
}` }`
req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload)) req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload))
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
handler := http.HandlerFunc(addRoute)
origPathValidator := pathValidator origPathValidator := pathValidator
defer func() { pathValidator = origPathValidator }() defer func() { pathValidator = origPathValidator }()
pathValidator = func(path string) error { return errors.New("Invalid route") } pathValidator = func(path string) error { return errors.New("Invalid route") }
handler.ServeHTTP(resp, req) addRoute(resp, req)
if resp.Code != http.StatusUnprocessableEntity { for _, e := range checkErrorResponse(resp.Result(), http.StatusUnprocessableEntity, "Invalid Route") {
t.Error("Invalid route registered") t.Error(e)
} }
} }
@@ -314,7 +334,6 @@ func TestRemoveRouteReturnsNotFound(t *testing.T) {
handler := mux.NewRouter() handler := mux.NewRouter()
handler.HandleFunc("/routes/{id}", removeRoute). handler.HandleFunc("/routes/{id}", removeRoute).
Methods("DELETE") Methods("DELETE")
funcRemove = func(id string) error { funcRemove = func(id string) error {
if id == "ROUTE_XXXXXXXXXXXXXXXXXX" { if id == "ROUTE_XXXXXXXXXXXXXXXXXX" {
return errors.New(id) return errors.New(id)
@@ -324,8 +343,9 @@ func TestRemoveRouteReturnsNotFound(t *testing.T) {
} }
handler.ServeHTTP(resp, req) handler.ServeHTTP(resp, req)
if resp.Code != http.StatusNotFound {
t.Errorf("HTTP status mismatch. Expected: %d, got: %d", http.StatusNotFound, resp.Code) for _, e := range checkErrorResponse(resp.Result(), http.StatusNotFound, "Route Not Found") {
t.Error(e)
} }
} }
@@ -344,6 +364,7 @@ func TestRemoveRouteReturnsNoContent(t *testing.T) {
} }
handler.ServeHTTP(resp, req) handler.ServeHTTP(resp, req)
if resp.Code != http.StatusNoContent { if resp.Code != http.StatusNoContent {
t.Errorf("HTTP status mismatch. Expected: %d, got: %d", http.StatusNoContent, resp.Code) t.Errorf("HTTP status mismatch. Expected: %d, got: %d", http.StatusNoContent, resp.Code)
} }
@@ -414,13 +435,12 @@ func TestGetRouteReturns404sWhenRouteDoesntExist(t *testing.T) {
handler.ServeHTTP(w, r) handler.ServeHTTP(w, r)
resp := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Route Not Found") {
if resp.StatusCode != http.StatusNotFound { t.Error(e)
t.Errorf("HTTP status mismatch. Expected: %d, got: %d", http.StatusNotFound, resp.StatusCode)
} }
} }
func TestGetRouteSetsCorrctContentType(t *testing.T) { func TestGetRouteSetsCorrectContentType(t *testing.T) {
handler := mux.NewRouter() handler := mux.NewRouter()
handler.HandleFunc("/routes/{id}", getRoute). handler.HandleFunc("/routes/{id}", getRoute).
Methods("GET") Methods("GET")
+27
View File
@@ -0,0 +1,27 @@
/*
* Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package control
import (
"log"
"net/http"
)
// Run Starts the control server listening in bindAddr
func Run(bindAddr string) {
log.Fatal(http.ListenAndServe(bindAddr, configRouter()))
}
+3 -1
View File
@@ -13,12 +13,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package data package data
import ( import (
"net/http" "net/http"
"github.com/BBVA/kapow/internal/server/model" "github.com/BBVA/kapow/internal/server/model"
"github.com/BBVA/kapow/internal/server/srverrors"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@@ -38,7 +40,7 @@ func checkHandler(fn resourceHandler) func(http.ResponseWriter, *http.Request) {
if h, ok := Handlers.Get(handlerID); ok { if h, ok := Handlers.Get(handlerID); ok {
fn(w, r, h) fn(w, r, h)
} else { } else {
w.WriteHeader(http.StatusNotFound) srverrors.WriteErrorResponse(http.StatusNotFound, "Handler ID Not Found", w)
} }
} }
} }
+3 -3
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package data package data
import ( import (
@@ -126,9 +127,8 @@ func TestCheckHandlerReturnsAFunctionsThat404sWhenHandlerDoesNotExist(t *testing
fn(w, r) fn(w, r)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Handler ID Not Found") {
if res.StatusCode != http.StatusNotFound { t.Error(e)
t.Errorf("Status code mismatch. Expected 404. Got %d", res.StatusCode)
} }
} }
+26 -19
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package data package data
import ( import (
@@ -23,15 +24,22 @@ import (
"strconv" "strconv"
"github.com/BBVA/kapow/internal/server/model" "github.com/BBVA/kapow/internal/server/model"
"github.com/BBVA/kapow/internal/server/srverrors"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
const (
ResourceItemNotFound = "Resource Item Not Found"
NonIntegerValue = "Non Integer Value"
InvalidStatusCode = "Invalid Status Code"
)
func getRequestBody(w http.ResponseWriter, r *http.Request, h *model.Handler) { func getRequestBody(w http.ResponseWriter, r *http.Request, h *model.Handler) {
w.Header().Add("Content-Type", "application/octet-stream") w.Header().Add("Content-Type", "application/octet-stream")
n, err := io.Copy(w, h.Request.Body) n, err := io.Copy(w, h.Request.Body)
if err != nil { if err != nil {
if n == 0 { if n == 0 {
w.WriteHeader(http.StatusInternalServerError) srverrors.WriteErrorResponse(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), w)
} else { } else {
// Only way to abort current connection as of go 1.13 // Only way to abort current connection as of go 1.13
// https://github.com/golang/go/issues/16542 // https://github.com/golang/go/issues/16542
@@ -63,7 +71,7 @@ func getRequestMatches(w http.ResponseWriter, r *http.Request, h *model.Handler)
if value, ok := vars[name]; ok { if value, ok := vars[name]; ok {
_, _ = w.Write([]byte(value)) _, _ = w.Write([]byte(value))
} else { } else {
w.WriteHeader(http.StatusNotFound) srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
} }
} }
@@ -73,7 +81,7 @@ func getRequestParams(w http.ResponseWriter, r *http.Request, h *model.Handler)
if values, ok := h.Request.URL.Query()[name]; ok { if values, ok := h.Request.URL.Query()[name]; ok {
_, _ = w.Write([]byte(values[0])) _, _ = w.Write([]byte(values[0]))
} else { } else {
w.WriteHeader(http.StatusNotFound) srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
} }
} }
@@ -83,7 +91,7 @@ func getRequestHeaders(w http.ResponseWriter, r *http.Request, h *model.Handler)
if values, ok := h.Request.Header[textproto.CanonicalMIMEHeaderKey(name)]; ok { if values, ok := h.Request.Header[textproto.CanonicalMIMEHeaderKey(name)]; ok {
_, _ = w.Write([]byte(values[0])) _, _ = w.Write([]byte(values[0]))
} else { } else {
w.WriteHeader(http.StatusNotFound) srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
} }
} }
@@ -93,7 +101,7 @@ func getRequestCookies(w http.ResponseWriter, r *http.Request, h *model.Handler)
if cookie, err := h.Request.Cookie(name); err == nil { if cookie, err := h.Request.Cookie(name); err == nil {
_, _ = w.Write([]byte(cookie.Value)) _, _ = w.Write([]byte(cookie.Value))
} else { } else {
w.WriteHeader(http.StatusNotFound) srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
} }
} }
@@ -109,11 +117,11 @@ func getRequestForm(w http.ResponseWriter, r *http.Request, h *model.Handler) {
// We tried to exercise this execution path but didn't know how. // We tried to exercise this execution path but didn't know how.
err := h.Request.ParseForm() err := h.Request.ParseForm()
if err != nil { if err != nil {
w.WriteHeader(http.StatusNotFound) srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
} else if values, ok := h.Request.Form[name]; ok { } else if values, ok := h.Request.Form[name]; ok {
_, _ = w.Write([]byte(values[0])) _, _ = w.Write([]byte(values[0]))
} else { } else {
w.WriteHeader(http.StatusNotFound) srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
} }
} }
@@ -124,7 +132,7 @@ func getRequestFileName(w http.ResponseWriter, r *http.Request, h *model.Handler
if err == nil { if err == nil {
_, _ = w.Write([]byte(header.Filename)) _, _ = w.Write([]byte(header.Filename))
} else { } else {
w.WriteHeader(http.StatusNotFound) srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
} }
} }
@@ -135,7 +143,7 @@ func getRequestFileContent(w http.ResponseWriter, r *http.Request, h *model.Hand
if err == nil { if err == nil {
_, _ = io.Copy(w, file) _, _ = io.Copy(w, file)
} else { } else {
w.WriteHeader(http.StatusNotFound) srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
} }
} }
@@ -144,17 +152,16 @@ func getRequestFileContent(w http.ResponseWriter, r *http.Request, h *model.Hand
func setResponseStatus(w http.ResponseWriter, r *http.Request, h *model.Handler) { func setResponseStatus(w http.ResponseWriter, r *http.Request, h *model.Handler) {
sb, err := ioutil.ReadAll(r.Body) sb, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) srverrors.WriteErrorResponse(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), w)
return return
} }
si, err := strconv.Atoi(string(sb)) if si, err := strconv.Atoi(string(sb)); err != nil {
if http.StatusText(si) == "" { srverrors.WriteErrorResponse(http.StatusUnprocessableEntity, NonIntegerValue, w)
w.WriteHeader(http.StatusBadRequest) } else if http.StatusText(si) == "" {
} else if err == nil { srverrors.WriteErrorResponse(http.StatusBadRequest, InvalidStatusCode, w)
h.Writer.WriteHeader(int(si))
} else { } else {
w.WriteHeader(http.StatusBadRequest) h.Writer.WriteHeader(int(si))
} }
} }
@@ -162,7 +169,7 @@ func setResponseHeaders(w http.ResponseWriter, r *http.Request, h *model.Handler
name := mux.Vars(r)["name"] name := mux.Vars(r)["name"]
vb, err := ioutil.ReadAll(r.Body) vb, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) srverrors.WriteErrorResponse(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), w)
return return
} }
@@ -178,7 +185,7 @@ func setResponseCookies(w http.ResponseWriter, r *http.Request, h *model.Handler
name := mux.Vars(r)["name"] name := mux.Vars(r)["name"]
vb, err := ioutil.ReadAll(r.Body) vb, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) srverrors.WriteErrorResponse(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), w)
return return
} }
@@ -191,6 +198,6 @@ func setResponseBody(w http.ResponseWriter, r *http.Request, h *model.Handler) {
if n > 0 { if n > 0 {
panic("Truncated body") panic("Truncated body")
} }
w.WriteHeader(http.StatusInternalServerError) srverrors.WriteErrorResponse(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), w)
} }
} }
+44 -62
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package data package data
import ( import (
@@ -120,9 +121,8 @@ func TestGetRequestBody500sWhenHandlerRequestErrors(t *testing.T) {
getRequestBody(w, r, &h) getRequestBody(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) {
if res.StatusCode != http.StatusInternalServerError { t.Error(e)
t.Error("status not 500")
} }
} }
@@ -369,9 +369,8 @@ func TestGetRequestMatchesReturnsNotFoundWhenMatchDoesntExists(t *testing.T) {
getRequestMatches(w, r, &h) getRequestMatches(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
if res.StatusCode != http.StatusNotFound { t.Error(e)
t.Errorf("Status code mismatch. Expected: 404. Got: %d", res.StatusCode)
} }
} }
@@ -433,9 +432,8 @@ func TestGetRequestParams404sWhenParamDoesntExist(t *testing.T) {
getRequestParams(w, r, &h) getRequestParams(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
if res.StatusCode != http.StatusNotFound { t.Error(e)
t.Errorf("Status code mismatch. Expected: 404. Got: %d", res.StatusCode)
} }
} }
@@ -478,14 +476,15 @@ func TestGetRequestHeadersSetsOctectStreamContentType(t *testing.T) {
Request: httptest.NewRequest("GET", "/", nil), Request: httptest.NewRequest("GET", "/", nil),
Writer: httptest.NewRecorder(), Writer: httptest.NewRecorder(),
} }
h.Request.Header.Set("bar", "BAZ")
r := createMuxRequest("/handlers/HANDLERID/request/headers/{name}", "/handlers/HANDLERID/request/headers/bar", "GET", nil) r := createMuxRequest("/handlers/HANDLERID/request/headers/{name}", "/handlers/HANDLERID/request/headers/bar", "GET", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
getRequestHeaders(w, r, &h) getRequestHeaders(w, r, &h)
res := w.Result() res := w.Result()
if res.Header.Get("Content-Type") != "application/octet-stream" { if v := res.Header.Get("Content-Type"); v != "application/octet-stream" {
t.Error("Content Type mismatch") t.Errorf("Content Type mismatch. Expected: application/octet-stream. Got: %q", v)
} }
} }
@@ -567,9 +566,8 @@ func TestGetRequestHeaders404sWhenHeaderDoesntExist(t *testing.T) {
getRequestHeaders(w, r, &h) getRequestHeaders(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
if res.StatusCode != http.StatusNotFound { t.Error(e)
t.Error("Status code mismatch")
} }
} }
@@ -652,9 +650,8 @@ func TestGetRequestCookies404sIfCookieDoesntExist(t *testing.T) {
getRequestCookies(w, r, &h) getRequestCookies(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
if res.StatusCode != http.StatusNotFound { t.Error(e)
t.Errorf("Status code mismatch. Expected: 404, Got: %d", res.StatusCode)
} }
} }
@@ -754,9 +751,8 @@ func TestGetRequestForm404sWhenFieldDoesntExist(t *testing.T) {
getRequestForm(w, r, &h) getRequestForm(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
if res.StatusCode != http.StatusNotFound { t.Error(e)
t.Errorf("Status code mismatch. Expected: 404, Got: %d", res.StatusCode)
} }
} }
@@ -810,9 +806,8 @@ func TestGetRequestForm404sWhenFormDoesntExist(t *testing.T) {
getRequestForm(w, r, &h) getRequestForm(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
if res.StatusCode != http.StatusNotFound { t.Error(e)
t.Errorf("Status code mismatch. Expected: 404, Got: %d", res.StatusCode)
} }
} }
@@ -885,9 +880,8 @@ func TestGetRequestFileName404sWhenFileDoesntExist(t *testing.T) {
getRequestFileName(w, r, &h) getRequestFileName(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
if res.StatusCode != http.StatusNotFound { t.Error(e)
t.Errorf("Status code mismatch. Expected: 404, Got: %d", res.StatusCode)
} }
} }
@@ -902,9 +896,8 @@ func TestGetRequestFileName404sWhenFormDoesntExist(t *testing.T) {
getRequestFileName(w, r, &h) getRequestFileName(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
if res.StatusCode != http.StatusNotFound { t.Error(e)
t.Errorf("Status code mismatch. Expected: 404, Got: %d", res.StatusCode)
} }
} }
@@ -966,9 +959,8 @@ func TestGetRequestFileContent404sWhenFileDoesntExist(t *testing.T) {
getRequestFileContent(w, r, &h) getRequestFileContent(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
if res.StatusCode != http.StatusNotFound { t.Error(e)
t.Errorf("Status code mismatch. Expected: 404, Got: %d", res.StatusCode)
} }
} }
@@ -983,9 +975,8 @@ func TestGetRequestFileContent404sWhenFormDoesntExist(t *testing.T) {
getRequestFileContent(w, r, &h) getRequestFileContent(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
if res.StatusCode != http.StatusNotFound { t.Error(e)
t.Errorf("Status code mismatch. Expected: 404, Got: %d", res.StatusCode)
} }
} }
@@ -1009,9 +1000,8 @@ func TestGetRequestFileContent500sWhenHandlerRequestErrors(t *testing.T) {
getRequestFileContent(w, r, &h) getRequestFileContent(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) {
if res.StatusCode != http.StatusInternalServerError { t.Error(e)
t.Error("status not 500", res.StatusCode)
} }
} }
@@ -1058,9 +1048,8 @@ func TestSetResponseStatus400sWhenNonparseableStatusCode(t *testing.T) {
setResponseStatus(w, r, &h) setResponseStatus(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusUnprocessableEntity, "Non Integer Value") {
if res.StatusCode != http.StatusBadRequest { t.Error(e)
t.Errorf("Status code mismatch. Expected: 400, Got: %d", res.StatusCode)
} }
} }
@@ -1074,9 +1063,8 @@ func TestSetResponseStatus500sWhenErrorReadingRequest(t *testing.T) {
setResponseStatus(w, r, &h) setResponseStatus(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) {
if res.StatusCode != http.StatusInternalServerError { t.Error(e)
t.Errorf("Status code mismatch. Expected: 500, Got: %d", res.StatusCode)
} }
} }
@@ -1092,9 +1080,8 @@ func TestSetResponseStatus400sWhenStatusCodeNotSupportedByGo(t *testing.T) {
setResponseStatus(w, r, &h) setResponseStatus(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusBadRequest, "Invalid Status Code") {
if res.StatusCode != http.StatusBadRequest { t.Error(e)
t.Errorf("Status code mismatch. Expected: 400, Got: %d", res.StatusCode)
} }
} }
@@ -1162,9 +1149,8 @@ func TestSetResponseHeaders500sWhenErrorReadingRequest(t *testing.T) {
setResponseHeaders(w, r, &h) setResponseHeaders(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) {
if res.StatusCode != http.StatusInternalServerError { t.Error(e)
t.Errorf("Status code mismatch. Expected: 500, Got: %d", res.StatusCode)
} }
} }
@@ -1181,9 +1167,8 @@ func TestSetResponseHeaders400sOnInvalidHeaderKey(t *testing.T) {
setResponseHeaders(w, r, &h) setResponseHeaders(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusBadRequest, "Invalid Header Name") {
if res.StatusCode != http.StatusBadRequest { t.Error(e)
t.Errorf("Status code mismatch. Expected: 400, Got: %d", res.StatusCode)
} }
} }
@@ -1200,9 +1185,8 @@ func TestSetResponseHeaders400sOnInvalidHeaderValue(t *testing.T) {
setResponseHeaders(w, r, &h) setResponseHeaders(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusBadRequest, "Invalid Header Value") {
if res.StatusCode != http.StatusBadRequest { t.Error(e)
t.Errorf("Status code mismatch. Expected: 400, Got: %d", res.StatusCode)
} }
} }
@@ -1250,9 +1234,8 @@ func TestSetResponseCookies500sWhenErrorReadingRequest(t *testing.T) {
setResponseCookies(w, r, &h) setResponseCookies(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) {
if res.StatusCode != http.StatusInternalServerError { t.Error(e)
t.Errorf("Status code mismatch. Expected: 500, Got: %d", res.StatusCode)
} }
} }
@@ -1322,9 +1305,8 @@ func TestSetResponseBody500sWhenReaderFailsInFirstRead(t *testing.T) {
setResponseBody(w, r, &h) setResponseBody(w, r, &h)
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) {
if res.StatusCode != http.StatusInternalServerError { t.Error(e)
t.Errorf("Status code mismatch. Expected: 500, Got: %d", res.StatusCode)
} }
} }
+5 -1
View File
@@ -13,12 +13,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package data package data
import ( import (
"log" "log"
"net/http" "net/http"
"github.com/BBVA/kapow/internal/server/srverrors"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@@ -35,7 +37,9 @@ func configRouter(rs []routeSpec) (r *mux.Router) {
} }
r.HandleFunc( r.HandleFunc(
"/handlers/{handlerID}/{resource:.*}", "/handlers/{handlerID}/{resource:.*}",
func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) }) func(w http.ResponseWriter, r *http.Request) {
srverrors.WriteErrorResponse(http.StatusBadRequest, "Invalid Resource Path", w)
})
return r return r
} }
+30 -3
View File
@@ -13,16 +13,44 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package data package data
import ( import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/BBVA/kapow/internal/server/model" "github.com/BBVA/kapow/internal/server/model"
"github.com/BBVA/kapow/internal/server/srverrors"
) )
func checkErrorResponse(r *http.Response, expectedErrcode int, expectedReason string) []error {
errList := make([]error, 0)
if r.StatusCode != expectedErrcode {
errList = append(errList, fmt.Errorf("HTTP status mismatch. Expected: %d, got: %d", expectedErrcode, r.StatusCode))
}
if v := r.Header.Get("Content-Type"); v != "application/json; charset=utf-8" {
errList = append(errList, fmt.Errorf("Content-Type header mismatch. Expected: %q, got: %q", "application/json; charset=utf-8", v))
}
errMsg := srverrors.ServerErrMessage{}
if bodyBytes, err := ioutil.ReadAll(r.Body); err != nil {
errList = append(errList, fmt.Errorf("Unexpected error reading response body: %v", err))
} else if err := json.Unmarshal(bodyBytes, &errMsg); err != nil {
errList = append(errList, fmt.Errorf("Response body contains invalid JSON entity: %v", err))
} else if errMsg.Reason != expectedReason {
errList = append(errList, fmt.Errorf("Unexpected reason in response. Expected: %q, got: %q", expectedReason, errMsg.Reason))
}
return errList
}
func TestConfigRouterReturnsRouterWithDecoratedRoutes(t *testing.T) { func TestConfigRouterReturnsRouterWithDecoratedRoutes(t *testing.T) {
var handlerID string var handlerID string
rs := []routeSpec{ rs := []routeSpec{
@@ -49,8 +77,7 @@ func TestConfigRouterReturnsRouterThat400sOnUnconfiguredResources(t *testing.T)
m.ServeHTTP(w, httptest.NewRequest("GET", "/handlers/FOO/dummy", nil)) m.ServeHTTP(w, httptest.NewRequest("GET", "/handlers/FOO/dummy", nil))
res := w.Result() for _, e := range checkErrorResponse(w.Result(), http.StatusBadRequest, "Invalid Resource Path") {
if res.StatusCode != http.StatusBadRequest { t.Error(e)
t.Errorf("Status code mismatch. Expected 400. Got %d", res.StatusCode)
} }
} }
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package data package data
import ( import (
+1
View File
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package data package data
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package model package model
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package model package model
// Route contains the data needed to represent a Kapow! user route. // Route contains the data needed to represent a Kapow! user route.
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package server package server
import ( import (
+19
View File
@@ -0,0 +1,19 @@
package srverrors
import (
"encoding/json"
"net/http"
)
type ServerErrMessage struct {
Reason string `json:"reason"`
}
func WriteErrorResponse(statusCode int, reasonMsg string, res http.ResponseWriter) {
respBody := ServerErrMessage{}
respBody.Reason = reasonMsg
bb, _ := json.Marshal(respBody)
res.Header().Set("Content-Type", "application/json; charset=utf-8")
res.WriteHeader(statusCode)
_, _ = res.Write(bb)
}
+47
View File
@@ -0,0 +1,47 @@
package srverrors_test
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/BBVA/kapow/internal/server/srverrors"
)
func TestWriteErrorResponseSetsAppJsonContentType(t *testing.T) {
w := httptest.NewRecorder()
srverrors.WriteErrorResponse(0, "Not Important Here", w)
if v := w.Result().Header.Get("Content-Type"); v != "application/json; charset=utf-8" {
t.Errorf("Content-Type header mismatch. Expected: %q, got: %q", "application/json; charset=utf-8", v)
}
}
func TestWriteErrorResponseSetsRequestedStatusCode(t *testing.T) {
w := httptest.NewRecorder()
srverrors.WriteErrorResponse(http.StatusGone, "Not Important Here", w)
if v := w.Result().StatusCode; v != http.StatusGone {
t.Errorf("Status code mismatch. Expected: %d, got: %d", http.StatusGone, v)
}
}
func TestWriteErrorResponseSetsBodyCorrectly(t *testing.T) {
expectedReason := "Something Not Found"
w := httptest.NewRecorder()
srverrors.WriteErrorResponse(http.StatusNotFound, expectedReason, w)
errMsg := srverrors.ServerErrMessage{}
if bodyBytes, err := ioutil.ReadAll(w.Result().Body); err != nil {
t.Errorf("Unexpected error reading response body: %v", err)
} else if err := json.Unmarshal(bodyBytes, &errMsg); err != nil {
t.Errorf("Response body contains invalid JSON entity: %v", err)
} else if errMsg.Reason != expectedReason {
t.Errorf("Unexpected reason in response. Expected: %q, got: %q", expectedReason, errMsg.Reason)
}
}
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package mux package mux
import ( import (
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package mux package mux
import ( import (
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package mux package mux
import ( import (
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package mux package mux
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package mux package mux
import ( import (
+1
View File
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package mux package mux
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package user package user
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package spawn package spawn
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package spawn package spawn
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package user package user
import ( import (
+1
View File
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package user package user
import ( import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package main package main
import ( import (
-1
View File
@@ -1 +0,0 @@
go run . route add -c 'echo foo' /
+4 -1
View File
@@ -6,4 +6,7 @@ sync:
pipenv sync --dev pipenv sync --dev
test: sync test: sync
pipenv run make -C ../spec/test KAPOW_DATAAPI_URL=http://localhost:8081 pipenv run make -C ../spec/test
fix:
KAPOW_DATAAPI_URL=http://localhost:8081 pipenv run make -C ../spec/test fix
+15 -10
View File
@@ -178,14 +178,14 @@ async def get_field(request):
try: try:
connection = CONNECTIONS[id] connection = CONNECTIONS[id]
except KeyError: except KeyError:
response = web.Response(status=404, reason="Handler ID Not Found") response = web.json_response(data=error_body("Handler ID Not Found"), status=404, reason="Not Found")
else: else:
try: try:
content = await connection.get(field) content = await connection.get(field)
except ValueError: except ValueError:
return web.Response(status=400, reason="Invalid Resource Path") return web.json_response(data=error_body("Invalid Resource Path"), status=400, reason="Bad Request")
except KeyError: except KeyError:
return web.Response(status=404, reason="Resource Item Not Found") return web.json_response(data=error_body("Resource Item Not Found"), status=404, reason="Not Found")
if isinstance(content, StreamReader): if isinstance(content, StreamReader):
response = web.StreamResponse(status=200, reason="OK") response = web.StreamResponse(status=200, reason="OK")
@@ -210,8 +210,10 @@ async def set_field(request):
try: try:
connection = CONNECTIONS[id] connection = CONNECTIONS[id]
except ValueError:
return web.json_response(data=error_body("Invalid Resource Path"), status=400, reason="Bad Request")
except KeyError: except KeyError:
response = web.Response(status=404, reason="Handler ID Not Found") response = web.json_response(data=error_body("Handler ID Not Found"), status=404, reason="Not Found")
else: else:
try: try:
await connection.set(field, request.content) await connection.set(field, request.content)
@@ -275,6 +277,9 @@ def handle_route(entrypoint, command):
######################################################################## ########################################################################
def error_body(reason):
return {"reason": reason, "foo": "bar"}
def get_routes(app): def get_routes(app):
async def _get_routes(request): async def _get_routes(request):
"""Return the list of registered routes.""" """Return the list of registered routes."""
@@ -302,7 +307,7 @@ def get_route(app):
"entrypoint": r.entrypoint, "entrypoint": r.entrypoint,
"command": r.command}) "command": r.command})
else: else:
return web.Response(status=404, reason="Not Found") return web.json_response(data=error_body("Route Not Found"), status=404, reason="Not Found")
return _get_route return _get_route
@@ -312,7 +317,7 @@ def insert_route(app):
try: try:
content = await request.json() content = await request.json()
except ValueError: except ValueError:
return web.Response(status=400, reason="Malformed JSON") return web.json_response(data=error_body("Malformed JSON"), status=400, reason="Bad Request")
try: try:
index = int(content["index"]) index = int(content["index"])
@@ -330,7 +335,7 @@ def insert_route(app):
+ [route] + [route]
+ app["user_routes"][index:])) + app["user_routes"][index:]))
except (InvalidRouteError, KeyError, AssertionError, ValueError) as exc: except (InvalidRouteError, KeyError, AssertionError, ValueError) as exc:
return web.Response(status=422, reason="Invalid Route") return web.json_response(data=error_body("Invalid Route"), status=422, reason="Unprocessable Entity")
else: else:
app["user_routes"].insert(index, route) app["user_routes"].insert(index, route)
return web.json_response({"id": route.id, return web.json_response({"id": route.id,
@@ -348,7 +353,7 @@ def append_route(app):
try: try:
content = await request.json() content = await request.json()
except ValueError as exc: except ValueError as exc:
return web.Response(status=400, reason="Malformed JSON") return web.json_response(data=error_body("Malformed JSON"), status=400, reason="Bad Request")
try: try:
method = content.get("method", "GET") method = content.get("method", "GET")
@@ -362,7 +367,7 @@ def append_route(app):
handler=handle_route(entrypoint, command)) handler=handle_route(entrypoint, command))
app.change_routes(app["user_routes"] + [route]) app.change_routes(app["user_routes"] + [route])
except (InvalidRouteError, KeyError) as exc: except (InvalidRouteError, KeyError) as exc:
return web.Response(status=422, reason="Invalid Route") return web.json_response(data=error_body("Invalid Route"), status=422, reason="Unprocessable Entity")
else: else:
app["user_routes"].append(route) app["user_routes"].append(route)
return web.json_response({"id": route.id, return web.json_response({"id": route.id,
@@ -381,7 +386,7 @@ def delete_route(app):
id = request.match_info["id"] id = request.match_info["id"]
routes = [r for r in app["user_routes"] if r.id != id] routes = [r for r in app["user_routes"] if r.id != id]
if len(routes) == len(app["user_routes"]): if len(routes) == len(app["user_routes"]):
return web.Response(status=404, reason="Not Found") return web.json_response(data=error_body("Route Not Found"), status=404, reason="Not Found")
else: else:
app.change_routes(routes) app.change_routes(routes)
app["user_routes"] = routes app["user_routes"] = routes
-19
View File
@@ -1,19 +0,0 @@
#!/bin/bash
#
# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
kapow route add -X POST '/eval' -c '$($(kapow get /request/body) | kapow set /response/stream)'
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env sh
curl -X POST --data-binary @- http://localhost:8080/eval <<EOF
touch /tmp/kapow_was_here
EOF
echo 'Proof of success:'
ls -l /tmp/kapow_was_here
-35
View File
@@ -1,35 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Nmap</title>
</head>
<body>
<form id="nmap-params" method="post" action="nmap.xml">
<fieldset>
<legend>Nmap parameters</legend>
<div>
<label for="target_spec">Target Specification:</label>
<input name="target_spec" type="text" placeholder="ip, domain, network, range" value="127.0.0.1" required autofocus>
<p>
Can pass hostnames, IP addresses, networks, etc. e.g.:
scanme.nmap.org, microsoft.com/24, 192.168.0.1;
10.0.0-255.1-254
</p>
</div>
<div>
<label for="port_ranges">Port Ranges:</label>
<input name="port_ranges" type="text" placeholder="port, range, list" value="8080" required>
<p>
Only scan specified ports. e.g.: 22; 1-65535;
U:53,111,137,T:21-25,80,139,8080,S:9
</p>
</div>
<div>
<input name="scan" type="submit" value="Scan">
<input name="reset" type="reset" value="Reset">
</div>
</fieldset>
</form>
</body>
</html>
-55
View File
@@ -1,55 +0,0 @@
#!/bin/bash
#
# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
# Nmap produces an XML report, suitable for rendering in a web browser
#
# Call examples:
#
# $ browser http://localhost:8080
#
# $ curl -v http://localhost:8080/nmap.xml -d 'target_spec=127.0.0.1&port_ranges=9000'
#
kapow route add -X GET / - <<-'EOF'
cat nmap-web.html | kapow set /response/body
EOF
kapow route add -X GET /nmap.xsl - <<-'EOF'
curl --silent https://svn.nmap.org/nmap/docs/nmap.xsl \
| kapow set /response/body
EOF
kapow route add -X POST /nmap.xml - <<-'EOF'
TARGET_SPEC=$(kapow get /request/form/target_spec)
: ${TARGET_SPEC:=127.0.0.1}
PORT_RANGES=$(kapow get /request/form/port_ranges)
: ${PORT_RANGES:=8080}
nmap \
-Pn \
-n \
-p "$PORT_RANGES" \
-oX - \
--stylesheet /nmap.xsl \
"$TARGET_SPEC" \
| kapow set /response/body
EOF
-7
View File
@@ -1,7 +0,0 @@
FROM bbvalabsci/kapow:latest
RUN apk add nmap
COPY nmap.pow /tmp/
CMD ["server", "/tmp/nmap.pow"]
-19
View File
@@ -1,19 +0,0 @@
#!/bin/bash
#
# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
kapow route add -X GET '/list/{ip}' -c 'nmap -sL $(kapow get /request/matches/ip) | kapow set /response/body'
-41
View File
@@ -1,41 +0,0 @@
#!/bin/bash
#
# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
kapow route add /list/files -c 'ls -la $(kapow get /request/params/path) | kapow set /response/body'
kapow route add /list/processes -c 'ps -aux | kapow set /response/body'
kapow route add /show/cpuinfo -c 'kapow set /response/body < /proc/cpuinfo'
kapow route add /show/memory -c 'free -m | kapow set /response/body'
kapow route add /show/disk -c 'df -h | kapow set /response/body'
kapow route add /show/connections -c 'ss -pluton | kapow set /response/body'
kapow route add /show/mounts -c 'mount | kapow set /response/body'
kapow route add /tail/dmesg - <<-'EOF'
kapow set /response/headers/Content-Type text/plain
dmesg -w | kapow set /response/stream
EOF
kapow route add /tail/journal - <<-'EOF'
kapow set /response/headers/Content-Type text/plain
journalctl -f | kapow set /response/stream
EOF
-26
View File
@@ -1,26 +0,0 @@
#!/bin/bash
#
# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
kapow route add -X POST --entrypoint '/bin/zsh -c' '/convert/{from}/{to}' - <<-'EOF'
pandoc --from=$(kapow get /request/matches/from) \
--to=$(kapow get /request/matches/to) \
--output=>(kapow set /response/body) \
=(kapow get /request/body)
EOF
kapow route add -X GET '/formats/input' -c 'pandoc --list-input-formats | kapow set /response/body'
kapow route add -X GET '/formats/output' -c 'pandoc --list-output-formats | grep -v pdf | kapow set /response/body'
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env sh
curl -X POST --data-binary @- http://localhost:8080/convert/markdown/man <<EOF
# This is not a pipe
1. hello
1. goodbye
EOF
-41
View File
@@ -1,41 +0,0 @@
<html>
<head>
<title>PDF Editor</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/siimple/dist/siimple.min.css">
<head>
<body>
<div class="siimple-content siimple-content--extra-large">
<div class="siimple-grid">
<div class="siimple-grid-row">
<div class="siimple-grid-col siimple-grid-col--12">
<div class="siimple-grid-col siimple-grid-col--6">
<div class="siimple-form">
<form action="/editor/pdf" method="post" target="result" id="editor">
<div class="siimple-form-title">AWYSIWYG PDF Editor</div>
<div class="siimple-form-field">
<div class="siimple-form-field-label">InputFormat</div>
<select name="from">
<option value="markdown">Markdown</option>
<option value="rst">ReStructuredText</option>
</select>
</div>
<div class="siimple-form-field">
<div class="siimple-form-field-label">InputFormat</div>
<textarea class="siimple-textarea siimple-textarea--fluid" rows="25" name="content">Example text</textarea>
</div>
<div class="siimple-form-field">
<div class="siimple-btn siimple-btn--blue" onclick="document.getElementById('editor').submit();">Preview!</div>
</div>
</form>
</div>
</div>
<div class="siimple-grid-col siimple-grid-col--6">
<iframe name="result" src="" style="height: 100%; width: 100%;"></iframe>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
-20
View File
@@ -1,20 +0,0 @@
#!/bin/bash
#
# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
kapow route add -X POST --entrypoint ./topdf '/editor/pdf'
kapow route add / -c 'kapow set /response/headers/Content-Type text/html && kapow set /response/body < pdfeditor.html'
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/zsh
#
# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
tmpfile=$(mktemp --suffix=.pdf)
pandoc --from=$(kapow get /request/form/from) --to=pdf --output=${tmpfile} -t latex =(kapow get /request/form/content)
if [ $? -eq 0 ]; then
kapow set /response/headers/Content-Type application/pdf
kapow set /response/body < ${tmpfile}
kapow set /response/status 200
else
kapow set /response/status 500
fi
rm -f ${tmpfile}
-16
View File
@@ -1,16 +0,0 @@
Remote tcpdump sniffer with source filtering
============================================
1. Add any filter you want to the `tcpdump` command inside `tcpdump.pow` to filter
any traffic you don't want to be sniffed!
2. For the sake of simplicity, run `sudo -E kapow server tcpdump.pow`. In a
production environment, `tcpdump` should be run with the appropiate permissions,
but kapow can (and should) run as an unprivileged user.
3. In your local machine run:
```bash
curl http://localhost:8080/sniff/<network-interface> | sudo -E wireshark -k -i -
```
Again, for the sake of simplicity, `Wireshark` is running as root. If you don't want
to run it this way, follow this guide:
https://gist.github.com/MinaMikhailcom/0825906230cbbe478faf4d08abe9d11a
4. Profit!
-1
View File
@@ -1 +0,0 @@
kapow route add /sniff/{iface} -c 'tcpdump -i "$(kapow get /request/matches/iface)" -U -s0 -w - "not port 8080" | kapow set /response/stream'
-43
View File
@@ -1,43 +0,0 @@
#!/bin/bash
#
# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
kapow route add / - <<-'EOF'
kapow set /response/headers/Content-Type text/html
kapow set /response/body <<-HTML
<html>
<body>
<a href='javascript: Array.from(document.querySelectorAll("a")).filter(x => x.href.indexOf("magnet") != -1 ).map(x => x.href = "http://localhost:8080/save/magnet?link="+encodeURI(x.href))'>Add me to your bookmarks!</a>
</body>
</html>
HTML
EOF
kapow route add /save/magnet -e '/bin/bash -c' - <<-'EOF'
link=$(kapow get /request/params/link)
[ -z $link ] && kapow set /response/status 400 && exit 0
watch_folder=/tmp
cd $watch_folder
[[ "$link" =~ xt=urn:btih:([^&/]+) ]] || exit;
echo "d10:magnet-uri${#link}:${link}e" > "meta-${BASH_REMATCH[1]}.torrent"
kapow set /response/status 302
kapow set /response/headers/Location /torrent/list
EOF
kapow route add /torrent/list -c 'kapow set /response/body "Not Implemented Yet"'
+42 -21
View File
@@ -131,14 +131,19 @@ whole lifetime of the server.
conservative in what you do, be liberal in what you accept from others. conservative in what you do, be liberal in what you accept from others.
* We reuse conventions of well-established software projects, such as Docker. * We reuse conventions of well-established software projects, such as Docker.
* All requests and responses will leverage JSON as the data encoding method. * All requests and responses will leverage JSON as the data encoding method.
* The API calls responses have two parts: * The API calls responses have several parts:
* The HTTP status code (e.g., `400`, which is a bad request). The target * The HTTP status code (e.g., `400`, which is a bad request). The target
audience of this information is the client code. The client can thus use audience of this information is the client code. The client can thus use
this information to control the program flow. this information to control the program flow.
* The body is optional * The body is optional depending on the request method and the status code. For
error responses (4xx and 5xx) a json body is included with a reason phrase.
The target audience in this case is the human operating the client. The
human can use this information to make a decision on how to proceed.
* All successful API calls will return a representation of the *final* state * All successful API calls will return a representation of the *final* state
attained by the objects which have been addressed (either requested, set or attained by the objects which have been addressed (either requested, set or
deleted). deleted).
* When several error conditions can happen at the same time, the order of the
checks is implementation-defined.
For instance, given this request: For instance, given this request:
```http ```http
@@ -163,6 +168,15 @@ Content-Length: 189
] ]
``` ```
While an error response may look like this:
```http
404 Not Found
Content-Type: application/json
Content-Length: 25
{"reason": "Not Found"}
```
## API Elements ## API Elements
@@ -242,8 +256,8 @@ A new id is created for the appended route so it can be referenced later.
} }
``` ```
* **Error Responses**: * **Error Responses**:
* **Code**: `400 Bad Request` * **Code**: `400`; **Reason**: `Malformed JSON`
* **Code**: `422 Unprocessable Entity` * **Code**: `422`; **Reason**: `Invalid Route`
* **Sample Call**:<br /> * **Sample Call**:<br />
```sh ```sh
$ curl -X POST --data-binary @- $KAPOW_URL/routes <<EOF $ curl -X POST --data-binary @- $KAPOW_URL/routes <<EOF
@@ -295,8 +309,8 @@ A new id is created for the appended route so it can be referenced later.
} }
``` ```
* **Error Responses**: * **Error Responses**:
* **Code**: `400 Bad Request` * **Code**: `400`; Reason: `Malformed JSON`
* **Code**: `422 Unprocessable Entity` * **Code**: `422`; Reason: `Invalid Route`
* **Sample Call**:<br /> * **Sample Call**:<br />
```sh ```sh
$ curl -X PUT --data-binary @- $KAPOW_URL/routes <<EOF` $ curl -X PUT --data-binary @- $KAPOW_URL/routes <<EOF`
@@ -329,7 +343,7 @@ Removes the route identified by `{id}`.
* **Success Responses**: * **Success Responses**:
* **Code**: `204 No Content` * **Code**: `204 No Content`
* **Error Responses**: * **Error Responses**:
* **Code**: `404 Not Found` * **Code**: `404`; Reason: `Route Not Found`
* **Sample Call**:<br /> * **Sample Call**:<br />
```sh ```sh
$ curl -X DELETE $KAPOW_URL/routes/ROUTE_1f186c92_f906_4506_9788_a1f541b11d0f $ curl -X DELETE $KAPOW_URL/routes/ROUTE_1f186c92_f906_4506_9788_a1f541b11d0f
@@ -357,7 +371,7 @@ Retrieves the information about the route identified by `{id}`.
} }
``` ```
* **Error Responses**: * **Error Responses**:
* **Code**: `404 Not Found` * **Code**: `404`; Reason: `Route Not Found`
* **Sample Call**:<br /> * **Sample Call**:<br />
```sh ```sh
$ curl -X GET $KAPOW_URL/routes/ROUTE_1f186c92_f906_4506_9788_a1f541b11d0f $ curl -X GET $KAPOW_URL/routes/ROUTE_1f186c92_f906_4506_9788_a1f541b11d0f
@@ -381,11 +395,16 @@ response.
* The HTTP status code (e.g., `400`, which is a bad request). The target * The HTTP status code (e.g., `400`, which is a bad request). The target
audience of this information is the client code. The client can thus use audience of this information is the client code. The client can thus use
this information to control the program flow. this information to control the program flow.
* The HTTP body. On read requests, containing the data retrieved as * The HTTP reason phrase. The target audience in this case is the human
'application/octet-stream' mime type. operating the client. The human can use this information to make a
decision on how to proceed.
* Regarding HTTP request and response bodies: * Regarding HTTP request and response bodies:
* The response body will be empty in case of error. * In case of error the response body will be a json entity containing a reason
* It will transport binary data in other case. phrase. The target audience in this case is the human operating the client.
The human can use this information to make a decision on how to proceed.
* It will transport binary data in any other case.
* When several error conditions can happen at the same time, the order of the
checks is implementation-defined.
## API Elements ## API Elements
@@ -510,12 +529,11 @@ path doesn't exist or is invalid.
**Header**: `Content-Type: application/octet-stream`<br /> **Header**: `Content-Type: application/octet-stream`<br />
**Content**: The value of the resource. Note that it may be empty. **Content**: The value of the resource. Note that it may be empty.
* **Error Responses**: * **Error Responses**:
* **Code**: `400 Bad Request`<br /> * **Code**: `400`; Reason: `Invalid Resource Path`<br />
**Notes**: An invalid resource path has been requested. Check the list of **Notes**: Check the list of valid resource paths at the top of this section.
valid resource paths at the top of this section. * **Code**: `404`; Reason: `Handler ID Not Found`<br />
* **Code**: `404 Not Found`<br />
**Notes**: Refers to the handler resource itself. **Notes**: Refers to the handler resource itself.
* **Code**: `404 Not Found`<br /> * **Code**: `404`; Reason: `Resource Item Not Found`<br />
**Notes**: Refers to the named item in the corresponding data API resource. **Notes**: Refers to the named item in the corresponding data API resource.
* **Sample Call**:<br /> * **Sample Call**:<br />
```sh ```sh
@@ -534,10 +552,13 @@ path doesn't exist or is invalid.
* **Success Responses**: * **Success Responses**:
* **Code**: `200 OK` * **Code**: `200 OK`
* **Error Responses**: * **Error Responses**:
* **Code**: `400 Bad Request`<br /> * **Code**: `400`; Reason: `Invalid Resource Path`<br />
**Notes**: An invalid resource path has been requested. Check the list of **Notes**: Check the list of valid resource paths at the top of this section.
valid resource paths at the top of this section. * **Code**: `422`; Reason: `Non Integer Value`<br />
* **Code**: `404 Not Found`<br /> **Notes**: When setting the status code with a non integer value.
* **Code**: `400`; Reason: `Invalid Status Code`<br />
**Notes**: When setting a non-supported status code.
* **Code**: `404`; Reason: `Handler ID Not Found`<br />
**Notes**: Refers to the handler resource itself. **Notes**: Refers to the handler resource itself.
* **Sample Call**:<br /> * **Sample Call**:<br />
```sh ```sh
+1 -1
View File
@@ -11,6 +11,6 @@ wip:
test: lint test: lint
pipenv run behave --no-capture --tags=~@skip pipenv run behave --no-capture --tags=~@skip
fix: lint fix: lint
KAPOW_DEBUG_TESTS=1 pipenv run behave --stop --no-capture KAPOW_DEBUG_TESTS=1 pipenv run behave --stop --no-capture --tags=~@skip
catalog: catalog:
pipenv run behave --format steps.usage --dry-run --no-summary -q pipenv run behave --format steps.usage --dry-run --no-summary -q
@@ -28,4 +28,10 @@ Feature: Kapow! server reject append requests with malformed JSON bodies.
Hi! I am an invalid JSON document. Hi! I am an invalid JSON document.
""" """
Then I get 400 as response code Then I get 400 as response code
# And I get "Malformed JSON" as response reason phrase And the response header "Content-Type" contains "application/json"
And I get the following response body:
"""
{
"reason": "Malformed JSON"
}
"""
@@ -31,7 +31,13 @@ Feature: Kapow! server rejects requests with semantic errors.
} }
""" """
Then I get 422 as response code Then I get 422 as response code
# And I get "Invalid Route" as response reason phrase And the response header "Content-Type" contains "application/json"
And I get the following response body:
"""
{
"reason": "Invalid Route"
}
"""
Scenario: Error because bad route format. Scenario: Error because bad route format.
If a request contains an invalid expression in the If a request contains an invalid expression in the
@@ -48,4 +54,10 @@ Feature: Kapow! server rejects requests with semantic errors.
} }
""" """
Then I get 422 as response code Then I get 422 as response code
# And I get "Invalid Route" as response reason phrase And the response header "Content-Type" contains "application/json"
And I get the following response body:
"""
{
"reason": "Invalid Route"
}
"""
@@ -33,7 +33,7 @@ Feature: Append new routes in Kapow! server.
} }
""" """
Then I get 201 as response code Then I get 201 as response code
# And I get "Created" as response reason phrase And I get "Created" as response reason phrase
And I get the following response body: And I get the following response body:
""" """
{ {
@@ -64,7 +64,7 @@ Feature: Append new routes in Kapow! server.
} }
""" """
Then I get 201 as response code Then I get 201 as response code
# And I get "Created" as response reason phrase And I get "Created" as response reason phrase
And I get the following response body: And I get the following response body:
""" """
{ {
@@ -24,4 +24,10 @@ Feature: Fail to delete a route in Kapow! server.
Given I have a just started Kapow! server Given I have a just started Kapow! server
When I delete the route with id "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx" When I delete the route with id "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx"
Then I get 404 as response code Then I get 404 as response code
# And I get "Not Found" as response reason phrase And the response header "Content-Type" contains "application/json"
And I get the following response body:
"""
{
"reason": "Route Not Found"
}
"""
@@ -26,4 +26,4 @@ Feature: Delete routes in Kapow! server.
| GET | /qux/{dirname} | /bin/sh -c | ls -la /request/params/dirname \| kapow set /response/body | | GET | /qux/{dirname} | /bin/sh -c | ls -la /request/params/dirname \| kapow set /response/body |
When I delete the first route When I delete the first route
Then I get 204 as response code Then I get 204 as response code
# And I get "No Content" as response reason phrase And I get "No Content" as response reason phrase
@@ -24,4 +24,10 @@ Feature: Fail to retrieve route details in Kapow! server.
Given I have a just started Kapow! server Given I have a just started Kapow! server
When I get the route with id "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx" When I get the route with id "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx"
Then I get 404 as response code Then I get 404 as response code
# And I get "Not Found" as response reason phrase And the response header "Content-Type" contains "application/json"
And I get the following response body:
"""
{
"reason": "Route Not Found"
}
"""
@@ -26,7 +26,7 @@ Feature: Retrieve route details in Kapow! server.
| GET | /qux/{dirname} | /bin/sh -c | ls -la /request/params/dirname \| kapow set /response/body | | GET | /qux/{dirname} | /bin/sh -c | ls -la /request/params/dirname \| kapow set /response/body |
When I get the first route When I get the first route
Then I get 200 as response code Then I get 200 as response code
# And I get "OK" as response reason phrase And I get "OK" as response reason phrase
And I get the following response body: And I get the following response body:
""" """
{ {
@@ -37,4 +37,10 @@ Feature: Kapow! server rejects insertion requests with malformed JSON bodies.
} }
""" """
Then I get 400 as response code Then I get 400 as response code
# And I get "Malformed JSON" as response reason phrase And the response header "Content-Type" contains "application/json"
And I get the following response body:
"""
{
"reason": "Malformed JSON"
}
"""
@@ -31,7 +31,7 @@ Feature: Kapow! server rejects insertion requests with semantic errors.
} }
""" """
Then I get 422 as response code Then I get 422 as response code
# And I get "Invalid Route" as response reason phrase And I get "Invalid Route" as response reason phrase
Scenario: Error because wrong route specification. Scenario: Error because wrong route specification.
If a request contains an invalid expression in the If a request contains an invalid expression in the
@@ -49,7 +49,7 @@ Feature: Kapow! server rejects insertion requests with semantic errors.
} }
""" """
Then I get 422 as response code Then I get 422 as response code
# And I get "Invalid Route" as response reason phrase And I get "Invalid Route" as response reason phrase
Scenario: Error because negative index specified. Scenario: Error because negative index specified.
If a request contains a negative number in the If a request contains a negative number in the
@@ -67,4 +67,10 @@ Feature: Kapow! server rejects insertion requests with semantic errors.
} }
""" """
Then I get 422 as response code Then I get 422 as response code
# And I get "Invalid Route" as response reason phrase And the response header "Content-Type" contains "application/json"
And I get the following response body:
"""
{
"reason": "Invalid Route"
}
"""
@@ -40,7 +40,7 @@ Feature: Insert new routes in Kapow! server.
} }
""" """
Then I get 201 as response code Then I get 201 as response code
# And I get "Created" as response reason phrase And I get "Created" as response reason phrase
And I get the following response body: And I get the following response body:
""" """
{ {
@@ -69,7 +69,7 @@ Feature: Insert new routes in Kapow! server.
} }
""" """
Then I get 201 as response code Then I get 201 as response code
# And I get "Created" as response reason phrase And I get "Created" as response reason phrase
And I get the following response body: And I get the following response body:
""" """
{ {
@@ -25,7 +25,7 @@ Feature: Listing routes in a Kapow! server.
Given I have a just started Kapow! server Given I have a just started Kapow! server
When I request a routes listing When I request a routes listing
Then I get 200 as response code Then I get 200 as response code
# And I get "OK" as response reason phrase And I get "OK" as response reason phrase
And I get the following response body: And I get the following response body:
""" """
[] []
@@ -41,7 +41,7 @@ Feature: Listing routes in a Kapow! server.
| GET | /qux/{dirname} | /bin/sh -c | ls -la /request/params/dirname \| kapow set /response/body | | GET | /qux/{dirname} | /bin/sh -c | ls -la /request/params/dirname \| kapow set /response/body |
When I request a routes listing When I request a routes listing
Then I get 200 as response code Then I get 200 as response code
# And I get "OK" as response reason phrase And I get "OK" as response reason phrase
And I get the following response body: And I get the following response body:
""" """
[ [
@@ -25,14 +25,10 @@ Feature: Fail to retrieve resources from nonexistent handler in Kapow! server.
Given I have a running Kapow! server Given I have a running Kapow! server
When I get the resource "/request/path" for the handler with id "XXXXXXXXXX" When I get the resource "/request/path" for the handler with id "XXXXXXXXXX"
Then I get 404 as response code Then I get 404 as response code
# And I get "Handler ID Not Found" as response reason phrase And the response header "Content-Type" contains "application/json"
And I get the following response body:
Scenario: Try to get an invalid resource from a nonexistent handler. """
A request to retrieve an invalid resource from a nonexistent {
handler will trigger an invalid resource path error "reason": "Handler ID Not Found"
even if the resource is invalid. }
"""
Given I have a running Kapow! server
When I get the resource "/invalid/path" for the handler with id "XXXXXXXXXX"
Then I get 400 as response code
# And I get "Invalid Resource Path" as response reason phrase
@@ -27,4 +27,10 @@ Feature: Fail to retrieve an invalid resource for a handler in Kapow! server.
When I send a request to the testing route "/foo" When I send a request to the testing route "/foo"
And I get the resource "/invented/path" And I get the resource "/invented/path"
Then I get 400 as response code Then I get 400 as response code
# And I get "Invalid Resource Path" as response reason phrase And the response header "Content-Type" contains "application/json"
And I get the following response body:
"""
{
"reason": "Invalid Resource Path"
}
"""
@@ -28,4 +28,10 @@ Feature: Fail to retrieve nonexistent resource items in Kapow! server.
When I send a request to the testing route "/foo" When I send a request to the testing route "/foo"
And I get the resource "/request/params/meloinvento" And I get the resource "/request/params/meloinvento"
Then I get 404 as response code Then I get 404 as response code
# And I get "Resource Item Not Found" as response reason phrase And the response header "Content-Type" contains "application/json"
And I get the following response body:
"""
{
"reason": "Resource Item Not Found"
}
"""
@@ -28,7 +28,7 @@ Feature: Retrieve a resource from a handler in Kapow! server.
When I send a request to the testing route "/foo" When I send a request to the testing route "/foo"
And I get the resource "/request/path" And I get the resource "/request/path"
Then I get 200 as response code Then I get 200 as response code
# And I get "OK" as response reason phrase And I get "OK" as response reason phrase
And I get the following response raw body: And I get the following response raw body:
""" """
/foo /foo
@@ -44,7 +44,7 @@ Feature: Retrieve a resource from a handler in Kapow! server.
When I send a request to the testing route "/foo?name=bar" When I send a request to the testing route "/foo?name=bar"
And I get the resource "/request/params/name" And I get the resource "/request/params/name"
Then I get 200 as response code Then I get 200 as response code
# And I get "OK" as response reason phrase And I get "OK" as response reason phrase
And I get the following response raw body: And I get the following response raw body:
""" """
bar bar
@@ -34,7 +34,7 @@ Feature: Retrieve request resources from a handler in Kapow! server.
| body | | bodyVal1 | | body | | bodyVal1 |
And I get the resource "<resourcePath>" And I get the resource "<resourcePath>"
Then I get 200 as response code Then I get 200 as response code
# And I get "OK" as response reason phrase And I get "OK" as response reason phrase
And I get the following response raw body: And I get the following response raw body:
""" """
<value> <value>
@@ -41,7 +41,7 @@ Feature: Setting values for handler response resources in Kapow! server.
And I set the resource "<resourcePath>" with value "<value>" And I set the resource "<resourcePath>" with value "<value>"
And I release the testing request And I release the testing request
Then I get 200 as response code Then I get 200 as response code
# And I get "OK" as response reason phrase And I get "OK" as response reason phrase
And I get the value "<value>" for the response "<fieldType>" named "<elementName>" in the testing request And I get the value "<value>" for the response "<fieldType>" named "<elementName>" in the testing request
Examples: Examples:
+11 -1
View File
@@ -52,7 +52,7 @@ class Env(EnvironConfig):
#: Where the User Interface is #: Where the User Interface is
KAPOW_USER_URL = StringVar(default="http://localhost:8080") KAPOW_USER_URL = StringVar(default="http://localhost:8080")
KAPOW_BOOT_TIMEOUT = IntVar(default=10) KAPOW_BOOT_TIMEOUT = IntVar(default=1000)
KAPOW_DEBUG_TESTS = BooleanVar(default=False) KAPOW_DEBUG_TESTS = BooleanVar(default=False)
@@ -185,6 +185,16 @@ def step_impl(context, code):
assert context.testing_response.status_code == int(code), f"Got {context.testing_response.status_code} instead" assert context.testing_response.status_code == int(code), f"Got {context.testing_response.status_code} instead"
@then('the response header "{header_name}" contains "{value}"')
def step_impl(context, header_name, value):
assert context.response.headers.get(header_name, "").split(';')[0] == value, f"Got {context.response.headers.get(header_name)} instead"
@then('the testing response header {header_name} contains {value}')
def step_impl(context, header_name, value):
assert context.testing_response.headers.get(header_name) == value, f"Got {context.testing_response.headers.get(header_name)} instead"
@then('I get "{reason}" as response reason phrase') @then('I get "{reason}" as response reason phrase')
def step_impl(context, reason): def step_impl(context, reason):
assert context.response.reason == reason, f"Got {context.response.reason} instead" assert context.response.reason == reason, f"Got {context.response.reason} instead"