Skip to content

Instantly share code, notes, and snippets.

@pohmelie
Last active December 19, 2022 08:31
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save pohmelie/d1f3ae729472aa652177c2f954bbee08 to your computer and use it in GitHub Desktop.
Save pohmelie/d1f3ae729472aa652177c2f954bbee08 to your computer and use it in GitHub Desktop.
nginx/openresty reverse proxy ntlm support

nginx/openresty reverse proxy ntlm support

Problem

This code allows you to pass ntlm auth in nginx reverse proxy mode. The problem with plain nginx is that ntlm requires one tcp connection for multiple http requests. Even if browser respect this behaviour, nginx will create/took new connection for each request to ntlm-awared server.

Solution

Implement nginx-like stream proxy, but parse http to understand end of sequence (first request after ntlm auth). We need end of sequence, since browser can reuse opened tcp connection and send another request, which will be passed to ntlm-aware server and this is not you expect.

Installation

Put ntlm.lua to lualib path of openresty.

Linux

You need to install lua-http-parser into openresty lualib path with luarocks.

Windows

You can use binaries below (probably you need to rename .dll to .so, since openresty luajit have some problems with import paths) and put them into lualib. Binaries tested with openresty-1.13.6.2-win64.

Usage

location /foobar {
     access_by_lua_block {require("ntlm").passthrough("1.2.3.4", 5678, 10)}
}

Limitations

  1. This code do not change http headers and body, so if your openresty location do not mimic ntlm-aware server, then this will not work. But, since there is strong parser this can be imporved, but requires much work.
  2. This code uses timeouts for socket read operations, so any request can't be shorter in time, than this timeout. This is solvable part, but requires more digging into http and will increase code complexity.
local ngx = require("ngx")
local lhp = require("http.parser")
local M = {}
local function log(level, t, ...)
local m = string.format(t, ...)
ngx.log(level, m)
return m
end
M.RequestWatcher = {}
function M.RequestWatcher.new(self, url)
local o = {
url = url,
end_of_sequence = false,
}
setmetatable(o, self)
self.__index = self
o.parser = lhp.request({
on_url=function(...) o:on_url(...) end,
})
return o
end
function M.RequestWatcher.on_url(self, url)
log(ngx.DEBUG, "request watcher: check url")
if self.url ~= url then
log(ngx.DEBUG, "request watcher: url mismatch, expect '%s', but got '%s'", self.url, url)
self.end_of_sequence = true
end
end
function M.RequestWatcher.execute(self, bytes)
return self.parser:execute(bytes)
end
M.ResponseWatcher = {}
function M.ResponseWatcher.new(self)
local o = {
non_auth_code = false,
end_of_sequence = false,
}
setmetatable(o, self)
self.__index = self
o.parser = lhp.response({
on_status=function(...) o:on_status(...) end,
on_message_complete=function(...) o:on_message_complete(...) end,
})
return o
end
function M.ResponseWatcher.on_message_complete(self)
log(ngx.DEBUG, "response watcher: message complete")
if self.non_auth_code then
self.end_of_sequence = true
end
end
function M.ResponseWatcher.on_status(self, code, text)
log(ngx.DEBUG, "response watcher: %s %s", code, text)
if code ~= 401 then
self.non_auth_code = true
end
end
function M.ResponseWatcher.execute(self, bytes)
return self.parser:execute(bytes)
end
local function transfer(source, destination, prefix, watcher)
while true do
local data, receive_err, partial = source:receive(8192)
if receive_err and receive_err ~= "timeout" then
log(ngx.ERR, "transfer[%s]: read error %s", prefix, receive_err)
end
for i, d in pairs({data, partial}) do
if #d ~= 0 then
log(ngx.DEBUG, "transfer[%s]: sending %d bytes", prefix, #d)
local bytes, send_err = destination:send(d)
if send_err then
log(ngx.ERR, "transfer[%s]: write error %s", prefix, send_err)
return receive_err, send_err
else
log(ngx.DEBUG, "transfer[%s]: %d bytes sent", prefix, bytes)
end
if watcher then
watcher:execute(d)
if watcher.end_of_sequence then
return
end
end
end
end
if receive_err and receive_err ~= "timeout" then
return receive_err, send_err
end
end
end
function M.passthrough(host, port, timeout, location_prefix_to_strip)
log(ngx.INFO, "passthrough[%s:%s]: started", host, port)
local url = ngx.var.uri
local req_sock, err = ngx.req.socket(true)
if err then
return log(ngx.ERR, "get req_sock error %s", err)
end
if timeout then
req_sock:settimeouts(10000, 10000, timeout)
end
local resp_sock = ngx.socket.tcp()
local ok, err = resp_sock:connect(host, port)
if not ok then
return log(ngx.ERR, "connect to %s:%s failed cause %s", host, port, err)
end
if timeout then
resp_sock:settimeouts(10000, 10000, timeout)
end
local headers = ngx.req.raw_header()
if location_prefix_to_strip then
headers = string.gsub(ngx.req.raw_header(), location_prefix_to_strip, "", 1)
end
local bytes, err = resp_sock:send(headers)
if err then
return log(ngx.ERR, "send to %s:%s failed cause %s", host, port, err)
end
local request_watcher = M.RequestWatcher:new(url)
request_watcher:execute(headers)
local request_coroutine = ngx.thread.spawn(transfer, req_sock, resp_sock, "request", request_watcher)
local response_coroutine = ngx.thread.spawn(transfer, resp_sock, req_sock, "response", M.ResponseWatcher:new())
ngx.thread.wait(request_coroutine, response_coroutine)
ngx.thread.kill(request_coroutine)
ngx.thread.kill(response_coroutine)
resp_sock:close()
log(ngx.INFO, "passthrough[%s:%s]: done", host, port)
end
return M
@Dmitry-NC
Copy link

Good day
Thank you for your work!
This only works for static pages. All dynamic pages (for example GET /xxxx/?auth=win HTTP/1.1) do not open, the connection to the server immediately stops.
In the log writes:

[lua] ntlm.lua: 9: log (): request watcher: check url
[lua] ntlm.lua: 9: log (): request watcher: url mismatch, expect '/ xxxxx /', but got '/ xxxxx /? auth = win'
lua tcp socket read timed out, client: .....

If you analyze packets with tcpdump, then proxy makes the correct request and after authorization receives the page, but does not send it to the browser, proxy closes the connection.
Is there a solution to this problem?

@pohmelie
Copy link
Author

pohmelie commented Nov 6, 2019

Yes. It looks like you can fix this by changing on_url method to not set end_of_sequence mark on url mismatch. You need smarter code for end_of_sequence test.

@z2z
Copy link

z2z commented May 9, 2020

Testing in win10,

		location /foobar {
			access_by_lua_block {require("ntlm").passthrough("https://ipa.demo1.freeipa.org", 5678, 10)}
	    }
2020/05/09 10:37:58 [notice] 11508#12040: openresty/1.15.8.3
2020/05/09 10:37:58 [info] 11508#12040: OS: 300000 build:16299, "", suite:100, type:1
2020/05/09 10:37:58 [notice] 11508#12040: create thread 6780
2020/05/09 10:37:58 [notice] 11508#12040: create thread 1808
2020/05/09 10:37:58 [notice] 11508#12040: create thread 2096
2020/05/09 10:38:19 [error] 4956#9936: *20 lua entry thread aborted: runtime error: access_by_lua(nginx.conf:76):1: attempt to index a userdata value
stack traceback:
coroutine 0:
	access_by_lua(nginx.conf:76): in main chunk, client: 127.0.0.1, server: localhost, request: "GET /foobar HTTP/1.1", host: "localhost:8080"

@mvthul
Copy link

mvthul commented Oct 25, 2020

Pfff too much info is there any step by step install how to implement this? Examples with details what to adjust on needs as example upstream nginx config?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment