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