Files
Irohoho 900514c2b3 fix(koreader): respect KOReader global HTTP proxy settings (#1562)
* 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>
2026-09-26 11:15:47 -06:00

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