6681f82aaf37317fb43324aaf7b7e6478468e75b
[project/luci.git] / libs / httpclient / luasrc / httpclient.lua
1 --[[
2 LuCI - Lua Configuration Interface
3
4 Copyright 2009 Steven Barth <steven@midlink.org>
5
6 Licensed under the Apache License, Version 2.0 (the "License");
7 you may not use this file except in compliance with the License.
8 You may obtain a copy of the License at
9
10         http://www.apache.org/licenses/LICENSE-2.0
11
12 $Id$
13 ]]--
14
15 require "nixio.util"
16 local nixio = require "nixio"
17
18 local ltn12 = require "luci.ltn12"
19 local util = require "luci.util"
20 local table = require "table"
21 local http = require "luci.http.protocol"
22
23 local type, pairs, tonumber, print = type, pairs, tonumber, print
24
25 module "luci.httpclient"
26
27 function chunksource(sock, buffer)
28         buffer = buffer or ""
29         return function()
30                 local output
31                 local _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
32                 while not count and #buffer <= 1024 do
33                         local newblock, code = sock:recv(1024 - #buffer)
34                         if not newblock then
35                                 return nil, code
36                         end
37                         buffer = buffer .. newblock  
38                         _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
39                 end
40                 count = tonumber(count, 16)
41                 if not count then
42                         return nil, -1, "invalid encoding"
43                 elseif count == 0 then
44                         return nil
45                 elseif count + 2 <= #buffer - endp then
46                         output = buffer:sub(endp+1, endp+count)
47                         buffer = buffer:sub(endp+count+3)
48                         return output
49                 else
50                         output = buffer:sub(endp+1, endp+count)
51                         buffer = ""
52                         if count - #output > 0 then
53                                 local remain, code = sock:recvall(count-#output)
54                                 if not remain then
55                                         return nil, code
56                                 end
57                                 output = output .. remain
58                                 count, code = sock:recvall(2)
59                         else
60                                 count, code = sock:recvall(count+2-#buffer+endp)
61                         end
62                         if not count then
63                                 return nil, code
64                         end
65                         return output
66                 end
67         end
68 end
69
70
71 function request_to_buffer(uri, options)
72         local source, code, msg = request_to_source(uri, options)
73         local output = {}
74         
75         if not source then
76                 return nil, code, msg
77         end
78         
79         source, code = ltn12.pump.all(source, (ltn12.sink.table(output)))
80         
81         if not source then
82                 return nil, code
83         end
84         
85         return table.concat(output)
86 end
87
88 function request_to_source(uri, options)
89         local status, response, buffer, sock = request_raw(uri, options)
90         if not status then
91                 return status, response, buffer
92         elseif status ~= 200 and status ~= 206 then
93                 return nil, status, response
94         end
95         
96         if response["Transfer-Encoding"] == "chunked" then
97                 return chunksource(sock, buffer)
98         else
99                 return ltn12.source.cat(ltn12.source.string(buffer), sock:blocksource())
100         end
101 end
102
103 --
104 -- GET HTTP-resource
105 --
106 function request_raw(uri, options)
107         options = options or {}
108         local pr, host, port, path = uri:match("(%w+)://([%w-.]+):?([0-9]*)(.*)")
109         if not host then
110                 return nil, -1, "unable to parse URI"
111         end
112         
113         if pr ~= "http" then
114                 return nil, -2, "protocol not supported"
115         end
116         
117         port = #port > 0 and port or "80"
118         path = #path > 0 and path or "/"
119         
120         options.depth = options.depth or 10
121         local headers = options.headers or {}
122         local protocol = options.protocol or "HTTP/1.1"
123         local method  = options.method or "GET"
124         headers["User-Agent"] = headers["User-Agent"] or "LuCI httpclient 0.1"
125         
126         if headers.Connection == nil then
127                 headers.Connection = "close"
128         end
129         
130         local sock, code, msg = nixio.connect(host, port)
131         if not sock then
132                 return nil, code, msg
133         end
134         
135         sock:setsockopt("socket", "sndtimeo", options.sndtimeo or 15)
136         sock:setsockopt("socket", "rcvtimeo", options.rcvtimeo or 15)
137         
138         -- Pre assemble fixes   
139         if protocol == "HTTP/1.1" then
140                 headers.Host = headers.Host or host
141         end
142         
143         if type(options.body) == "table" then
144                 options.body = http.urlencode_params(options.body)
145                 headers["Content-Type"] = headers["Content-Type"] or 
146                         "application/x-www-form-urlencoded"
147         end
148
149         if type(options.body) == "string" then
150                 headers["Content-Length"] = headers["Content-Length"] or #options.body
151         end
152         
153         -- Assemble message
154         local message = {method .. " " .. path .. " " .. protocol}
155         
156         for k, v in pairs(headers) do
157                 if v then
158                         message[#message+1] = k .. ": " .. v
159                 end
160         end
161         message[#message+1] = ""
162         message[#message+1] = ""
163         
164         -- Send request
165         sock:sendall(table.concat(message, "\r\n"))
166         
167         if type(options.body) == "string" then
168                 sock:sendall(options.body)
169         end
170         
171         -- Create source and fetch response
172         local linesrc = sock:linesource()
173         local line, code, error = linesrc()
174         
175         if not line then
176                 return nil, code, error
177         end
178         
179         local protocol, status, msg = line:match("^(HTTP/[0-9.]+) ([0-9]+) (.*)")
180         
181         if not protocol then
182                 return nil, -3, "invalid response magic: " .. line
183         end
184         
185         local response = {Status=line}
186         
187         line = linesrc()
188         while line and line ~= "" do
189                 local key, val = line:match("^([%w-]+)%s?:%s?(.*)")
190                 if key and key ~= "Status" then
191                         response[key] = val
192                 end
193                 line = linesrc()
194         end
195         
196         if not line then
197                 return nil, -4, "protocol error"
198         end
199         
200         -- Follow 
201         local code = tonumber(status)
202         if code and options.depth > 0 then
203                 if code == 301 or code == 302 or code == 307 and response.Location then
204                         local nexturi = response.Location
205                         if not nexturi:find("https?://") then
206                                 nexturi = pr .. "://" .. host .. ":" .. port .. nexturi
207                         end
208                         
209                         options.depth = options.depth - 1
210                         sock:close()
211                         
212                         return request_raw(nexturi, options)
213                 end
214         end
215         
216         return code, response, linesrc(true), sock
217 end