Skip to content

Commit

Permalink
Merge pull request #1123 from kapishmalik/add-support-for-remote-post…
Browse files Browse the repository at this point in the history
…-serve-action

Add support for remote post serve action
  • Loading branch information
kapishmalik authored Apr 7, 2024
2 parents ed193e4 + 2dd7f62 commit eb31e77
Show file tree
Hide file tree
Showing 19 changed files with 462 additions and 62 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2

- name: Install Go
uses: actions/setup-go@v4
with:
go-version-file: go.mod

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
Expand Down
58 changes: 56 additions & 2 deletions core/action/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/url"
"os"
"os/exec"
"path"
Expand All @@ -19,10 +21,11 @@ import (
type Action struct {
Binary string
Script *os.File
Remote string
DelayInMs int
}

func NewAction(actionName, binary, scriptContent string, delayInMs int) (*Action, error) {
func NewLocalAction(actionName, binary, scriptContent string, delayInMs int) (*Action, error) {

scriptInfo := &Action{}
if strings.TrimSpace(actionName) == "" {
Expand All @@ -41,6 +44,19 @@ func NewAction(actionName, binary, scriptContent string, delayInMs int) (*Action
return scriptInfo, nil
}

func NewRemoteAction(actionName, host string, delayInMs int) (*Action, error) {

if strings.TrimSpace(actionName) == "" {
return nil, errors.New("empty action name passed")
}

if !isValidURL(host) {
return nil, errors.New("remote host is invalid")
}

return &Action{Remote: host, DelayInMs: delayInMs}, nil
}

func setBinary(action *Action, binary string) error {
action.Binary = binary
return nil
Expand Down Expand Up @@ -96,11 +112,12 @@ func (action *Action) GetActionView(actionName string) v2.ActionView {
ActionName: actionName,
Binary: action.Binary,
ScriptContent: scriptContent,
Remote: action.Remote,
DelayInMs: action.DelayInMs,
}
}

func (action *Action) ExecuteLocally(pair *models.RequestResponsePair) error {
func (action *Action) Execute(pair *models.RequestResponsePair) error {

pairViewBytes, err := json.Marshal(pair.ConvertToRequestResponsePairView())
if err != nil {
Expand All @@ -114,6 +131,35 @@ func (action *Action) ExecuteLocally(pair *models.RequestResponsePair) error {
//adding 200 ms to include some buffer for it to return response
time.Sleep(time.Duration(200+action.DelayInMs) * time.Millisecond)

//if it is remote callback
if action.Remote != "" {

req, err := http.NewRequest("POST", action.Remote, bytes.NewBuffer(pairViewBytes))
if err != nil {
log.WithFields(log.Fields{
"error": err.Error(),
}).Error("Error when building request to remote post serve action")
return err
}

req.Header.Add("Content-Type", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
log.WithFields(log.Fields{
"error": err.Error(),
}).Error("Error when communicating with remote post serve action")
return err
}

if resp.StatusCode != 200 {
log.Error("Remote post serve action did not process payload")
return nil
}
log.Info("Remote post serve action invoked successfully")
return nil
}

actionCommand := exec.Command(action.Binary, action.Script.Name())
actionCommand.Stdin = bytes.NewReader(pairViewBytes)
var stdout bytes.Buffer
Expand All @@ -137,3 +183,11 @@ func (action *Action) ExecuteLocally(pair *models.RequestResponsePair) error {
}
return nil
}

func isValidURL(host string) bool {

if _, err := url.ParseRequestURI(host); err == nil {
return true
}
return false
}
110 changes: 103 additions & 7 deletions core/action/action_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package action_test

import (
"github.com/gorilla/mux"
"net/http"
"net/http/httptest"
"testing"

"github.com/SpectoLabs/hoverfly/core/action"
Expand All @@ -10,10 +13,10 @@ import (

const pythonBasicScript = "import sys\nprint(sys.stdin.readlines()[0])"

func Test_NewActionMethod(t *testing.T) {
func Test_NewLocalActionMethod(t *testing.T) {
RegisterTestingT(t)

newAction, err := action.NewAction("test-callback", "python3", "dummy-script", 1800)
newAction, err := action.NewLocalAction("test-callback", "python3", "dummy-script", 1800)

Expect(err).To(BeNil())
Expect(newAction).NotTo(BeNil())
Expand All @@ -26,10 +29,51 @@ func Test_NewActionMethod(t *testing.T) {
Expect(scriptContent).To(Equal("dummy-script"))
}

func Test_GetActionViewMethod(t *testing.T) {
func Test_NewRemoteActionMethodWithEmptyHost(t *testing.T) {
RegisterTestingT(t)

newAction, err := action.NewAction("test-callback", "python3", "dummy-script", 1800)
newAction, err := action.NewRemoteAction("test-callback", "", 1800)

Expect(err).NotTo(BeNil())
Expect(newAction).To(BeNil())
}

func Test_NewRemoteActionMethodWithInvalidHost(t *testing.T) {
RegisterTestingT(t)

newAction, err := action.NewRemoteAction("test-callback", "testing", 1800)

Expect(err).NotTo(BeNil())
Expect(err.Error()).To(Equal("remote host is invalid"))
Expect(newAction).To(BeNil())
}

func Test_NewRemoteActionMethodWithHttpHost(t *testing.T) {
RegisterTestingT(t)

newAction, err := action.NewRemoteAction("test-callback", "http://localhost", 1800)

Expect(err).To(BeNil())
Expect(newAction).NotTo(BeNil())
Expect(newAction.Remote).To(Equal("http://localhost"))
Expect(newAction.DelayInMs).To(Equal(1800))
}

func Test_NewRemoteActionMethodWithHttpsHost(t *testing.T) {
RegisterTestingT(t)

newAction, err := action.NewRemoteAction("test-callback", "https://test.com", 1800)

Expect(err).To(BeNil())
Expect(newAction).NotTo(BeNil())
Expect(newAction.Remote).To(Equal("https://test.com"))
Expect(newAction.DelayInMs).To(Equal(1800))
}

func Test_GetLocalActionViewMethod(t *testing.T) {
RegisterTestingT(t)

newAction, err := action.NewLocalAction("test-callback", "python3", "dummy-script", 1800)

Expect(err).To(BeNil())
actionView := newAction.GetActionView("test-callback")
Expand All @@ -40,9 +84,24 @@ func Test_GetActionViewMethod(t *testing.T) {
Expect(actionView.DelayInMs).To(Equal(1800))
}

func Test_ExecuteLocallyPostServeAction(t *testing.T) {
func Test_GetRemoteActionViewMethod(t *testing.T) {
RegisterTestingT(t)

newAction, err := action.NewRemoteAction("test-callback", "http://localhost:8000", 1800)

Expect(err).To(BeNil())
actionView := newAction.GetActionView("test-callback")

Expect(actionView.ActionName).To(Equal("test-callback"))
Expect(actionView.Binary).To(Equal(""))
Expect(actionView.ScriptContent).To(Equal(""))
Expect(actionView.Remote).To(Equal("http://localhost:8000"))
Expect(actionView.DelayInMs).To(Equal(1800))
}

func Test_ExecuteLocalPostServeAction(t *testing.T) {
RegisterTestingT(t)
newAction, err := action.NewAction("test-callback", "python3", pythonBasicScript, 0)
newAction, err := action.NewLocalAction("test-callback", "python3", pythonBasicScript, 0)

Expect(err).To(BeNil())

Expand All @@ -51,6 +110,43 @@ func Test_ExecuteLocallyPostServeAction(t *testing.T) {

originalPair := models.RequestResponsePair{Response: resp, Request: req}

err = newAction.ExecuteLocally(&originalPair)
err = newAction.Execute(&originalPair)
Expect(err).To(BeNil())
}

func Test_ExecuteRemotePostServeAction(t *testing.T) {
RegisterTestingT(t)
muxRouter := mux.NewRouter()
muxRouter.HandleFunc("/process", processHandlerOkay).Methods("POST")
server := httptest.NewServer(muxRouter)
defer server.Close()

originalPair := models.RequestResponsePair{
Response: models.ResponseDetails{
Body: "Normal body",
},
}

newAction, err := action.NewRemoteAction("test-callback", server.URL+"/process", 0)
Expect(err).To(BeNil())
err = newAction.Execute(&originalPair)
Expect(err).To(BeNil())
}

func Test_ExecuteRemotePostServeAction_WithUnReachableHost(t *testing.T) {
originalPair := models.RequestResponsePair{
Response: models.ResponseDetails{
Body: "Normal body",
},
}

newAction, err := action.NewRemoteAction("test-callback", "http://test", 0)
Expect(err).To(BeNil())

err = newAction.Execute(&originalPair)
Expect(err).NotTo(BeNil())
}

func processHandlerOkay(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}
4 changes: 2 additions & 2 deletions core/action/postserveactiondetails_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
func Test_SetPostServeActionMethod(t *testing.T) {
RegisterTestingT(t)

newAction, err := action.NewAction("test-callback", "python3", "dummy script", 1800)
newAction, err := action.NewLocalAction("test-callback", "python3", "dummy script", 1800)
Expect(err).To(BeNil())

unit := action.NewPostServeActionDetails()
Expand All @@ -25,7 +25,7 @@ func Test_SetPostServeActionMethod(t *testing.T) {
func Test_DeletePostServeActionMethod(t *testing.T) {
RegisterTestingT(t)

newAction, err := action.NewAction("test-callback", "python3", "dummy script", 1800)
newAction, err := action.NewLocalAction("test-callback", "python3", "dummy script", 1800)
Expect(err).To(BeNil())

unit := action.NewPostServeActionDetails()
Expand Down
15 changes: 14 additions & 1 deletion core/cmd/hoverfly/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,14 +559,27 @@ func main() {
}

if fileContents, err := ioutil.ReadFile(splitPostServeAction[2]); err == nil {
err = hoverfly.SetPostServeAction(splitPostServeAction[0], splitPostServeAction[1], string(fileContents), delayInMs)
err = hoverfly.SetLocalPostServeAction(splitPostServeAction[0], splitPostServeAction[1], string(fileContents), delayInMs)
if err != nil {
log.WithFields(log.Fields{
"error": err.Error(),
"import": v,
}).Fatal("Failed to import post serve action")
}
}
} else if len(splitPostServeAction) == 3 {
delayInMs, err := strconv.Atoi(splitPostServeAction[2])
if err != nil {
//default to 1000 incase of error
delayInMs = 1000
}
err = hoverfly.SetRemotePostServeAction(splitPostServeAction[0], splitPostServeAction[1], delayInMs)
if err != nil {
log.WithFields(log.Fields{
"error": err.Error(),
"import": v,
}).Fatal("Failed to import post serve action")
}
} else {
log.WithFields(log.Fields{
"import": v,
Expand Down
10 changes: 7 additions & 3 deletions core/handlers/v2/hoverfly_postserveactiondetails_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import (

type HoverflyPostServeActionDetails interface {
GetAllPostServeActions() PostServeActionDetailsView
SetPostServeAction(string, string, string, int) error
SetLocalPostServeAction(string, string, string, int) error
SetRemotePostServeAction(string, string, int) error
DeletePostServeAction(string) error
}

Expand Down Expand Up @@ -51,12 +52,15 @@ func (postServeActionDetailsHandler *HoverflyPostServeActionDetailsHandler) Put(
return
}

err = postServeActionDetailsHandler.Hoverfly.SetPostServeAction(actionRequest.ActionName, actionRequest.Binary, actionRequest.ScriptContent, actionRequest.DelayInMs)
if actionRequest.Remote != "" {
err = postServeActionDetailsHandler.Hoverfly.SetRemotePostServeAction(actionRequest.ActionName, actionRequest.Remote, actionRequest.DelayInMs)
} else {
err = postServeActionDetailsHandler.Hoverfly.SetLocalPostServeAction(actionRequest.ActionName, actionRequest.Binary, actionRequest.ScriptContent, actionRequest.DelayInMs)
}
if err != nil {
handlers.WriteErrorResponse(w, err.Error(), 400)
return
}

postServeActionDetailsHandler.Get(w, req, next)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ func (HoverflyPostServeActionDetailsStub) GetAllPostServeActions() PostServeActi
return PostServeActionDetailsView{Actions: actions}
}

func (HoverflyPostServeActionDetailsStub) SetPostServeAction(string, string, string, int) error {
func (HoverflyPostServeActionDetailsStub) SetLocalPostServeAction(string, string, string, int) error {
return nil
}

func (HoverflyPostServeActionDetailsStub) SetRemotePostServeAction(string, string, int) error {
return nil
}

Expand Down
5 changes: 3 additions & 2 deletions core/handlers/v2/postserveactiondetails_views.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ type PostServeActionDetailsView struct {

type ActionView struct {
ActionName string `json:"actionName"`
Binary string `json:"binary"`
ScriptContent string `json:"script"`
Binary string `json:"binary,omitempty"`
ScriptContent string `json:"script,omitempty"`
Remote string `json:"remote,omitempty"`
DelayInMs int `json:"delayInMs,omitempty"`
}
2 changes: 1 addition & 1 deletion core/hoverfly.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func (hf *Hoverfly) processRequest(req *http.Request) *http.Response {

if result.PostServeActionInputDetails != nil {
if postServeAction, ok := hf.PostServeActionDetails.Actions[result.PostServeActionInputDetails.PostServeAction]; ok {
go postServeAction.ExecuteLocally(result.PostServeActionInputDetails.Pair)
go postServeAction.Execute(result.PostServeActionInputDetails.Pair)
}
}

Expand Down
17 changes: 15 additions & 2 deletions core/hoverfly_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -471,9 +471,22 @@ func (hf *Hoverfly) GetAllPostServeActions() v2.PostServeActionDetailsView {
}
}

func (hf *Hoverfly) SetPostServeAction(actionName string, binary string, scriptContent string, delayInMs int) error {
func (hf *Hoverfly) SetLocalPostServeAction(actionName string, binary string, scriptContent string, delayInMs int) error {

action, err := action.NewAction(actionName, binary, scriptContent, delayInMs)
action, err := action.NewLocalAction(actionName, binary, scriptContent, delayInMs)
if err != nil {
return err
}
err = hf.PostServeActionDetails.SetAction(actionName, action)
if err != nil {
return err
}
return nil
}

func (hf *Hoverfly) SetRemotePostServeAction(actionName, remote string, delayInMs int) error {

action, err := action.NewRemoteAction(actionName, remote, delayInMs)
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit eb31e77

Please sign in to comment.