Merge pull request #479 from moorereason/iss312-http-request

Add request source
This commit is contained in:
Adnan Hajdarević 2020-11-21 18:58:19 +01:00 committed by GitHub
commit 3e18a060ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 136 additions and 28 deletions

View file

@ -1,5 +1,5 @@
# Referencing request values # Referencing request values
There are three types of request values: There are four types of request values:
1. HTTP Request Header values 1. HTTP Request Header values
@ -19,7 +19,23 @@ There are three types of request values:
} }
``` ```
3. Payload (JSON or form-value encoded) 3. HTTP Request parameters
```json
{
"source": "request",
"name": "method"
}
```
```json
{
"source": "request",
"name": "remote-addr"
}
```
4. Payload (JSON or form-value encoded)
```json ```json
{ {
"source": "payload", "source": "payload",
@ -57,7 +73,7 @@ There are three types of request values:
If the payload contains a key with the specified name "commits.0.commit.id", then the value of that key has priority over the dot-notation referencing. If the payload contains a key with the specified name "commits.0.commit.id", then the value of that key has priority over the dot-notation referencing.
3. XML Payload 4. XML Payload
Referencing XML payload parameters is much like the JSON examples above, but XML is more complex. Referencing XML payload parameters is much like the JSON examples above, but XML is more complex.
Element attributes are prefixed by a hyphen (`-`). Element attributes are prefixed by a hyphen (`-`).

View file

@ -35,6 +35,7 @@ const (
SourceQuery string = "url" SourceQuery string = "url"
SourceQueryAlias string = "query" SourceQueryAlias string = "query"
SourcePayload string = "payload" SourcePayload string = "payload"
SourceRequest string = "request"
SourceString string = "string" SourceString string = "string"
SourceEntirePayload string = "entire-payload" SourceEntirePayload string = "entire-payload"
SourceEntireQuery string = "entire-query" SourceEntireQuery string = "entire-query"
@ -438,12 +439,30 @@ func (ha *Argument) Get(r *Request) (string, error) {
case SourceHeader: case SourceHeader:
source = &r.Headers source = &r.Headers
key = textproto.CanonicalMIMEHeaderKey(ha.Name) key = textproto.CanonicalMIMEHeaderKey(ha.Name)
case SourceQuery, SourceQueryAlias: case SourceQuery, SourceQueryAlias:
source = &r.Query source = &r.Query
case SourcePayload: case SourcePayload:
source = &r.Payload source = &r.Payload
case SourceString: case SourceString:
return ha.Name, nil return ha.Name, nil
case SourceRequest:
if r == nil || r.RawRequest == nil {
return "", errors.New("request is nil")
}
switch strings.ToLower(ha.Name) {
case "remote-addr":
return r.RawRequest.RemoteAddr, nil
case "method":
return r.RawRequest.Method, nil
default:
return "", fmt.Errorf("unsupported request key: %q", ha.Name)
}
case SourceEntirePayload: case SourceEntirePayload:
res, err := json.Marshal(&r.Payload) res, err := json.Marshal(&r.Payload)
if err != nil { if err != nil {
@ -451,6 +470,7 @@ func (ha *Argument) Get(r *Request) (string, error) {
} }
return string(res), nil return string(res), nil
case SourceEntireHeaders: case SourceEntireHeaders:
res, err := json.Marshal(&r.Headers) res, err := json.Marshal(&r.Headers)
if err != nil { if err != nil {
@ -458,6 +478,7 @@ func (ha *Argument) Get(r *Request) (string, error) {
} }
return string(res), nil return string(res), nil
case SourceEntireQuery: case SourceEntireQuery:
res, err := json.Marshal(&r.Query) res, err := json.Marshal(&r.Query)
if err != nil { if err != nil {

View file

@ -255,27 +255,31 @@ func TestExtractParameter(t *testing.T) {
var argumentGetTests = []struct { var argumentGetTests = []struct {
source, name string source, name string
headers, query, payload map[string]interface{} headers, query, payload map[string]interface{}
request *http.Request
value string value string
ok bool ok bool
}{ }{
{"header", "a", map[string]interface{}{"A": "z"}, nil, nil, "z", true}, {"header", "a", map[string]interface{}{"A": "z"}, nil, nil, nil, "z", true},
{"url", "a", nil, map[string]interface{}{"a": "z"}, nil, "z", true}, {"url", "a", nil, map[string]interface{}{"a": "z"}, nil, nil, "z", true},
{"payload", "a", nil, nil, map[string]interface{}{"a": "z"}, "z", true}, {"payload", "a", nil, nil, map[string]interface{}{"a": "z"}, nil, "z", true},
{"string", "a", nil, nil, map[string]interface{}{"a": "z"}, "a", true}, {"request", "METHOD", nil, nil, map[string]interface{}{"a": "z"}, &http.Request{Method: "POST", RemoteAddr: "127.0.0.1:1234"}, "POST", true},
{"request", "remote-addr", nil, nil, map[string]interface{}{"a": "z"}, &http.Request{Method: "POST", RemoteAddr: "127.0.0.1:1234"}, "127.0.0.1:1234", true},
{"string", "a", nil, nil, map[string]interface{}{"a": "z"}, nil, "a", true},
// failures // failures
{"header", "a", nil, map[string]interface{}{"a": "z"}, map[string]interface{}{"a": "z"}, "", false}, // nil headers {"header", "a", nil, map[string]interface{}{"a": "z"}, map[string]interface{}{"a": "z"}, nil, "", false}, // nil headers
{"url", "a", map[string]interface{}{"A": "z"}, nil, map[string]interface{}{"a": "z"}, "", false}, // nil query {"url", "a", map[string]interface{}{"A": "z"}, nil, map[string]interface{}{"a": "z"}, nil, "", false}, // nil query
{"payload", "a", map[string]interface{}{"A": "z"}, map[string]interface{}{"a": "z"}, nil, "", false}, // nil payload {"payload", "a", map[string]interface{}{"A": "z"}, map[string]interface{}{"a": "z"}, nil, nil, "", false}, // nil payload
{"foo", "a", map[string]interface{}{"A": "z"}, nil, nil, "", false}, // invalid source {"foo", "a", map[string]interface{}{"A": "z"}, nil, nil, nil, "", false}, // invalid source
} }
func TestArgumentGet(t *testing.T) { func TestArgumentGet(t *testing.T) {
for _, tt := range argumentGetTests { for _, tt := range argumentGetTests {
a := Argument{tt.source, tt.name, "", false} a := Argument{tt.source, tt.name, "", false}
r := &Request{ r := &Request{
Headers: tt.headers, Headers: tt.headers,
Query: tt.query, Query: tt.query,
Payload: tt.payload, Payload: tt.payload,
RawRequest: tt.request,
} }
value, err := a.Get(r) value, err := a.Get(r)
if (err == nil) != tt.ok || value != tt.value { if (err == nil) != tt.ok || value != tt.value {

View file

@ -252,6 +252,21 @@
"include-command-output-in-response": true, "include-command-output-in-response": true,
"include-command-output-in-response-on-error": true "include-command-output-in-response-on-error": true
}, },
{
"id": "request-source",
"pass-arguments-to-command": [
{
"source": "request",
"name": "method"
},
{
"source": "request",
"name": "remote-addr"
}
],
"execute-command": "{{ .Hookecho }}",
"include-command-output-in-response": true
},
{ {
"id": "static-params-ok", "id": "static-params-ok",
"execute-command": "{{ .Hookecho }}", "execute-command": "{{ .Hookecho }}",

View file

@ -152,6 +152,15 @@
include-command-output-in-response: true include-command-output-in-response: true
include-command-output-in-response-on-error: true include-command-output-in-response-on-error: true
- id: request-source
pass-arguments-to-command:
- source: request
name: method
- source: request
name: remote-addr
execute-command: '{{ .Hookecho }}'
include-command-output-in-response: true
- id: static-params-ok - id: static-params-ok
execute-command: '{{ .Hookecho }}' execute-command: '{{ .Hookecho }}'
include-command-output-in-response: true include-command-output-in-response: true

View file

@ -129,8 +129,24 @@ func TestWebhook(t *testing.T) {
t.Errorf("POST %q: failed to ready body: %s", tt.desc, err) t.Errorf("POST %q: failed to ready body: %s", tt.desc, err)
} }
if res.StatusCode != tt.respStatus || string(body) != tt.respBody { // Test body
t.Errorf("failed %q (id: %s):\nexpected status: %#v, response: %s\ngot status: %#v, response: %s\ncommand output:\n%s\n", tt.desc, tt.id, tt.respStatus, tt.respBody, res.StatusCode, body, b) {
var bodyFailed bool
if res.StatusCode != tt.respStatus {
bodyFailed = true
}
if tt.bodyIsRE {
bodyFailed = string(body) == tt.respBody
} else {
r := regexp.MustCompile(tt.respBody)
bodyFailed = !r.Match(body)
}
if bodyFailed {
t.Errorf("failed %q (id: %s):\nexpected status: %#v, response: %s\ngot status: %#v, response: %s\ncommand output:\n%s\n", tt.desc, tt.id, tt.respStatus, tt.respBody, res.StatusCode, body, b)
}
} }
if tt.logMatch == "" { if tt.logMatch == "" {
@ -303,6 +319,7 @@ var hookHandlerTests = []struct {
headers map[string]string headers map[string]string
contentType string contentType string
body string body string
bodyIsRE bool
respStatus int respStatus int
respBody string respBody string
@ -459,6 +476,7 @@ var hookHandlerTests = []struct {
"watchers":1 "watchers":1
} }
}`, }`,
false,
http.StatusOK, http.StatusOK,
`arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz `arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00 env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
@ -473,6 +491,7 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
nil, nil,
"application/x-www-form-urlencoded", "application/x-www-form-urlencoded",
`payload={"canon_url": "https://bitbucket.org","commits": [{"author": "marcus","branch": "master","files": [{"file": "somefile.py","type": "modified"}],"message": "Added some more things to somefile.py\n","node": "620ade18607a","parents": ["702c70160afc"],"raw_author": "Marcus Bertrand <marcus@somedomain.com>","raw_node": "620ade18607ac42d872b568bb92acaa9a28620e9","revision": null,"size": -1,"timestamp": "2012-05-30 05:58:56","utctimestamp": "2014-11-07 15:19:02+00:00"}],"repository": {"absolute_url": "/webhook/testing/","fork": false,"is_private": true,"name": "Project X","owner": "marcus","scm": "git","slug": "project-x","website": "https://atlassian.com/"},"user": "marcus"}`, `payload={"canon_url": "https://bitbucket.org","commits": [{"author": "marcus","branch": "master","files": [{"file": "somefile.py","type": "modified"}],"message": "Added some more things to somefile.py\n","node": "620ade18607a","parents": ["702c70160afc"],"raw_author": "Marcus Bertrand <marcus@somedomain.com>","raw_node": "620ade18607ac42d872b568bb92acaa9a28620e9","revision": null,"size": -1,"timestamp": "2012-05-30 05:58:56","utctimestamp": "2014-11-07 15:19:02+00:00"}],"repository": {"absolute_url": "/webhook/testing/","fork": false,"is_private": true,"name": "Project X","owner": "marcus","scm": "git","slug": "project-x","website": "https://atlassian.com/"},"user": "marcus"}`,
false,
http.StatusOK, http.StatusOK,
`success`, `success`,
``, ``,
@ -526,6 +545,7 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
], ],
"total_commits_count": 4 "total_commits_count": 4
}`, }`,
false,
http.StatusOK, http.StatusOK,
`arg: b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327 John Smith john@example.com `arg: b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327 John Smith john@example.com
`, `,
@ -547,6 +567,7 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
<message id="1" from_user="1" to_user="2">Hello!!</message> <message id="1" from_user="1" to_user="2">Hello!!</message>
</messages> </messages>
</app>`, </app>`,
false,
http.StatusOK, http.StatusOK,
`success`, `success`,
``, ``,
@ -569,6 +590,7 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
"sg_message_id": "sg_message_id" "sg_message_id": "sg_message_id"
} }
]`, ]`,
false,
http.StatusOK, http.StatusOK,
`success`, `success`,
``, ``,
@ -601,6 +623,7 @@ Content-Transfer-Encoding: binary
binary data binary data
--xxx--`, --xxx--`,
false,
http.StatusOK, http.StatusOK,
`success`, `success`,
``, ``,
@ -614,6 +637,7 @@ binary data
nil, nil,
"application/json", "application/json",
`{"exists": 1}`, `{"exists": 1}`,
false,
http.StatusOK, http.StatusOK,
`success`, `success`,
``, ``,
@ -627,6 +651,7 @@ binary data
nil, nil,
"application/json", "application/json",
`{"exists": 1}`, `{"exists": 1}`,
false,
http.StatusOK, http.StatusOK,
`Hook rules were not satisfied.`, `Hook rules were not satisfied.`,
`parameter node not found`, `parameter node not found`,
@ -668,6 +693,7 @@ binary data
}, },
"ref":"refs/heads/master" "ref":"refs/heads/master"
}`, }`,
false,
http.StatusOK, http.StatusOK,
`arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz `arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00 env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
@ -710,6 +736,7 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
}, },
"ref":"refs/heads/master" "ref":"refs/heads/master"
}`, }`,
false,
http.StatusOK, http.StatusOK,
`arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz `arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
`, `,
@ -724,34 +751,50 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
map[string]string{"X-Hub-Signature": "33f9d709782f62b8b4a0178586c65ab098a39fe2"}, map[string]string{"X-Hub-Signature": "33f9d709782f62b8b4a0178586c65ab098a39fe2"},
"application/json", "application/json",
``, ``,
false,
http.StatusOK, http.StatusOK,
``, ``,
``, ``,
}, },
{
"request-source",
"request-source",
nil,
"POST",
map[string]string{"X-Hub-Signature": "33f9d709782f62b8b4a0178586c65ab098a39fe2"},
"application/json",
`{}`,
true,
http.StatusOK,
`arg: POST 127.0.0.1:.*
`,
``,
},
// test with disallowed global HTTP method // test with disallowed global HTTP method
{"global disallowed method", "bitbucket", []string{"Post "}, "GET", nil, `{}`, "application/json", http.StatusMethodNotAllowed, ``, ``}, {"global disallowed method", "bitbucket", []string{"Post "}, "GET", nil, `{}`, "application/json", false, http.StatusMethodNotAllowed, ``, ``},
// test with disallowed HTTP method // test with disallowed HTTP method
{"disallowed method", "github", nil, "Get", nil, `{}`, "application/json", http.StatusMethodNotAllowed, ``, ``}, {"disallowed method", "github", nil, "Get", nil, `{}`, "application/json", false, http.StatusMethodNotAllowed, ``, ``},
// test with custom return code // test with custom return code
{"empty payload", "github", nil, "POST", nil, "application/json", `{}`, http.StatusBadRequest, `Hook rules were not satisfied.`, ``}, {"empty payload", "github", nil, "POST", nil, "application/json", `{}`, false, http.StatusBadRequest, `Hook rules were not satisfied.`, ``},
// test with custom invalid http code, should default to 200 OK // test with custom invalid http code, should default to 200 OK
{"empty payload", "bitbucket", nil, "POST", nil, "application/json", `{}`, http.StatusOK, `Hook rules were not satisfied.`, ``}, {"empty payload", "bitbucket", nil, "POST", nil, "application/json", `{}`, false, http.StatusOK, `Hook rules were not satisfied.`, ``},
// test with no configured http return code, should default to 200 OK // test with no configured http return code, should default to 200 OK
{"empty payload", "gitlab", nil, "POST", nil, "application/json", `{}`, http.StatusOK, `Hook rules were not satisfied.`, ``}, {"empty payload", "gitlab", nil, "POST", nil, "application/json", `{}`, false, http.StatusOK, `Hook rules were not satisfied.`, ``},
// test capturing command output // test capturing command output
{"don't capture output on success by default", "capture-command-output-on-success-not-by-default", nil, "POST", nil, "application/json", `{}`, http.StatusOK, ``, ``}, {"don't capture output on success by default", "capture-command-output-on-success-not-by-default", nil, "POST", nil, "application/json", `{}`, false, http.StatusOK, ``, ``},
{"capture output on success with flag set", "capture-command-output-on-success-yes-with-flag", nil, "POST", nil, "application/json", `{}`, http.StatusOK, `arg: exit=0 {"capture output on success with flag set", "capture-command-output-on-success-yes-with-flag", nil, "POST", nil, "application/json", `{}`, false, http.StatusOK, `arg: exit=0
`, ``}, `, ``},
{"don't capture output on error by default", "capture-command-output-on-error-not-by-default", nil, "POST", nil, "application/json", `{}`, http.StatusInternalServerError, `Error occurred while executing the hook's command. Please check your logs for more details.`, ``}, {"don't capture output on error by default", "capture-command-output-on-error-not-by-default", nil, "POST", nil, "application/json", `{}`, false, http.StatusInternalServerError, `Error occurred while executing the hook's command. Please check your logs for more details.`, ``},
{"capture output on error with extra flag set", "capture-command-output-on-error-yes-with-extra-flag", nil, "POST", nil, "application/json", `{}`, http.StatusInternalServerError, `arg: exit=1 {"capture output on error with extra flag set", "capture-command-output-on-error-yes-with-extra-flag", nil, "POST", nil, "application/json", `{}`, false, http.StatusInternalServerError, `arg: exit=1
`, ``}, `, ``},
// Check logs // Check logs
{"static params should pass", "static-params-ok", nil, "POST", nil, "application/json", `{}`, http.StatusOK, "arg: passed\n", `(?s)command output: arg: passed`}, {"static params should pass", "static-params-ok", nil, "POST", nil, "application/json", `{}`, false, http.StatusOK, "arg: passed\n", `(?s)command output: arg: passed`},
{"command with space logs warning", "warn-on-space", nil, "POST", nil, "application/json", `{}`, http.StatusInternalServerError, "Error occurred while executing the hook's command. Please check your logs for more details.", `(?s)error in exec:.*use 'pass[-]arguments[-]to[-]command' to specify args`}, {"command with space logs warning", "warn-on-space", nil, "POST", nil, "application/json", `{}`, false, http.StatusInternalServerError, "Error occurred while executing the hook's command. Please check your logs for more details.", `(?s)error in exec:.*use 'pass[-]arguments[-]to[-]command' to specify args`},
{"unsupported content type error", "github", nil, "POST", map[string]string{"Content-Type": "nonexistent/format"}, "application/json", `{}`, http.StatusBadRequest, `Hook rules were not satisfied.`, `(?s)error parsing body payload due to unsupported content type header:`}, {"unsupported content type error", "github", nil, "POST", map[string]string{"Content-Type": "nonexistent/format"}, "application/json", `{}`, false, http.StatusBadRequest, `Hook rules were not satisfied.`, `(?s)error parsing body payload due to unsupported content type header:`},
} }
// buffer provides a concurrency-safe bytes.Buffer to tests above. // buffer provides a concurrency-safe bytes.Buffer to tests above.