mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
900514c2b3
* fix(koreader): respect KOReader global HTTP proxy settings Allow KOReader's Network -> Proxy settings (stored in G_reader_settings) to be respected for both JSON API requests and binary downloads. Also support explicit proxy in BookOrbitApi options, and route Tailscale tailnet addresses (100.x.y.z and *.ts.net) through the local userspace HTTP proxy (127.0.0.1:1056) when running on e-ink devices lacking TUN. Fixes #753 * fix(koreader): address review feedback on host parsing and HTTPS proxy safety * fix(koreader): simplify getProxy to respect KOReader proxy settings without vendor fallbacks * fix: tunnel KOReader HTTPS through configured HTTP proxies Route HTTPS requests through CONNECT so proxy-only servers stay reachable without exposing BookOrbit credentials to the proxy. Restrict redirects to the original host and allow secure upgrades for API calls and downloads. Bump the plugin version so installed clients discover the fix. --------- Co-authored-by: Andrej <acikarski14@gmail.com> Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
119 lines
4.6 KiB
Lua
119 lines
4.6 KiB
Lua
local Proxy = {}
|
|
|
|
local function proxyConfig(proxy_url)
|
|
if type(proxy_url) ~= "string" or proxy_url:find("[\r\n]") then return nil end
|
|
local url = require("socket.url")
|
|
local parsed = url.parse(proxy_url)
|
|
local port = parsed and tonumber(parsed.port or 3128)
|
|
if not parsed or parsed.scheme ~= "http" or not parsed.host
|
|
or not port or port < 1 or port > 65535 or port ~= math.floor(port) then
|
|
return nil
|
|
end
|
|
|
|
local authorization = ""
|
|
if parsed.user and parsed.password then
|
|
local user = url.unescape(parsed.user)
|
|
local password = url.unescape(parsed.password)
|
|
if user:find("[\r\n]") or password:find("[\r\n]") then return nil end
|
|
authorization = "Proxy-Authorization: Basic "
|
|
.. require("mime").b64(user .. ":" .. password) .. "\r\n"
|
|
end
|
|
|
|
return {
|
|
host = parsed.host:match("^%[(.*)%]$") or parsed.host,
|
|
port = port,
|
|
authorization = authorization,
|
|
}
|
|
end
|
|
|
|
local function readLine(sock, remaining)
|
|
local chars = {}
|
|
for _ = 1, remaining do
|
|
local char, err = sock:receive(1)
|
|
if not char then return nil, err end
|
|
if char == "\n" then
|
|
if chars[#chars] == "\r" then chars[#chars] = nil end
|
|
return table.concat(chars)
|
|
end
|
|
chars[#chars + 1] = char
|
|
end
|
|
return nil, "proxy_response_too_large"
|
|
end
|
|
|
|
function Proxy.connectSocket(proxy_url)
|
|
return function()
|
|
local config = proxyConfig(proxy_url)
|
|
if not config then error("invalid_http_proxy") end
|
|
|
|
local socket = require("socket")
|
|
local conn = { sock = assert(socket.tcp()) }
|
|
function conn:settimeout(...)
|
|
return self.sock:settimeout(...)
|
|
end
|
|
function conn:close()
|
|
return self.sock:close()
|
|
end
|
|
function conn:connect(host, port)
|
|
local target_port = tonumber(port)
|
|
if type(host) ~= "string" or host:find("[%c%s]")
|
|
or not target_port or target_port < 1 or target_port > 65535
|
|
or target_port ~= math.floor(target_port) then
|
|
return nil, "invalid_https_target"
|
|
end
|
|
local connected, connect_err = self.sock:connect(config.host, config.port)
|
|
if not connected then return nil, connect_err end
|
|
local tls_host = host:match("^%[(.*)%]$") or host
|
|
local authority = tls_host:find(":", 1, true)
|
|
and "[" .. tls_host .. "]:" .. tostring(target_port)
|
|
or tls_host .. ":" .. tostring(target_port)
|
|
local sent, send_err = self.sock:send("CONNECT " .. authority .. " HTTP/1.1\r\nHost: "
|
|
.. authority .. "\r\n" .. config.authorization .. "\r\n")
|
|
if not sent then return nil, send_err end
|
|
|
|
local status, status_err = readLine(self.sock, 8192)
|
|
if not status then return nil, status_err end
|
|
if status:match("^HTTP/%d+%.%d+%s+(%d%d%d)") ~= "200" then
|
|
return nil, "proxy_connect_failed"
|
|
end
|
|
local header_bytes = #status + 2
|
|
local headers_complete = false
|
|
for _ = 1, 64 do
|
|
local line, line_err = readLine(self.sock, 8192 - header_bytes)
|
|
if not line then return nil, line_err end
|
|
header_bytes = header_bytes + #line + 2
|
|
if header_bytes > 8192 then return nil, "proxy_response_too_large" end
|
|
if line == "" then
|
|
headers_complete = true
|
|
break
|
|
end
|
|
end
|
|
if not headers_complete then return nil, "proxy_response_too_large" end
|
|
|
|
-- Keep the same TLS policy as KOReader's LuaSec HTTPS transport.
|
|
local tls, tls_err = require("ssl").wrap(self.sock, {
|
|
protocol = "any",
|
|
options = { "all", "no_sslv2", "no_sslv3", "no_tlsv1" },
|
|
verify = "none",
|
|
mode = "client",
|
|
})
|
|
if not tls then return nil, tls_err end
|
|
tls:sni(tls_host)
|
|
tls:settimeout(require("ssl.https").TIMEOUT)
|
|
self.sock = tls
|
|
local handshook, handshake_err = tls:dohandshake()
|
|
if not handshook then return nil, handshake_err end
|
|
for name, method in pairs(getmetatable(tls).__index) do
|
|
if type(method) == "function" then
|
|
self[name] = function(_, ...)
|
|
return method(self.sock, ...)
|
|
end
|
|
end
|
|
end
|
|
return 1
|
|
end
|
|
return conn
|
|
end
|
|
end
|
|
|
|
return Proxy
|