167 lines
3.7 KiB
Go
167 lines
3.7 KiB
Go
// Send HTTP request according to the specification.
|
|
package engine
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/nalgeon/codapi/config"
|
|
"github.com/nalgeon/codapi/httpx"
|
|
"github.com/nalgeon/codapi/logx"
|
|
)
|
|
|
|
// An HTTP engine sends HTTP requests.
|
|
type HTTP struct {
|
|
hosts map[string]string
|
|
}
|
|
|
|
// NewHTTP creates a new HTTP engine.
|
|
func NewHTTP(cfg *config.Config, sandbox, command string) Engine {
|
|
if len(cfg.HTTP.Hosts) == 0 {
|
|
msg := fmt.Sprintf("%s %s: http engine requires at least one allowed URL", sandbox, command)
|
|
panic(msg)
|
|
}
|
|
return &HTTP{hosts: cfg.HTTP.Hosts}
|
|
}
|
|
|
|
// Exec sends an HTTP request according to the spec
|
|
// and returns the response as text with status, headers and body.
|
|
func (e *HTTP) Exec(req Request) Execution {
|
|
// build request from spec
|
|
httpReq, err := e.parse(req.Files.First())
|
|
if err != nil {
|
|
err = fmt.Errorf("parse spec: %w", err)
|
|
return Fail(req.ID, err)
|
|
}
|
|
|
|
// send request and receive response
|
|
allowed := e.translateHost(httpReq)
|
|
if !allowed {
|
|
err = fmt.Errorf("host not allowed: %s", httpReq.Host)
|
|
return Fail(req.ID, err)
|
|
}
|
|
|
|
logx.Log("%s: %s %s", req.ID, httpReq.Method, httpReq.URL.String())
|
|
resp, err := httpx.Do(httpReq)
|
|
if err != nil {
|
|
err = fmt.Errorf("http request: %w", err)
|
|
return Fail(req.ID, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// read response body
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
err = NewExecutionError("read response", err)
|
|
return Fail(req.ID, err)
|
|
}
|
|
|
|
// build text representation of request
|
|
stdout := e.responseText(resp, body)
|
|
return Execution{
|
|
ID: req.ID,
|
|
OK: true,
|
|
Stdout: stdout,
|
|
}
|
|
}
|
|
|
|
// parse parses the request specification.
|
|
func (e *HTTP) parse(text string) (*http.Request, error) {
|
|
lines := strings.Split(text, "\n")
|
|
if len(lines) == 0 {
|
|
return nil, errors.New("empty request")
|
|
}
|
|
|
|
lineIdx := 0
|
|
|
|
// parse method and URL
|
|
var method, url string
|
|
methodURL := strings.Fields(lines[0])
|
|
if len(methodURL) >= 2 {
|
|
method = methodURL[0]
|
|
url = methodURL[1]
|
|
} else {
|
|
method = http.MethodGet
|
|
url = methodURL[0]
|
|
}
|
|
|
|
lineIdx++
|
|
|
|
// parse URL parameters
|
|
var urlParams strings.Builder
|
|
for i := lineIdx; i < len(lines); i++ {
|
|
line := strings.TrimSpace(lines[i])
|
|
if strings.HasPrefix(line, "?") || strings.HasPrefix(line, "&") {
|
|
urlParams.WriteString(line)
|
|
lineIdx++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
// parse headers
|
|
headers := make(http.Header)
|
|
for i := lineIdx; i < len(lines); i++ {
|
|
line := strings.TrimSpace(lines[i])
|
|
if line == "" {
|
|
break
|
|
}
|
|
headerParts := strings.SplitN(line, ":", 2)
|
|
if len(headerParts) == 2 {
|
|
headers.Add(strings.TrimSpace(headerParts[0]), strings.TrimSpace(headerParts[1]))
|
|
lineIdx++
|
|
}
|
|
}
|
|
|
|
lineIdx += 1
|
|
|
|
// parse body
|
|
var bodyRdr io.Reader
|
|
if lineIdx < len(lines) {
|
|
body := strings.Join(lines[lineIdx:], "\n")
|
|
bodyRdr = strings.NewReader(body)
|
|
}
|
|
|
|
// create request
|
|
req, err := http.NewRequest(method, url+urlParams.String(), bodyRdr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header = headers
|
|
return req, nil
|
|
}
|
|
|
|
// translateHost translates the requested host into the allowed one.
|
|
// Returns false if the requested host is not allowed.
|
|
func (e *HTTP) translateHost(req *http.Request) bool {
|
|
host := e.hosts[req.Host]
|
|
if host == "" {
|
|
return false
|
|
}
|
|
req.URL.Host = host
|
|
return true
|
|
}
|
|
|
|
// responseText returns the response as text with status, headers and body.
|
|
func (e *HTTP) responseText(resp *http.Response, body []byte) string {
|
|
var b bytes.Buffer
|
|
// status line
|
|
b.WriteString(
|
|
fmt.Sprintf("%s %d %s\n", resp.Proto, resp.StatusCode, http.StatusText(resp.StatusCode)),
|
|
)
|
|
// headers
|
|
for name := range resp.Header {
|
|
b.WriteString(fmt.Sprintf("%s: %s\n", name, resp.Header.Get(name)))
|
|
}
|
|
// body
|
|
if len(body) > 0 {
|
|
b.WriteByte('\n')
|
|
b.Write(body)
|
|
}
|
|
return b.String()
|
|
}
|