-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathhttp_server.lua
More file actions
executable file
·497 lines (445 loc) · 12.8 KB
/
http_server.lua
File metadata and controls
executable file
·497 lines (445 loc) · 12.8 KB
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
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
#!/usr/bin/env lua
local socket = require "socket"
local url = require "socket.url"
---Парсинг конфигов
---@type table<string, boolean|string>
Config = {
port = "8080",
root = ".",
host = "0.0.0.0",
}
for _, a in ipairs(arg) do
local conf, key
if a:match "'" then
conf, key = a:match [[%-%-(%w+)='(.-)']]
elseif a:match '"' then
conf, key = a:match [[%-%-(%w+)="(.-)"]]
else
conf, key = a:match [[%-%-(%w+)=?(%g*)]]
end
if conf then
Config[conf] = (key ~= "") and key or true
end
end
if Config.help then
print([[Usage:
http_server
Args:
--help показать это сообщение
--host=0.0.0.0 хост, который будет прослушивать сервер
--port=8080 порт, который будет прослушивать сервер
--root=<dir> путь к корневой папке файлов сервера
--rules=<file> путь к файлу правил
--sslcert=<file> путь к файлу ssl сертификата
--sslkey=<file> путь к файлу ключа от сертификата
--sslca=<file> путь к файлу корневого сертификата]])
return
end
Codes = {
[200] = "OK",
[301] = "Moved Permanently",
[404] = "Page not found",
[500] = "Internal server error",
}
if string.match(Config.root, "/$") then Config.root = Config.root:sub(1, -2) end
print("[INFO] Root:", Config.root)
if Config.root ~= "." then
package.path = table.concat({
Config.root .. "/?.lua",
Config.root .. "/?/init.lua",
package.path
}, ";")
end
local ssl, ssl_ctx, err
if Config.sslcert and Config.sslkey then
local succ, ssll = pcall(require, "ssl")
if succ then
local params = {
mode = "server",
protocol = "any",
key = Config.sslkey,
certificate = Config.sslcert,
ca = Config.sslca,
verify = { "none" },
options = { "all" },
}
ssl_ctx, err = ssll.newcontext(params)
if ssl_ctx then
ssl = ssll
print("[INFO] OpneSSL enabled")
else
print("[WARN] OpenSSL loading error:", err)
package.loaded.ssl = nil
end
elseif ssll then
print("[WARN] OpennSSL loading error:", ssl)
end
else
print("[INFO] SSL disabled")
end
local recvt = {}
local sendt = {}
function table.search(list, data)
for i, d in ipairs(list) do
if d == data then return i end
end
end
function table.removedata(list, data)
local pos = table.search(list, data)
if pos then
table.remove(list, pos)
end
end
---@type Server[]
local threads = {}
local last_thread = 0
local rules = {}
if type(Config.rules) == "string" then
local rules_file, err = loadfile(Config.rules, "t", _ENV)
if rules_file then
rules = rules_file()
print("[INFO] Rules loaded")
elseif err then
print("[ERROR]", err)
else
print("[WARN] Rules not found.")
end
end
local start_line_fmt = "(%w+)%s+(%g+)%s+(%w+)/([%d%.]+)"
local function parse_start_line(start_line)
local request = {startline = start_line}
request.method, request.rawurl, request.protoname, request.protover = start_line:match(start_line_fmt)
return request
end
local header_match = "(%g+): ([%g ]+)"
local function read_request(client)
repeat
local start_line, err = client:receive("*l")
if start_line then
local request = parse_start_line(start_line)
local raw_headers = {}
request.headers = {}
local reading = true
while reading do
local header_line, err = client:receive("*l")
reading = (header_line ~= "" and header_line ~= nil)
if reading then
table.insert(raw_headers, header_line)
local key, value = string.match(header_line, header_match)
if key then
request.headers[key:lower()] = value
end
end
end
request.header = table.concat(raw_headers, "\n")
request.url = url.parse(request.rawurl, {})
return request
elseif err == "timeout" then
coroutine.yield()
elseif err == "closed" then
print("Client closed")
return
else
io.stderr:write("[ERROR] Client " .. err .. "\n")
return
end
until start_line
end
local startline_fmt = "HTTP/1.1 %d %s\n"
local function resp_startline(code, mess)
return startline_fmt:format(code, mess)
end
local header_fmt = "%s: %s"
local function concat_headers(headers)
local out = {}
for i, k in pairs(headers) do
local header = (header_fmt):format(i, k)
table.insert(out, header)
end
return table.concat(out, "\n")
end
---@class Server
---@field receiving boolean стоит ли клиент в очереди на чтение данных
---@field sending boolean стоит ли клиент в очереди на отправку данных
---@field closed boolean?
---@field startline_sended boolean?
---@field header_sended boolean?
---@field body_sended boolean?
---@field client Socket Socket.TCP клиент
---@field request table
---@field response table
---@field thread thread
---@field datagramsize number
local server_obj = {}
server_obj.receiving = true
server_obj.sending = false
server_obj.datagramsize = socket._DATAGRAMSIZE
---Добавить клиента в очеред на получение данных
---@param state boolean
function server_obj:setreceiving(state)
if self.receiving == state then return end
if self.receiving then
table.removedata(recvt, self.client)
else
table.insert(recvt, self.client)
end
self.receiving = state or false
end
---Добавить клиент в очередь на отправку данных
---@param state boolean
function server_obj:setsending(state)
if self.sending == state then return end
if self.sending then
table.removedata(sendt, self.client)
else
table.insert(sendt, self.client)
end
self.sending = state or false
end
---Отсылает стартовую строку
function server_obj:sendstartline()
if self.startline_sended then return end
self.client:send(resp_startline(self.response.code, self.response.mess))
self.startline_sended = true
end
---Отсылает, если надо, заголовки
function server_obj:sendheaders()
local response = self.response
if self.header_sended == true then return end
local out = concat_headers(response.headers) .. "\n\n"
self:sendstartline()
self.client:send(out)
self.header_sended = true
end
---Отсылает, если надо, тело, сохраненное в response.body, а перед этим headers
function server_obj:sendbody()
local response = self.response
if self.body_sended or not response then return end
local body, client = response.body, self.client
if #body > 0 then
local bodytext = table.concat(body)
response.headers["Content-Length"] = #bodytext
self:sendheaders()
client:send(bodytext)
else
self:sendheaders()
end
self.body_sended = true
end
---Отсылает, если надо, заголовки и тело страницы, и закрывает соединение
function server_obj:closecon()
if self.closed then return end
self:sendbody()
self.client:close()
self.closed = true
end
---Отправляет ошибку и закрывает соединение
---@param code number
---@param text string
function server_obj:error(code, text)
local response = self.response
if response then
local err_mess = Codes[code]
response.code = code
response.mess = err_mess
response.headers["Content-Type"] = "text/html; charset=utf-8"
if text then
text = text:gsub("\n", "<br>")
else
text = ""
end
table.insert(response.body,
("<!DOCTYPE html><html lang='en'><body><h1>%s</h1><br><p>%s</p></body></html>"):format(err_mess, text))
end
end
server_obj.__index = server_obj
server_obj.ROOT_DIR = Config.root
local args_fmt = "([^&=?]+)=([^&=?]+)"
---Парсит аргументы формата www-form-urlencoded
---@param query string
---@return table
function server_obj.parsequery(query)
local args = {}
for key, value in string.gmatch(query,
args_fmt) do
args[key] = url.unescape(value)
end
return args
end
local env_mt = { __index = _G }
---@param threaddata Server
---@return function?
local function thread_func(threaddata)
local client = threaddata.client
if ssl_ctx then
repeat
local suc, err = client:dohandshake()
if not suc and err == "wantread" then
threaddata:setreceiving(true)
threaddata:setsending(false)
coroutine.yield()
elseif not suc and err == "wantwrite" then
threaddata:setreceiving(false)
threaddata:setsending(true)
coroutine.yield()
elseif not suc then
io.stderr:write("[ERROR] Handshake: " .. tostring(err) .. "\n")
return
end
until suc
end
repeat
threaddata:setreceiving(true)
threaddata:setsending(false)
local request = read_request(client)
if request then
if ssl_ctx then request.headers.connection = nil end --ossl зависает при keepalive true
local response = {
headers = {
["Content-Type"] = "text/html; charset=utf-8",
["Date"] = os.date("!%c GMT"),
},
body = {},
code = 200,
mess = "OK",
}
if request.headers.connection == "keep-alive" then
client:setoption("keepalive", true)
else
response.headers.Connection = "close"
end
threaddata.request = request
threaddata.response = response
threaddata.startline_sended = false
threaddata.header_sended = false
threaddata.body_sended = false
for _, rule in ipairs(rules) do
if request.rawurl:match(rule.regex) then
rule.func(threaddata)
end
end
threaddata:setreceiving(false)
threaddata:setsending(true)
coroutine.yield()
if string.find(request.url.path, ".lua") then -- если обратились к lua фалу
local threadenv = setmetatable({
server = threaddata,
request = request,
response = response,
client = threaddata.client,
echo = function(...)
local args = table.pack(...)
for i = 1, args.n do
table.insert(threaddata.response.body, tostring(args[i]))
end
end
}, env_mt)
local script_func, err = loadfile(
Config.root .. request.url.path, "bt", threadenv) -- загрузка скрипта
if script_func then
local ret, err = xpcall(script_func, debug.traceback)
if not ret then
io.stderr:write("[ERROR] " .. err .. "\n")
threaddata:error(500, err)
end
else
io.stderr:write("[ERROR] " .. err .. "\n")
threaddata:error(500, err)
end
threaddata:sendbody()
else
local f = io.open(Config.root .. request.url.path, "rb")
if f then
local data_lenghth = f:seek("end"); f:seek("set")
response.headers["Content-Length"] =
tostring(data_lenghth)
threaddata:sendheaders()
for d in f:lines(socket._DATAGRAMSIZE) do
client:send(d)
coroutine.yield()
end
f:close()
else
print("[ERROR] Page not found", request.url.path)
threaddata:error(500, "File "
.. request.url.path .. " not found.")
threaddata:sendbody()
end
end
if request.headers.connection ~= "keep-alive" then
return
end
else
return
end
until not request and threaddata.closed
end
local function process_subpool(subpool, pool)
for _, client in ipairs(subpool) do
local t = threads[client]
if t then
local cr = t.thread
local status, error = coroutine.resume(cr)
if status and coroutine.status(cr) == "dead" then
t:closecon()
table.removedata(pool, client)
threads[client] = nil
elseif status == false then
io.stderr:write("[ERROR] " .. error .. " in " .. last_thread .. "\n")
t:error(500, error)
t:closecon()
table.removedata(pool, client)
threads[client] = nil
end
end
end
end
-- create a TCP socket and bind it to the local host, at any port
local server = assert(socket.tcp())
server:setoption("reuseaddr", true)
server:settimeout(300)
assert(server:bind(Config.host, tonumber(Config.port)))
server:listen(socket._SETSIZE)
print("[INFO] Max connections count", socket._SETSIZE)
-- Print IP and port
print(("[INFO] Listening on IP=%s, PORT=%d."):format(server:getsockname()))
local function process_client(client)
server:settimeout(0)
local newth = coroutine.create(thread_func)
local threaddata = setmetatable({
client = client,
thread = newth,
}, server_obj)
threads[client] = threaddata
table.insert(recvt, client)
coroutine.resume(newth, threaddata)
end
while true do
local client, err = server:accept()
if client then
client:settimeout(0)
if ssl_ctx then
local ssl_client, err = ssl.wrap(client, ssl_ctx)
if ssl_client then
process_client(ssl_client)
else
ssl_client:close()
print("[ERROR] Wrap:", err)
end
else
process_client(client)
end
elseif err == "timeout" then
if #recvt > 0 or #sendt > 0 then
local readyread, readysend, err = socket.select(recvt, sendt, 0.001)
if err ~= "timeout" then
process_subpool(readyread, recvt)
process_subpool(readysend, sendt)
end
else
server:settimeout(300)
end
else
print("Error happened while getting the connection.nError:", err)
end
end