Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

html-query-feature #246

Merged
merged 8 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type RegexCaptureConf struct {
type capturePath struct {
JsonPath *string `json:"json_path"`
XPath *string `json:"xpath"`
XpathHtml *string `json:"xpath_html"`
RegExp *RegexCaptureConf `json:"regexp"`
From string `json:"from"` // body,header,cookie
CookieName *string `json:"cookie_name"`
Expand Down Expand Up @@ -375,6 +376,7 @@ func stepToScenarioStep(s step) (types.ScenarioStep, error) {
capConf := types.EnvCaptureConf{
JsonPath: path.JsonPath,
Xpath: path.XPath,
XpathHtml: path.XpathHtml,
Name: name,
From: types.SourceType(path.From),
Key: path.HeaderKey,
Expand Down
22 changes: 20 additions & 2 deletions core/scenario/scripting/assertion/assert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ func TestAssert(t *testing.T) {
expected: true,
},
{
input: `equals(xml_path("//item/title"),"ABC")`,
input: `equals(xpath("//item/title"),"ABC")`,
envs: &evaluator.AssertEnv{
Body: `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
Expand All @@ -502,6 +502,19 @@ func TestAssert(t *testing.T) {

expected: true,
},
{
input: `equals(html_path("//body/h1"),"ABC")`,
envs: &evaluator.AssertEnv{
Body: `<!DOCTYPE html>
<html>
<body>
<h1>ABC</h1>
</body>
</html>`,
},

expected: true,
},
{
input: "equals(cookies.test.value, \"value\")",
envs: &evaluator.AssertEnv{
Expand Down Expand Up @@ -790,7 +803,12 @@ func TestAssert(t *testing.T) {
expectedError: "ArgumentError",
},
{
input: "xml_path(23)", // arg must be string
input: "xpath(23)", // arg must be string
expected: false,
expectedError: "ArgumentError",
},
{
input: "html_path(23)", // arg must be string
expected: false,
expectedError: "ArgumentError",
},
Expand Down
9 changes: 9 additions & 0 deletions core/scenario/scripting/assertion/evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ func Eval(node ast.Node, env *AssertEnv, receivedMap map[string]interface{}) (in
}
}
return xmlExtract(env.Body, xpath)
case HTMLPATH:
html, ok := args[0].(string)
if !ok {
return false, ArgumentError{
msg: "htmlpath must be a string",
wrappedErr: nil,
}
}
return htmlExtract(env.Body, html)
case REGEXP:
regexp, ok := args[1].(string)
if !ok {
Expand Down
9 changes: 8 additions & 1 deletion core/scenario/scripting/assertion/evaluator/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ var xmlExtract = func(source interface{}, xPath string) (interface{}, error) {
return val, err
}

var htmlExtract = func(source interface{}, xPath string) (interface{}, error) {
val, err := extraction.ExtractFromHtml(source, xPath)
return val, err
}

var regexExtract = func(source interface{}, xPath string, matchNo int64) (interface{}, error) {
val, err := extraction.ExtractWithRegex(source, types.RegexCaptureConf{
Exp: &xPath,
Expand Down Expand Up @@ -194,6 +199,7 @@ var assertionFuncMap = map[string]struct{}{
IN: {},
JSONPATH: {},
XMLPATH: {},
HTMLPATH: {},
REGEXP: {},
EXISTS: {},
CONTAINS: {},
Expand All @@ -216,7 +222,8 @@ const (
EQUALS = "equals"
IN = "in"
JSONPATH = "json_path"
XMLPATH = "xml_path"
XMLPATH = "xpath"
HTMLPATH = "html_path"
REGEXP = "regexp"
EXISTS = "exists"
CONTAINS = "contains"
Expand Down
14 changes: 14 additions & 0 deletions core/scenario/scripting/extraction/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ func Extract(source interface{}, ce types.EnvCaptureConf) (val interface{}, err
val, err = ExtractWithRegex(source, *ce.RegExp)
} else if ce.Xpath != nil {
val, err = ExtractFromXml(source, *ce.Xpath)
} else if ce.XpathHtml != nil {
val, err = ExtractFromHtml(source, *ce.XpathHtml)
}
case types.Cookie:
cookies := source.(map[string]*http.Cookie)
Expand Down Expand Up @@ -111,6 +113,18 @@ func ExtractFromXml(source interface{}, xPath string) (interface{}, error) {
}
}

func ExtractFromHtml(source interface{}, xPath string) (interface{}, error) {
xe := htmlExtractor{}
switch s := source.(type) {
case []byte: // from response body
return xe.extractFromByteSlice(s, xPath)
case string: // from response header
return xe.extractFromString(s, xPath)
default:
return "", fmt.Errorf("Unsupported type for extraction source")
}
}

type ExtractionError struct { // UnWrappable
msg string
wrappedErr error
Expand Down
43 changes: 43 additions & 0 deletions core/scenario/scripting/extraction/html.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package extraction

import (
"bytes"
"fmt"

"github.com/antchfx/htmlquery"
)

type htmlExtractor struct {
}

func (xe htmlExtractor) extractFromByteSlice(source []byte, xPath string) (interface{}, error) {
reader := bytes.NewBuffer(source)
rootNode, err := htmlquery.Parse(reader)
if err != nil {
return nil, err
}

// returns the first matched element
foundNode, err := htmlquery.Query(rootNode, xPath)
if foundNode == nil || err != nil {
return nil, fmt.Errorf("no match for the xPath_html: %s", xPath)
}

return foundNode.FirstChild.Data, nil
}

func (xe htmlExtractor) extractFromString(source string, xPath string) (interface{}, error) {
reader := bytes.NewBufferString(source)
rootNode, err := htmlquery.Parse(reader)
if err != nil {
return nil, err
}

// returns the first matched element
foundNode, err := htmlquery.Query(rootNode, xPath)
if foundNode == nil || err != nil {
return nil, fmt.Errorf("no match for this xpath_html")
}

return foundNode.FirstChild.Data, nil
}
120 changes: 120 additions & 0 deletions core/scenario/scripting/extraction/html_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package extraction

import (
"fmt"
"strings"
"testing"
)

func TestHtmlExtraction(t *testing.T) {
expected := "Html Title"
HtmlSource := fmt.Sprintf(`<!DOCTYPE html>
<html>
<body>
<h1>%s</h1>
<p>My first paragraph.</p>
</body>
</html>`, expected)

xe := htmlExtractor{}
xpath := "//body/h1"
val, err := xe.extractFromByteSlice([]byte(HtmlSource), xpath)

if err != nil {
t.Errorf("TestHtmlExtraction %v", err)
}

if !strings.EqualFold(val.(string), expected) {
t.Errorf("TestHtmlExtraction expected: %s, got: %s", expected, val)
}
}

func TestHtmlExtractionSeveralNode(t *testing.T) {
//should extract only the first one
expected := "Html Title"
HtmlSource := fmt.Sprintf(`<!DOCTYPE html>
<html>
<body>
<h1>%s</h1>
<h1>another node</h1>
<p>My first paragraph.</p>
</body>
</html>`, expected)

xe := htmlExtractor{}
xpath := "//h1"
val, err := xe.extractFromByteSlice([]byte(HtmlSource), xpath)

if err != nil {
t.Errorf("TestHtmlExtraction %v", err)
}

if !strings.EqualFold(val.(string), expected) {
t.Errorf("TestHtmlExtraction expected: %s, got: %s", expected, val)
}
}

func TestHtmlExtraction_PathNotFound(t *testing.T) {
expected := "XML Title"
xmlSource := fmt.Sprintf(`<!DOCTYPE html>
<html>
<body>
<h1>%s</h1>
<h1>another node</h1>
<p>My first paragraph.</p>
</body>
</html>`, expected)

xe := htmlExtractor{}
xpath := "//h2"
_, err := xe.extractFromByteSlice([]byte(xmlSource), xpath)

if err == nil {
t.Errorf("TestHtmlExtraction_PathNotFound, should be err, got :%v", err)
}
}

func TestInvalidHtml(t *testing.T) {
xmlSource := `invalid html source`

xe := htmlExtractor{}
xpath := "//input"
_, err := xe.extractFromByteSlice([]byte(xmlSource), xpath)

if err == nil {
t.Errorf("TestInvalidXml, should be err, got :%v", err)
}
}

func TestHtmlComplexExtraction(t *testing.T) {
expected := "Html Title"
HtmlSource := fmt.Sprintf(`<!DOCTYPE html>
<html>
<body>
<script>
if (typeof resourceLoadedSuccessfully === "function") {
resourceLoadedSuccessfully();
}
$(() => {
typeof cssVars === "function" && cssVars({onlyLegacy: true});
})
var trackGeoLocation = false;
alert('#@=$*€');
</script>
<h1>%s</h1>
<p>My first paragraph.</p>
</body>
</html>`, expected)

xe := htmlExtractor{}
xpath := "//body/h1"
val, err := xe.extractFromByteSlice([]byte(HtmlSource), xpath)

if err != nil {
t.Errorf("TestHtmlExtraction %v", err)
}

if !strings.EqualFold(val.(string), expected) {
t.Errorf("TestHtmlExtraction expected: %s, got: %s", expected, val)
}
}
5 changes: 3 additions & 2 deletions core/types/scenario.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ type RegexCaptureConf struct {
type EnvCaptureConf struct {
JsonPath *string `json:"json_path"`
Xpath *string `json:"xpath"`
XpathHtml *string `json:"xpath_html"`
RegExp *RegexCaptureConf `json:"regexp"`
Name string `json:"as"`
From SourceType `json:"from"`
Expand Down Expand Up @@ -339,9 +340,9 @@ func validateCaptureConf(conf EnvCaptureConf) error {
}
}

if conf.From == Body && conf.JsonPath == nil && conf.RegExp == nil && conf.Xpath == nil {
if conf.From == Body && conf.JsonPath == nil && conf.RegExp == nil && conf.Xpath == nil && conf.XpathHtml == nil {
return CaptureConfigError{
msg: fmt.Sprintf("%s, one of json_path, regexp, xpath key must be specified when extracting from body", conf.Name),
msg: fmt.Sprintf("%s, one of json_path, regexp, xpath or xpath_html key must be specified when extracting from body", conf.Name),
}
}

Expand Down
22 changes: 19 additions & 3 deletions engine_docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,8 @@ If Ddosify can't receive the response for a request, that step is marked as Fail
| `not` | ( param `bool` ) | returns converse of given param |
| `range` | ( param `int`, low `int`,high `int` ) | returns param is in range of [low,high): low is included, high is not included. |
| `json_path` | ( json_path `string`) | extracts from response body using given json path |
| `xml_path` | ( xpath `string` ) | extracts from response body using given xml path |
| `xpath` | ( xpath `string` ) | extracts from response body using given xml path |
| `html_path` | ( html `string` ) | extracts from response body using given html path |
| `regexp` | ( param `any`, regexp `string`, matchNo `int` ) | extracts from given value in the first parameter using given regular expression |

### Operators
Expand All @@ -707,6 +708,7 @@ If Ddosify can't receive the response for a request, that step is marked as Fail
| `status_code != 500` | same as preceding one|
| `equals(json_path(\"employees.0.name\"),\"Name\")` | checks if json extracted value is equal to "Name"|
| `equals(xpath(\"//item/title\"),\"ABC\")` | checks if xml extracted value is equal to "ABC" |
| `equals(html_path(\"//body/h1\"),\"ABC\")` | checks if html extracted value is equal to "ABC" |
| `equals(variables.x,100)` | checks if `x` variable coming from global or captured variables is equal to 100|
| `equals(variables.x,variables.y)` | checks if variables `x` and `y` are equal to each other |
| `equals_on_file(body,\"file.json\")` | reads from file.json and compares response body with read file |
Expand Down Expand Up @@ -761,7 +763,7 @@ Unlike assertions focused on individual steps, which determine the success or fa
| `less_than(fail_count_perc,0.05)` | Fail count percentage should be less than 5% |

## Correlation
Ddosify enables you to capture variables from steps using **json_path**, **xpath**, or **regular expressions**. Later, in the subsequent steps, you can inject both the captured variables and the scenario-scoped global variables.
Ddosify enables you to capture variables from steps using **json_path**, **xpath**, **xpath_html**, or **regular expressions**. Later, in the subsequent steps, you can inject both the captured variables and the scenario-scoped global variables.

> **:warning: Points to keep in mind**
> - You must specify **'header_key'** when capturing from header.
Expand Down Expand Up @@ -792,7 +794,7 @@ ddosify -config ddosify_config_correlation.json -debug
}
```

### Capture With XPath
### Capture With XPath on xml
```json
{
"steps": [
Expand All @@ -805,6 +807,20 @@ ddosify -config ddosify_config_correlation.json -debug
}
```

### Capture With XPath on html
```json
{
"steps": [
{
"capture_env": {
"TITLE" :{"from":"body","xpath_html":"//body/h1"},
}
}
]
}
```


### Capture With Regular Expressions
```json
{
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ require (
)

require (
github.com/antchfx/xpath v1.2.1 // indirect
github.com/antchfx/htmlquery v1.3.0
github.com/antchfx/xpath v1.2.3 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/jaswdr/faker v1.10.2 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
Expand Down
Loading
Loading