Skip to content

RFC: Functional Option Patterns for Protocols #411

@terrorbyte

Description

@terrorbyte

This proposal is to replace the very large (and growing) pile of options for protocol with a functional options pattern. We currently have 8 variants of the the HTTPSendAndRecv function in protocol, the longest of which has the comically large function signature of HTTPSendAndRecvWithHeadersNoRedirect. This is likely to grow and feels partially unorganized in the root protocol package which has no separation of protocols, and the only separation is inside of the sub-packages.

Instead of growing the protocols I propose that we instead provide a protocol.HTTP type and a func NewRequest(verb string, url string, payload string, opts ...Option) (*http.Response, string, bool) signature that allows for dynamic options. The theory is that instead of the ubiquitous:

resp, body, ok := protocol.HTTPSendAndRecvWithHeadersNoRedirect("GET", url, "", headers)

You would call:

resp, body, ok := protocol.HTTP.NewRequest("GET", url, "", 
   protocol.WithHeaders(headers), 
   protocol.WithoutRedirect())

or without a data parameter have default empty and

resp, body, ok := protocol.HTTP.NewRequest("GET", url, 
   protocol.WithData("{}"), 
   protocol.WithHeaders(headers), 
   protocol.WithoutRedirect())

The With and Without components could also be removed and shorten the options to Headers and Redirect(bool) signatures.


This does increase verbosity a bit, but does allow for quick modifications to a single request function without having to continue to increase the amount of signatures. The flexibility of having a subset of options also has a few benefits:

  • It also could allow us to do interesting things like introduce default hooks to the calls so that you could add modifying functions to the requests.
  • Options can be added without breaking API easier than full functions
  • Other protocols could be merged back into protocol with another type and struct (could be seen as a positive or negative)
  • Lower level additions could be made, such as with options to support uTLS handshakes that could be customized or match our GlobalUA

Here is a bit of a incomplete mock of this that I was doing inside of protocol/http prior to discussions of a v2:

package http

import (
	"crypto/tls"
	"io"
	"net"
	"net/http"
	nethttp "net/http"
	"net/url"
	"strings"
	"time"

	"github.com/vulncheck-oss/go-exploit/output"
	"github.com/vulncheck-oss/go-exploit/transform"
)

// GlobalUA is the default User-Agent for all go-exploit comms
//
//go:embed http-user-agent.txt
var GlobalUA string

// GlobalCommTimeout is the default timeout for all socket communications.
var GlobalCommTimeout = 10

type options struct {
	redirect            bool
	cache               bool
	urlEncodeParameters bool
	redirectDepth       *int
	parameters          *map[string]string
	headers             *map[string]string
	basicAuth           *string
	useragent           *string
	payload             *string
	client              *nethttp.Client
	cookies             []*nethttp.Cookie
}

type Option func(options *options) bool

func WithoutRedirect() Option {
	return func(options *options) bool {
		options.redirect = false
		return true
	}
}

func WithParameters(params map[string]string, urlEncode bool) Option {
	return func(options *options) bool {
		paramsCopy := make(map[string]string)
		if urlEncode {
			for k, v := range params {
				paramsCopy[k] = url.QueryEscape(v)
			}
		} else {
			paramsCopy = params
		}
		options.parameters = &paramsCopy

		return true
	}
}

func WithData(data string) Option {
	return func(option *options) bool {
		option.payload = &data
		return true
	}
}

func WithBasicAuth(username, password string) Option {
	return func(option *options) bool {
		b := "Basic " + transform.EncodeBase64(username+":"+password)
		option.basicAuth = &b
		return true
	}
}

func WithHeaders(params map[string]string) Option {
	return func(option *options) bool {
		option.headers = &params
		return true
	}
}

func NewRequest(verb string, url string, payload string, opts ...Option) (*nethttp.Response, string, bool) {
	var options options
	// set defaults
	options.redirect = true
	options.cache = false
	options.urlEncodeParameters = false
	options.useragent = &GlobalUA

	// options.client = &nethttp.Client{}
	for _, opt := range opts {
		ok := opt(&options)
		if !ok {
			return &nethttp.Response{}, "", false
		}
	}
	if options.client == nil {
		if !options.redirect {
			options.client = &http.Client{
				Transport: &http.Transport{
					Proxy: http.ProxyFromEnvironment,
					Dial: (&net.Dialer{
						Timeout: time.Duration(GlobalCommTimeout) * time.Second,
					}).Dial,
					TLSClientConfig: (&tls.Config{
						InsecureSkipVerify: true,
						// We have no control over the SSL versions supported on the remote target. Be permissive for more targets.
						MinVersion: tls.VersionSSL30,
					}),
				},
				Timeout: time.Duration(GlobalCommTimeout) * time.Second,
				CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
					return http.ErrUseLastResponse
				},
			}
		} else {
			options.client = &http.Client{
				Transport: &http.Transport{
					Proxy: http.ProxyFromEnvironment,
					Dial: (&net.Dialer{
						Timeout: time.Duration(GlobalCommTimeout) * time.Second,
					}).Dial,
					TLSClientConfig: (&tls.Config{
						InsecureSkipVerify: true,
						// We have no control over the SSL versions supported on the remote target. Be permissive for more targets.
						MinVersion: tls.VersionSSL30,
					}),
				},
				Timeout: time.Duration(GlobalCommTimeout) * time.Second,
			}
		}
	}
	req, err := http.NewRequest(verb, url, strings.NewReader(payload))
	if err != nil {
		output.PrintfFrameworkError("HTTP request creation error: %s", err)

		return nil, "", false
	}

	if options.headers != nil {
		for key, value := range *options.headers {
			if key == "Host" {
				// host can't be set directly
				req.Host = value
			} else {
				// don't use the Set function because the module might modify key. Set the header directly.
				req.Header[key] = []string{value}
			}
		}
	}

	// set headers on the request
	req.Header.Set("User-Agent", *options.useragent)
	if !options.redirect {
		// ignore the redirect
		options.client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
			return http.ErrUseLastResponse
		}
	}
	resp, err := options.client.Do(req)
	if err != nil {
		output.PrintfFrameworkError("HTTP request error: %s", err)

		return resp, "", false
	}
	defer resp.Body.Close()

	bodyBytes, _ := io.ReadAll(resp.Body)

	return resp, string(bodyBytes), true
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-breakRequires breaking the API, tag these for things that are wanted for a major version bumprfc

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions