diff --git a/x-pack/agent/dev-tools/cmd/fakewebapi/README.md b/x-pack/agent/dev-tools/cmd/fakewebapi/README.md new file mode 100644 index 000000000000..23853de9a5c4 --- /dev/null +++ b/x-pack/agent/dev-tools/cmd/fakewebapi/README.md @@ -0,0 +1,13 @@ +Fakewebapi is a simple test only Webserver + +The server implements the minimal calls and response to do high level testing of the agent: + +- Enroll successfully an Agent. +- Allow an Agent to periodically check in. + + +By default the server will return an empty list of actions, it's possible at runtime to change the returned +data by using the `push.sh` script. The script will POST a JSON document to return on the next request. + +Read the code of `push.sh` and the `fetch.sh` script for the usage information. + diff --git a/x-pack/agent/dev-tools/cmd/fakewebapi/action_example.json b/x-pack/agent/dev-tools/cmd/fakewebapi/action_example.json new file mode 100644 index 000000000000..5ed8c8aa19ca --- /dev/null +++ b/x-pack/agent/dev-tools/cmd/fakewebapi/action_example.json @@ -0,0 +1,33 @@ +{ + "actions": [ + { + "type": "POLICY_CHANGE", + "id": "id-1", + "data": { + "policy": { + "id": "policy-id", + "outputs": { + "default": { + "hosts": "https://localhost:9200" + } + }, + "streams": [ + { + "id": "string", + "type": "logs", + "path": "/var/log/hello.log", + "output": { + "use_output": "default" + } + } + ] + } + } + }, + { + "type": "WHAT_TO_DO_WITH_IT", + "id": "id2" + } + ], + "success": true +} diff --git a/x-pack/agent/dev-tools/cmd/fakewebapi/checkin.json b/x-pack/agent/dev-tools/cmd/fakewebapi/checkin.json new file mode 100644 index 000000000000..7a7ecd4e2d1a --- /dev/null +++ b/x-pack/agent/dev-tools/cmd/fakewebapi/checkin.json @@ -0,0 +1,4 @@ +{ + "events": [] +} + diff --git a/x-pack/agent/dev-tools/cmd/fakewebapi/fetch.sh b/x-pack/agent/dev-tools/cmd/fakewebapi/fetch.sh new file mode 100755 index 000000000000..c84d305081c4 --- /dev/null +++ b/x-pack/agent/dev-tools/cmd/fakewebapi/fetch.sh @@ -0,0 +1,5 @@ +#!/bin/sh +APIKEY=${1:-"abc123"} +AGENTID=${2:-"agent007"} +FILE=${3:-"checkin.json"} +curl -H "Authorization: ApiKey $APIKEY" -X POST --data "@$FILE" http://localhost:8080/api/fleet/agents/$AGENTID/checkin diff --git a/x-pack/agent/dev-tools/cmd/fakewebapi/main.go b/x-pack/agent/dev-tools/cmd/fakewebapi/main.go new file mode 100644 index 000000000000..4cd2b06a8513 --- /dev/null +++ b/x-pack/agent/dev-tools/cmd/fakewebapi/main.go @@ -0,0 +1,166 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package main + +import ( + "bytes" + "encoding/json" + "flag" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "regexp" + "strings" + "sync" + "time" + + "github.com/elastic/beats/x-pack/agent/pkg/fleetapi" +) + +var ( + host string + apiKey string + mutex sync.Mutex + + pathCheckin = regexp.MustCompile(`^/api/fleet/agents/(.+)/checkin`) + checkinResponse = response{Actions: make([]action, 0), Success: true} +) + +type response struct { + Actions []action `json:"actions"` + Success bool `json:"success"` +} + +type action interface{} + +func init() { + flag.StringVar(&apiKey, "apikey", "abc123", "API Key to authenticate") + flag.StringVar(&host, "host", "localhost:8080", "The IP and port to use for the webserver") +} + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/api/fleet/agents/enroll", handlerEnroll) + mux.HandleFunc("/admin/actions", handlerAction) + mux.HandleFunc("/", handlerRoot) + + log.Printf("Starting webserver and listening on %s", host) + + listener, err := net.Listen("tcp", host) + if err != nil { + panic(err) + } + defer listener.Close() + + http.Serve(listener, mux) +} + +func handlerRoot(w http.ResponseWriter, r *http.Request) { + if pathCheckin.MatchString(r.URL.Path) { + authHandler(handlerCheckin, apiKey)(w, r) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{ "message": "Hello!"}`)) + log.Println("Root hello!") + log.Println("Path: ", r.URL.Path) +} + +func handlerEnroll(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + + response := &fleetapi.EnrollResponse{ + Action: "created", + Success: true, + Item: fleetapi.EnrollItemResponse{ + ID: "a4937110-e53e-11e9-934f-47a8e38a522c", + Active: true, + PolicyID: "default", + Type: fleetapi.PermanentEnroll, + EnrolledAt: time.Now(), + UserProvidedMetadata: make(map[string]interface{}), + LocalMetadata: make(map[string]interface{}), + AccessAPIKey: apiKey, + }, + } + + b, err := json.Marshal(response) + if err != nil { + log.Printf("failed to enroll: %+v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + w.Write(b) + log.Println("Enroll response:", string(b)) +} + +func handlerCheckin(w http.ResponseWriter, r *http.Request) { + mutex.Lock() + defer mutex.Unlock() + + b, err := json.Marshal(checkinResponse) + if err != nil { + log.Printf("Failed to checkin, error: %+v", err) + http.Error(w, "Internal Server error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(b) + log.Println("Checkin response: ", string(b)) +} + +func handlerAction(w http.ResponseWriter, r *http.Request) { + mutex.Lock() + defer mutex.Unlock() + if r.Method != "POST" { + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + + resp := response{} + + var buf bytes.Buffer + tee := io.TeeReader(r.Body, &buf) + + c, err := ioutil.ReadAll(tee) + if err != nil { + log.Printf("Fails to update the actions") + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + decoder := json.NewDecoder(&buf) + err = decoder.Decode(&resp) + if err != nil { + log.Printf("Fails to update the actions") + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + checkinResponse = resp + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{ "success": true }`)) + log.Println("Action request: ", string(c)) +} + +func authHandler(handler http.HandlerFunc, apiKey string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + const key = "Authorization" + const prefix = "ApiKey " + + v := strings.TrimPrefix(r.Header.Get(key), prefix) + if v != apiKey { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + handler(w, r) + } +} diff --git a/x-pack/agent/dev-tools/cmd/fakewebapi/push.sh b/x-pack/agent/dev-tools/cmd/fakewebapi/push.sh new file mode 100755 index 000000000000..602df720cb11 --- /dev/null +++ b/x-pack/agent/dev-tools/cmd/fakewebapi/push.sh @@ -0,0 +1,3 @@ +#!/bin/sh +FILE=${1:-"action_example.json"} +curl -X POST --data "@$FILE" http://localhost:8080/admin/actions