httpclient.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. // Package httpclient provides Lua functions for a HTTP client
  2. package httpclient
  3. import (
  4. "net/http"
  5. "net/url"
  6. "strconv"
  7. "strings"
  8. "github.com/ddliu/go-httpclient"
  9. log "github.com/sirupsen/logrus"
  10. "github.com/xyproto/algernon/lua/convert"
  11. lua "github.com/xyproto/gopher-lua"
  12. )
  13. // HTTPClient represents a HTTP client
  14. type HTTPClient struct {
  15. client *httpclient.HttpClient
  16. cookieMap map[string]string
  17. userAgent string
  18. language string
  19. timeout int
  20. invalid bool
  21. }
  22. // NewHTTPClient creates a HTTPClient struct
  23. func NewHTTPClient() *HTTPClient {
  24. var hc HTTPClient
  25. hc.client = httpclient.NewHttpClient()
  26. hc.timeout = 10
  27. return &hc
  28. }
  29. // Begin applies the settings in the HTTPClient struct and returns a httpclient.HttpClient
  30. func (hc *HTTPClient) Begin() *httpclient.HttpClient {
  31. hclient := hc.client.Begin()
  32. if hc.userAgent != "" {
  33. hclient = hclient.WithOption(httpclient.OPT_USERAGENT, hc.userAgent)
  34. }
  35. if hc.timeout != 0 {
  36. hclient = hclient.WithOption(httpclient.OPT_TIMEOUT, hc.timeout)
  37. }
  38. if hc.language != "" {
  39. hclient = hclient.WithHeader("Accept-Language", hc.language)
  40. }
  41. if len(hc.cookieMap) != 0 {
  42. for k, v := range hc.cookieMap {
  43. hclient = hclient.WithCookie(&http.Cookie{
  44. Name: k,
  45. Value: v,
  46. })
  47. }
  48. }
  49. return hclient.WithOption(httpclient.OPT_UNSAFE_TLS, hc.invalid)
  50. }
  51. const (
  52. // HTTPClientClass is an identifier for the HTTPClient class in Lua
  53. HTTPClientClass = "HTTPClient"
  54. )
  55. // Get the first argument, "self", and cast it from userdata to a library (which is really a hash map).
  56. func checkHTTPClientClass(L *lua.LState) *HTTPClient {
  57. ud := L.CheckUserData(1)
  58. if hc, ok := ud.Value.(*HTTPClient); ok {
  59. return hc
  60. }
  61. L.ArgError(1, "HTTPClient expected")
  62. return nil
  63. }
  64. // Create a new httpclient.HttpClient. The Lua function takes no arguments.
  65. func constructHTTPClient(L *lua.LState, userAgent string) (*lua.LUserData, error) {
  66. // Create a new HTTP Client
  67. hc := NewHTTPClient()
  68. // Default user agent is the same as the server name
  69. hc.userAgent = userAgent
  70. // Create a new userdata struct
  71. ud := L.NewUserData()
  72. ud.Value = hc
  73. L.SetMetatable(ud, L.GetTypeMetatable(HTTPClientClass))
  74. return ud, nil
  75. }
  76. // hcGet is a Lua function for running the GET method on a given URL.
  77. // The first argument is the URL.
  78. // It can also take the following optional arguments:
  79. // * A table with URL arguments
  80. // * A table with HTTP headers
  81. // The response body is returned as a string.
  82. func hcGet(L *lua.LState) int {
  83. hc := checkHTTPClientClass(L) // arg 1
  84. URL := L.ToString(2) // arg 2
  85. if URL == "" {
  86. L.ArgError(2, "URL expected")
  87. return 0 // no results
  88. }
  89. // URL VALUES
  90. uv := make(url.Values)
  91. argTable := L.ToTable(3) // arg 3 (optiona)
  92. if argTable != nil {
  93. argMap := convert.Table2interfaceMap(argTable)
  94. for k, interfaceValue := range argMap {
  95. switch v := interfaceValue.(type) {
  96. case int:
  97. uv.Add(k, strconv.Itoa(v))
  98. case string:
  99. uv.Add(k, v)
  100. default:
  101. // TODO: Also support floats?
  102. log.Warn("Unrecognized value in table:", v)
  103. }
  104. }
  105. }
  106. encodedValues := uv.Encode()
  107. if encodedValues != "" {
  108. URL += "?" + encodedValues
  109. }
  110. // HTTP HEADERS
  111. headers := make(map[string]string)
  112. headerTable := L.ToTable(4) // arg 4 (optional)
  113. if headerTable != nil {
  114. headerMap := convert.Table2interfaceMap(headerTable)
  115. for k, interfaceValue := range headerMap {
  116. switch v := interfaceValue.(type) {
  117. case int:
  118. headers[k] = strconv.Itoa(v)
  119. case string:
  120. headers[k] = v
  121. default:
  122. log.Warn("Unrecognized value in table:", v)
  123. }
  124. }
  125. }
  126. // log.Info("GET " + URL)
  127. // GET the given URL with the given HTTP headers
  128. resp, err := hc.Begin().Do("GET", URL, headers, nil)
  129. if err != nil {
  130. log.Error(err)
  131. return 0 // no results
  132. }
  133. // Read the returned body
  134. bodyString, err := resp.ToString()
  135. if err != nil {
  136. log.Error(err)
  137. return 0 // no results
  138. }
  139. // Return a string
  140. L.Push(lua.LString(bodyString))
  141. return 1 // number of results
  142. }
  143. // hcPost is a Lua function for running the POST method on a given URL.
  144. // The first argument is the URL.
  145. // It can also take the following optional arguments:
  146. // * A table with URL arguments
  147. // * A table with HTTP headers
  148. // * A string that is the POST body
  149. // The response body is returned as a string.
  150. func hcPost(L *lua.LState) int {
  151. hc := checkHTTPClientClass(L) // arg 1
  152. URL := L.ToString(2) // arg 2
  153. if URL == "" {
  154. L.ArgError(2, "URL expected")
  155. return 0 // no results
  156. }
  157. // URL VALUES
  158. uv := make(url.Values)
  159. argTable := L.ToTable(3) // arg 3 (optiona)
  160. if argTable != nil {
  161. argMap := convert.Table2interfaceMap(argTable)
  162. for k, interfaceValue := range argMap {
  163. switch v := interfaceValue.(type) {
  164. case int:
  165. uv.Add(k, strconv.Itoa(v))
  166. case string:
  167. uv.Add(k, v)
  168. default:
  169. // TODO: Also support floats?
  170. log.Warn("Unrecognized value in table:", v)
  171. }
  172. }
  173. }
  174. encodedValues := uv.Encode()
  175. if encodedValues != "" {
  176. URL += "?" + encodedValues
  177. }
  178. // HTTP HEADERS
  179. headers := make(map[string]string)
  180. headerTable := L.ToTable(4) // arg 4 (optional)
  181. if headerTable != nil {
  182. headerMap := convert.Table2interfaceMap(headerTable)
  183. for k, interfaceValue := range headerMap {
  184. switch v := interfaceValue.(type) {
  185. case int:
  186. headers[k] = strconv.Itoa(v)
  187. case string:
  188. headers[k] = v
  189. default:
  190. log.Warn("Unrecognized value in table:", v)
  191. }
  192. }
  193. }
  194. // Body
  195. bodyReader := strings.NewReader(L.ToString(5)) // arg 5 (optional)
  196. // log.Info("POST " + URL)
  197. // POST the given URL with the given HTTP headers
  198. resp, err := hc.Begin().Do("POST", URL, headers, bodyReader)
  199. if err != nil {
  200. log.Error(err)
  201. return 0 // no results
  202. }
  203. // Read the returned body
  204. bodyString, err := resp.ToString()
  205. if err != nil {
  206. log.Error(err)
  207. return 0 // no results
  208. }
  209. // Return a string
  210. L.Push(lua.LString(bodyString))
  211. return 1 // number of results
  212. }
  213. // hcDo is a Lua function for running a custom HTTP method on a given URL.
  214. // The first argument is the method, like PUT or DELETE.
  215. // The second argument is the URL.
  216. // It can also take the following optional arguments:
  217. // * A table with URL arguments
  218. // * A table with HTTP headers
  219. // The response body is returned as a string.
  220. func hcDo(L *lua.LState) int {
  221. hc := checkHTTPClientClass(L) // arg 1
  222. method := L.ToString(2) // arg 2
  223. if method == "" {
  224. L.ArgError(2, "Method expected (ie. PUT)")
  225. }
  226. URL := L.ToString(3) // arg 3
  227. if URL == "" {
  228. L.ArgError(3, "URL expected")
  229. return 0 // no results
  230. }
  231. // URL VALUES
  232. uv := make(url.Values)
  233. argTable := L.ToTable(4) // arg 4 (optiona)
  234. if argTable != nil {
  235. argMap := convert.Table2interfaceMap(argTable)
  236. for k, interfaceValue := range argMap {
  237. switch v := interfaceValue.(type) {
  238. case int:
  239. uv.Add(k, strconv.Itoa(v))
  240. case string:
  241. uv.Add(k, v)
  242. default:
  243. // TODO: Also support floats?
  244. log.Warn("Unrecognized value in table:", v)
  245. }
  246. }
  247. }
  248. encodedValues := uv.Encode()
  249. if encodedValues != "" {
  250. URL += "?" + encodedValues
  251. }
  252. // HTTP HEADERS
  253. headers := make(map[string]string)
  254. headerTable := L.ToTable(5) // arg 5 (optional)
  255. if headerTable != nil {
  256. headerMap := convert.Table2interfaceMap(headerTable)
  257. for k, interfaceValue := range headerMap {
  258. switch v := interfaceValue.(type) {
  259. case int:
  260. headers[k] = strconv.Itoa(v)
  261. case string:
  262. headers[k] = v
  263. default:
  264. log.Warn("Unrecognized value in table:", v)
  265. }
  266. }
  267. }
  268. // log.Info(method + " " + URL)
  269. // Connect to the given URL with the given method and the given HTTP headers
  270. resp, err := hc.Begin().Do(method, URL, headers, nil)
  271. if err != nil {
  272. log.Error(err)
  273. return 0 // no results
  274. }
  275. // Read the returned body
  276. bodyString, err := resp.ToString()
  277. if err != nil {
  278. log.Error(err)
  279. return 0 // no results
  280. }
  281. // Return a string
  282. L.Push(lua.LString(bodyString))
  283. return 1 // number of results
  284. }
  285. // hcString is a Lua function that returns a descriptive string
  286. func hcString(L *lua.LState) int {
  287. L.Push(lua.LString("HTTP client based on github.com/ddliu/go-httpclient"))
  288. return 1 // number of results
  289. }
  290. // hcSetUserAgent is a Lua function for setting the user agent string
  291. func hcSetUserAgent(L *lua.LState) int {
  292. hc := checkHTTPClientClass(L) // arg 1
  293. userAgent := L.ToString(2) // arg 2
  294. if userAgent == "" {
  295. L.ArgError(2, "User agent string expected")
  296. return 0 // no results
  297. }
  298. hc.userAgent = userAgent
  299. return 0 // no results
  300. }
  301. // hcSetInvalid is a Lua function for setting if invalid TLS certificates are OK or not
  302. func hcSetInvalid(L *lua.LState) int {
  303. hc := checkHTTPClientClass(L) // arg 1
  304. invalid := L.CheckBool(2) // arg 2
  305. hc.invalid = invalid
  306. return 0 // no results
  307. }
  308. // hcSetCookie sets a cookie name/value on this HTTP client object
  309. func hcSetCookie(L *lua.LState) int {
  310. hc := checkHTTPClientClass(L) // arg 1
  311. key := L.ToString(2) // arg 2
  312. if key == "" {
  313. L.ArgError(2, "Expected a cookie key string")
  314. return 0 // no results
  315. }
  316. value := L.ToString(3) // arg 3
  317. if value == "" {
  318. L.ArgError(3, "Expected a cookie value string")
  319. return 0 // no results
  320. }
  321. hc.cookieMap[key] = value
  322. return 0 // no results
  323. }
  324. // hcSetTimeout is a Lua function for setting the timeout
  325. func hcSetTimeout(L *lua.LState) int {
  326. hc := checkHTTPClientClass(L) // arg 1
  327. timeout := L.ToInt(2) // arg 2
  328. if timeout == 0 {
  329. L.ArgError(2, "Expected a timeout (in seconds)")
  330. return 0 // no results
  331. }
  332. hc.timeout = timeout
  333. return 0 // no results
  334. }
  335. // hcSetLanguage is a Lua function for setting the desired language
  336. // for HTTP request.
  337. func hcSetLanguage(L *lua.LState) int {
  338. hc := checkHTTPClientClass(L) // arg 1
  339. language := L.ToString(2) // arg 2
  340. if language == "" {
  341. L.ArgError(2, "Accept-Language string expected (ie. \"en-us\")")
  342. return 0 // no results
  343. }
  344. hc.language = language
  345. return 0 // no results
  346. }
  347. // The hash map methods that are to be registered
  348. var hcMethods = map[string]lua.LGFunction{
  349. "__tostring": hcString,
  350. "SetLanguage": hcSetLanguage,
  351. "SetTimeout": hcSetTimeout,
  352. "SetCookie": hcSetCookie,
  353. "SetUserAgent": hcSetUserAgent,
  354. "SetInvalid": hcSetInvalid,
  355. "GET": hcGet,
  356. "POST": hcPost,
  357. "DO": hcDo,
  358. // TODO: Consider also implementing support for cookies
  359. }
  360. // Load makes functions related to httpclient available to the given Lua state
  361. func Load(L *lua.LState, userAgent string) {
  362. // Register the HTTPClient class and the methods that belongs with it.
  363. metaTableHC := L.NewTypeMetatable(HTTPClientClass)
  364. metaTableHC.RawSetH(lua.LString("__index"), metaTableHC)
  365. L.SetFuncs(metaTableHC, hcMethods)
  366. // The constructor for HTTPClient
  367. L.SetGlobal("HTTPClient", L.NewFunction(func(L *lua.LState) int {
  368. // Construct a new HTTPClient
  369. userdata, err := constructHTTPClient(L, userAgent)
  370. if err != nil {
  371. log.Error(err)
  372. return 0 // Number of returned values
  373. }
  374. // Return the Lua Page object
  375. L.Push(userdata)
  376. return 1 // number of results
  377. }))
  378. // Make a HTTP GET request to the given URL
  379. L.SetGlobal("GET", L.NewFunction(func(L *lua.LState) int {
  380. // Construct a new HTTPClient
  381. userdata, err := constructHTTPClient(L, userAgent)
  382. if err != nil {
  383. log.Error(err)
  384. return 0 // Number of returned values
  385. }
  386. L.Insert(userdata, 0)
  387. return hcGet(L) // Return the number of returned values
  388. }))
  389. // Make a HTTP POST request to the given URL
  390. L.SetGlobal("POST", L.NewFunction(func(L *lua.LState) int {
  391. // Construct a new HTTPClient
  392. userdata, err := constructHTTPClient(L, userAgent)
  393. if err != nil {
  394. log.Error(err)
  395. return 0 // Number of returned values
  396. }
  397. L.Insert(userdata, 0)
  398. return hcPost(L) // Return the number of returned values
  399. }))
  400. // Make a custom HTTP request to a given URL, like "PUT"
  401. L.SetGlobal("DO", L.NewFunction(func(L *lua.LState) int {
  402. // Construct a new HTTPClient
  403. userdata, err := constructHTTPClient(L, userAgent)
  404. if err != nil {
  405. log.Error(err)
  406. return 0 // Number of returned values
  407. }
  408. L.Insert(userdata, 0)
  409. return hcDo(L) // Return the number of returned values
  410. }))
  411. }