LiquidProxy Lua Edition
1local ch = require "coro-http"
2local fs = require "fs"
3---@diagnostic disable-next-line: undefined-field
4local btoa = require "base64".decode
5
6local ports = Config.ports.http
7local mod_secure = Config.secure.mod.http
8local mod = Config.mod.http
9
10local webui, wus, whtest
11local authpls = "Please authenticate :)"
12local authplsl = tostring(authpls:len())
13if mod.webui then
14 webui, wus = (table.unpack or unpack)(require "app.http.webui")
15
16 local wuih = mod.webui.hosts
17 function whtest(url)
18 if wuih then
19 if table.has(wuih, url) then return true end
20 if url:sub(1, 4) == "www." then
21 if table.has(wuih, url:sub(5)) then return true end
22 end
23 end
24 return false
25 end
26end
27
28HTTPCatchAlls = {}
29HTTPMatches = {}
30local hmfn = {}
31
32if fs.existsSync "scripts" then
33 for _, file in pairs(fs.readdirSync "scripts") do
34 if file:find("%.lua$") then
35 local func, rules = require("scripts."..file:match("(.+)%.lua"))
36 if func then table.insert(HTTPCatchAlls, func) end
37 for host, func in pairs(rules or {}) do
38 if hmfn[host] then
39 l:error("%s conflicts with rule in %s and %s", host, hmfn[host], file)
40 os.exit(1)
41 else
42 HTTPMatches[host] = func
43 hmfn[host] = file
44 end
45 end
46 end
47 end
48end
49
50-- 0.3, 1.0, 1.1, 1.2, 1.3
51function Ver2Num(ver)
52 if ver:sub(1,4) == "TLSv" then
53 return tonumber(ver:sub(5))
54 else -- SSL
55 return tonumber("0."..ver:sub(5))
56 end
57end
58local maxver = Ver2Num((Config.secure.tls.max))
59
60local function haw(req, socket)
61 if socket.ssl then
62 if Ver2Num(req.socket.ssl:get("version")) <= maxver then
63 return true
64 end
65 end
66 if mod_secure.password then
67 local a = req["Proxy-Authorization"] or req["Authorization"]
68 if a then
69 if a:sub(1, 6) == "Basic " then
70 local u, p = btoa(a:sub(7)):match("^([^:]*):?(.+)$")
71 if u == "" then
72 if mod_secure.require_username then
73 l:debug "Auth fail: No username but server requires it" return false
74 end
75 elseif u ~= mod_secure.username then
76 if Config.secure.username_whitelist[u] then
77 return true
78 else
79 l:debug "Auth fail: Wrong and not whitelisted username" return false
80 end
81 end
82 if p == mod_secure.password then
83 return true
84 else
85 l:debug "Auth fail: Wrong password" return false
86 end
87 end
88 else
89 l:debug "Auth fail: No authorization header" return
90 end
91 end
92
93 l:debug "Auth fail: Unspecified" return false
94end
95function HTTPAuth(req, socket)
96 local ip = socket:getpeername().ip
97 if AllowedIPs[ip] then return true end
98 if haw(req, socket) then
99 RemoveIP(ip) return true
100 else
101 AddIP(ip) return false
102 end
103end
104
105local plainproxy = require "app.http.plain"
106local connectproxy = require "app.http.connect"
107
108local authb = "Please authenticate :)\r\n"
109local auth_webui = {
110 code = 401,
111 {"WWW-Authenticate", mod.webui.realm and "Basic realm=\""..mod.webui.realm.."\"" or "Basic"},
112 {"Content-Length", authb:len()}
113}
114local auth_proxy = {
115 code = 407,
116 {"Proxy-Authenticate", "Basic"},
117 {"Content-Lenghth", authb:len()}
118}
119
120local nob = Config.mod.http.forbidden_response
121local noh = {code = 200}
122if nob then
123 nob = nob.."r\n"
124 noh = {
125 code = 200,
126 {"Content-Type", "text/plain"},
127 {"Content-Length", nob:len()}
128 }
129end
130
131local issb = "Internal Server Error\r\n"
132local issh = {
133 code = 500,
134 {"Content-Length", issb:len()}
135}
136
137local head_metatable = {
138 __index = function(t, k)
139 local ct = rawget(t, "_cache")
140 if not ct then
141 ct = {}
142 rawset(t, "_cache", ct)
143 end
144 local c = ct[k]
145 if c then return c end
146 for _, t in pairs(t) do
147 if type(t) == "table" and t[1] == k then
148 return t[2]
149 end
150 end
151 end
152}
153
154---@return table headers
155---@return string? body
156local function onReq(req, body, socket)
157 setmetatable(req, head_metatable)
158 local ip = socket:getpeername().ip
159 if BannedIPs[ip] then
160 return noh, nob
161 end
162 local suc, a, b = xpcall(function()
163 if req.method == "CONNECT" then
164 if wus and whtest(req.path:match("^([^:]+)")) then
165 return wus(req)
166 end
167 return connectproxy(req, socket)
168 else
169 if req.path:sub(1, 7) == "http://" then -- This can never be HTTPS
170 if not HTTPAuth(req, socket) then
171 return auth_proxy, authb
172 end
173 if webui and whtest(req.path:sub(8):match("^(.-)/")) then
174 if mod_secure.webui_authenticate then
175 if not HTTPAuth(req, socket) then
176 return auth_webui, authb
177 end
178 end
179 return webui(req)
180 end
181 return plainproxy(req, body)
182 else
183 if req.path:sub(1, 1) ~= "/" or not (webui and mod.webui.proxyless) then
184 return noh, nob
185 else
186 if mod_secure.webui_authenticate then
187 if not HTTPAuth(req, socket) then
188 return auth_webui, authb
189 end
190 end
191 return webui(req)
192 end
193 end
194 end
195 end, function(err)
196 return debug.traceback(err, 1)
197 end)
198 if not suc then
199 l:error(a)
200 return issh, issb
201 end
202 ---@diagnostic disable-next-line: return-type-mismatch
203 return a, b
204end
205
206if ports.plain then
207 ch.createServer("0.0.0.0", ports.plain, onReq)
208 LogStarted("HTTP", "plain", ports.plain)
209end
210
211if ports.secure then
212 --TODO: remove diag
213---@diagnostic disable-next-line: redundant-parameter
214 ch.createServer("0.0.0.0", ports.secure, onReq, {key = Key, cert = Cert, server = true})
215 LogStarted("HTTP", "secure", ports.secure)
216end