123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 |
- // Package httpclient provides Lua functions for a HTTP client
- package httpclient
- import (
- "net/http"
- "net/url"
- "strconv"
- "strings"
- "github.com/ddliu/go-httpclient"
- log "github.com/sirupsen/logrus"
- "github.com/xyproto/algernon/lua/convert"
- lua "github.com/xyproto/gopher-lua"
- )
- // HTTPClient represents a HTTP client
- type HTTPClient struct {
- client *httpclient.HttpClient
- cookieMap map[string]string
- userAgent string
- language string
- timeout int
- invalid bool
- }
- // NewHTTPClient creates a HTTPClient struct
- func NewHTTPClient() *HTTPClient {
- var hc HTTPClient
- hc.client = httpclient.NewHttpClient()
- hc.timeout = 10
- return &hc
- }
- // Begin applies the settings in the HTTPClient struct and returns a httpclient.HttpClient
- func (hc *HTTPClient) Begin() *httpclient.HttpClient {
- hclient := hc.client.Begin()
- if hc.userAgent != "" {
- hclient = hclient.WithOption(httpclient.OPT_USERAGENT, hc.userAgent)
- }
- if hc.timeout != 0 {
- hclient = hclient.WithOption(httpclient.OPT_TIMEOUT, hc.timeout)
- }
- if hc.language != "" {
- hclient = hclient.WithHeader("Accept-Language", hc.language)
- }
- if len(hc.cookieMap) != 0 {
- for k, v := range hc.cookieMap {
- hclient = hclient.WithCookie(&http.Cookie{
- Name: k,
- Value: v,
- })
- }
- }
- return hclient.WithOption(httpclient.OPT_UNSAFE_TLS, hc.invalid)
- }
- const (
- // HTTPClientClass is an identifier for the HTTPClient class in Lua
- HTTPClientClass = "HTTPClient"
- )
- // Get the first argument, "self", and cast it from userdata to a library (which is really a hash map).
- func checkHTTPClientClass(L *lua.LState) *HTTPClient {
- ud := L.CheckUserData(1)
- if hc, ok := ud.Value.(*HTTPClient); ok {
- return hc
- }
- L.ArgError(1, "HTTPClient expected")
- return nil
- }
- // Create a new httpclient.HttpClient. The Lua function takes no arguments.
- func constructHTTPClient(L *lua.LState, userAgent string) (*lua.LUserData, error) {
- // Create a new HTTP Client
- hc := NewHTTPClient()
- // Default user agent is the same as the server name
- hc.userAgent = userAgent
- // Create a new userdata struct
- ud := L.NewUserData()
- ud.Value = hc
- L.SetMetatable(ud, L.GetTypeMetatable(HTTPClientClass))
- return ud, nil
- }
- // hcGet is a Lua function for running the GET method on a given URL.
- // The first argument is the URL.
- // It can also take the following optional arguments:
- // * A table with URL arguments
- // * A table with HTTP headers
- // The response body is returned as a string.
- func hcGet(L *lua.LState) int {
- hc := checkHTTPClientClass(L) // arg 1
- URL := L.ToString(2) // arg 2
- if URL == "" {
- L.ArgError(2, "URL expected")
- return 0 // no results
- }
- // URL VALUES
- uv := make(url.Values)
- argTable := L.ToTable(3) // arg 3 (optiona)
- if argTable != nil {
- argMap := convert.Table2interfaceMap(argTable)
- for k, interfaceValue := range argMap {
- switch v := interfaceValue.(type) {
- case int:
- uv.Add(k, strconv.Itoa(v))
- case string:
- uv.Add(k, v)
- default:
- // TODO: Also support floats?
- log.Warn("Unrecognized value in table:", v)
- }
- }
- }
- encodedValues := uv.Encode()
- if encodedValues != "" {
- URL += "?" + encodedValues
- }
- // HTTP HEADERS
- headers := make(map[string]string)
- headerTable := L.ToTable(4) // arg 4 (optional)
- if headerTable != nil {
- headerMap := convert.Table2interfaceMap(headerTable)
- for k, interfaceValue := range headerMap {
- switch v := interfaceValue.(type) {
- case int:
- headers[k] = strconv.Itoa(v)
- case string:
- headers[k] = v
- default:
- log.Warn("Unrecognized value in table:", v)
- }
- }
- }
- // log.Info("GET " + URL)
- // GET the given URL with the given HTTP headers
- resp, err := hc.Begin().Do("GET", URL, headers, nil)
- if err != nil {
- log.Error(err)
- return 0 // no results
- }
- // Read the returned body
- bodyString, err := resp.ToString()
- if err != nil {
- log.Error(err)
- return 0 // no results
- }
- // Return a string
- L.Push(lua.LString(bodyString))
- return 1 // number of results
- }
- // hcPost is a Lua function for running the POST method on a given URL.
- // The first argument is the URL.
- // It can also take the following optional arguments:
- // * A table with URL arguments
- // * A table with HTTP headers
- // * A string that is the POST body
- // The response body is returned as a string.
- func hcPost(L *lua.LState) int {
- hc := checkHTTPClientClass(L) // arg 1
- URL := L.ToString(2) // arg 2
- if URL == "" {
- L.ArgError(2, "URL expected")
- return 0 // no results
- }
- // URL VALUES
- uv := make(url.Values)
- argTable := L.ToTable(3) // arg 3 (optiona)
- if argTable != nil {
- argMap := convert.Table2interfaceMap(argTable)
- for k, interfaceValue := range argMap {
- switch v := interfaceValue.(type) {
- case int:
- uv.Add(k, strconv.Itoa(v))
- case string:
- uv.Add(k, v)
- default:
- // TODO: Also support floats?
- log.Warn("Unrecognized value in table:", v)
- }
- }
- }
- encodedValues := uv.Encode()
- if encodedValues != "" {
- URL += "?" + encodedValues
- }
- // HTTP HEADERS
- headers := make(map[string]string)
- headerTable := L.ToTable(4) // arg 4 (optional)
- if headerTable != nil {
- headerMap := convert.Table2interfaceMap(headerTable)
- for k, interfaceValue := range headerMap {
- switch v := interfaceValue.(type) {
- case int:
- headers[k] = strconv.Itoa(v)
- case string:
- headers[k] = v
- default:
- log.Warn("Unrecognized value in table:", v)
- }
- }
- }
- // Body
- bodyReader := strings.NewReader(L.ToString(5)) // arg 5 (optional)
- // log.Info("POST " + URL)
- // POST the given URL with the given HTTP headers
- resp, err := hc.Begin().Do("POST", URL, headers, bodyReader)
- if err != nil {
- log.Error(err)
- return 0 // no results
- }
- // Read the returned body
- bodyString, err := resp.ToString()
- if err != nil {
- log.Error(err)
- return 0 // no results
- }
- // Return a string
- L.Push(lua.LString(bodyString))
- return 1 // number of results
- }
- // hcDo is a Lua function for running a custom HTTP method on a given URL.
- // The first argument is the method, like PUT or DELETE.
- // The second argument is the URL.
- // It can also take the following optional arguments:
- // * A table with URL arguments
- // * A table with HTTP headers
- // The response body is returned as a string.
- func hcDo(L *lua.LState) int {
- hc := checkHTTPClientClass(L) // arg 1
- method := L.ToString(2) // arg 2
- if method == "" {
- L.ArgError(2, "Method expected (ie. PUT)")
- }
- URL := L.ToString(3) // arg 3
- if URL == "" {
- L.ArgError(3, "URL expected")
- return 0 // no results
- }
- // URL VALUES
- uv := make(url.Values)
- argTable := L.ToTable(4) // arg 4 (optiona)
- if argTable != nil {
- argMap := convert.Table2interfaceMap(argTable)
- for k, interfaceValue := range argMap {
- switch v := interfaceValue.(type) {
- case int:
- uv.Add(k, strconv.Itoa(v))
- case string:
- uv.Add(k, v)
- default:
- // TODO: Also support floats?
- log.Warn("Unrecognized value in table:", v)
- }
- }
- }
- encodedValues := uv.Encode()
- if encodedValues != "" {
- URL += "?" + encodedValues
- }
- // HTTP HEADERS
- headers := make(map[string]string)
- headerTable := L.ToTable(5) // arg 5 (optional)
- if headerTable != nil {
- headerMap := convert.Table2interfaceMap(headerTable)
- for k, interfaceValue := range headerMap {
- switch v := interfaceValue.(type) {
- case int:
- headers[k] = strconv.Itoa(v)
- case string:
- headers[k] = v
- default:
- log.Warn("Unrecognized value in table:", v)
- }
- }
- }
- // log.Info(method + " " + URL)
- // Connect to the given URL with the given method and the given HTTP headers
- resp, err := hc.Begin().Do(method, URL, headers, nil)
- if err != nil {
- log.Error(err)
- return 0 // no results
- }
- // Read the returned body
- bodyString, err := resp.ToString()
- if err != nil {
- log.Error(err)
- return 0 // no results
- }
- // Return a string
- L.Push(lua.LString(bodyString))
- return 1 // number of results
- }
- // hcString is a Lua function that returns a descriptive string
- func hcString(L *lua.LState) int {
- L.Push(lua.LString("HTTP client based on github.com/ddliu/go-httpclient"))
- return 1 // number of results
- }
- // hcSetUserAgent is a Lua function for setting the user agent string
- func hcSetUserAgent(L *lua.LState) int {
- hc := checkHTTPClientClass(L) // arg 1
- userAgent := L.ToString(2) // arg 2
- if userAgent == "" {
- L.ArgError(2, "User agent string expected")
- return 0 // no results
- }
- hc.userAgent = userAgent
- return 0 // no results
- }
- // hcSetInvalid is a Lua function for setting if invalid TLS certificates are OK or not
- func hcSetInvalid(L *lua.LState) int {
- hc := checkHTTPClientClass(L) // arg 1
- invalid := L.CheckBool(2) // arg 2
- hc.invalid = invalid
- return 0 // no results
- }
- // hcSetCookie sets a cookie name/value on this HTTP client object
- func hcSetCookie(L *lua.LState) int {
- hc := checkHTTPClientClass(L) // arg 1
- key := L.ToString(2) // arg 2
- if key == "" {
- L.ArgError(2, "Expected a cookie key string")
- return 0 // no results
- }
- value := L.ToString(3) // arg 3
- if value == "" {
- L.ArgError(3, "Expected a cookie value string")
- return 0 // no results
- }
- hc.cookieMap[key] = value
- return 0 // no results
- }
- // hcSetTimeout is a Lua function for setting the timeout
- func hcSetTimeout(L *lua.LState) int {
- hc := checkHTTPClientClass(L) // arg 1
- timeout := L.ToInt(2) // arg 2
- if timeout == 0 {
- L.ArgError(2, "Expected a timeout (in seconds)")
- return 0 // no results
- }
- hc.timeout = timeout
- return 0 // no results
- }
- // hcSetLanguage is a Lua function for setting the desired language
- // for HTTP request.
- func hcSetLanguage(L *lua.LState) int {
- hc := checkHTTPClientClass(L) // arg 1
- language := L.ToString(2) // arg 2
- if language == "" {
- L.ArgError(2, "Accept-Language string expected (ie. \"en-us\")")
- return 0 // no results
- }
- hc.language = language
- return 0 // no results
- }
- // The hash map methods that are to be registered
- var hcMethods = map[string]lua.LGFunction{
- "__tostring": hcString,
- "SetLanguage": hcSetLanguage,
- "SetTimeout": hcSetTimeout,
- "SetCookie": hcSetCookie,
- "SetUserAgent": hcSetUserAgent,
- "SetInvalid": hcSetInvalid,
- "GET": hcGet,
- "POST": hcPost,
- "DO": hcDo,
- // TODO: Consider also implementing support for cookies
- }
- // Load makes functions related to httpclient available to the given Lua state
- func Load(L *lua.LState, userAgent string) {
- // Register the HTTPClient class and the methods that belongs with it.
- metaTableHC := L.NewTypeMetatable(HTTPClientClass)
- metaTableHC.RawSetH(lua.LString("__index"), metaTableHC)
- L.SetFuncs(metaTableHC, hcMethods)
- // The constructor for HTTPClient
- L.SetGlobal("HTTPClient", L.NewFunction(func(L *lua.LState) int {
- // Construct a new HTTPClient
- userdata, err := constructHTTPClient(L, userAgent)
- if err != nil {
- log.Error(err)
- return 0 // Number of returned values
- }
- // Return the Lua Page object
- L.Push(userdata)
- return 1 // number of results
- }))
- // Make a HTTP GET request to the given URL
- L.SetGlobal("GET", L.NewFunction(func(L *lua.LState) int {
- // Construct a new HTTPClient
- userdata, err := constructHTTPClient(L, userAgent)
- if err != nil {
- log.Error(err)
- return 0 // Number of returned values
- }
- L.Insert(userdata, 0)
- return hcGet(L) // Return the number of returned values
- }))
- // Make a HTTP POST request to the given URL
- L.SetGlobal("POST", L.NewFunction(func(L *lua.LState) int {
- // Construct a new HTTPClient
- userdata, err := constructHTTPClient(L, userAgent)
- if err != nil {
- log.Error(err)
- return 0 // Number of returned values
- }
- L.Insert(userdata, 0)
- return hcPost(L) // Return the number of returned values
- }))
- // Make a custom HTTP request to a given URL, like "PUT"
- L.SetGlobal("DO", L.NewFunction(func(L *lua.LState) int {
- // Construct a new HTTPClient
- userdata, err := constructHTTPClient(L, userAgent)
- if err != nil {
- log.Error(err)
- return 0 // Number of returned values
- }
- L.Insert(userdata, 0)
- return hcDo(L) // Return the number of returned values
- }))
- }
|