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