aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--README.md95
-rwxr-xr-xmirror-checker.lua420
2 files changed, 515 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..415025c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,95 @@
1Lua script for checking the health of Devuan Linux package mirrors.
2
3This is currently under development, not everything has been written yet.
4
5mirror-checker-lua is a Lua script used by the Devuan mirror admins
6(maybe, if they like it) to check the health of Devuan Linux package
7mirrors. Originally there was bash scripts for this job, then Evilham
8wrote some Python scripts, now onefang has written it in Lua. We all
9have different tastes in languages. lol
10
11The main difference is that this Lua version tries to do everything, and
12will be maintained. Currently the shell scripts and Python scripts are
13actually being used I think. Evilham asked me to write this, after I
14badgered him about his Python scripts.
15
16The source code is at https://sledjhamr.org/cgit/mirror-checker-lua/
17
18The issue tracker is at https://sledjhamr.org/mantisbt/project_page.php?project_id=13
19
20
21Installation.
22-------------
23
24Download the source. You may want to put the actual mirror-checker.lua
25in someplace liku /usr/local/bin and make sure it is executable.
26
27It should run on any recent Linux, you'll need to have the following
28installed -
29
30Luajit
31
32wget
33
34dig, part of BIND. On Debian based systems it'll be in the dnsutils
35package.
36
37LuaSocket, on Debian based systems it'll be in the lua-socket package.
38
39ionice, on Debian based systems it'll be in the util-linux package.
40
41
42Using it.
43---------
44
45These examples assume you are running it from the source code directory.
46A directory will be created called results, it'll be full of log files
47and any files that get downloaded. There will also be results/email and
48results/web directories, with the notification email and web pages with
49the easy to read emails (once I write that bit).
50
51Note that unlike typical commands, you can't run single character options
52together, so this is wrong -
53
54./mirror-checker.lua -vvv
55
56Instead do this -
57
58./mirror-checker.lua -v -v -v
59
60Just run the script to do all of the tests -
61
62./mirror-checker.lua
63
64Which will print any errors. If you don't want to see errors -
65
66./mirror-checker.lua -q
67
68If you want to see warnings to (as usual, the more -v options, the more
69details) -
70
71./mirror-checker.lua -v
72
73Or use the usual options for the help and version number -
74
75./mirror-checker.lua -h
76./mirror-checker.lua --help
77./mirror-checker.lua --version
78
79To run the tests on a specific mirror, for example pkgmaster.devuan.org -
80
81./mirror-checker.lua pkgmaster.devuan.org
82
83You can use the --tests option to tune which tests are run, for example
84to stop IPv6 tests, coz you don't have IPv6 -
85
86./mirror-checker.lua --tests=-IPv6
87
88To do the same, but not run the HTTPS tests either -
89
90./mirror-checker.lua --tests=-IPv6,-https
91
92To only run the http integrity tests, only on IPv6 -
93
94./mirror-checker.lua --tests=http,Integrity,IPv6
95
diff --git a/mirror-checker.lua b/mirror-checker.lua
new file mode 100755
index 0000000..34b2ec8
--- /dev/null
+++ b/mirror-checker.lua
@@ -0,0 +1,420 @@
1#!/usr/bin/env luajit
2
3
4local args = {...}
5
6
7verbosity = 0
8keep = false
9options =
10{
11 referenceSite =
12 {
13 typ = "string",
14 help = "",
15 value = "pkgmaster.devuan.org",
16 },
17 tests =
18 {
19 typ = "table",
20 help = "",
21 value =
22 {
23 "IPv4",
24 "IPv6",
25-- "ftp",
26 "http",
27 "https",
28-- "rsync",
29 "DNS-RR",
30 "Protocol",
31-- "URL-Sanity",
32-- "Integrity",
33-- "Updated",
34 },
35 },
36}
37
38local defaultURL = {scheme = "http"}
39local download = "wget -np -N -r -P results "
40local releases = {"jessie", "ascii", "beowulf", "ceres"}
41local releaseFiles =
42{
43 -- Release file.
44 "/Release",
45 "/InRelease",
46 "/main/binary-all/Packages.gz",
47 -- Contents files.
48 "/main/Contents-all.gz",
49 "/main/Contents-amd64.gz",
50 "/main/Contents-arm64.gz",
51 "-security/main/Contents-all.gz",
52 "-security/main/Contents-amd64.gz",
53 "-security/main/Contents-arm64.gz",
54}
55local referenceDebs =
56{
57 -- Devuan package. NOTE this one likely should not get redirected, but that's more a warning than an error.
58 "merged/pool/DEVUAN/main/d/desktop-base/desktop-base_2.0.3_all.deb",
59 -- Debian package.
60 "merged/pool/DEBIAN/main/d/dash/dash_0.5.8-2.4_amd64.deb",
61 -- Debian security package. NOTE this one should always be redirected?
62 "merged/pool/DEBIAN-SECURITY/updates/main/a/apt/apt-transport-https_1.4.9_amd64.deb",
63}
64local arg = {}
65local sendArgs = ""
66local log
67
68
69local socket = require 'socket'
70local ftp = require 'socket.ftp'
71local http = require 'socket.http'
72local url = require 'socket.url'
73
74
75-- Use this to print a table.
76printTable = function (table, space, name)
77 print(space .. name .. ": ")
78 print(space .. "{")
79 printTableSub(table, space .. " ")
80 print(space .. "}")
81end
82printTableSub = function (table, space)
83 for k, v in pairs(table) do
84 if type(v) == "table" then
85 printTable(v, space, k)
86 elseif type(v) == "string" then
87 print(space .. k .. ': "' .. v .. '";')
88 elseif type(v) == "function" then
89 print(space .. "function " .. k .. "();")
90 elseif type(v) == "userdata" then
91 print(space .. "userdata " .. k .. ";")
92 elseif type(v) == "boolean" then
93 if (v) then
94 print(space .. "boolean " .. k .. " TRUE ;")
95 else
96 print(space .. "boolean " .. k .. " FALSE ;")
97 end
98 else
99 print(space .. k .. ": " .. v .. ";")
100 end
101 end
102end
103
104local log = function(v, t, s)
105 if v <= verbosity then
106 if 3 <= verbosity then t = os.date() .. " " .. t end
107 print(t .. ": " .. s)
108 end
109 if nil ~= log then
110 log:write(os.date() .. " " .. t .. ": " .. s .. "/n")
111 log:flush()
112 end
113end
114local D = function(s) log(3, "DEBUG ", s) end
115local I = function(s) log(2, "INFO ", s) end
116local W = function(s) log(1, "WARNING ", s) end
117local E = function(s) log(0, "ERROR ", s) end
118local C = function(s) log(-1, "CRITICAL", s) end
119
120local testing = function(t)
121 for i, v in pairs(options.tests.value) do
122 if t == v then return true end
123 end
124 return false
125end
126
127local checkExes = function (exe)
128 local count = io.popen('ps x | grep "' .. exe .. '" | grep -v " grep " | wc -l'):read("*l")
129 D(count .. " " .. exe .. " commands left.")
130 return tonumber(count)
131end
132
133
134local IP = {}
135local gatherIPs = function (host)
136 if nil == IP[host] then
137 local IPs
138 local dig = io.popen('dig +keepopen +noall +nottlid +answer ' .. host .. ' A ' .. host .. ' AAAA ' .. host .. ' CNAME ' .. host .. ' SRV | sort | uniq')
139 repeat
140 IPs = dig:read("*l")
141 if nil ~= IPs then
142 for k, t, v in IPs:gmatch("([%w_%-%.]*)%.%s*IN%s*(%a*)%s*(.*)") do
143 if "." == v:sub(-1, -1) then v = v:sub(1, -2) end
144 if nil == IP[k] then IP[k] = {} end
145 IP[k][v] = t
146 end
147 end
148 until nil == IPs
149 end
150end
151
152-- Returns FTP directory listing
153local nlst = function (u)
154 local t = {}
155 local p = url.parse(u)
156 p.command = "nlst"
157 p.sink = ltn12.sink.table(t)
158 local r, e = ftp.get(p)
159 return r and table.concat(t), e
160end
161
162checkURL = function (host, URL, r)
163 if nil == r then r = 0 end
164 local check = "Checking file"
165 if 0 < r then
166 check = "Redirecting to"
167-- checkIP(host)
168 end
169 if 10 < r then
170 E("too many redirects! " .. check .. " " .. host .. " -> " .. URL)
171 return
172 end
173 local PU = url.parse(URL, defaultURL)
174 D(" " .. PU.scheme .. " :// " .. check .. " " .. host .. " -> " .. URL)
175 if not testing(PU.scheme) then D("not testing " .. PU.scheme .. " " .. host .. " -> " .. URL); return end
176 local hd = {Host = host}
177 local p, c, h, s = http.request{method = "HEAD", redirect = false, url = URL, headers = hd}
178 if nil == p then E(c .. "! " .. check .. " " .. host .. " -> " .. URL) else
179 l = h.location
180 if nil ~= l then
181 local pu = url.parse(l, defaultURL)
182 if l == URL then
183 E("redirect loop! " .. check .. " " .. host .. " -> " .. URL)
184 else
185 if nil == pu.host then
186 W("no location host! " .. check .. " " .. host .. " -> " .. URL .. " -> " .. l)
187 checkURL(host, PU.scheme .. "://" .. PU.host .. l, r + 1)
188 else
189 if testing("Protocol") and pu.scheme ~= PU.scheme then
190 W("protocol changed during redirect! " .. check .. " " .. host .. " -> " .. URL .. " -> " .. l)
191 end
192 checkURL(pu.host, l, r + 1)
193 end
194 end
195 end
196 end
197end
198
199local checkPaths = function (host, ip, path)
200 I(" Checking IP " .. host .. " -> " .. ip .. " " .. path)
201 for i, s in pairs(referenceDebs) do
202 if testing("http") then checkURL(host, "http://" .. ip .. path .. s) end
203 if testing("https") then checkURL(host, "https://" .. ip .. path .. s) end
204 end
205
206 for i, s in pairs(releases) do
207 for j, k in pairs(releaseFiles) do
208 if testing("http") then checkURL(host, "http://" .. ip .. path .. "merged/dists/" .. s .. k) end
209 if testing("https") then checkURL(host, "https://" .. ip .. path .. "merged/dists/" .. s .. k) end
210 end
211 end
212end
213
214local execute = function (s)
215 D("executing " .. s)
216 os.execute(s)
217end
218
219forkIP = function (orig, host)
220 if nil == host then host = orig end
221 local po = url.parse("http://" .. orig, defaultURL)
222 local ph = url.parse("http://" .. host, defaultURL)
223 gatherIPs(ph.host)
224 for k, v in pairs(IP[ph.host]) do
225 if v == "A" then
226 if testing("IPv4") then execute("sleep 1; ionice -c3 ./mirror-checker.lua " .. sendArgs .. " " .. host .. " " .. k .. " &") end
227 elseif v == "AAAA" then
228 if testing("IPv6") then execute("sleep 1; ionice -c3 ./mirror-checker.lua " .. sendArgs .. " " .. host .. " [" .. k .. "] &") end
229 elseif v == "CNAME" then
230 forkIP(orig, k)
231 end
232 end
233end
234
235checkIP = function (orig, host, path, ip)
236 if nil ~= ip then
237 checkPaths(orig, ip, path)
238 else
239 D("checkIP " .. orig .. " " .. host)
240 gatherIPs(host)
241 for k, v in pairs(IP[host]) do
242 if v == "A" then
243 if testing("IPv4") then checkPaths(orig, k, path) end
244 elseif v == "AAAA" then
245 if testing("IPv6") then checkPaths(orig, "[" .. k .. "]", path) end
246 elseif v == "CNAME" then
247 checkIP(orig, k, path)
248 end
249 end
250 end
251end
252
253local checkHost = function (host, path, ip)
254 if nil == path then path = "/" else
255 if "/" == path:sub(-1, -1) then
256 W("slash at end of BaseURL in mirror_list.txt! " .. host .. " " .. path)
257 else
258 path = path .. "/"
259 end
260 end
261 checkIP(host, host, path, ip)
262end
263
264local downloads = function (host, URL, IP)
265 if nil == URL then URL = "/" end
266 if nil == IP then IP = "" else IP = "-" .. IP end
267 local log = " --rejected-log=results/wget-%s_REJECTS-" .. host .. IP .. ".log -a results/wget-%s-" .. host .. IP ..".log "
268 I("starting file download commands for " .. host .. " " .. URL)
269 local cm = "ionice -c3 " .. download .. log:format("debs", "debs")
270 for i, s in pairs(referenceDebs) do
271 cm = cm .. " https://" .. host .. URL .. "/" .. s
272 end
273 for i, s in pairs(releases) do
274 execute(cm .. " &")
275 cm = "ionice -c3 " .. download .. log:format(s, s)
276 for j, k in pairs(releaseFiles) do
277 cm = cm .. " https://" .. host .. URL .. "/merged/dists/" .. s .. k
278 end
279 end
280 execute(cm .. " &")
281end
282
283local getMirrors = function ()
284 local mirrors = {}
285 local host = ""
286 local m = {}
287 local URL = "https://" .. options.referenceSite.value .. "/mirror_list.txt"
288 I("getting mirrors.")
289 local p, c, h = http.request(URL)
290 if nil == p then E(c .. " fetching " .. URL) else
291 for l in p:gmatch("\n*([^\n]+)\n*") do
292 local t, d = l:match("(%a*):%s*(.*)")
293 if "FQDN" == t then
294 if "" ~= host then
295 mirrors[host] = m
296 m = {}
297 end
298 host = d
299 end
300 m[t] = d
301 end
302 if "" ~= host then
303 mirrors[host] = m
304 end
305 end
306 return mirrors
307end
308
309
310if 0 ~= #args then
311 local option = ""
312 for i, a in pairs(args) do
313 if ("--help" == a) or ("-h" == a) then
314 print("I should write some docs, huh?")
315 elseif "--version" == a then
316 print("mirror-checker-lua version 0.1 alpha")
317 elseif "-v" == a then
318 verbosity = verbosity + 1
319 sendArgs = sendArgs .. a .. " "
320 elseif "-q" == a then
321 verbosity = -1
322 sendArgs = sendArgs .. a .. " "
323 elseif "-k" == a then
324 keep = true
325 elseif "--" == a:sub(1, 2) then
326 local s, e = a:find("=")
327 if nil == s then e = -1 end
328 option = a:sub(3, e - 1)
329 local o = options[option]
330 if nil == o then
331 print("Unknown option --" .. option)
332 option = ""
333 else
334 option = a
335 sendArgs = sendArgs .. a .. " "
336 local s, e = a:find("=")
337 if nil == s then e = 0 end
338 option = a:sub(3, e - 1)
339 if "table" == options[option].typ then
340 local result = {}
341 for t in (a:sub(e + 1) .. ","):gmatch("([+%-]?%w*),") do
342 local f = t:sub(1, 1)
343 local n = t:sub(2, -1)
344 if ("+" ~= f) and ("-" ~= f) then
345 table.insert(result, t)
346 end
347 end
348 if 0 ~= #result then
349 options[option].value = result
350 else
351 for t in (a:sub(e + 1) .. ","):gmatch("([+%-]?%w*),") do
352 local f = t:sub(1, 1)
353 local n = t:sub(2, -1)
354 if "+" == f then
355 table.insert(options[option].value, n)
356 elseif "-" == f then
357 local r = {}
358 for i, k in pairs(options[option].value) do
359 if k ~= n then table.insert(r, k) end
360 end
361 options[option].value = r
362 end
363 end
364 end
365 else
366 options[option].value = a
367 end
368 option = ""
369 end
370 elseif "-" == a:sub(1, 1) then
371 print("Unknown option " .. a)
372 else
373 table.insert(arg, a)
374 end
375 end
376end
377
378--printTable(options.tests.value, "", "tests")
379
380execute("mkdir -p results")
381
382if 0 < #arg then
383 if nil ~= arg[2]
384 log = io.open ("mirror-checker-lua_" .. arg[1] .. "_" .. arg[2] .. ".log", "a+")
385 else
386 log = io.open ("mirror-checker-lua_" .. arg[1] .. ".log", "a+" [, mode])
387 end
388 local pu = url.parse("http://" .. arg[1], defaultURL)
389 I("Starting tests for " ..pu.host .. " with these tests - " .. table.concat(options.tests.value, ", "))
390 if nil ~= arg[2] then I(" Using IP " .. arg[2]) end
391 if testing("Integrity") or testing("Updated") then
392 if not keep then execute("rm -fr results/" .. pu.host) end
393 downloads(pu.host, pu.path, arg[2])
394 checkExes("mirror-checker.lua " .. sendArgs)
395 checkExes(download)
396 end
397 checkHost(pu.host, pu.path, arg[2])
398else
399 if not keep then os.execute("rm -f results/*.log") end
400 log = io.open ("mirror-checker-lua.log", "a+")
401 I("Starting tests " .. table.concat(options.tests.value, ", "))
402 execute("mkdir -p results")
403 local mirrors = getMirrors()
404 mirrors[options.referenceSite.value] = nil
405-- checkHost(options.referenceSite.value)
406 forkIP(options.referenceSite.value)
407-- checkHost("deb.devuan.org")
408 forkIP("deb.devuan.org")
409 for k, m in pairs(mirrors) do
410 local pu = url.parse("http://" .. m.BaseURL, defaultURL)
411-- checkHost(pu.host)
412 forkIP(m.BaseURL)
413 checkExes("mirror-checker.lua " .. sendArgs)
414 if testing("Integrity") or testing("Updated") then checkExes(download) end
415 end
416 while 1 <= checkExes("mirror-checker.lua " .. sendArgs) do os.execute("sleep 30") end
417 if testing("Integrity") or testing("Updated") then
418 while 1 < checkExes(download) do os.execute("sleep 30") end
419 end
420end