| 1 | -- Prosody IM |
| 2 | -- Copyright (C) 2008-2010 Matthew Wild |
| 3 | -- Copyright (C) 2008-2010 Waqas Hussain |
| 4 | -- |
| 5 | -- This project is MIT/X11 licensed. Please see the |
| 6 | -- COPYING file in the source package for more information. |
| 7 | -- |
| 8 | |
| 9 | |
| 10 | local socket = require "socket" |
| 11 | local mime = require "mime" |
| 12 | local url = require "socket.url" |
| 13 | local httpstream_new = require "util.httpstream".new; |
| 14 | |
| 15 | local server = require "net.server" |
| 16 | |
| 17 | local connlisteners_get = require "net.connlisteners".get; |
| 18 | local listener = connlisteners_get("httpclient") or error("No httpclient listener!"); |
| 19 | |
| 20 | local t_insert, t_concat = table.insert, table.concat; |
| 21 | local pairs, ipairs = pairs, ipairs; |
| 22 | local tonumber, tostring, xpcall, select, debug_traceback, char, format = |
| 23 | tonumber, tostring, xpcall, select, debug.traceback, string.char, string.format; |
| 24 | |
| 25 | local log = require "util.logger".init("http"); |
| 26 | |
| 27 | module "http" |
| 28 | |
| 29 | function urlencode(s) return s and (s:gsub("%W", function (c) return format("%%%02x", c:byte()); end)); end |
| 30 | function urldecode(s) return s and (s:gsub("%%(%x%x)", function (c) return char(tonumber(c,16)); end)); end |
| 31 | |
| 32 | local function _formencodepart(s) |
| 33 | return s and (s:gsub("%W", function (c) |
| 34 | if c ~= " " then |
| 35 | return format("%%%02x", c:byte()); |
| 36 | else |
| 37 | return "+"; |
| 38 | end |
| 39 | end)); |
| 40 | end |
| 41 | function formencode(form) |
| 42 | local result = {}; |
| 43 | for _, field in ipairs(form) do |
| 44 | t_insert(result, _formencodepart(field.name).."=".._formencodepart(field.value)); |
| 45 | end |
| 46 | return t_concat(result, "&"); |
| 47 | end |
| 48 | |
| 49 | local function request_reader(request, data, startpos) |
| 50 | if not request.parser then |
| 51 | local function success_cb(r) |
| 52 | if request.callback then |
| 53 | for k,v in pairs(r) do request[k] = v; end |
| 54 | request.callback(r.body, r.code, request); |
| 55 | request.callback = nil; |
| 56 | end |
| 57 | destroy_request(request); |
| 58 | end |
| 59 | local function error_cb(r) |
| 60 | if request.callback then |
| 61 | request.callback(r or "connection-closed", 0, request); |
| 62 | request.callback = nil; |
| 63 | end |
| 64 | destroy_request(request); |
| 65 | end |
| 66 | local function options_cb() |
| 67 | return request; |
| 68 | end |
| 69 | request.parser = httpstream_new(success_cb, error_cb, "client", options_cb); |
| 70 | end |
| 71 | request.parser:feed(data); |
| 72 | end |
| 73 | |
| 74 | local function handleerr(err) log("error", "Traceback[http]: %s: %s", tostring(err), debug_traceback()); end |
| 75 | function request(u, ex, callback) |
| 76 | local req = url.parse(u); |
| 77 | |
| 78 | if not (req and req.host) then |
| 79 | callback(nil, 0, req); |
| 80 | return nil, "invalid-url"; |
| 81 | end |
| 82 | |
| 83 | if not req.path then |
| 84 | req.path = "/"; |
| 85 | end |
| 86 | |
| 87 | local custom_headers, body; |
| 88 | local default_headers = { ["Host"] = req.host, ["User-Agent"] = "Prosody XMPP Server" } |
| 89 | |
| 90 | |
| 91 | if req.userinfo then |
| 92 | default_headers["Authorization"] = "Basic "..mime.b64(req.userinfo); |
| 93 | end |
| 94 | |
| 95 | if ex then |
| 96 | custom_headers = ex.headers; |
| 97 | req.onlystatus = ex.onlystatus; |
| 98 | body = ex.body; |
| 99 | if body then |
| 100 | req.method = "POST "; |
| 101 | default_headers["Content-Length"] = tostring(#body); |
| 102 | default_headers["Content-Type"] = "application/x-www-form-urlencoded"; |
| 103 | end |
| 104 | if ex.method then req.method = ex.method; end |
| 105 | end |
| 106 | |
| 107 | req.handler, req.conn = server.wrapclient(socket.tcp(), req.host, req.port or 80, listener, "*a"); |
| 108 | req.write = function (...) return req.handler:write(...); end |
| 109 | req.conn:settimeout(0); |
| 110 | local ok, err = req.conn:connect(req.host, req.port or 80); |
| 111 | if not ok and err ~= "timeout" then |
| 112 | callback(nil, 0, req); |
| 113 | return nil, err; |
| 114 | end |
| 115 | |
| 116 | local request_line = { req.method or "GET", " ", req.path, " HTTP/1.1\r\n" }; |
| 117 | |
| 118 | if req.query then |
| 119 | t_insert(request_line, 4, "?"); |
| 120 | t_insert(request_line, 5, req.query); |
| 121 | end |
| 122 | |
| 123 | req.write(t_concat(request_line)); |
| 124 | local t = { [2] = ": ", [4] = "\r\n" }; |
| 125 | if custom_headers then |
| 126 | for k, v in pairs(custom_headers) do |
| 127 | t[1], t[3] = k, v; |
| 128 | req.write(t_concat(t)); |
| 129 | default_headers[k] = nil; |
| 130 | end |
| 131 | end |
| 132 | |
| 133 | for k, v in pairs(default_headers) do |
| 134 | t[1], t[3] = k, v; |
| 135 | req.write(t_concat(t)); |
| 136 | default_headers[k] = nil; |
| 137 | end |
| 138 | req.write("\r\n"); |
| 139 | |
| 140 | if body then |
| 141 | req.write(body); |
| 142 | end |
| 143 | |
| 144 | req.callback = function (content, code, request) log("debug", "Calling callback, status %s", code or "---"); return select(2, xpcall(function () return callback(content, code, request) end, handleerr)); end |
| 145 | req.reader = request_reader; |
| 146 | req.state = "status"; |
| 147 | |
| 148 | listener.register_request(req.handler, req); |
| 149 | |
| 150 | return req; |
| 151 | end |
| 152 | |
| 153 | function destroy_request(request) |
| 154 | if request.conn then |
| 155 | request.conn = nil; |
| 156 | request.handler:close() |
| 157 | listener.ondisconnect(request.handler, "closed"); |
| 158 | end |
| 159 | end |
| 160 | |
| 161 | _M.urlencode = urlencode; |
| 162 | |
| 163 | return _M; |