Hi all, there has been some work done by FlowFuse @joepavitt and a couple of other contribotors @fullmetal-fred and @kitori on standardising authorised client information for Dashboard 2.
Having seen this, I thought it would be good to bring the same data standard into UIBUILDER as well.
Whereas for D2, you need a plugin, for UIBUILDER I wanted to build in some basics and then allow a hook to allow other methods to be used if needed.
In doing so however, I did note a few potential issues with the processes being used. Notably:
- Client IP addresses are not very easy to correctly obtain.
- Identifying the authentication provider and the authenticated user details is HARD because there are no standard HTTP headers to use.
To deal with (1), I've replaced my own processing with a copy based on: request-ip/src/index.js at master · pbojinov/request-ip · GitHub - see the code below for the 2 functions. It would be advisable for D2 and the plugins to use the same approach.
To deal with (2), I have the following code for which I would welcome feedback:
// socket is called `conn` in D2 I think.
const headers = socket.request.headers
const handshake = socket.handshake
const realClientIP = getClientRealIpAddress(socket)
let authProvider
if (headers['cf-access-authenticated-user-email']) authProvider = 'CloudFlare Access'
else if (handshake.auth?.user?.userId) authProvider = 'FlowFuse'
else if (headers['x-user-id']) authProvider = 'Keycloak'
else if (headers['remote-user'] || headers['x-remote-user']) authProvider = 'Custom'
else if (headers['x-forwarded-user']) authProvider = 'Proxied Custom'
const userID = headers['cf-access-user'] || headers['cf-access-authenticated-user-email'] || handshake.auth?.user?.userId || headers['remote-user'] || headers['x-remote-user'] || headers['x-forwarded-user'] || headers['x-user-id'] || undefined
const email = headers['cf-access-authenticated-user-email'] || headers['remote-email'] || headers['x-user-email'] || undefined
const name = headers['remote-name'] || headers['x-remote-name'] || handshake.auth?.user?.name
if (authProvider !== undefined && userID !== undefined) {
out._client = {
userId: userID,
socketId: socket.id,
email: email,
provider: authProvider,
agent: headers['user-agent'] || null,
ip: realClientIP,
host: headers['host'],
name: name,
}
if (headers['x-forwarded-groups']) out._client.groups = headers['x-forwarded-groups']
if (headers['x-user-role']) out._client.role = headers['x-user-role']
if (handshake.auth?.user?.image) out._client.image = handshake.auth.user.image
}
Please do let me know if you think anything is missing or wrong.
For UIBUILDER v7, the msg._client
produced from above will be added to any msg received from the front-end clients assuming both authProvider
and userID
can be worked out.
Here is the code for working out the real client IP address:
/** Parse x-forwarded-for headers.
* Borrowed from https://github.com/pbojinov/request-ip/blob/master/src/index.js
* @param {string|string[]} value - The value to be parsed.
* @returns {string|null} First known IP address, if any.
*/
function getClientIpFromXForwardedFor(value) {
if (!value) return null
if (Array.isArray(value)) value = value[0]
// x-forwarded-for may return multiple IP addresses in the format:
// "client IP, proxy 1 IP, proxy 2 IP"
// Therefore, the right-most IP address is the IP address of the most recent proxy
// and the left-most IP address is the IP address of the originating client.
// source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
// Azure Web App's also adds a port for some reason, so we'll only use the first part (the IP)
const forwardedIps = value.split(',').map((e) => {
const ip = e.trim()
if (ip.includes(':')) {
const splitted = ip.split(':')
// make sure we only use this if it's ipv4 (ip:port)
if (splitted.length === 2) {
return splitted[0]
}
}
return ip
})
// Sometimes IP addresses in this header can be 'unknown' (http://stackoverflow.com/a/11285650).
// Therefore taking the right-most IP address that is not unknown
// A Squid configuration directive can also set the value to "unknown" (http://www.squid-cache.org/Doc/config/forwarded_for/)
for (let i = 0; i < forwardedIps.length; i++) {
if (forwardedIps[i] !== 'unknown') {
return forwardedIps[i]
}
}
// If no value in the split list is an ip, return null
return null
}
/** Get client real ip address
* Borrowed from https://github.com/pbojinov/request-ip/blob/master/src/index.js
* @param {socketio.Socket} socket Socket.IO socket object
* @returns {string | string[] | undefined} Best estimate of the client's real IP address
*/
function getClientRealIpAddress(socket) {
const headers = socket.request.headers
// Standard headers used by Amazon EC2, Heroku, and others.
if (headers['x-client-ip']) return headers['x-client-ip']
// Load-balancers (AWS ELB) or proxies.
const xForwardedFor = getClientIpFromXForwardedFor(headers['x-forwarded-for'])
if (xForwardedFor) return xForwardedFor
// Cloudflare. @see https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-
// CF-Connecting-IP - applied to every request to the origin.
if (headers['cf-connecting-ip']) return headers['cf-connecting-ip']
// DigitalOcean. @see https://www.digitalocean.com/community/questions/app-platform-client-ip
// DO-Connecting-IP - applied to app platform servers behind a proxy.
if (headers['do-connecting-ip']) return headers['do-connecting-ip']
// Fastly and Firebase hosting header (When forwared to cloud function)
if (headers['fastly-client-ip']) return headers['fastly-client-ip']
// Akamai and Cloudflare: True-Client-IP.
if (headers['true-client-ip']) return headers['true-client-ip']
// Default nginx proxy/fcgi alternative to x-forwarded-for, used by some proxies.
if (headers['x-real-ip']) return headers['x-real-ip']
// (Rackspace LB and Riverbed's Stingray)
// http://www.rackspace.com/knowledge_center/article/controlling-access-to-linux-cloud-sites-based-on-the-client-ip-address
// https://splash.riverbed.com/docs/DOC-1926
if (headers['x-cluster-client-ip']) return headers['x-cluster-client-ip']
if (headers['x-forwarded']) return headers['x-forwarded']
if (headers['forwarded-for']) return headers['forwarded-for']
if (headers.forwarded) return headers.forwarded
// Google Cloud App Engine
// https://cloud.google.com/appengine/docs/standard/go/reference/request-response-headers
if (headers['x-appengine-user-ip']) return headers['x-appengine-user-ip']
// else get ip from socket.request that returns the reference to the request that originated the underlying engine.io Client
if ( socket.request?.connection?.remoteAddress ) return socket.request.connection.remoteAddress
// else get ip from socket.handshake that is a object that contains handshake details
return socket.handshake.address
} // --- End of getClientRealIpAddress --- //
Obviously, all this code is Apache 2 licensed and can be freely reused.