This forum uses cookies
This forum makes use of cookies to store your login information if you are registered, and your last visit if you are not. Cookies are small text documents stored on your computer; the cookies set by this forum can only be used on this website and pose no security risk. Cookies on this forum also track the specific topics you have read and when you last read them. Please confirm that you accept these cookies being set.

Display MJPEG stream in iframe with Reverse Proxy for auth
#1
Hi, 

Im using the followig code to create a reverse proxy to authenticate with a Camera NVR and attempt to display the MJPEG stream in an iframe. This is to display the nvr camera feed in the visu without the user needing to fill the auth each time.

Currently i can display a snapshot ( the /path 6 url ) but i would like display the MJPEG stream ( the /path5 url ).
The MJPEG stream works in the iframe without the proxy if i set the src as the nvr url directly and manually fill the auth form when it pops up.
The Auth is successfully prefilled using the proxy when displaying the snapshot but i have only been successful in display a snapshot and not the MJPEG stream url.

Also open to any alterations as im unsure if the current method will drain memory with the proxy handling the MJPEG stream.

current code below:

Code:
if not server then
    local http = require("socket.http")
    local ltn12 = require("ltn12")
    local url = require("socket.url")
    local md5 = require("encdec").md5 -- Using the md5 library for hash computation

    local port = 7673

    local urlMappings = {
        ["/path1"] = "http://nodejs.org", -- Replace with the target URL for path1 -- test proxy redirect
        ["/path5"] = "https://<NVR IP>/cgi-bin/mjpg/video.cgi?channel=3&subtype=1", -- Replace with the target URL for path5
       ["/path6"] = "https://<NVR IP>/cgi-bin/snapshot.cgi?channel=1", -- Replace with the target URL for path6
        -- Add more mappings as needed
    }

    -- Set up the login credentials for digest authentication
    local username = "user"
    local password = "password"

-- Function to generate the Digest authentication header with the correct nonce
local function generateDigestHeader(uri, method, realm, nonce, qop, opaque)
    local ha1 = md5(username .. ":" .. realm .. ":" .. password)
    local ha2 = md5(method .. ":" .. uri)
    local nc = "00000001"
    local cnonce = "0a4f113b"
    local response = md5(ha1 .. ":" .. nonce .. ":" .. nc .. ":" .. cnonce .. ":" .. qop .. ":" .. ha2)
    return string.format('Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s", nc=%s, cnonce="%s", qop="%s", opaque="%s"',
        username, realm, nonce, uri, response, nc, cnonce, qop, opaque)
end

    -- Function to handle incoming queries and perform digest authentication
    function handleQuery(query)
    local method, path, _ = query:match("(%u+)%s+([^%s]+)")
    path = path:gsub("?.*", "")

    local targetURL = urlMappings[path]

    if targetURL then
        -- Perform digest authentication before sending the response
        local response_body = {}
        local response_headers = {}
        local res, status_code, response_headers, status_string = http.request {
            url = targetURL,
            headers = {
                ["Accept"] = "*/*", -- Accept all content types
            },
            method = "HEAD",
            create = function()
                local conn = socket.tcp()
                conn:settimeout(5) -- Set a timeout for the initial connection
                return conn
            end,
            sink = ltn12.sink.table(response_body),
            headers = response_headers, -- Store the response headers in the table
        }

        if res and response_headers["www-authenticate"] then
            local realm = response_headers["www-authenticate"]:match('realm="(.-)"')
            local nonce = response_headers["www-authenticate"]:match('nonce="(.-)"')
            local qop = response_headers["www-authenticate"]:match('qop="(.-)"')
            local opaque = response_headers["www-authenticate"]:match('opaque="(.-)"')

            -- Perform the actual HTTPS request with digest authentication
            local auth_response_body = {}
            local auth_res, auth_status_code, auth_response_headers, auth_status_string = http.request {
                url = targetURL,
                headers = {
                    ["Authorization"] = generateDigestHeader(targetURL, "GET", realm, nonce, qop, opaque),
                    ["Accept"] = "*/*", -- Accept all content types
                },
                method = "GET",
                sink = ltn12.sink.table(auth_response_body),
                create = function()
                    local conn = socket.tcp()
                    conn:settimeout(10) -- Set a timeout for the actual request
                    return conn
                end,
            }
        log(auth_response_headers["content-type"])

            if auth_res and auth_status_code == 200 then
                -- Check if the response is an image (e.g., for snapshot requests)
                local content_type = auth_response_headers["content-type"] or ""
                local image_types = { ["image/jpeg"] = true, ["image/png"] = true, ["image/gif"] = true }

                if image_types[content_type] then
                    -- If it's an image, set the appropriate Content-Type header
                    local response = "HTTP/1.1 200 OK\r\nContent-Type: " .. content_type .. "\r\n\r\n" .. table.concat(auth_response_body)
                    log(query, "Success - Image")
                    return response
                else
                    -- For other types of responses, pass them through as they are
                    local response = "HTTP/1.1 200 OK\r\n\r\n" .. table.concat(auth_response_body)
                    log(query, "Success - Other")
                    return response
                end
            else
                -- If the request failed, log and return a 500 error response
                log(query, "Error: Digest authentication request failed with status code " .. auth_status_code)
                return "HTTP/1.1 500 Internal Server Error\r\n\r\n"
            end
        else
            -- If the initial connection failed or no digest headers were received, log and return a 500 error response
            log(query, "Error: Failed to establish a connection with the server or no digest headers received.")
            return "HTTP/1.1 500 Internal Server Error\r\n\r\n"
        end
    else
        return "HTTP/1.1 404 Not Found\r\n\r\n"
    end
end

    -- Create a server socket to listen for incoming queries
    local server = assert(socket.bind("*", port))
    log("Proxy server is listening on localhost:" .. port .. "\n")

    while true do
        local client = server:accept()
        local query = client:receive()

        local response = handleQuery(query)
        client:send(response)
        client:close()
    end
end
Reply
#2
MJPEG is an endless stream so it won't work the same way as snapshot (request -> response). Proxying a video stream is too demanding on the CPU anyway. It's recommended to use a different device like RPi for this.
Reply
#3
Is there any other approach that can be used to complete the Auth?

I can simply put the MJPEG video stream url in the iframe but then the user needs to enter the auth each time which is a nuisance.
Reply
#4
You can stay with snapshot and keep refreshing frame. Not fully live stream but should work.
------------------------------
Ctrl+F5
Reply
#5
Thanks, I’ll work with this for now and see how it goes
Reply
#6
(26.07.2023, 09:14)Diggerz Wrote: Thanks, I’ll work with this for now and see how it goes

What happens if you use the basic auth also in the url like: https://user:password@<NVR IP>/cgi-bin/mjpg/video.cgi?channel=3&subtype=1
Reply
#7
Passing the credentials in the url like that does not work due to security updates in firmwares and also browser security.

Is it possible to use JavaScript to fill the auth form with event listeners on the iframe? Or is this simply not possible.

I’m having trouble with the proxy and the iframe refresh times, the images stall or go blank upon some refresh’s.
Reply
#8
I haven’t had a lot of success getting the MJPEG stream to work with auto auth. I think I’ll try the rpi rout. Before I make a start does anyone have any suggestions or something already created?
Reply
#9
Try one of these:
https://github.com/legege/node-mjpeg-proxy
https://github.com/vvidic/mjpeg-proxy
Reply
#10
Thanks for the pointers and the recomendation on the RPi. I've set up a proxy using express on the pi and it handles the auth. I can now stream the NVR camera MJPEG urls in an iframe without the auth poping up each time.
Reply


Forum Jump: