-
Notifications
You must be signed in to change notification settings - Fork 0
/
api.lua
237 lines (212 loc) · 6.63 KB
/
api.lua
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
local api_base = {}
local http_api = ...
local cache_time = tonumber(block_vps.get_setting("block_vps_cache_time")) or 30000 -- ~8 hours
local max_try_count = tonumber(block_vps.get_setting("block_vps_max_try")) or 3 -- how many API do we try to use before aborting
local enabled_sources = string.split(block_vps.get_setting("block_vps_datasources")
or "iphub, iphub_legacy, nastyhosts", ",")
local function gen_stub(name)
return function(self) error("'" .. name .. "' must be implemented") end
end
function busy_wait(s)
local ntime = os.clock() + s
repeat until os.clock() > ntime
end
api_base.generate_request = gen_stub("generate_request")
api_base.handle_response_data = gen_stub("handle_response_data")
function api_base:is_api_available()
return not self.temp_disable
end
function api_base:is_data_stale(data)
return (data.last_update + cache_time) < os.time()
end
function api_base:sync_http_fetch(request)
local handle = http_api.fetch_async(request)
local res = http_api.fetch_async_get(handle)
local time_taken = 0
while not res.completed do
busy_wait(0.005)
time_taken = time_taken + 0.005
if time_taken > 1 then
return { succeeded=false, timeout=true }
end
res = http_api.fetch_async_get(handle)
end
return res
end
-- a bit messy there should be an easier way to implement this but this works
local recent_max_size = 15
local function record_request_result(self, result)
if not self.recent_requests then
self.recent_requests = {}
for i = 1, recent_max_size do
self.recent_requests[i] = 1
end
end
if not self.current_offset then self.current_offset = 1 end
self.recent_requests[self.current_offset] = result
self.current_offset = self.current_offset + 1
if self.current_offset > recent_max_size then
self.current_offset = 1
end
end
local function get_recent_success_rate(self)
local total = 0
for _, v in ipairs(self.recent_requests) do
total = total + v
end
return recent_max_size / total
end
function api_base:is_response_valid(response)
if response.succeeded == false or response.code ~= 200 then
record_request_result(self, 0)
if response.timeout then
core.log("error", "[block_vps] Getting IP info took too long.")
else
core.log("error", "[block_vps] Failed to look up ip address, error code :" .. tostring(response.code))
end
local success_rate = get_recent_success_rate(self)
if success_rate < 0.6 then
self.recent_requests = {}
self.temp_disable = true
core.after(1200, function() self.temp_disable = false end)
core.log("error", "[block_vps] disabled '" + self.name + "' for 20 minutes due to high look up failure rate ( > 40%)")
elseif success_rate <= 0.8 then
self.temp_disable = true
core.after(300, function() self.temp_disable = false end)
core.log("warning", "[block_vps] disabled '" + self.name + "' for 5 minutes due to high look up failure rate ( >= 20%)")
end
return false
end
record_request_result(self, 1)
return true
end
local function gen_request(self, ip)
local request = self:generate_request(ip)
assert(type(request) == "table" and type(request.url) == "string",
"generate_request must return a table compatible with the HTTP API")
return request
end
local function handle_response(self, response, ip)
if self:is_response_valid(response) then
local info = self:handle_response_data(ip, response.data)
if info and not info.last_update then
info.last_update = os.time()
info.api = self.name
end
return info
else
return nil
end
end
function api_base:get_ip_info_sync(ip)
local response = self:sync_http_fetch(gen_request(self, ip))
return handle_response(self, response, ip)
end
local in_progress = {}
function api_base:get_ip_info_async(ip, callback, ...)
local request = gen_request(self, ip)
in_progress[http_api.fetch_async(request)] = {callback = callback, ip = ip, api = self, arg = {...}}
end
local timer = 0
core.register_globalstep(function(dtime)
timer = timer + dtime
if timer >= 0.5 then
timer = 0
for k,v in pairs(in_progress) do
local res = http_api.fetch_async_get(k)
if res.completed then
local info = handle_response(v.api, res, v.ip)
v.callback(v.ip, info, unpack(v.arg))
in_progress[k] = nil
end
end
end
end)
local datasources = {}
-- todo: add security checks to this and allow other mods to use it
function block_vps.register_datasource(name, api)
setmetatable(api, {__index = api_base})
api.name = name
datasources[name] = api
end
local function get_datasource(ignore)
for _, name in ipairs(enabled_sources) do
name = name:trim()
local skip = false
for _, ignored in ipairs(ignore) do
if ignored == name then
skip = true
end
end
if not skip then
local current_source = datasources[name]
if current_source and current_source:is_api_available() then
return current_source, name
end
end
end
core.log("error", "[block_vps] No datasource is currently usable.")
end
local ip_info_cache = {}
function block_vps.get_ip_info_sync(ip)
-- Check if we already looked up that IP recently and return from cache
local info = ip_info_cache[ip]
if info then
local source = datasources[info.api]
if not source:is_data_stale(info) then
return info
end
end
local ignored_datasources = {}
local try_count = 1
while true do
-- Get the API
local source, name = get_datasource(ignored_datasources)
if not source then
return nil -- ran out of working APIs...
end
local info = source:get_ip_info_sync(ip)
if info then
ip_info_cache[ip] = info
return info
else
try_count = try_count + 1
if try_count > max_try_count then
return nil -- too many attempts aborting
end
table.insert(ignored_datasources, name)
end
end
end
local function get_info_async(ip, callback, try_count, ignored_datasources, ...)
local source, name = get_datasource(ignored_datasources)
if not source then
callback(ip, nil, ...) -- ran out of working APIs...
end
source:get_ip_info_async(ip, function(ip, ip_info, ...)
try_count = try_count + 1
if not ip_info and try_count <= max_try_count then
table.insert(ignored_datasources, name)
get_info_async(ip, callback, try_count, ignored_datasources, ...)
else
-- might be better to return stale information if available?
ip_info_cache[ip] = ip_info
callback(ip, ip_info, ...)
end
end,
...)
end
function block_vps.get_ip_info(ip, callback, ...)
-- Check if we already looked up that IP recently and return from cache
local info = ip_info_cache[ip]
if info then
local source = datasources[info.api]
if not source:is_data_stale(info) then
callback(ip, info, ...)
return
end
end
local ignored_datasources = {}
local try_count = 1
get_info_async(ip, callback, try_count, ignored_datasources, ...)
end