Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GUACAMOLE-269: Allow backspace key behavior to be configured #156

Merged
merged 22 commits into from
Apr 2, 2018

Conversation

necouchman
Copy link
Contributor

Server-side changes that allow the backspace key to be configured, and, for the SSH protocol, also pass through that behavior to the SSH server via the TTY Mode Encoding options. This replaces #153.

@@ -1594,7 +1599,7 @@ static int __guac_terminal_send_key(guac_terminal* term, int keysym, int pressed
/* Non-printable keys */
else {

if (keysym == 0xFF08) return guac_terminal_send_string(term, "\x7F"); /* Backspace */
if (keysym == 0xFF08) return guac_terminal_send_string(term, &term->backspace); /* Backspace */
Copy link
Contributor

Choose a reason for hiding this comment

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

guac_terminal_send_string() requires a null-terminated string. As &term->backspace is a pointer to a single character, there is no guarantee of null terminator, and this may read without bound, resulting in connection closure or dumping of memory data to the remote terminal.

@@ -223,6 +223,11 @@ typedef struct guac_ssh_settings {
*/
int server_alive_interval;

/**
* The decismal ASCII code of the command to send for backspace.
Copy link
Contributor

Choose a reason for hiding this comment

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

"decismal" is a typo, but that may be moot as this is technically incorrect. While the string from which this int was parsed may have been decimal, at this point it's just an int.

#include <string.h>

guac_ssh_ttymodes init_ttymodes() {
// Simple allocation for a placeholder
Copy link
Contributor

Choose a reason for hiding this comment

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

Comments for C source within Guacamole should use /* ... */ syntax, even if only a single line.

0
};

return empty_modes;
Copy link
Contributor

Choose a reason for hiding this comment

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

Assigning/returning a struct by value like this actually does an implicit copy in C.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated this to a pointer - not sure if that's what you had in mind, but I think it works better any way with what I was trying to do in the other functions.

@mike-jumper
Copy link
Contributor

Before I continue with this review, do you think that init_ttymodes() / add_ttymode() / sizeof_ttymodes() / ttymodes_to_array() might be overkill for the problem at hand? There's a lot of complex logic and dynamic allocation happening there for what boils down to a single opcode.

@necouchman
Copy link
Contributor Author

Before I continue with this review, do you think that init_ttymodes() / add_ttymode() / sizeof_ttymodes() / ttymodes_to_array() might be overkill for the problem at hand? There's a lot of complex logic and dynamic allocation happening there for what boils down to a single opcode.

You're probably right - it definitely felt a little overkill-ish while writing it; however, I was trying to both solve the problem of reading the new SSH setting into the array while also making it so that this could be expanded and reused easily in the future. While this particular JIRA issue deals with the behavior of the backspace key, there are several other TTY Mode Encoding options specified in the SSH RFC, and I didn't want to make the assumption that this is the only one we'd ever be interested in implementing.

However, if you think that's a problem we should worry about when the second opcode comes along, that's fine with me - I can back off this and just write a function that generates the pointer array with the VERASE opcode and value and the ending opcode. Let me know which route you think is best and I'll be happy to do it.

@mike-jumper
Copy link
Contributor

You're probably right - it definitely felt a little overkill-ish while writing it; however, I was trying to both solve the problem of reading the new SSH setting into the array while also making it so that this could be expanded and reused easily in the future. ...

Future-proofing these changes is not a bad idea, but let's do so without complex dynamic allocation. In this case:

  1. We know ahead of time the necessary size of the mode array.
  2. The mode array itself needs to live only for the duration of that initial call which allocates the PTY.

For the sake of simplicity, I'd say we should really just use a statically-allocated array for now. If you feel that dynamic allocation would be more appropriate, then we should make use of our knowledge of the necessary size and allocate the necessary space ahead of time (without resizing) and be careful to free the allocated data once it is no longer needed.

@necouchman
Copy link
Contributor Author

@mike-jumper
Well, I stuck with dynamic allocation, mainly because I was having a silly amount of trouble figuring out how to properly handle standard arrays in the data structure. But, I'm allocating everything ahead of time with known sizes, so hopefully that addresses any concerns. I'm happy to take other suggestions, though.

I also added some calls to free the data structures - I think I got everything, but worth double-checking to make sure.

@mike-jumper
Copy link
Contributor

Hm ... there has to be a cleaner way. The current implementation is:

  1. An internal representation of the TTY modes array is dynamically allocated with guac_ssh_ttymodes_init()
  2. The single TTY mode is added to that array dynamically with guac_ssh_ttymodes_add()
  3. That internal representation is converted to the representation required by SSH through more dynamic allocation via guac_ssh_ttymodes_size() and guac_ssh_ttymodes_to_array()
  4. All memory allocated above is manually freed via calls to free().

I have a few problems with this. We are dynamically allocating an intermediate representation which really has no purpose other than to be converted into another dynamically-allocated representation, both of which have the same lifespan. Abstraction is a great thing, but the complexity of this abstraction seems to outweigh the intended benefit.

I suggest:

  • Look for ways to avoid so much dynamic allocation. Perhaps there is a way to avoid the intermediate representation which ultimately gets thrown away?

  • Look for ways to abstract things such that they (1) simplify your current implementation and (2) make future development easier. Perhaps the intermediate representation could be passed in directly without dynamic allocation? Might variadic functions achieve this better? Something like:

    size = guac_ssh_ttymodes_init(&some_static_buffer, sizeof(some_static_buffer),
                GUAC_SSH_TTY_OP_VERASE, settings->backspace);
    

    would abstract things away, require only a single function call, and would not depend on dynamic allocation.

@necouchman
Copy link
Contributor Author

Hm ... there has to be a cleaner way.

Oh, I have no doubt at all there is a cleaner way! :-)

Perhaps there is a way to avoid the intermediate representation which ultimately gets thrown away?

Yeah, I'll see what I can do about this. I was initially trying to create a data structure that could be cast into a char * and passed directly to the libssh2_channel_request_pty_ex() function. I thought I remembered from somewhere in my past that doing this:

struct ttymode {
    char opcode;
    uint32_t value;
} ttymode;

Would result in memory being allocated in such a way that it could then be cast into char * and you'd end up with the single opcode byte and the four value bytes all together, which is what the function expects. Doesn't seem to work out, though. If that is supposed to work, that seems like the simplest route to be, because the members can then be accessed easily (array[0].opcode and array[0].value) but then the entire thing can be passed directly to the function. Am I remembering that correctly, or is there something else I'm not factoring in to how that allocation works?

@mike-jumper
Copy link
Contributor

Unfortunately, that isn't safe across different architectures.

A C struct provides a guarantee regarding order in memory (opcode will definitely come before value), but it does not provide guarantees regarding alignment (there may be padding before or after those members). Some systems may have alignment requirements that mean opcode will not be directly adjacent to value in memory.

There are also no guarantees regarding byte order for things like a uint32_t. Mathematical operations will work transparently, but the byte order used by the architecture for 32-bit integers may differ from the byte order required by the SSH server.

@necouchman necouchman force-pushed the working/backspace-config branch 2 times, most recently from ae1664d to 40e3508 Compare March 8, 2018 16:23
@necouchman
Copy link
Contributor Author

Okay, I've removed all of the dynamic allocation and most of the functions, aside from init. I'm using a variadic function, with a custom data structure to simplify counting and passing arguments through to that function. Let me know how this looks.

As usual, Mike, thanks for the patience and help working through this...

@necouchman
Copy link
Contributor Author

@mike-jumper Any other changes needed to wrap this one up?

#define GUAC_SSH_TTY_OP_VERASE 3

/**
* Simple structure that defines a opcode and
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a way you could describe this which provides information/context beyond the members of the structure?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Took a stab at some more thorough documentation.

*/
typedef struct guac_ssh_ttymode {
char opcode;
uint32_t value;
Copy link
Contributor

Choose a reason for hiding this comment

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

Structure members need to be documented.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Documented.

} guac_ssh_ttymode;

/**
* Initialize an array with the specified opcode/value
Copy link
Contributor

Choose a reason for hiding this comment

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

Initializes an array ... why? What's special about the array? Anything you can think of that might make the use of this function clear?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Beefed up this documentation.

if (keysym == 0xFF08) return guac_terminal_send_string(term, "\x7F"); /* Backspace */
/* Backspace can vary based on configuration of terminal by client. */
if (keysym == 0xFF08) {
char* backspace_str = malloc(sizeof(char) * 2);
Copy link
Contributor

Choose a reason for hiding this comment

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

Keep in mind that dynamic allocation incurs overhead. In some cases, it makes good sense to do this, but it's hard to justify for a 2-byte string. A static buffer would be more appropriate here, particularly since you can initialize things cleanly and clearly inline.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switched to static.

char* backspace_str = malloc(sizeof(char) * 2);
backspace_str[0] = term->backspace;
backspace_str[1] = '\0';
return guac_terminal_send_string(term, backspace_str);
Copy link
Contributor

Choose a reason for hiding this comment

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

Returning here without freeing the memory allocated above will leak memory each time backspace is pressed. Probably shouldn't be dynamically allocating at all, though (see above).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Staticized.

/* Put the end opcode in the last opcode space */
opcode_array[num_opcodes * GUAC_SSH_TTY_OPCODE_SIZE] = GUAC_SSH_TTY_OP_END;

return 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

The documentation states that this function returns the size of the array (or rather, I assume, the number of bytes written to the array), but this isn't happening here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Documentation has been fixed.


/**
* Initialize an array with the specified opcode/value
* pairs, and return the size, in bytes, of the array.
Copy link
Contributor

Choose a reason for hiding this comment

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

Do mean the number of bytes written to the array? Ignoring that the size of the array is already known to the caller, returning the size of the array is not particularly useful, especially since the array may be larger than necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Documentation updated.

int offset = i * GUAC_SSH_TTY_OPCODE_SIZE;

/* Get the next argument to this function */
guac_ssh_ttymode ttymode = va_arg(args, guac_ssh_ttymode);
Copy link
Contributor

Choose a reason for hiding this comment

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

The structure in this case is rather small, so this does not make much of a difference in this case, but beware that passing structures by value will result in a full copy of that structure, with this assignment resulting in another full copy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it possible to do this by pointer/reference rather than value? I'm not familiar with the va_arg macro and how to use it - do I just change the argument to &guac_ssh_ttymode and the assignment to guac_ssh_ttymode* ttymode?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I got this going, too - again, probably worth someone taking a look just to make sure. Seems to work, though.


/* Place opcode and value in array */
opcode_array[offset] = ttymode.opcode;
opcode_array[offset + 1] = (ttymode.value >> 24) & 0xFF;
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't need to manually calculate the offset for each byte written. If you have a pointer to the next byte to be written within the array, this could be as simple as:

*(current++) = whatever_you_want_to_write;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, took a stab at this, and I think I got it working correctly - at least, the code runs and behaves as expected. Might be worth someone looking it over, though, just to make sure I did it in a way that isn't going to hit some corner case at some point down the road.

}

/* Put the end opcode in the last opcode space */
opcode_array[num_opcodes * GUAC_SSH_TTY_OPCODE_SIZE] = GUAC_SSH_TTY_OP_END;
Copy link
Contributor

Choose a reason for hiding this comment

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

To confirm: the final "END" does not need the full 5-byte opcode and value? Just the opcode?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's correct - as soon as it encounters the 0 opcode in one of the expected positions (5 byte offsets, basically) it stops.

@necouchman
Copy link
Contributor Author

Okay, @mike-jumper, next round of updates done - let me know what else needs attention!

/* Put the end opcode in the last opcode space */
*(current) = GUAC_SSH_TTY_OP_END;

return 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

I recommend instead returning the size of the data written to the array, with zero indicating failure. This would remove the need for the caller to know ahead of time the size of the data this function will write (which is an odd requirement for what is otherwise an abstraction).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good - is there a more elegant way than just adding an integer byte counter and incrementing along the way?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've gone the byte counter route - if you have a suggestion for another way to do it, let me know.

if (keysym == 0xFF08) {
char backspace_str[2];
backspace_str[0] = term->backspace;
backspace_str[1] = '\0';
Copy link
Contributor

Choose a reason for hiding this comment

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

You can clean this up as:

char backspace_str[2] = { term->backspace, '\0' };

or even:

char backspace_str[] = { term->backspace, '\0' };

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Of course. Done & thanks.

/* Set up the ttymode array prior to requesting the PTY */
if (guac_ssh_ttymodes_init(ssh_ttymodes, sizeof(ssh_ttymodes),
num_tty_opcodes, (guac_ssh_ttymode){ GUAC_SSH_TTY_OP_VERASE, settings->backspace}))
guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Error configuring TTY mode encoding.");
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you mean by "configuring TTY mode encoding"? Is there a way to phrase this which would better represent what is failing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tried to clean up this error message bit.


/* Initialize the variable argument list. */
va_list args;
va_start(args, num_opcodes);
Copy link
Contributor

Choose a reason for hiding this comment

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

There must be a matching call to va_end() before the end of this function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added.

@@ -296,9 +301,15 @@ void* ssh_client_thread(void* data) {

}

/* Set up the ttymode array prior to requesting the PTY */
if (guac_ssh_ttymodes_init(ssh_ttymodes, sizeof(ssh_ttymodes),
num_tty_opcodes, (guac_ssh_ttymode){ GUAC_SSH_TTY_OP_VERASE, settings->backspace}))
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at the source for guac_ssh_ttymodes_init(), doesn't this need to be a pointer to a guac_ssh_ttymode? ie: &((guac_ssh_ttymode) { GUAC_SSH_TTY_OP_VERASE, settings->backspace }) ?

Alternatively, there's no need for all these parameters to be of the same type. It would also be possible to do something like:

if (guac_ssh_ttymodes_init(ssh_ttymodes, sizeof(ssh_ttymodes),
        GUAC_SSH_TTY_OP_VERASE, settings->backspace, GUAC_SSH_TTY_OP_END))

The code is your oyster.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored the guac_ssh_ttymodes_init() function. I may have introduced some other bugs or conditions that need to be handled, so let me know what you think about it.

@@ -191,6 +192,10 @@ void* ssh_client_thread(void* data) {
return NULL;
}

/* Initialize a ttymode array */
const int num_tty_opcodes = 1;
char ssh_ttymodes[(GUAC_SSH_TTY_OPCODE_SIZE * num_tty_opcodes) + 1];
Copy link
Contributor

Choose a reason for hiding this comment

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

Not really a dealbreaker, but if you want to ensure this is a constant expression while also abstracting away the calculations required, this could be handled via a macro defined in ttymode.h. For example:

#define GUAC_SSH_TTYMODES_SIZE(num_opcodes) ((GUAC_SSH_TTY_OPCODE_SIZE * num_opcodes) + 1)

You could then rely upon:

char ssh_ttymodes[GUAC_SSH_TTYMODES_SIZE(1)];

... and, of course, documenting within guac_ssh_ttymodes_init() how the minimum size for the provided buffer should be calculated.

Magic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Brilliant. I've used your macro and refactored code and comments with that.

@@ -191,6 +192,10 @@ void* ssh_client_thread(void* data) {
return NULL;
}

/* Initialize a ttymode array */
Copy link
Contributor

Choose a reason for hiding this comment

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

Point of clarification - nothing is being initialized here, only declared. With that much being obvious (that the array is being declared), rather than change "Initialize" to "Declare", I'd say just kill this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed.

@necouchman
Copy link
Contributor Author

Cleaned those items up.

int ttymodeBytes = guac_ssh_ttymodes_init(ssh_ttymodes,
GUAC_SSH_TTY_OP_VERASE, settings->backspace, GUAC_SSH_TTY_OP_END);
if (ttymodeBytes < 1)
guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Error storing TTY mode encoding \
Copy link
Contributor

Choose a reason for hiding this comment

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

This will actually log:

Error storing TTY mode encoding                 opcodes and values in array.

as the whitespace at the beginning of the following line is part of the string.

To split strings in C, all you need to do is place two strings next to each other. They're automatically concatenated by the compiler:

guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Error storing TTY mode encoding "
        "opcodes and values in array.");

As this message is meant for the user/admin, I suggest rephrasing to describe things from that perspective. For example:

Unable to set TTY modes. Backspace may not work as expected.

at the warning level may make more sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to warning.

/* We're done processing arguments. */
va_end(args);

return bytes;
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI - you can actually do:

current - opcode_array

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, of course.

* the size of the number of opcodes plus the single byte
* end opcode.
*/
#define GUAC_SSH_TTYMODES_SIZE(num_opcodes) ((GUAC_SSH_TTY_OPCODE_SIZE * num_opcodes) + 1)
Copy link
Contributor

Choose a reason for hiding this comment

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

Please document num_opcodes with @param, just as you would for a function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Documented that, and also documented the return value with @returns - not sure if that should be done, too?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yup, definitely. Good catch.

@necouchman
Copy link
Contributor Author

Next round done.

@asfgit asfgit merged commit dc1918b into apache:master Apr 2, 2018
@necouchman necouchman deleted the working/backspace-config branch April 2, 2018 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants