Skip to content

Conversation

@stephenchengCloud
Copy link
Collaborator

@stephenchengCloud stephenchengCloud commented Nov 25, 2025

When a VNC console connection is rejected due to the session limit,
users want to know which user(s) are currently connected. However,
displaying usernames in HTTP error responses may raise privacy concerns
in some deployments, so also add a configure to enable/disable the
display of the usernames.

The main changes are:
1. To contain the active users in the response message,
changed the active_connections to record the existing users
and use the unique session id to identify them in case
multiple connections have the same user name.

2. Added escape_char to escape html special characters

3. Added `include_console_username_in_error` to enable/disable
   the display of user names

Tested:

  1. display-user-name-in-XC = true by default
# xe pool-param-set uuid=576102a6-c567-c753-5a0e-9a0614674d09 limit-console-sessions=true

Included the currently connected user root in the http response message:

Nov 25 08:35:32 genuk-21-06d xapi: [debug||1441 HTTPS 10.70.0.166->:::80|Connection to VM console R:c7ad391b568e|console] limit_console_sessions is true. Console connection is rejected for VM OpaqueRef:a071805a-370a-c36b-1608-90e42ec28130, active connections: 1
Nov 25 08:35:32 genuk-21-06d xapi: [debug||1441 HTTPS 10.70.0.166->:::80|Connection to VM console R:c7ad391b568e|console] Sending console limit exceeded response: <html><body><h1>Connection Limit Exceeded</h1><p>User 'root' is currently connected to this console (VM OpaqueRef:a071805a-370a-c36b-1608-90e42ec28130). No more connections are allowed when limit_console_sessions is enabled.</p></body></html>
  1. Set display-user-name-in-XC = false
    No user name in the http response message:
Nov 25 08:47:11 genuk-21-06d xapi: [debug||282 HTTPS 10.70.0.167->:::80|Connection to VM console R:2ff9049ff22a|console] limit_console_sessions is true. Console connection is rejected for VM OpaqueRef:a071805a-370a-c36b-1608-90e42ec28130, active connections: 1
Nov 25 08:47:11 genuk-21-06d xapi: [debug||282 HTTPS 10.70.0.167->:::80|Connection to VM console R:2ff9049ff22a|console] Sending console limit exceeded response: <html><body><h1>Connection Limit Exceeded</h1><p>There're users currently connected to this console (VM OpaqueRef:a071805a-370a-c36b-1608-90e42ec28130). No more connections are allowed when limit_console_sessions is enabled.</p></body></html>


let ssh_auto_mode_default = ref true

let display_user_name_in_XC = ref true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a general way, it could be include_console_username_in_error.

in
Printf.sprintf
"<html><body><h1>Connection Limit Exceeded</h1><p>%s currently \
connected to this console (VM %s). No more connections are allowed \
Copy link
Member

@minglumlu minglumlu Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connected -> connecting

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connected is correct.
The message would be:
User 'root' is currently connected to ...


let active_connections : int VMMap.t ref = ref VMMap.empty
(* VMMap maps VM IDs to a list of (user_name, session_id) tuples representing active connections *)
let active_connections : (string * string) list VMMap.t ref = ref VMMap.empty
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer a record type if possible to avoid that these strings are confused.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. record is much better.

| Some _ | None ->
active_connections := VMMap.remove vm_id !active_connections
| Some connections ->
let updated_connections =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could do:

match List.filter (fun (_, sid) -> sid <> session_id) connections with
| [] -> ..
|  upd -> ..

VMMap.find_opt vm_id !active_connections |> Option.value ~default:[]
in
let count = List.length connections in
if is_limit_enabled && count > 0 then (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would suffice to check against the empty list and to avoid the length call.

| c ->
String.make 1 c
in
String.fold_left (fun acc c -> acc ^ escape_char c) "" s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an expensive way to create a string. We should not use this if this is happen frequently or the string is long. The string by default is constructed one character at a time, creating a lot of garbage along the way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably have code for this; a function like html_escape should be put into an existing library to make it more accessible and to avoid that we re-implement it.

Copy link
Collaborator Author

@stephenchengCloud stephenchengCloud Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for telling me. I'll look for existing library or make it a common function.

Copy link
Contributor

@lindig lindig Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If nothing exists, I suggest:

  • have a quick scan over the string. If no character needs escaping, use the string as is
  • if there is something to do, use Buffer to create the resulting string

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found an exsiting function:
Xapi_stdext_std.Xstringext.String.escaped

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This existing function is slow, too - but we are going to improve it.

Copy link
Member

@psafont psafont left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commit message is lacking information on why is this change needed.

I understand is a refinement on the previous design when there was no option to avoid leaking user names to clients, but this needs to be spelled out at least in the commit message.

match connected_users with
| [user] ->
Printf.sprintf "User '%s' is" (html_escape user)
| users ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be empty.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, though it's unlikely to be empty. We have to consider the case.

Comment on lines 50 to 64
with_lock mutex (fun () ->
match VMMap.find_opt vm_id !active_connections with
| Some n when n > 1 ->
active_connections := VMMap.add vm_id (n - 1) !active_connections
| Some _ | None ->
active_connections := VMMap.remove vm_id !active_connections
| Some connections ->
let updated_connections =
List.filter (fun (_, sid) -> sid <> session_id) connections
in
if updated_connections = [] then
active_connections := VMMap.remove vm_id !active_connections
else
active_connections :=
VMMap.add vm_id updated_connections !active_connections
| None ->
(* Unlikely *)
()
Copy link
Member

@minglumlu minglumlu Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

with_lock mutex (fun () ->
  let f conns = Option.bind conns (fun conns ->
    match List.filter (fun (_, sid) -> sid <> session_id) conns with
    | [] -> None
    | l -> Some l
    )
  in
  active_connections := VMMap.update vm_id f !active_connections
)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Elegant!

)
)

let get_connected_users vm_id =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to get the users from try_add.

@stephenchengCloud stephenchengCloud force-pushed the private/stephenche/CP-310425 branch 2 times, most recently from 7d30cf5 to 22c1839 Compare November 25, 2025 11:37
@stephenchengCloud
Copy link
Collaborator Author

The commit message is lacking information on why is this change needed.

I understand is a refinement on the previous design when there was no option to avoid leaking user names to clients, but this needs to be spelled out at least in the commit message.

Updated the commit message.

@stephenchengCloud
Copy link
Collaborator Author

All comments resolved.
Tested:

Nov 25 11:18:36 genuk-21-06d xapi: [debug||215 HTTPS 10.70.0.166->:::80|Connection to VM console R:7fad226cc58f|console] limit_console_sessions is true. Console connection is rejected for VM OpaqueRef:a071805a-370a-c36b-1608-90e42ec28130, active connections: 1
Nov 25 11:18:36 genuk-21-06d xapi: [debug||215 HTTPS 10.70.0.166->:::80|Connection to VM console R:7fad226cc58f|console] Sending console limit exceeded response: <html><body><h1>Connection Limit Exceeded</h1><p>User 'root' is currently connected to this console (VM OpaqueRef:a071805a-370a-c36b-1608-90e42ec28130). No more connections are allowed when limit_console_sessions is enabled.</p></body></html>

@stephenchengCloud stephenchengCloud force-pushed the private/stephenche/CP-310425 branch from 22c1839 to 846927e Compare November 26, 2025 02:14
if !Xapi_globs.include_console_username_in_error then
let users_text =
match connected_users with
| [] ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since now the connected_users comes from try_add, it must not be empty. So it would be an internal error or bug in this case that I think raise Failure after an error logging can be used.

Comment on lines 268 to 272
Printf.sprintf
"<html><body><h1>Connection Limit Exceeded</h1><p>There're users \
currently connected to this console (VM %s). No more connections are \
allowed when limit_console_sessions is enabled.</p></body></html>"
vm_id
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplication could be resolved like:

let body =
  let users_text =
    match (!Xapi_globs.include_console_username_in_error, connected_users) with
    | true, [] ->
        error "The connected user list should not be empty." ;
        raise Failure
    | true, [user] ->
        Printf.sprintf "User '%s' is" (html_escape user)
    | true, users ->
        let escaped_users = List.map html_escape users in
        Printf.sprintf "Users '%s' are" (String.concat ", " escaped_users)
    | false, _ ->
        "There're users"
  in
  Printf.sprintf
    "<html><body><h1>Connection Limit Exceeded</h1><p>%s currently \
     connected to this console (VM %s). No more connections are allowed \
     when limit_console_sessions is enabled.</p></body></html>"
    users_text vm_id
in

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I need to get more used to using match instead of if in OCaml. :)

When a VNC console connection is rejected due to the session limit,
users want to know which user(s) are currently connected. However,
displaying usernames in HTTP error responses may raise privacy concerns
in some deployments, so also add a configure to enable/disable the
display of the usernames.

The main changes are:
1. To contain the active users in the response message,
changed the active_connections to record the existing users
and use the unique session id to identify them in case
multiple connections have the same user name.

2. Use Http_svr.escape to escape html special characters

3. Added `include_console_username_in_error` to enable/disable
   the display of user names

Signed-off-by: Stephen Cheng <stephen.cheng@citrix.com>
@stephenchengCloud stephenchengCloud force-pushed the private/stephenche/CP-310425 branch from 846927e to f55ac2c Compare November 26, 2025 04:44
let f conns =
Option.bind conns (fun conns ->
match
List.filter (fun conn -> conn.session_id <> session_id) conns
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is dropping all entries for a given session_id when a connection finishes. However, it is possible to use the same session_id on multiple connections.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that is the case, we need to define an unique id for each connection added.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants