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.

HTTP PUT image to 2N intercom
#1
Hello!
Using a SE spaceLYnk I'm trying to send an image to a 2N IP Verso intercom when the firealarm is activated and remove the image when the fire alarm resets.

The intercom uses HTTPS and Digest authentication.
I've got it working in postman but not from the SpaceLYnk.
I only got the DELETE part working with basic authentication.

Code:
-- /img/STOP_DNE_241x320.png (I want to use this)
-- /img/fire_241x320.png (Alternate image for testing)

fire = grp.getvalue('32/1/1')
--log(fire)

if fire == true then
  log('FIREALARM ACTIVE')

-- Send image to intercom
-- https://10.0.0.230/api/display/image?display=ext1
 
elseif fire == false then
  log('No fire')
 
-- Delete image from intercom
-- https://10.0.0.230/api/display/image?display=ext1

local https = require("ssl.https")
local ltn12 = require("ltn12")

local response_body = {}
local request_body = ""
local res, code, response_headers, status = https.request{
  url = "https://test:test@10.0.0.230/api/display/image?display=ext1",
  method = "DELETE",
  source = ltn12.source.string(request_body),
  sink = ltn12.sink.table(response_body),
  protocol = "tlsv1_2"
}.

if code == 200 then
  log("Image deleted successfully!")
else
  log("Error deleting image: "..code)
end

else
  log('ERROR')
end

I found some threads here about digest authentication and image handling, but I'm not experienced with coding so I was not able to put the code together from what I found to do what I need.
I have saved the image to /img/ which i created in the root of the ftp server logged in as ftp.

Here's a link to the 2N API documentation, part 5.9.2 is about sending images and clearing the display.
https://wiki.2n.com/hip/hapi/latest/en
Reply
#2
Try this. filedata variable must contain your image in JPEG format. Use io.readfile() to read a local file.
Code:
require('ltn12')
require('ssl.https')

boundary = os.date('%d%m%Y%H%M%S')
filedata = '...' -- image data as a binary string

body = table.concat({
  '--' .. boundary,
  'Content-Disposition: form-data; name="blob-image"; filename="picture.jpeg"',
  'Content-Type: image/jpeg',
  '',
  filedata,
  '--' .. boundary .. '--',
  ''
}, '\r\n')

resp = {}

res, code, response_headers, status = ssl.https.request({
  url = 'https://test:test@10.0.0.230/api/display/image?display=ext1',
  sink = ltn12.sink.table(resp),
  method = 'PUT',
  source = ltn12.source.string(body),
  headers = {
    ['content-length'] = #body,
    ['content-type'] = 'multipart/form-data; boundary=' .. boundary
  }
})

log(res, code, table.concat(resp))

For digest auth you can use this example: https://forum.logicmachine.net/showthrea...64#pid9364
Replace ssl.https.request in the above code with request from the digest code.
Reply
#3
I updated the filedata line 
Code:
filedata = 'io.readfile(/img/STOP_DNE_241x320.jpeg)' -- image data as a binary string
But I get this error now
Code:
08.02.2023 22:19:12
* arg: 1
  * number: 1
* arg: 2
  * number: 200
* arg: 3
  * string: {
  "success" : false,
  "error" : {
    "code" : 12,
    "param" : "blob-image",
    "description" : "invalid parameter value"
  }
}
Reply
#4
You converted the whole command into a string by placing the '' at the wrong position ..

Try: filedata = io.readfile(‘/img/STOP_DNE_241x320.jpeg')
Reply
#5
Ok, tried that now, new error.

Code:
09.02.2023 06:25:54
User script:19: invalid value (nil) at index 5 in table for 'concat'
stack traceback:
[C]: in function 'concat'
I'm suspecting that either the path to the file is wrong or there's a permission missing somewhere.
I've tried using /img/FILE and /data/ftp/img/FILE
Reply
#6
Try this (assuming that you've uploaded the image to Vis. graphics > Image / backgrounds):
Code:
filedata = io.readfile(‘/www/scada/resources/img/STOP_DNE_241x320.jpeg')
Reply
#7
Thanks! Finally got it working.

It might have been your suggestion, or it might have been my typo in the filename that fixed it... 241/214...

Now I just have to get digest auth working and I'm all set Smile

Code:
09.02.2023 15:38:52
* arg: 1
  * number: 1
* arg: 2
  * number: 200
* arg: 3
  * string: {
  "success" : true
}
Reply
#8
Thank you so much!
Finally got it working with digest, it could problably be done prettier, but this works for me  Big Grin

Code:
fire = grp.getvalue("32/1/1")

if fire == true then -- Uploads image to intercom
    log("FIREALARM ACTIVE")

    local skthttp = require("socket.http")
    local skturl = require("socket.url")
    local ltn12 = require("ltn12")
    local md5sum = require("encdec").md5

    boundary = os.date("%d%m%Y%H%M%S")
    filedata = io.readfile("/www/scada/resources/img/STOP_DNE_214x320.jpeg") -- image data as a binary string

    body =
        table.concat(
        {
            "--" .. boundary,
            'Content-Disposition: form-data; name="blob-image"; filename="picture.jpeg"',
            "Content-Type: image/jpeg",
            "",
            filedata,
            "--" .. boundary .. "--",
            ""
        },
        "\r\n"
    )

    resp = {}

    local hash = function(...)
        return md5sum(table.concat({...}, ":"))
    end

    local parse_header = function(header)
        local result = {}
        for key, value in (header .. ","):gmatch("(%w+)=(.-),") do
            if value:sub(1, 1) == '"' then -- strip quotes
                result[key:lower()] = value:sub(2, -2)
            else
                result[key:lower()] = value
            end
        end
        return result
    end

    local make_digest_header = function(headers)
        local digest = {}
        for _, header in ipairs(headers) do
            if not header.unquote then
                header[2] = '"' .. header[2] .. '"'
            end

            digest[#digest + 1] = header[1] .. "=" .. header[2]
        end
        return "Digest " .. table.concat(digest, ", ")
    end

    local _request = function(req)
        if not req.url then
            return nil, "missing url"
        end

        local url = skturl.parse(req.url)
        local user, password = url.user, url.password
        local sink = req.sink

        if not user or not password then
            return nil, "missing credentials in url"
        end

        url.user, url.password, url.authority, url.userinfo = nil, nil, nil, nil
        req.url = skturl.build(url)
        local source
        if req.source then
            local chunks = {}
            local capture = function(chunk)
                if chunk then
                    chunks[#chunks + 1] = chunk
                end
                return chunk
            end
            local chunk_id = 0
            source = function()
                chunk_id = chunk_id + 1
                return chunks[chunk_id]
            end
            req.source = ltn12.source.chain(req.source, capture)
        end
        req.sink = nil
        local body, code, hdrs = skthttp.request(req)
        if code == 401 and hdrs["www-authenticate"] then
            local ht = parse_header(hdrs["www-authenticate"])
            if not ht.realm or not ht.nonce then
                return nil, "missing realm/nonce from response"
            end
            local qop = ht.qop
            if qop and qop ~= "auth" then
                return nil, "unsupported qop " .. tostring(qop)
            end
            if ht.algorithm and ht.algorithm:lower() ~= "md5" then
                return nil, "unsupported algo " .. tostring(ht.algorithm)
            end
            local nc = "00000001"
            local cnonce = string.format("%08x", os.time())
            local uri = skturl.build({path = url.path, query = url.query})
            local method = req.method or "GET"
            local response = hash(hash(user, ht.realm, password), ht.nonce, nc, cnonce, "auth", hash(method, uri))
            req.headers = req.headers or {}
            local auth = {
                {"username", user},
                {"realm", ht.realm},
                {"nonce", ht.nonce},
                {"uri", uri},
                {"cnonce", cnonce},
                {"nc", nc, unquote = true},
                {"qop", "auth"},
                {"algorithm", "MD5"},
                {"response", response}
            }
            if ht.opaque then
                table.insert(auth, {"opaque", ht.opaque})
            end
            req.headers.authorization = make_digest_header(auth)
            if not req.headers.cookie and hdrs["set-cookie"] then
                -- not really correct but enough for httpbin
                local cookie = (hdrs["set-cookie"] .. ";"):match("(.-=.-)[;,]")
                if cookie then
                    req.headers.cookie = "$Version: 0; " .. cookie .. ";"
                end
            end
            if req.source then
                req.source = source
            end
            req.sink = sink
            body, code, hdrs = skthttp.request(req)
        end

        return body, code, hdrs
    end

    local request = function(url)
        local t = type(url)
        if t == "table" then
            return _request(table.clone(url))
        elseif t == "string" then
            local req = {}
            local _, code, headers = _request({url = url, sink = ltn12.sink.table(req)})
            return table.concat(req), code, headers
        end
    end

    res, code, response_headers, status =
        request(
        {
            url = "https://test:test@10.0.0.230/api/display/image?display=ext1",
            sink = ltn12.sink.table(resp),
            method = "PUT",
            source = ltn12.source.string(body),
            headers = {
                ["content-length"] = #body,
                ["content-type"] = "multipart/form-data; boundary=" .. boundary
            }
        }
    )

    log(res, code, table.concat(resp))

elseif fire == false then -- Removes image from intercom
    log("Firealarm reset")

    local skthttp = require("socket.http")
    local skturl = require("socket.url")
    local ltn12 = require("ltn12")
    local md5sum = require("encdec").md5

    local hash = function(...)
        return md5sum(table.concat({...}, ":"))
    end

    local parse_header = function(header)
        local result = {}
        for key, value in (header .. ","):gmatch("(%w+)=(.-),") do
            if value:sub(1, 1) == '"' then -- strip quotes
                result[key:lower()] = value:sub(2, -2)
            else
                result[key:lower()] = value
            end
        end
        return result
    end

    local make_digest_header = function(headers)
        local digest = {}
        for _, header in ipairs(headers) do
            if not header.unquote then
                header[2] = '"' .. header[2] .. '"'
            end

            digest[#digest + 1] = header[1] .. "=" .. header[2]
        end
        return "Digest " .. table.concat(digest, ", ")
    end

    local _request = function(req)
        if not req.url then
            return nil, "missing url"
        end

        local url = skturl.parse(req.url)
        local user, password = url.user, url.password
        local sink = req.sink

        if not user or not password then
            return nil, "missing credentials in url"
        end

        url.user, url.password, url.authority, url.userinfo = nil, nil, nil, nil
        req.url = skturl.build(url)
        local source
        if req.source then
            local chunks = {}
            local capture = function(chunk)
                if chunk then
                    chunks[#chunks + 1] = chunk
                end
                return chunk
            end
            local chunk_id = 0
            source = function()
                chunk_id = chunk_id + 1
                return chunks[chunk_id]
            end
            req.source = ltn12.source.chain(req.source, capture)
        end
        req.sink = nil
        local body, code, hdrs = skthttp.request(req)
        if code == 401 and hdrs["www-authenticate"] then
            local ht = parse_header(hdrs["www-authenticate"])
            if not ht.realm or not ht.nonce then
                return nil, "missing realm/nonce from response"
            end
            local qop = ht.qop
            if qop and qop ~= "auth" then
                return nil, "unsupported qop " .. tostring(qop)
            end
            if ht.algorithm and ht.algorithm:lower() ~= "md5" then
                return nil, "unsupported algo " .. tostring(ht.algorithm)
            end
            local nc = "00000001"
            local cnonce = string.format("%08x", os.time())
            local uri = skturl.build({path = url.path, query = url.query})
            local method = req.method or "GET"
            local response = hash(hash(user, ht.realm, password), ht.nonce, nc, cnonce, "auth", hash(method, uri))
            req.headers = req.headers or {}
            local auth = {
                {"username", user},
                {"realm", ht.realm},
                {"nonce", ht.nonce},
                {"uri", uri},
                {"cnonce", cnonce},
                {"nc", nc, unquote = true},
                {"qop", "auth"},
                {"algorithm", "MD5"},
                {"response", response}
            }
            if ht.opaque then
                table.insert(auth, {"opaque", ht.opaque})
            end
            req.headers.authorization = make_digest_header(auth)
            if not req.headers.cookie and hdrs["set-cookie"] then
                -- not really correct but enough for httpbin
                local cookie = (hdrs["set-cookie"] .. ";"):match("(.-=.-)[;,]")
                if cookie then
                    req.headers.cookie = "$Version: 0; " .. cookie .. ";"
                end
            end
            if req.source then
                req.source = source
            end
            req.sink = sink
            body, code, hdrs = skthttp.request(req)
        end

        return body, code, hdrs
    end

    local request = function(url)
        local t = type(url)
        if t == "table" then
            return _request(table.clone(url))
        elseif t == "string" then
            local req = {}
            local _, code, headers = _request({url = url, sink = ltn12.sink.table(req)})
            return table.concat(req), code, headers
        end
    end

    local response_body = {}
    local request_body = ""
    local res, code, response_headers, status =
        request {
        url = "https://test:test@10.0.0.230/api/display/image?display=ext1",
        method = "DELETE",
        source = ltn12.source.string(request_body),
        sink = ltn12.sink.table(response_body),
        protocol = "tlsv1_2"
    }

    if code == 200 then
        log("Image deleted successfully!")
    else
        log("Error deleting image: " .. code)
    end
else
    log("ERROR")
end
Reply


Forum Jump: