feat: initial public version
This commit is contained in:
166
engine/http.go
Normal file
166
engine/http.go
Normal file
@@ -0,0 +1,166 @@
|
||||
// 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()
|
||||
}
|
||||
Reference in New Issue
Block a user