zhangjinkun 6 жил өмнө
parent
commit
188d46b996

+ 24 - 0
common/src/github.com/cjoudrey/gluahttp/.gitignore

@@ -0,0 +1,24 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof

+ 15 - 0
common/src/github.com/cjoudrey/gluahttp/.travis.yml

@@ -0,0 +1,15 @@
+language: go
+
+go:
+  - "1.8"
+  - "1.9"
+  - "1.10"
+
+install:
+  - go get github.com/yuin/gopher-lua
+
+script:
+ - go test -v
+
+notifications:
+  email: false

+ 22 - 0
common/src/github.com/cjoudrey/gluahttp/LICENSE

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Christian Joudrey
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+

+ 235 - 0
common/src/github.com/cjoudrey/gluahttp/README.md

@@ -0,0 +1,235 @@
+# gluahttp
+
+[![](https://travis-ci.org/cjoudrey/gluahttp.svg)](https://travis-ci.org/cjoudrey/gluahttp)
+
+gluahttp provides an easy way to make HTTP requests from within [GopherLua](https://github.com/yuin/gopher-lua).
+
+## Installation
+
+```
+go get github.com/cjoudrey/gluahttp
+```
+
+## Usage
+
+```go
+import "github.com/yuin/gopher-lua"
+import "github.com/cjoudrey/gluahttp"
+
+func main() {
+    L := lua.NewState()
+    defer L.Close()
+
+    L.PreloadModule("http", NewHttpModule(&http.Client{}).Loader)
+
+    if err := L.DoString(`
+
+        local http = require("http")
+
+        response, error_message = http.request("GET", "http://example.com", {
+            query="page=1"
+            headers={
+                Accept="*/*"
+            }
+        })
+
+    `); err != nil {
+        panic(err)
+    }
+}
+```
+
+## API
+
+- [`http.delete(url [, options])`](#httpdeleteurl--options)
+- [`http.get(url [, options])`](#httpgeturl--options)
+- [`http.head(url [, options])`](#httpheadurl--options)
+- [`http.patch(url [, options])`](#httppatchurl--options)
+- [`http.post(url [, options])`](#httpposturl--options)
+- [`http.put(url [, options])`](#httpputurl--options)
+- [`http.request(method, url [, options])`](#httprequestmethod-url--options)
+- [`http.request_batch(requests)`](#httprequest_batchrequests)
+- [`http.response`](#httpresponse)
+
+### http.delete(url [, options])
+
+**Attributes**
+
+| Name    | Type   | Description |
+| ------- | ------ | ----------- |
+| url     | String | URL of the resource to load |
+| options | Table  | Additional options |
+
+**Options**
+
+| Name    | Type   | Description |
+| ------- | ------ | ----------- |
+| query   | String | URL encoded query params |
+| cookies | Table  | Additional cookies to send with the request |
+| headers | Table  | Additional headers to send with the request |
+
+**Returns**
+
+[http.response](#httpresponse) or (nil, error message)
+
+### http.get(url [, options])
+
+**Attributes**
+
+| Name    | Type   | Description |
+| ------- | ------ | ----------- |
+| url     | String | URL of the resource to load |
+| options | Table  | Additional options |
+
+**Options**
+
+| Name    | Type   | Description |
+| ------- | ------ | ----------- |
+| query   | String | URL encoded query params |
+| cookies | Table  | Additional cookies to send with the request |
+| headers | Table  | Additional headers to send with the request |
+
+**Returns**
+
+[http.response](#httpresponse) or (nil, error message)
+
+### http.head(url [, options])
+
+**Attributes**
+
+| Name    | Type   | Description |
+| ------- | ------ | ----------- |
+| url     | String | URL of the resource to load |
+| options | Table  | Additional options |
+
+**Options**
+
+| Name    | Type   | Description |
+| ------- | ------ | ----------- |
+| query   | String | URL encoded query params |
+| cookies | Table  | Additional cookies to send with the request |
+| headers | Table  | Additional headers to send with the request |
+
+**Returns**
+
+[http.response](#httpresponse) or (nil, error message)
+
+### http.patch(url [, options])
+
+**Attributes**
+
+| Name    | Type   | Description |
+| ------- | ------ | ----------- |
+| url     | String | URL of the resource to load |
+| options | Table  | Additional options |
+
+**Options**
+
+| Name    | Type   | Description |
+| ------- | ------ | ----------- |
+| query   | String | URL encoded query params |
+| cookies | Table  | Additional cookies to send with the request |
+| body    | String | Request body. |
+| form    | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
+| headers | Table  | Additional headers to send with the request |
+
+**Returns**
+
+[http.response](#httpresponse) or (nil, error message)
+
+### http.post(url [, options])
+
+**Attributes**
+
+| Name    | Type   | Description |
+| ------- | ------ | ----------- |
+| url     | String | URL of the resource to load |
+| options | Table  | Additional options |
+
+**Options**
+
+| Name    | Type   | Description |
+| ------- | ------ | ----------- |
+| query   | String | URL encoded query params |
+| cookies | Table  | Additional cookies to send with the request |
+| body    | String | Request body. |
+| form    | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
+| headers | Table  | Additional headers to send with the request |
+
+**Returns**
+
+[http.response](#httpresponse) or (nil, error message)
+
+### http.put(url [, options])
+
+**Attributes**
+
+| Name    | Type   | Description |
+| ------- | ------ | ----------- |
+| url     | String | URL of the resource to load |
+| options | Table  | Additional options |
+
+**Options**
+
+| Name    | Type   | Description |
+| ------- | ------ | ----------- |
+| query   | String | URL encoded query params |
+| cookies | Table  | Additional cookies to send with the request |
+| body    | String | Request body. |
+| form    | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
+| headers | Table  | Additional headers to send with the request |
+
+**Returns**
+
+[http.response](#httpresponse) or (nil, error message)
+
+### http.request(method, url [, options])
+
+**Attributes**
+
+| Name    | Type   | Description |
+| ------- | ------ | ----------- |
+| method  | String | The HTTP request method |
+| url     | String | URL of the resource to load |
+| options | Table  | Additional options |
+
+**Options**
+
+| Name    | Type   | Description |
+| ------- | ------ | ----------- |
+| query   | String | URL encoded query params |
+| cookies | Table  | Additional cookies to send with the request |
+| body    | String | Request body. |
+| form    | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
+| headers | Table  | Additional headers to send with the request |
+
+**Returns**
+
+[http.response](#httpresponse) or (nil, error message)
+
+### http.request_batch(requests)
+
+**Attributes**
+
+| Name     | Type  | Description |
+| -------- | ----- | ----------- |
+| requests | Table | A table of requests to send. Each request item is by itself a table containing [http.request](#httprequestmethod-url--options) parameters for the request |
+
+**Returns**
+
+[[http.response](#httpresponse)] or ([[http.response](#httpresponse)], [error message])
+
+### http.response
+
+The `http.response` table contains information about a completed HTTP request.
+
+**Attributes**
+
+| Name        | Type   | Description |
+| ----------- | ------ | ----------- |
+| body        | String | The HTTP response body |
+| body_size   | Number | The size of the HTTP reponse body in bytes |
+| headers     | Table  | The HTTP response headers |
+| cookies     | Table  | The cookies sent by the server in the HTTP response |
+| status_code | Number | The HTTP response status code |
+| url         | String | The final URL the request ended pointing to after redirects |

+ 215 - 0
common/src/github.com/cjoudrey/gluahttp/gluahttp.go

@@ -0,0 +1,215 @@
+package gluahttp
+
+import "github.com/yuin/gopher-lua"
+import "net/http"
+import "fmt"
+import "errors"
+import "io/ioutil"
+import "strings"
+
+type httpModule struct {
+	do func(req *http.Request) (*http.Response, error)
+}
+
+type empty struct{}
+
+func NewHttpModule(client *http.Client) *httpModule {
+	return NewHttpModuleWithDo(client.Do)
+}
+
+func NewHttpModuleWithDo(do func(req *http.Request) (*http.Response, error)) *httpModule {
+	return &httpModule{
+		do: do,
+	}
+}
+
+func (h *httpModule) Loader(L *lua.LState) int {
+	mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
+		"get":           h.get,
+		"delete":        h.delete,
+		"head":          h.head,
+		"patch":         h.patch,
+		"post":          h.post,
+		"put":           h.put,
+		"request":       h.request,
+		"request_batch": h.requestBatch,
+	})
+	registerHttpResponseType(mod, L)
+	L.Push(mod)
+	return 1
+}
+
+func (h *httpModule) get(L *lua.LState) int {
+	return h.doRequestAndPush(L, "get", L.ToString(1), L.ToTable(2))
+}
+
+func (h *httpModule) delete(L *lua.LState) int {
+	return h.doRequestAndPush(L, "delete", L.ToString(1), L.ToTable(2))
+}
+
+func (h *httpModule) head(L *lua.LState) int {
+	return h.doRequestAndPush(L, "head", L.ToString(1), L.ToTable(2))
+}
+
+func (h *httpModule) patch(L *lua.LState) int {
+	return h.doRequestAndPush(L, "patch", L.ToString(1), L.ToTable(2))
+}
+
+func (h *httpModule) post(L *lua.LState) int {
+	return h.doRequestAndPush(L, "post", L.ToString(1), L.ToTable(2))
+}
+
+func (h *httpModule) put(L *lua.LState) int {
+	return h.doRequestAndPush(L, "put", L.ToString(1), L.ToTable(2))
+}
+
+func (h *httpModule) request(L *lua.LState) int {
+	return h.doRequestAndPush(L, L.ToString(1), L.ToString(2), L.ToTable(3))
+}
+
+func (h *httpModule) requestBatch(L *lua.LState) int {
+	requests := L.ToTable(1)
+	amountRequests := requests.Len()
+
+	errs := make([]error, amountRequests)
+	responses := make([]*lua.LUserData, amountRequests)
+	sem := make(chan empty, amountRequests)
+
+	i := 0
+
+	requests.ForEach(func(_ lua.LValue, value lua.LValue) {
+		requestTable := toTable(value)
+
+		if requestTable != nil {
+			method := requestTable.RawGet(lua.LNumber(1)).String()
+			url := requestTable.RawGet(lua.LNumber(2)).String()
+			options := toTable(requestTable.RawGet(lua.LNumber(3)))
+
+			go func(i int, L *lua.LState, method string, url string, options *lua.LTable) {
+				response, err := h.doRequest(L, method, url, options)
+
+				if err == nil {
+					errs[i] = nil
+					responses[i] = response
+				} else {
+					errs[i] = err
+					responses[i] = nil
+				}
+
+				sem <- empty{}
+			}(i, L, method, url, options)
+		} else {
+			errs[i] = errors.New("Request must be a table")
+			responses[i] = nil
+			sem <- empty{}
+		}
+
+		i = i + 1
+	})
+
+	for i = 0; i < amountRequests; i++ {
+		<-sem
+	}
+
+	hasErrors := false
+	errorsTable := L.NewTable()
+	responsesTable := L.NewTable()
+	for i = 0; i < amountRequests; i++ {
+		if errs[i] == nil {
+			responsesTable.Append(responses[i])
+			errorsTable.Append(lua.LNil)
+		} else {
+			responsesTable.Append(lua.LNil)
+			errorsTable.Append(lua.LString(fmt.Sprintf("%s", errs[i])))
+			hasErrors = true
+		}
+	}
+
+	if hasErrors {
+		L.Push(responsesTable)
+		L.Push(errorsTable)
+		return 2
+	} else {
+		L.Push(responsesTable)
+		return 1
+	}
+}
+
+func (h *httpModule) doRequest(L *lua.LState, method string, url string, options *lua.LTable) (*lua.LUserData, error) {
+	req, err := http.NewRequest(strings.ToUpper(method), url, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if options != nil {
+		if reqCookies, ok := options.RawGet(lua.LString("cookies")).(*lua.LTable); ok {
+			reqCookies.ForEach(func(key lua.LValue, value lua.LValue) {
+				req.AddCookie(&http.Cookie{Name: key.String(), Value: value.String()})
+			})
+		}
+
+		switch reqQuery := options.RawGet(lua.LString("query")).(type) {
+		case lua.LString:
+			req.URL.RawQuery = reqQuery.String()
+		}
+
+		body := options.RawGet(lua.LString("body"))
+		if _, ok := body.(lua.LString); !ok {
+			// "form" is deprecated.
+			body = options.RawGet(lua.LString("form"))
+			// Only set the Content-Type to application/x-www-form-urlencoded
+			// when someone uses "form", not for "body".
+			if _, ok := body.(lua.LString); ok {
+				req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+			}
+		}
+
+		switch reqBody := body.(type) {
+		case lua.LString:
+			body := reqBody.String()
+			req.ContentLength = int64(len(body))
+			req.Body = ioutil.NopCloser(strings.NewReader(body))
+		}
+
+		// Set these last. That way the code above doesn't overwrite them.
+		if reqHeaders, ok := options.RawGet(lua.LString("headers")).(*lua.LTable); ok {
+			reqHeaders.ForEach(func(key lua.LValue, value lua.LValue) {
+				req.Header.Set(key.String(), value.String())
+			})
+		}
+	}
+
+	res, err := h.do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	defer res.Body.Close()
+	body, err := ioutil.ReadAll(res.Body)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return newHttpResponse(res, &body, len(body), L), nil
+}
+
+func (h *httpModule) doRequestAndPush(L *lua.LState, method string, url string, options *lua.LTable) int {
+	response, err := h.doRequest(L, method, url, options)
+
+	if err != nil {
+		L.Push(lua.LNil)
+		L.Push(lua.LString(fmt.Sprintf("%s", err)))
+		return 2
+	}
+
+	L.Push(response)
+	return 1
+}
+
+func toTable(v lua.LValue) *lua.LTable {
+	if lv, ok := v.(*lua.LTable); ok {
+		return lv
+	}
+	return nil
+}

+ 441 - 0
common/src/github.com/cjoudrey/gluahttp/gluahttp_test.go

@@ -0,0 +1,441 @@
+package gluahttp
+
+import "github.com/yuin/gopher-lua"
+import "testing"
+import "io/ioutil"
+import "net/http"
+import "net"
+import "fmt"
+import "net/http/cookiejar"
+import "strings"
+
+func TestRequestNoMethod(t *testing.T) {
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.request()
+
+		assert_equal(nil, response)
+		assert_contains('unsupported protocol scheme ""', error)
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestRequestNoUrl(t *testing.T) {
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.request("get")
+
+		assert_equal(nil, response)
+		assert_contains('unsupported protocol scheme ""', error)
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestRequestBatch(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		responses, errors = http.request_batch({
+			{"get", "http://`+listener.Addr().String()+`", {query="page=1"}},
+			{"post", "http://`+listener.Addr().String()+`/set_cookie"},
+			{"post", ""},
+			1
+		})
+
+		-- the requests are send asynchronously, so the errors might not be in the same order
+		-- if we don't sort, the test will be flaky
+		local errorStrings = {}
+		for _, err in pairs(errors) do
+			table.insert(errorStrings, tostring(err))
+		end
+		table.sort(errorStrings)
+
+		assert_contains('Post : unsupported protocol scheme ""', errorStrings[1])
+		assert_equal('Request must be a table', errorStrings[2])
+		assert_equal(nil, errorStrings[3])
+		assert_equal(nil, errorStrings[4])
+
+		assert_equal('Requested GET / with query "page=1"', responses[1]["body"])
+		assert_equal('Cookie set!', responses[2]["body"])
+		assert_equal('12345', responses[2]["cookies"]["session_id"])
+		assert_equal(nil, responses[3])
+		assert_equal(nil, responses[4])
+
+		responses, errors = http.request_batch({
+			{"get", "http://`+listener.Addr().String()+`/get_cookie"}
+		})
+
+		assert_equal(nil, errors)
+		assert_equal("session_id=12345", responses[1]["body"])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestRequestGet(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.request("get", "http://`+listener.Addr().String()+`")
+
+		assert_equal('Requested GET / with query ""', response['body'])
+		assert_equal(200, response['status_code'])
+		assert_equal('29', response['headers']['Content-Length'])
+		assert_equal('text/plain; charset=utf-8', response['headers']['Content-Type'])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestRequestGetWithRedirect(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.request("get", "http://`+listener.Addr().String()+`/redirect")
+
+		assert_equal('Requested GET / with query ""', response['body'])
+		assert_equal(200, response['status_code'])
+		assert_equal('http://`+listener.Addr().String()+`/', response['url'])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestRequestPostForm(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.request("post", "http://`+listener.Addr().String()+`", {
+			form="username=bob&password=secret"
+		})
+
+		assert_equal(
+			'Requested POST / with query ""' ..
+			'Content-Type: application/x-www-form-urlencoded' ..
+			'Content-Length: 28' ..
+			'Body: username=bob&password=secret', response['body'])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestRequestHeaders(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.request("post", "http://`+listener.Addr().String()+`", {
+			headers={
+				["Content-Type"]="application/json"
+			}
+		})
+
+		assert_equal(
+			'Requested POST / with query ""' ..
+			'Content-Type: application/json' ..
+			'Content-Length: 0' ..
+			'Body: ', response['body'])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestRequestQuery(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.request("get", "http://`+listener.Addr().String()+`", {
+			query="page=2"
+		})
+
+		assert_equal('Requested GET / with query "page=2"', response['body'])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestGet(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.get("http://`+listener.Addr().String()+`", {
+			query="page=1"
+		})
+
+		assert_equal('Requested GET / with query "page=1"', response['body'])
+		assert_equal(200, response['status_code'])
+		assert_equal('35', response['headers']['Content-Length'])
+		assert_equal('text/plain; charset=utf-8', response['headers']['Content-Type'])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestDelete(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.delete("http://`+listener.Addr().String()+`", {
+			query="page=1"
+		})
+
+		assert_equal('Requested DELETE / with query "page=1"', response['body'])
+		assert_equal(200, response['status_code'])
+		assert_equal('38', response['headers']['Content-Length'])
+		assert_equal('text/plain; charset=utf-8', response['headers']['Content-Type'])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestHead(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.head("http://`+listener.Addr().String()+`/head", {
+			query="page=1"
+		})
+
+		assert_equal(200, response['status_code'])
+		assert_equal("/head?page=1", response['headers']['X-Request-Uri'])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestPost(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.post("http://`+listener.Addr().String()+`", {
+			body="username=bob&password=secret"
+		})
+
+		assert_equal(
+			'Requested POST / with query ""' ..
+			'Content-Type: ' ..
+			'Content-Length: 28' ..
+			'Body: username=bob&password=secret', response['body'])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestPatch(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.patch("http://`+listener.Addr().String()+`", {
+			body='{"username":"bob"}',
+			headers={
+				["Content-Type"]="application/json"
+			}
+		})
+
+		assert_equal(
+			'Requested PATCH / with query ""' ..
+			'Content-Type: application/json' ..
+			'Content-Length: 18' ..
+			'Body: {"username":"bob"}', response['body'])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestPut(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.put("http://`+listener.Addr().String()+`", {
+			body="username=bob&password=secret",
+			headers={
+				["Content-Type"]="application/x-www-form-urlencoded"
+			}
+		})
+
+		assert_equal(
+			'Requested PUT / with query ""' ..
+			'Content-Type: application/x-www-form-urlencoded' ..
+			'Content-Length: 28' ..
+			'Body: username=bob&password=secret', response['body'])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestResponseCookies(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.get("http://`+listener.Addr().String()+`/set_cookie")
+
+		assert_equal('Cookie set!', response["body"])
+		assert_equal('12345', response["cookies"]["session_id"])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestRequestCookies(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.get("http://`+listener.Addr().String()+`/get_cookie", {
+			cookies={
+				["session_id"]="test"
+			}
+		})
+
+		assert_equal('session_id=test', response["body"])
+		assert_equal(15, response["body_size"])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestResponseBodySize(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.get("http://`+listener.Addr().String()+`/")
+
+		assert_equal(29, response["body_size"])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestResponseBody(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+		response, error = http.get("http://`+listener.Addr().String()+`/")
+
+		assert_equal("Requested XXX / with query \"\"", string.gsub(response.body, "GET", "XXX"))
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func TestResponseUrl(t *testing.T) {
+	listener, _ := net.Listen("tcp", "127.0.0.1:0")
+	setupServer(listener)
+
+	if err := evalLua(t, `
+		local http = require("http")
+
+		response, error = http.get("http://`+listener.Addr().String()+`/redirect")
+		assert_equal("http://`+listener.Addr().String()+`/", response["url"])
+
+		response, error = http.get("http://`+listener.Addr().String()+`/get_cookie")
+		assert_equal("http://`+listener.Addr().String()+`/get_cookie", response["url"])
+	`); err != nil {
+		t.Errorf("Failed to evaluate script: %s", err)
+	}
+}
+
+func evalLua(t *testing.T, script string) error {
+	L := lua.NewState()
+	defer L.Close()
+
+	cookieJar, _ := cookiejar.New(nil)
+
+	L.PreloadModule("http", NewHttpModule(&http.Client{
+		Jar: cookieJar,
+	},
+	).Loader)
+
+	L.SetGlobal("assert_equal", L.NewFunction(func(L *lua.LState) int {
+		expected := L.Get(1)
+		actual := L.Get(2)
+
+		if expected.Type() != actual.Type() || expected.String() != actual.String() {
+			t.Errorf("Expected %s %q, got %s %q", expected.Type(), expected, actual.Type(), actual)
+		}
+
+		return 0
+	}))
+
+	L.SetGlobal("assert_contains", L.NewFunction(func(L *lua.LState) int {
+		contains := L.Get(1)
+		actual := L.Get(2)
+
+		if !strings.Contains(actual.String(), contains.String()) {
+			t.Errorf("Expected %s %q contains %s %q", actual.Type(), actual, contains.Type(), contains)
+		}
+
+		return 0
+	}))
+
+	return L.DoString(script)
+}
+
+func setupServer(listener net.Listener) {
+	mux := http.NewServeMux()
+	mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+		fmt.Fprintf(w, "Requested %s / with query %q", req.Method, req.URL.RawQuery)
+
+		if req.Method == "POST" || req.Method == "PATCH" || req.Method == "PUT" {
+			body, _ := ioutil.ReadAll(req.Body)
+			fmt.Fprintf(w, "Content-Type: %s", req.Header.Get("Content-Type"))
+			fmt.Fprintf(w, "Content-Length: %s", req.Header.Get("Content-Length"))
+			fmt.Fprintf(w, "Body: %s", body)
+		}
+	})
+	mux.HandleFunc("/head", func(w http.ResponseWriter, req *http.Request) {
+		if req.Method == "HEAD" {
+			w.Header().Set("X-Request-Uri", req.URL.String())
+			w.WriteHeader(http.StatusOK)
+		} else {
+			w.WriteHeader(http.StatusNotFound)
+		}
+	})
+	mux.HandleFunc("/set_cookie", func(w http.ResponseWriter, req *http.Request) {
+		http.SetCookie(w, &http.Cookie{Name: "session_id", Value: "12345"})
+		fmt.Fprint(w, "Cookie set!")
+	})
+	mux.HandleFunc("/get_cookie", func(w http.ResponseWriter, req *http.Request) {
+		session_id, _ := req.Cookie("session_id")
+		fmt.Fprint(w, session_id)
+	})
+	mux.HandleFunc("/redirect", func(w http.ResponseWriter, req *http.Request) {
+		http.Redirect(w, req, "/", http.StatusFound)
+	})
+	s := &http.Server{
+		Handler: mux,
+	}
+	go s.Serve(listener)
+}

+ 98 - 0
common/src/github.com/cjoudrey/gluahttp/httpresponsetype.go

@@ -0,0 +1,98 @@
+package gluahttp
+
+import "github.com/yuin/gopher-lua"
+import "net/http"
+
+const luaHttpResponseTypeName = "http.response"
+
+type luaHttpResponse struct {
+	res      *http.Response
+	body     lua.LString
+	bodySize int
+}
+
+func registerHttpResponseType(module *lua.LTable, L *lua.LState) {
+	mt := L.NewTypeMetatable(luaHttpResponseTypeName)
+	L.SetField(mt, "__index", L.NewFunction(httpResponseIndex))
+
+	L.SetField(module, "response", mt)
+}
+
+func newHttpResponse(res *http.Response, body *[]byte, bodySize int, L *lua.LState) *lua.LUserData {
+	ud := L.NewUserData()
+	ud.Value = &luaHttpResponse{
+		res:      res,
+		body:     lua.LString(*body),
+		bodySize: bodySize,
+	}
+	L.SetMetatable(ud, L.GetTypeMetatable(luaHttpResponseTypeName))
+	return ud
+}
+
+func checkHttpResponse(L *lua.LState) *luaHttpResponse {
+	ud := L.CheckUserData(1)
+	if v, ok := ud.Value.(*luaHttpResponse); ok {
+		return v
+	}
+	L.ArgError(1, "http.response expected")
+	return nil
+}
+
+func httpResponseIndex(L *lua.LState) int {
+	res := checkHttpResponse(L)
+
+	switch L.CheckString(2) {
+	case "headers":
+		return httpResponseHeaders(res, L)
+	case "cookies":
+		return httpResponseCookies(res, L)
+	case "status_code":
+		return httpResponseStatusCode(res, L)
+	case "url":
+		return httpResponseUrl(res, L)
+	case "body":
+		return httpResponseBody(res, L)
+	case "body_size":
+		return httpResponseBodySize(res, L)
+	}
+
+	return 0
+}
+
+func httpResponseHeaders(res *luaHttpResponse, L *lua.LState) int {
+	headers := L.NewTable()
+	for key, _ := range res.res.Header {
+		headers.RawSetString(key, lua.LString(res.res.Header.Get(key)))
+	}
+	L.Push(headers)
+	return 1
+}
+
+func httpResponseCookies(res *luaHttpResponse, L *lua.LState) int {
+	cookies := L.NewTable()
+	for _, cookie := range res.res.Cookies() {
+		cookies.RawSetString(cookie.Name, lua.LString(cookie.Value))
+	}
+	L.Push(cookies)
+	return 1
+}
+
+func httpResponseStatusCode(res *luaHttpResponse, L *lua.LState) int {
+	L.Push(lua.LNumber(res.res.StatusCode))
+	return 1
+}
+
+func httpResponseUrl(res *luaHttpResponse, L *lua.LState) int {
+	L.Push(lua.LString(res.res.Request.URL.String()))
+	return 1
+}
+
+func httpResponseBody(res *luaHttpResponse, L *lua.LState) int {
+	L.Push(res.body)
+	return 1
+}
+
+func httpResponseBodySize(res *luaHttpResponse, L *lua.LState) int {
+	L.Push(lua.LNumber(res.bodySize))
+	return 1
+}