1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
|
-- Copyright 2009 Steven Barth <steven@midlink.org>
-- Licensed to the public under the Apache License 2.0.
require "nixio.util"
local nixio = require "nixio"
local httpc = require "luci.httpclient"
local ltn12 = require "luci.ltn12"
local print, tonumber, require, unpack = print, tonumber, require, unpack
module "luci.httpclient.receiver"
local function prepare_fd(target)
-- Open fd for appending
local oflags = nixio.open_flags("wronly", "creat")
local file, code, msg = nixio.open(target, oflags)
if not file then
return file, code, msg
end
-- Acquire lock
local stat, code, msg = file:lock("tlock")
if not stat then
return stat, code, msg
end
file:seek(0, "end")
return file
end
local function splice_async(sock, pipeout, pipein, file, cb)
local ssize = 65536
local smode = nixio.splice_flags("move", "more", "nonblock")
-- Set pipe non-blocking otherwise we might end in a deadlock
local stat, code, msg = pipein:setblocking(false)
if stat then
stat, code, msg = pipeout:setblocking(false)
end
if not stat then
return stat, code, msg
end
local pollsock = {
{fd=sock, events=nixio.poll_flags("in")}
}
local pollfile = {
{fd=file, events=nixio.poll_flags("out")}
}
local done
local active -- Older splice implementations sometimes don't detect EOS
repeat
active = false
-- Socket -> Pipe
repeat
nixio.poll(pollsock, 15000)
stat, code, msg = nixio.splice(sock, pipeout, ssize, smode)
if stat == nil then
return stat, code, msg
elseif stat == 0 then
done = true
break
elseif stat then
active = true
end
until stat == false
-- Pipe -> File
repeat
nixio.poll(pollfile, 15000)
stat, code, msg = nixio.splice(pipein, file, ssize, smode)
if stat == nil then
return stat, code, msg
elseif stat then
active = true
end
until stat == false
if cb then
cb(file)
end
if not active then
-- We did not splice any data, maybe EOS, fallback to default
return false
end
until done
pipein:close()
pipeout:close()
sock:close()
file:close()
return true
end
local function splice_sync(sock, pipeout, pipein, file, cb)
local os = require "os"
local ssize = 65536
local smode = nixio.splice_flags("move", "more")
local stat
-- This is probably the only forking http-client ;-)
local pid, code, msg = nixio.fork()
if not pid then
return pid, code, msg
elseif pid == 0 then
pipein:close()
file:close()
repeat
stat, code = nixio.splice(sock, pipeout, ssize, smode)
until not stat or stat == 0
pipeout:close()
sock:close()
os.exit(stat or code)
else
pipeout:close()
sock:close()
repeat
stat, code, msg = nixio.splice(pipein, file, ssize, smode)
if cb then
cb(file)
end
until not stat or stat == 0
pipein:close()
file:close()
if not stat then
nixio.kill(pid, 15)
nixio.wait(pid)
return stat, code, msg
else
pid, msg, code = nixio.wait(pid)
if msg == "exited" then
if code == 0 then
return true
else
return nil, code, nixio.strerror(code)
end
else
return nil, -0x11, "broken pump"
end
end
end
end
function request_to_file(uri, target, options, cbs)
options = options or {}
cbs = cbs or {}
options.headers = options.headers or {}
local hdr = options.headers
local file, code, msg
if target then
file, code, msg = prepare_fd(target)
if not file then
return file, code, msg
end
local off = file:tell()
-- Set content range
if off > 0 then
hdr.Range = hdr.Range or ("bytes=" .. off .. "-")
end
end
local code, resp, buffer, sock = httpc.request_raw(uri, options)
if not code then
-- No success
if file then
file:close()
end
return code, resp, buffer
elseif hdr.Range and code ~= 206 then
-- We wanted a part but we got the while file
sock:close()
if file then
file:close()
end
return nil, -4, code, resp
elseif not hdr.Range and code ~= 200 then
-- We encountered an error
sock:close()
if file then
file:close()
end
return nil, -4, code, resp
end
if cbs.on_header then
local stat = {cbs.on_header(file, code, resp)}
if stat[1] == false then
if file then
file:close()
end
sock:close()
return unpack(stat)
elseif stat[2] then
file = file and stat[2]
end
end
if not file then
return nil, -5, "no target given"
end
local chunked = resp.headers["Transfer-Encoding"] == "chunked"
local stat
-- Write the buffer to file
file:writeall(buffer)
repeat
if not options.splice or not sock:is_socket() or chunked then
break
end
-- This is a plain TCP socket and there is no encoding so we can splice
local pipein, pipeout, msg = nixio.pipe()
if not pipein then
sock:close()
file:close()
return pipein, pipeout, msg
end
-- Adjust splice values
local ssize = 65536
local smode = nixio.splice_flags("move", "more")
-- Splicing 512 bytes should never block on a fresh pipe
local stat, code, msg = nixio.splice(sock, pipeout, 512, smode)
if stat == nil then
break
end
-- Now do the real splicing
local cb = cbs.on_write
if options.splice == "asynchronous" then
stat, code, msg = splice_async(sock, pipeout, pipein, file, cb)
elseif options.splice == "synchronous" then
stat, code, msg = splice_sync(sock, pipeout, pipein, file, cb)
else
break
end
if stat == false then
break
end
return stat, code, msg
until true
local src = chunked and httpc.chunksource(sock) or sock:blocksource()
local snk = file:sink()
if cbs.on_write then
src = ltn12.source.chain(src, function(chunk)
cbs.on_write(file)
return chunk
end)
end
-- Fallback to read/write
stat, code, msg = ltn12.pump.all(src, snk)
file:close()
sock:close()
return stat and true, code, msg
end
|