|
-- 2-https-proxy.lua |
|
-- KOReader userpatch: enable HTTPS requests through an HTTP CONNECT proxy. |
|
-- |
|
-- Stock LuaSec (ssl.https) refuses to use a proxy for HTTPS ("proxy not |
|
-- supported"). This is a problem when reaching tailnet services via a |
|
-- userspace Tailscale HTTP-CONNECT proxy (e.g. 127.0.0.1:1056) on devices |
|
-- with no TUN module, where DNS for *.ts.net can't be resolved locally. |
|
-- |
|
-- This patch reimplements ssl.https' internal `tcp` connector to open a |
|
-- CONNECT tunnel through the proxy (so the proxy resolves the hostname and |
|
-- relays the encrypted stream), and rewrites `request` to stop bailing out |
|
-- when a proxy is set. Ported from DhruvaSambrani's LuaSec patch |
|
-- (add-proxy-redirect-custom); see koreader/koreader#14693. |
|
-- |
|
-- Drop this file in koreader/patches/ . It runs at startup (priority 2, |
|
-- after base modules are available). |
|
|
|
local logger = require("logger") |
|
|
|
local ok, https = pcall(require, "ssl.https") |
|
if not ok or type(https) ~= "table" then |
|
logger.warn("[https-proxy patch] ssl.https not available, skipping") |
|
return |
|
end |
|
|
|
local socket = require("socket") |
|
local ssl = require("ssl") |
|
local http = require("socket.http") |
|
local url = require("socket.url") |
|
local mime = require("mime") |
|
|
|
local try = socket.try |
|
|
|
-- Default TLS config used by LuaSec's https.lua. |
|
-- Mirror the stock module's cfg so we don't weaken/alter TLS behaviour. |
|
local cfg = { |
|
protocol = "any", |
|
options = { "all", "no_sslv2", "no_sslv3", "no_tlsv1" }, |
|
verify = "none", |
|
} |
|
|
|
local PORT = https.PORT or 443 |
|
local TIMEOUT = https.TIMEOUT or 60 |
|
|
|
-- Re-register the SSL socket's methods onto the connection wrapper, |
|
-- same as stock https.lua's reg(). |
|
local function reg(conn) |
|
local mt = getmetatable(conn.sock).__index |
|
for name, method in pairs(mt) do |
|
if type(method) == "function" then |
|
conn[name] = function(self, ...) |
|
return method(self.sock, ...) |
|
end |
|
end |
|
end |
|
end |
|
|
|
-- Check if a host matches a no_proxy list (comma-separated; "*", exact, |
|
-- or leading-dot suffix match). |
|
local function should_bypass_proxy(host, no_proxy_str) |
|
if not no_proxy_str or no_proxy_str == "" then return false end |
|
host = string.lower(host) |
|
for pattern in string.gmatch(no_proxy_str, "([^,]+)") do |
|
pattern = string.gsub(pattern, "^%s*(.-)%s*$", "%1") |
|
pattern = string.lower(pattern) |
|
if pattern == "*" then return true end |
|
if host == pattern then return true end |
|
if string.sub(pattern, 1, 1) == "." then |
|
if string.sub(host, -#pattern) == pattern then |
|
return true |
|
end |
|
end |
|
end |
|
return false |
|
end |
|
|
|
-- Return a 'create' function that opens the (optionally proxied) connection. |
|
local function tcp(params, custom_create) |
|
params = params or {} |
|
for k, v in pairs(cfg) do |
|
params[k] = params[k] or v |
|
end |
|
params.mode = "client" |
|
|
|
local create = custom_create or params.create or socket.tcp |
|
|
|
local proxy_url = params.proxy or http.PROXY |
|
local proxy = nil |
|
local target_host = params.host |
|
local target_port = tonumber(params.port) or PORT |
|
local no_proxy_env = params.noproxy or http.NOPROXY |
|
|
|
if proxy_url then |
|
if should_bypass_proxy(target_host, no_proxy_env) then |
|
proxy = nil |
|
else |
|
proxy = url.parse(proxy_url) |
|
end |
|
end |
|
|
|
return function() |
|
local conn = {} |
|
conn.sock = try(create()) |
|
|
|
local st = getmetatable(conn.sock).__index.settimeout |
|
function conn:settimeout() |
|
return st(self.sock, TIMEOUT) |
|
end |
|
|
|
function conn:connect(host, port) |
|
if proxy then |
|
try(self.sock:connect(proxy.host, proxy.port)) |
|
|
|
local msg = "CONNECT " .. target_host .. ":" .. target_port .. " HTTP/1.1\r\n" |
|
msg = msg .. "Host: " .. target_host .. ":" .. target_port .. "\r\n" |
|
if proxy.user and proxy.password then |
|
local auth = mime.b64(proxy.user .. ":" .. proxy.password) |
|
msg = msg .. "Proxy-Authorization: Basic " .. auth .. "\r\n" |
|
end |
|
msg = msg .. "\r\n" |
|
|
|
try(self.sock:send(msg)) |
|
|
|
local line = try(self.sock:receive()) |
|
local code = string.match(line or "", "HTTP/%d%.%d (%d%d%d)") |
|
|
|
repeat |
|
line = try(self.sock:receive()) |
|
until line == "" or line == nil |
|
|
|
if code ~= "200" then |
|
return nil, "proxy connection failed: " .. tostring(code) |
|
end |
|
else |
|
try(self.sock:connect(host, port)) |
|
end |
|
|
|
self.sock = try(ssl.wrap(self.sock, params)) |
|
self.sock:sni(target_host) |
|
self.sock:settimeout(TIMEOUT) |
|
try(self.sock:dohandshake()) |
|
|
|
reg(self) |
|
return 1 |
|
end |
|
|
|
return conn |
|
end |
|
end |
|
|
|
local function default_https_port(u) |
|
return url.build(url.parse(u, { port = PORT })) |
|
end |
|
|
|
local function urlstring_totable(u, body, result_table) |
|
local ltn12 = require("ltn12") |
|
u = { |
|
url = default_https_port(u), |
|
method = body and "POST" or "GET", |
|
sink = ltn12.sink.table(result_table), |
|
} |
|
if body then |
|
u.source = ltn12.source.string(body) |
|
u.headers = { |
|
["content-length"] = #body, |
|
["content-type"] = "application/x-www-form-urlencoded", |
|
} |
|
end |
|
return u |
|
end |
|
|
|
-- Patched request: don't reject proxies; wire up the CONNECT-aware create fn. |
|
https.request = function(reqt, body) |
|
local result_table = {} |
|
local stringrequest = type(reqt) == "string" |
|
|
|
if stringrequest then |
|
reqt = urlstring_totable(reqt, body, result_table) |
|
else |
|
reqt.url = default_https_port(reqt.url) |
|
end |
|
|
|
local t_url = url.parse(reqt.url) |
|
reqt.host = t_url.host |
|
reqt.port = t_url.port |
|
|
|
local custom_create = reqt.create |
|
reqt.create = tcp(reqt, custom_create) |
|
reqt.proxy = nil |
|
|
|
local res, code, headers, status = http.request(reqt) |
|
|
|
if res and stringrequest then |
|
return table.concat(result_table), code, headers, status |
|
end |
|
return res, code, headers, status |
|
end |
|
|
|
logger.info("[https-proxy patch] ssl.https.request patched for CONNECT-proxy support") |