Skip to content

Commit 08912bf

Browse files
committed
Add request action protocols selection
1 parent fcb8445 commit 08912bf

File tree

7 files changed

+613
-96
lines changed

7 files changed

+613
-96
lines changed

TODO.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@ in the future.
4848

4949
#### Structure - Instances, Actions, Servers, Services
5050

51-
- http/2 requests
52-
- add http_version field to the request action
53-
- update the client to allow using http/2
5451
- extend request action to support file upload
5552
- it should chunked update and set option to set delay between chunks to be able to create server timeouts
5653
- it should also allow doing partial unfinished uploads
@@ -76,6 +73,9 @@ in the future.
7673
- support TLS config in bench action
7774
- this will likely require using custom transport
7875
- extend TLS config for request and bench to support client cert
76+
- support `protocols` field in bench action
77+
- extract the common logic
78+
- extend protocols to support http3 in bench and request action
7979
- integrate better instance action identification
8080
- it should introduce name for each action and also pass parent name to nested actions in `parallel` or `not`
8181
- add execute action custom environment variables support

conf/parser/parser.go

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,19 @@ package parser
1616

1717
import (
1818
"fmt"
19+
"math"
20+
"path/filepath"
21+
"reflect"
22+
"strconv"
23+
"strings"
24+
1925
"github.com/pkg/errors"
2026
"github.com/spf13/afero"
2127
"github.com/wstool/wst/app"
2228
"github.com/wstool/wst/conf/loader"
2329
"github.com/wstool/wst/conf/parser/factory"
2430
"github.com/wstool/wst/conf/parser/location"
2531
"github.com/wstool/wst/conf/types"
26-
"math"
27-
"path/filepath"
28-
"reflect"
29-
"strconv"
30-
"strings"
3132
)
3233

3334
type ConfigParam string
@@ -174,13 +175,36 @@ func (p *ConfigParser) processFactoryParam(
174175

175176
func (p *ConfigParser) processEnumParam(enums string, data interface{}, fieldName string) error {
176177
enumList := strings.Split(enums, "|")
177-
for _, enum := range enumList {
178-
if enum == data {
179-
return nil
178+
179+
// Check if data is a slice/array
180+
dataValue := reflect.ValueOf(data)
181+
if dataValue.Kind() == reflect.Slice || dataValue.Kind() == reflect.Array {
182+
// Validate each element in the array
183+
for i := 0; i < dataValue.Len(); i++ {
184+
elem := dataValue.Index(i).Interface()
185+
if !isValidEnum(elem, enumList) {
186+
return errors.Errorf("value %v at index %d is not valid for field %s (valid values: %s)",
187+
elem, i, fieldName, enums)
188+
}
180189
}
190+
return nil
181191
}
182192

183-
return errors.Errorf("values %v are not valid for field %s", enums, p.Pos())
193+
// Single value validation
194+
if !isValidEnum(data, enumList) {
195+
return errors.Errorf("value %v is not valid for field %s (valid values: %s)",
196+
data, fieldName, enums)
197+
}
198+
return nil
199+
}
200+
201+
func isValidEnum(value interface{}, enumList []string) bool {
202+
for _, enum := range enumList {
203+
if enum == fmt.Sprintf("%v", value) {
204+
return true
205+
}
206+
}
207+
return false
184208
}
185209

186210
func (p *ConfigParser) processKeysParam(keys string, data interface{}, fieldName string) error {

conf/parser/parser_test.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ package parser
1616

1717
import (
1818
"fmt"
19+
"math"
20+
"os"
21+
"reflect"
22+
"testing"
23+
1924
"github.com/pkg/errors"
2025
"github.com/spf13/afero"
2126
"github.com/stretchr/testify/assert"
@@ -28,10 +33,6 @@ import (
2833
appMocks "github.com/wstool/wst/mocks/generated/app"
2934
loaderMocks "github.com/wstool/wst/mocks/generated/conf/loader"
3035
factoryMocks "github.com/wstool/wst/mocks/generated/conf/parser/factory"
31-
"math"
32-
"os"
33-
"reflect"
34-
"testing"
3536
)
3637

3738
func Test_isValidParam(t *testing.T) {
@@ -293,19 +294,33 @@ func Test_ConfigParser_processEnumParam(t *testing.T) {
293294
wantErr bool
294295
}{
295296
{
296-
name: "Value found in enum list",
297+
name: "Value found in enum list for single value",
297298
enums: "enum1|enum2|enum3",
298299
data: "enum2",
299300
fieldName: "field",
300301
wantErr: false, // No error because data is in enum list
301302
},
302303
{
303-
name: "Value not found - should trigger error",
304+
name: "Value found in enum list for multiple values",
305+
enums: "enum1|enum2|enum3",
306+
data: []string{"enum2", "enum3"},
307+
fieldName: "field",
308+
wantErr: false, // No error because all data items are in enum list
309+
},
310+
{
311+
name: "Value not found in a single value",
304312
enums: "enum1|enum2|enum3",
305313
data: "enum4",
306314
fieldName: "field",
307315
wantErr: true, // Error because data is not in enum list
308316
},
317+
{
318+
name: "Value not found in multiple values",
319+
enums: "enum1|enum2|enum3",
320+
data: []string{"enum2", "enum4"},
321+
fieldName: "field",
322+
wantErr: true, // Error because one item in data is not in enum list
323+
},
309324
}
310325

311326
for _, tt := range tests {

conf/types/action.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ type RequestAction struct {
120120
OnFailure string `wst:"on_failure,enum=fail|ignore|skip,default=fail"`
121121
Id string `wst:"id,default=last"`
122122
Scheme string `wst:"scheme,enum=http|https,default=http"`
123+
Protocols []string `wst:"protocols,enum=http1.1|http2"`
123124
Path string `wst:"path"`
124125
EncodePath bool `wst:"encode_path,default=true"`
125126
Method string `wst:"method,enum=GET|HEAD|DELETE|POST|PUT|PATCH|PURGE,default=GET"`

run/actions/action/request/request.go

Lines changed: 92 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ import (
3131
"github.com/wstool/wst/run/services"
3232
)
3333

34+
type Protocol string
35+
36+
const (
37+
ProtocolHTTP11 Protocol = "http1.1"
38+
ProtocolHTTP2 Protocol = "http2"
39+
)
40+
3441
type Maker interface {
3542
Make(
3643
config *types.RequestAction,
@@ -67,6 +74,28 @@ func (m *ActionMaker) Make(
6774
return nil, errors.New("TLS configuration is only valid for HTTPS requests")
6875
}
6976

77+
// Set default protocols if not specified
78+
protocols := config.Protocols
79+
if len(protocols) == 0 {
80+
if config.Scheme == "https" {
81+
// Default for HTTPS: allow both HTTP/1.1 and HTTP/2
82+
protocols = []string{string(ProtocolHTTP11), string(ProtocolHTTP2)}
83+
} else {
84+
// Default for HTTP: only HTTP/1.1 (h2c is not commonly supported)
85+
protocols = []string{string(ProtocolHTTP11)}
86+
}
87+
}
88+
89+
// Convert strings to Protocol type and validate
90+
validatedProtocols := make([]Protocol, 0, len(protocols))
91+
for _, protoStr := range protocols {
92+
proto := Protocol(protoStr)
93+
if proto == ProtocolHTTP2 && config.Scheme != "https" {
94+
m.fnd.Logger().Infof("Using unencrypted HTTP/2 (h2c) over plain HTTP")
95+
}
96+
validatedProtocols = append(validatedProtocols, proto)
97+
}
98+
7099
return &Action{
71100
fnd: m.fnd,
72101
service: svc,
@@ -80,6 +109,7 @@ func (m *ActionMaker) Make(
80109
method: config.Method,
81110
headers: config.Headers,
82111
tls: &config.TLS,
112+
protocols: validatedProtocols,
83113
}, nil
84114
}
85115

@@ -121,6 +151,7 @@ type Action struct {
121151
method string
122152
headers types.Headers
123153
tls *types.TLSClientConfig
154+
protocols []Protocol
124155
}
125156

126157
func (a *Action) When() action.When {
@@ -136,34 +167,32 @@ func (a *Action) Timeout() time.Duration {
136167
}
137168

138169
func (a *Action) Execute(ctx context.Context, runData runtime.Data) (bool, error) {
139-
a.fnd.Logger().Infof("Executing request action")
170+
a.fnd.Logger().Infof("Executing request action with HTTP protocols: %v", a.protocols)
140171

141-
// Create transport.
172+
// Create transport
142173
tr := &http.Transport{}
174+
175+
// Configure protocols using the Protocols API
176+
protocolConfig := a.buildProtocolConfig()
177+
tr.Protocols = protocolConfig
178+
143179
if a.scheme == "https" {
144-
tlsConfig := &tls.Config{
145-
InsecureSkipVerify: a.tls.SkipVerify,
146-
}
147-
if a.tls.CACert != "" {
148-
caCert, err := a.service.FindCertificate(a.tls.CACert)
149-
if err != nil {
150-
return false, errors.Errorf("CA certificate %s not found", a.tls.CACert)
151-
}
152-
caCertPool := a.fnd.X509CertPool()
153-
if !caCertPool.AppendCertFromPEM(caCert.Certificate.CertificateData()) {
154-
return false, errors.New("failed to parse CA certificate")
155-
}
156-
tlsConfig.RootCAs = caCertPool.CertPool()
180+
tlsConfig, err := a.buildTLSConfig()
181+
if err != nil {
182+
return false, err
157183
}
158184
tr.TLSClientConfig = tlsConfig
159185
}
160186

187+
a.fnd.Logger().Debugf("Protocol configuration: HTTP/1=%t, HTTP/2=%t, UnencryptedHTTP/2=%t",
188+
protocolConfig.HTTP1(), protocolConfig.HTTP2(), protocolConfig.UnencryptedHTTP2())
189+
161190
publicUrl, err := a.service.PublicUrl(a.scheme, a.path)
162191
if err != nil {
163192
return false, err
164193
}
165194

166-
// Create the HTTP request.
195+
// Create the HTTP request
167196
req, err := http.NewRequestWithContext(ctx, a.method, publicUrl, nil)
168197
if err != nil {
169198
return false, err
@@ -178,27 +207,27 @@ func (a *Action) Execute(ctx context.Context, runData runtime.Data) (bool, error
178207
}
179208
}
180209

181-
// Add headers to the request.
210+
// Add headers to the request
182211
for key, value := range a.headers {
183212
req.Header.Add(key, value)
184213
}
185214
a.fnd.Logger().Debugf("Sending request: %s", requestToString(req))
186215

187-
// Send the request.
216+
// Send the request
188217
client := a.fnd.HttpClient(tr)
189218
resp, err := client.Do(req)
190219
if err != nil {
191220
return false, err
192221
}
193222
defer resp.Body.Close()
194223

195-
// Read the response body.
224+
// Read the response body
196225
body, err := readResponse(ctx, resp.Body)
197226
if err != nil {
198227
return false, err
199228
}
200229

201-
// Create a ResponseData instance to hold both body and headers.
230+
// Create a ResponseData instance to hold both body and headers
202231
responseData := ResponseData{
203232
Status: resp.Status,
204233
StatusCode: resp.StatusCode,
@@ -207,16 +236,57 @@ func (a *Action) Execute(ctx context.Context, runData runtime.Data) (bool, error
207236
Headers: resp.Header,
208237
}
209238

210-
// Store the ResponseData in runData.
239+
// Store the ResponseData in runData
211240
key := fmt.Sprintf("response/%s", a.id)
212-
a.fnd.Logger().Debugf("Storing response %s: %s", key, responseData)
241+
a.fnd.Logger().Debugf("Storing response %s: %s (protocol: %s)", key, responseData, resp.Proto)
213242
if err := runData.Store(key, responseData); err != nil {
214243
return false, err
215244
}
216245

217246
return true, nil
218247
}
219248

249+
func (a *Action) buildProtocolConfig() *http.Protocols {
250+
config := new(http.Protocols)
251+
252+
for _, proto := range a.protocols {
253+
switch proto {
254+
case ProtocolHTTP11:
255+
config.SetHTTP1(true)
256+
case ProtocolHTTP2:
257+
if a.scheme == "https" {
258+
// HTTP/2 over TLS
259+
config.SetHTTP2(true)
260+
} else {
261+
// HTTP/2 cleartext (h2c) over plain HTTP
262+
config.SetUnencryptedHTTP2(true)
263+
}
264+
}
265+
}
266+
267+
return config
268+
}
269+
270+
func (a *Action) buildTLSConfig() (*tls.Config, error) {
271+
tlsConfig := &tls.Config{
272+
InsecureSkipVerify: a.tls.SkipVerify,
273+
}
274+
275+
if a.tls.CACert != "" {
276+
caCert, err := a.service.FindCertificate(a.tls.CACert)
277+
if err != nil {
278+
return nil, errors.Errorf("CA certificate %s not found", a.tls.CACert)
279+
}
280+
caCertPool := a.fnd.X509CertPool()
281+
if !caCertPool.AppendCertFromPEM(caCert.Certificate.CertificateData()) {
282+
return nil, errors.New("failed to parse CA certificate")
283+
}
284+
tlsConfig.RootCAs = caCertPool.CertPool()
285+
}
286+
287+
return tlsConfig, nil
288+
}
289+
220290
func requestToString(req *http.Request) string {
221291
var headers string
222292
for name, values := range req.Header {

0 commit comments

Comments
 (0)