1package plugin
2
3import (
4 "io"
5 "net/http"
6 "strings"
7 "time"
8
9 lua "github.com/yuin/gopher-lua"
10)
11
12const (
13 httpTimeout = 10 * time.Second
14 httpMaxBodySize = 1 << 20 // 1 MB
15)
16
17var httpClient = &http.Client{
18 Timeout: httpTimeout,
19}
20
21// luaHTTP implements matcha.http(options) — make an HTTP request.
22//
23// options is a table with fields:
24// - url (string, required)
25// - method (string, optional, default "GET")
26// - headers (table, optional)
27// - body (string, optional)
28//
29// Returns (response_table, nil) on success or (nil, error_string) on failure.
30// response_table has fields: status (number), body (string), headers (table).
31func (m *Manager) luaHTTP(L *lua.LState) int {
32 opts := L.CheckTable(1)
33
34 // URL (required).
35 urlVal := opts.RawGetString("url")
36 if urlVal == lua.LNil {
37 L.Push(lua.LNil)
38 L.Push(lua.LString("missing required field: url"))
39 return 2
40 }
41 rawURL := urlVal.String()
42
43 // Scheme validation.
44 if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
45 L.Push(lua.LNil)
46 L.Push(lua.LString("unsupported URL scheme: only http and https are allowed"))
47 return 2
48 }
49
50 // Method (optional, default GET).
51 method := "GET"
52 if v := opts.RawGetString("method"); v != lua.LNil {
53 method = strings.ToUpper(v.String())
54 }
55
56 // Body (optional).
57 var bodyReader io.Reader
58 if v := opts.RawGetString("body"); v != lua.LNil {
59 bodyReader = strings.NewReader(v.String())
60 }
61
62 req, err := http.NewRequest(method, rawURL, bodyReader)
63 if err != nil {
64 L.Push(lua.LNil)
65 L.Push(lua.LString(err.Error()))
66 return 2
67 }
68
69 // Headers (optional).
70 if v := opts.RawGetString("headers"); v != lua.LNil {
71 if tbl, ok := v.(*lua.LTable); ok {
72 tbl.ForEach(func(k, v lua.LValue) {
73 req.Header.Set(k.String(), v.String())
74 })
75 }
76 }
77
78 resp, err := httpClient.Do(req)
79 if err != nil {
80 L.Push(lua.LNil)
81 L.Push(lua.LString(err.Error()))
82 return 2
83 }
84 defer resp.Body.Close()
85
86 body, err := io.ReadAll(io.LimitReader(resp.Body, httpMaxBodySize))
87 if err != nil {
88 L.Push(lua.LNil)
89 L.Push(lua.LString(err.Error()))
90 return 2
91 }
92
93 // Build response table.
94 result := L.NewTable()
95 result.RawSetString("status", lua.LNumber(resp.StatusCode))
96 result.RawSetString("body", lua.LString(string(body)))
97
98 headers := L.NewTable()
99 for k, vals := range resp.Header {
100 if len(vals) > 0 {
101 headers.RawSetString(strings.ToLower(k), lua.LString(vals[0]))
102 }
103 }
104 result.RawSetString("headers", headers)
105
106 L.Push(result)
107 L.Push(lua.LNil)
108 return 2
109}