cscg24-guacamole

CSCG 2024 Challenge 'Guacamole Mashup'
git clone https://git.sinitax.com/sinitax/cscg24-guacamole
Log | Files | Refs | sfeed.txt

sftp.c (32931B)


      1/*
      2 * Licensed to the Apache Software Foundation (ASF) under one
      3 * or more contributor license agreements.  See the NOTICE file
      4 * distributed with this work for additional information
      5 * regarding copyright ownership.  The ASF licenses this file
      6 * to you under the Apache License, Version 2.0 (the
      7 * "License"); you may not use this file except in compliance
      8 * with the License.  You may obtain a copy of the License at
      9 *
     10 *   http://www.apache.org/licenses/LICENSE-2.0
     11 *
     12 * Unless required by applicable law or agreed to in writing,
     13 * software distributed under the License is distributed on an
     14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     15 * KIND, either express or implied.  See the License for the
     16 * specific language governing permissions and limitations
     17 * under the License.
     18 */
     19
     20#include "common-ssh/sftp.h"
     21#include "common-ssh/ssh.h"
     22
     23#include <guacamole/client.h>
     24#include <guacamole/mem.h>
     25#include <guacamole/object.h>
     26#include <guacamole/protocol.h>
     27#include <guacamole/socket.h>
     28#include <guacamole/string.h>
     29#include <guacamole/user.h>
     30#include <libssh2.h>
     31
     32#include <fcntl.h>
     33#include <libgen.h>
     34#include <stdlib.h>
     35#include <string.h>
     36
     37int guac_common_ssh_sftp_normalize_path(char* fullpath,
     38        const char* path) {
     39
     40    int path_depth = 0;
     41    const char* path_components[GUAC_COMMON_SSH_SFTP_MAX_DEPTH];
     42
     43    /* If original path is not absolute, normalization fails */
     44    if (path[0] != '\\' && path[0] != '/')
     45        return 0;
     46
     47    /* Create scratch copy of path excluding leading slash (we will be
     48     * replacing path separators with null terminators and referencing those
     49     * substrings directly as path components) */
     50    char path_scratch[GUAC_COMMON_SSH_SFTP_MAX_PATH - 1];
     51    int length = guac_strlcpy(path_scratch, path + 1,
     52            sizeof(path_scratch));
     53
     54    /* Fail if provided path is too long */
     55    if (length >= sizeof(path_scratch))
     56        return 0;
     57
     58    /* Locate all path components within path */
     59    const char* current_path_component = &(path_scratch[0]);
     60    for (int i = 0; i <= length; i++) {
     61
     62        /* If current character is a path separator, parse as component */
     63        char c = path_scratch[i];
     64        if (c == '/' || c == '\\' || c == '\0') {
     65
     66            /* Terminate current component */
     67            path_scratch[i] = '\0';
     68
     69            /* If component refers to parent, just move up in depth */
     70            if (strcmp(current_path_component, "..") == 0) {
     71                if (path_depth > 0)
     72                    path_depth--;
     73            }
     74
     75            /* Otherwise, if component not current directory, add to list */
     76            else if (strcmp(current_path_component, ".") != 0
     77                    && strcmp(current_path_component, "") != 0) {
     78
     79                /* Fail normalization if path is too deep */
     80                if (path_depth >= GUAC_COMMON_SSH_SFTP_MAX_DEPTH)
     81                    return 0;
     82
     83                path_components[path_depth++] = current_path_component;
     84
     85            }
     86
     87            /* Update start of next component */
     88            current_path_component = &(path_scratch[i+1]);
     89
     90        } /* end if separator */
     91
     92    } /* end for each character */
     93
     94    /* Add leading slash for resulting absolute path */
     95    fullpath[0] = '/';
     96
     97    /* Append normalized components to path, separated by slashes */
     98    guac_strljoin(fullpath + 1, path_components, path_depth,
     99            "/", GUAC_COMMON_SSH_SFTP_MAX_PATH - 1);
    100
    101    return 1;
    102
    103}
    104
    105/**
    106 * Translates the last error message received by the SFTP layer of an SSH
    107 * session into a Guacamole protocol status code.
    108 *
    109 * @param filesystem
    110 *     The object (not guac_object) defining the filesystem associated with the
    111 *     SFTP and SSH sessions.
    112 *
    113 * @return
    114 *     The Guacamole protocol status code corresponding to the last reported
    115 *     error of the SFTP layer, if nay, or GUAC_PROTOCOL_STATUS_SUCCESS if no
    116 *     error has occurred.
    117 */
    118static guac_protocol_status guac_sftp_get_status(
    119        guac_common_ssh_sftp_filesystem* filesystem) {
    120
    121    /* Get libssh2 objects */
    122    LIBSSH2_SFTP*    sftp    = filesystem->sftp_session;
    123    LIBSSH2_SESSION* session = filesystem->ssh_session->session;
    124
    125    /* Return success code if no error occurred */
    126    if (libssh2_session_last_errno(session) != LIBSSH2_ERROR_SFTP_PROTOCOL)
    127        return GUAC_PROTOCOL_STATUS_SUCCESS;
    128
    129    /* Translate SFTP error codes defined by
    130     * https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02 (the most
    131     * commonly-implemented standard) */
    132    switch (libssh2_sftp_last_error(sftp)) {
    133
    134        /* SSH_FX_OK (not an error) */
    135        case 0:
    136            return GUAC_PROTOCOL_STATUS_SUCCESS;
    137
    138        /* SSH_FX_EOF (technically not an error) */
    139        case 1:
    140            return GUAC_PROTOCOL_STATUS_SUCCESS;
    141
    142        /* SSH_FX_NO_SUCH_FILE */
    143        case 2:
    144            return GUAC_PROTOCOL_STATUS_RESOURCE_NOT_FOUND;
    145
    146        /* SSH_FX_PERMISSION_DENIED */
    147        case 3:
    148            return GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN;
    149
    150        /* SSH_FX_FAILURE */
    151        case 4:
    152            return GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR;
    153
    154        /* SSH_FX_BAD_MESSAGE */
    155        case 5:
    156            return GUAC_PROTOCOL_STATUS_SERVER_ERROR;
    157
    158        /* SSH_FX_NO_CONNECTION / SSH_FX_CONNECTION_LOST */
    159        case 6:
    160        case 7:
    161            return GUAC_PROTOCOL_STATUS_UPSTREAM_TIMEOUT;
    162
    163        /* SSH_FX_OP_UNSUPPORTED */
    164        case 8:
    165            return GUAC_PROTOCOL_STATUS_UNSUPPORTED;
    166
    167        /* Return generic error if cause unknown */
    168        default:
    169            return GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR;
    170
    171    }
    172
    173}
    174
    175/**
    176 * Concatenates the given filename with the given path, separating the two
    177 * with a single forward slash. The full result must be no more than
    178 * GUAC_COMMON_SSH_SFTP_MAX_PATH bytes long, counting null terminator.
    179 *
    180 * @param fullpath
    181 *     The buffer to store the result within. This buffer must be at least
    182 *     GUAC_COMMON_SSH_SFTP_MAX_PATH bytes long.
    183 *
    184 * @param path
    185 *     The path to append the filename to.
    186 *
    187 * @param filename
    188 *     The filename to append to the path.
    189 *
    190 * @return
    191 *     Non-zero if the filename is valid and was successfully appended to the
    192 *     path, zero otherwise.
    193 */
    194static int guac_ssh_append_filename(char* fullpath, const char* path,
    195        const char* filename) {
    196
    197    int length;
    198
    199    /* Disallow "." as a filename */
    200    if (strcmp(filename, ".") == 0)
    201        return 0;
    202
    203    /* Disallow ".." as a filename */
    204    if (strcmp(filename, "..") == 0)
    205        return 0;
    206
    207    /* Filenames may not contain slashes */
    208    if (strchr(filename, '/') != NULL)
    209        return 0;
    210
    211    /* Copy base path */
    212    length = guac_strlcpy(fullpath, path, GUAC_COMMON_SSH_SFTP_MAX_PATH);
    213
    214    /*
    215     * Append trailing slash only if:
    216     *  1) Trailing slash is not already present
    217     *  2) Path is non-empty
    218     */
    219    if (length > 0 && fullpath[length - 1] != '/')
    220        length += guac_strlcpy(fullpath + length, "/",
    221                GUAC_COMMON_SSH_SFTP_MAX_PATH - length);
    222
    223    /* Append filename */
    224    length += guac_strlcpy(fullpath + length, filename,
    225            GUAC_COMMON_SSH_SFTP_MAX_PATH - length);
    226
    227    /* Verify path length is within maximum */
    228    if (length >= GUAC_COMMON_SSH_SFTP_MAX_PATH)
    229        return 0;
    230
    231    /* Append was successful */
    232    return 1;
    233
    234}
    235
    236/**
    237 * Concatenates the given paths, separating the two with a single forward
    238 * slash. The full result must be no more than GUAC_COMMON_SSH_SFTP_MAX_PATH
    239 * bytes long, counting null terminator.
    240 *
    241 * @param fullpath
    242 *     The buffer to store the result within. This buffer must be at least
    243 *     GUAC_COMMON_SSH_SFTP_MAX_PATH bytes long.
    244 *
    245 * @param path_a
    246 *     The path to place at the beginning of the resulting path.
    247 *
    248 * @param path_b
    249 *     The path to append after path_a within the resulting path.
    250 *
    251 * @return
    252 *     Non-zero if the paths were successfully concatenated together, zero
    253 *     otherwise.
    254 */
    255static int guac_ssh_append_path(char* fullpath, const char* path_a,
    256        const char* path_b) {
    257
    258    int length;
    259
    260    /* Copy first half of path */
    261    length = guac_strlcpy(fullpath, path_a, GUAC_COMMON_SSH_SFTP_MAX_PATH);
    262    if (length >= GUAC_COMMON_SSH_SFTP_MAX_PATH)
    263        return 0;
    264
    265    /* Ensure path ends with trailing slash */
    266    if (length == 0 || fullpath[length - 1] != '/')
    267        length += guac_strlcpy(fullpath + length, "/",
    268                GUAC_COMMON_SSH_SFTP_MAX_PATH - length);
    269
    270    /* Skip past leading slashes in second path */
    271    while (*path_b == '/')
    272       path_b++;
    273
    274    /* Append final half of path */
    275    length += guac_strlcpy(fullpath + length, path_b,
    276            GUAC_COMMON_SSH_SFTP_MAX_PATH - length);
    277
    278    /* Verify path length is within maximum */
    279    if (length >= GUAC_COMMON_SSH_SFTP_MAX_PATH)
    280        return 0;
    281
    282    /* Append was successful */
    283    return 1;
    284
    285}
    286
    287/**
    288 * Handler for blob messages which continue an inbound SFTP data transfer
    289 * (upload). The data associated with the given stream is expected to be a
    290 * pointer to an open LIBSSH2_SFTP_HANDLE for the file to which the data
    291 * should be written.
    292 *
    293 * @param user
    294 *     The user receiving the blob message.
    295 *
    296 * @param stream
    297 *     The Guacamole protocol stream associated with the received blob message.
    298 *
    299 * @param data
    300 *     The data received within the blob.
    301 *
    302 * @param length
    303 *     The length of the received data, in bytes.
    304 *
    305 * @return
    306 *     Zero if the blob is handled successfully, or non-zero on error.
    307 */
    308static int guac_common_ssh_sftp_blob_handler(guac_user* user,
    309        guac_stream* stream, void* data, int length) {
    310
    311    /* Pull file from stream */
    312    LIBSSH2_SFTP_HANDLE* file = (LIBSSH2_SFTP_HANDLE*) stream->data;
    313
    314    /* Attempt write */
    315    if (libssh2_sftp_write(file, data, length) == length) {
    316        guac_user_log(user, GUAC_LOG_DEBUG, "%i bytes written", length);
    317        guac_protocol_send_ack(user->socket, stream, "SFTP: OK",
    318                GUAC_PROTOCOL_STATUS_SUCCESS);
    319        guac_socket_flush(user->socket);
    320    }
    321
    322    /* Inform of any errors */
    323    else {
    324        guac_user_log(user, GUAC_LOG_INFO, "Unable to write to file");
    325        guac_protocol_send_ack(user->socket, stream, "SFTP: Write failed",
    326                GUAC_PROTOCOL_STATUS_SERVER_ERROR);
    327        guac_socket_flush(user->socket);
    328    }
    329
    330    return 0;
    331
    332}
    333
    334/**
    335 * Handler for end messages which terminate an inbound SFTP data transfer
    336 * (upload). The data associated with the given stream is expected to be a
    337 * pointer to an open LIBSSH2_SFTP_HANDLE for the file to which the data
    338 * has been written and which should now be closed.
    339 *
    340 * @param user
    341 *     The user receiving the end message.
    342 *
    343 * @param stream
    344 *     The Guacamole protocol stream associated with the received end message.
    345 *
    346 * @return
    347 *     Zero if the file is closed successfully, or non-zero on error.
    348 */
    349static int guac_common_ssh_sftp_end_handler(guac_user* user,
    350        guac_stream* stream) {
    351
    352    /* Pull file from stream */
    353    LIBSSH2_SFTP_HANDLE* file = (LIBSSH2_SFTP_HANDLE*) stream->data;
    354
    355    /* Attempt to close file */
    356    if (libssh2_sftp_close(file) == 0) {
    357        guac_user_log(user, GUAC_LOG_DEBUG, "File closed");
    358        guac_protocol_send_ack(user->socket, stream, "SFTP: OK",
    359                GUAC_PROTOCOL_STATUS_SUCCESS);
    360        guac_socket_flush(user->socket);
    361    }
    362    else {
    363        guac_user_log(user, GUAC_LOG_INFO, "Unable to close file");
    364        guac_protocol_send_ack(user->socket, stream, "SFTP: Close failed",
    365                GUAC_PROTOCOL_STATUS_SERVER_ERROR);
    366        guac_socket_flush(user->socket);
    367    }
    368
    369    return 0;
    370
    371}
    372
    373int guac_common_ssh_sftp_handle_file_stream(
    374        guac_common_ssh_sftp_filesystem* filesystem, guac_user* user,
    375        guac_stream* stream, char* mimetype, char* filename) {
    376
    377    char fullpath[GUAC_COMMON_SSH_SFTP_MAX_PATH];
    378    LIBSSH2_SFTP_HANDLE* file;
    379
    380    /* Ignore upload if uploads have been disabled */
    381    if (filesystem->disable_upload) {
    382        guac_user_log(user, GUAC_LOG_WARNING, "A upload attempt has "
    383                "been blocked due to uploads being disabled, however it "
    384                "should have been blocked at a higher level. This is likely "
    385                "a bug.");
    386        guac_protocol_send_ack(user->socket, stream, "SFTP: Upload disabled",
    387                GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN);
    388        guac_socket_flush(user->socket);
    389        return 0;
    390    }
    391
    392    /* Concatenate filename with path */
    393    if (!guac_ssh_append_filename(fullpath, filesystem->upload_path,
    394                filename)) {
    395
    396        guac_user_log(user, GUAC_LOG_DEBUG,
    397                "Filename \"%s\" is invalid or resulting path is too long",
    398                filename);
    399
    400        /* Abort transfer - invalid filename */
    401        guac_protocol_send_ack(user->socket, stream, 
    402                "SFTP: Illegal filename",
    403                GUAC_PROTOCOL_STATUS_CLIENT_BAD_REQUEST);
    404
    405        guac_socket_flush(user->socket);
    406        return 0;
    407    }
    408
    409    /* Open file via SFTP */
    410    file = libssh2_sftp_open(filesystem->sftp_session, fullpath,
    411            LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_TRUNC,
    412            S_IRUSR | S_IWUSR);
    413
    414    /* Inform of status */
    415    if (file != NULL) {
    416
    417        guac_user_log(user, GUAC_LOG_DEBUG,
    418                "File \"%s\" opened",
    419                fullpath);
    420
    421        guac_protocol_send_ack(user->socket, stream, "SFTP: File opened",
    422                GUAC_PROTOCOL_STATUS_SUCCESS);
    423        guac_socket_flush(user->socket);
    424    }
    425    else {
    426        guac_user_log(user, GUAC_LOG_INFO,
    427                "Unable to open file \"%s\"", fullpath);
    428        guac_protocol_send_ack(user->socket, stream, "SFTP: Open failed",
    429                guac_sftp_get_status(filesystem));
    430        guac_socket_flush(user->socket);
    431    }
    432
    433    /* Set handlers for file stream */
    434    stream->blob_handler = guac_common_ssh_sftp_blob_handler;
    435    stream->end_handler = guac_common_ssh_sftp_end_handler;
    436
    437    /* Store file within stream */
    438    stream->data = file;
    439    return 0;
    440
    441}
    442
    443/**
    444 * Handler for ack messages which continue an outbound SFTP data transfer
    445 * (download), signaling the current status and requesting additional data.
    446 * The data associated with the given stream is expected to be a pointer to an
    447 * open LIBSSH2_SFTP_HANDLE for the file from which the data is to be read.
    448 *
    449 * @param user
    450 *     The user receiving the ack message.
    451 *
    452 * @param stream
    453 *     The Guacamole protocol stream associated with the received ack message.
    454 *
    455 * @param message
    456 *     An arbitrary human-readable message describing the nature of the
    457 *     success or failure denoted by the ack message.
    458 *
    459 * @param status
    460 *     The status code associated with the ack message, which may indicate
    461 *     success or an error.
    462 *
    463 * @return
    464 *     Zero if the file is read from successfully, or non-zero on error.
    465 */
    466static int guac_common_ssh_sftp_ack_handler(guac_user* user,
    467        guac_stream* stream, char* message, guac_protocol_status status) {
    468
    469    /* Pull file from stream */
    470    LIBSSH2_SFTP_HANDLE* file = (LIBSSH2_SFTP_HANDLE*) stream->data;
    471
    472    /* If successful, read data */
    473    if (status == GUAC_PROTOCOL_STATUS_SUCCESS) {
    474
    475        /* Attempt read into buffer */
    476        char buffer[4096];
    477        int bytes_read = libssh2_sftp_read(file, buffer, sizeof(buffer)); 
    478
    479        /* If bytes read, send as blob */
    480        if (bytes_read > 0) {
    481            guac_protocol_send_blob(user->socket, stream,
    482                    buffer, bytes_read);
    483
    484            guac_user_log(user, GUAC_LOG_DEBUG, "%i bytes sent to user",
    485                    bytes_read);
    486
    487        }
    488
    489        /* If bytes could not be read, handle EOF or error condition */
    490        else {
    491
    492            /* If EOF, send end */
    493            if (bytes_read == 0) {
    494                guac_user_log(user, GUAC_LOG_DEBUG, "File sent");
    495                guac_protocol_send_end(user->socket, stream);
    496                guac_user_free_stream(user, stream);
    497            }
    498
    499            /* Otherwise, fail stream */
    500            else {
    501                guac_user_log(user, GUAC_LOG_INFO, "Error reading file");
    502                guac_protocol_send_end(user->socket, stream);
    503                guac_user_free_stream(user, stream);
    504            }
    505
    506            /* Close file */
    507            if (libssh2_sftp_close(file) == 0)
    508                guac_user_log(user, GUAC_LOG_DEBUG, "File closed");
    509            else
    510                guac_user_log(user, GUAC_LOG_INFO, "Unable to close file");
    511
    512        }
    513
    514        guac_socket_flush(user->socket);
    515
    516    }
    517
    518    /* Otherwise, return stream to user */
    519    else
    520        guac_user_free_stream(user, stream);
    521
    522    return 0;
    523}
    524
    525guac_stream* guac_common_ssh_sftp_download_file(
    526        guac_common_ssh_sftp_filesystem* filesystem, guac_user* user,
    527        char* filename) {
    528
    529    guac_stream* stream;
    530    LIBSSH2_SFTP_HANDLE* file;
    531
    532    /* Ignore download if downloads have been disabled */
    533    if (filesystem->disable_download) {
    534        guac_user_log(user, GUAC_LOG_WARNING, "A download attempt has "
    535                "been blocked due to downloads being disabled, however it "
    536                "should have been blocked at a higher level. This is likely "
    537                "a bug.");
    538        return NULL;
    539    }
    540
    541    /* Attempt to open file for reading */
    542    file = libssh2_sftp_open(filesystem->sftp_session, filename,
    543            LIBSSH2_FXF_READ, 0);
    544    if (file == NULL) {
    545        guac_user_log(user, GUAC_LOG_INFO, 
    546                "Unable to read file \"%s\"", filename);
    547        return NULL;
    548    }
    549
    550    /* Allocate stream */
    551    stream = guac_user_alloc_stream(user);
    552    stream->ack_handler = guac_common_ssh_sftp_ack_handler;
    553    stream->data = file;
    554
    555    /* Send stream start, strip name */
    556    filename = basename(filename);
    557    guac_protocol_send_file(user->socket, stream,
    558            "application/octet-stream", filename);
    559    guac_socket_flush(user->socket);
    560
    561    guac_user_log(user, GUAC_LOG_DEBUG, "Sending file \"%s\"", filename);
    562    return stream;
    563
    564}
    565
    566void guac_common_ssh_sftp_set_upload_path(
    567        guac_common_ssh_sftp_filesystem* filesystem, const char* path) {
    568
    569    guac_client* client = filesystem->ssh_session->client;
    570
    571    /* Ignore requests which exceed maximum-allowed path */
    572    int length = strnlen(path, GUAC_COMMON_SSH_SFTP_MAX_PATH)+1;
    573    if (length > GUAC_COMMON_SSH_SFTP_MAX_PATH) {
    574        guac_client_log(client, GUAC_LOG_ERROR,
    575                "Submitted path exceeds limit of %i bytes",
    576                GUAC_COMMON_SSH_SFTP_MAX_PATH);
    577        return;
    578    }
    579
    580    /* Copy path */
    581    memcpy(filesystem->upload_path, path, length);
    582    guac_client_log(client, GUAC_LOG_DEBUG, "Upload path set to \"%s\"", path);
    583
    584}
    585
    586/**
    587 * Handler for ack messages received due to receipt of a "body" or "blob"
    588 * instruction associated with a SFTP directory list operation.
    589 *
    590 * @param user
    591 *     The user receiving the ack message.
    592 *
    593 * @param stream
    594 *     The Guacamole protocol stream associated with the received ack message.
    595 *
    596 * @param message
    597 *     An arbitrary human-readable message describing the nature of the
    598 *     success or failure denoted by this ack message.
    599 *
    600 * @param status
    601 *     The status code associated with this ack message, which may indicate
    602 *     success or an error.
    603 *
    604 * @return
    605 *     Zero on success, non-zero on error.
    606 */
    607static int guac_common_ssh_sftp_ls_ack_handler(guac_user* user,
    608        guac_stream* stream, char* message, guac_protocol_status status) {
    609
    610    int bytes_read;
    611
    612    char filename[GUAC_COMMON_SSH_SFTP_MAX_PATH];
    613    LIBSSH2_SFTP_ATTRIBUTES attributes;
    614
    615    guac_common_ssh_sftp_ls_state* list_state =
    616        (guac_common_ssh_sftp_ls_state*) stream->data;
    617
    618    guac_common_ssh_sftp_filesystem* filesystem = list_state->filesystem;
    619
    620    LIBSSH2_SFTP* sftp = filesystem->sftp_session;
    621
    622    /* If unsuccessful, free stream and abort */
    623    if (status != GUAC_PROTOCOL_STATUS_SUCCESS) {
    624        libssh2_sftp_closedir(list_state->directory);
    625        guac_user_free_stream(user, stream);
    626        guac_mem_free(list_state);
    627        return 0;
    628    }
    629
    630    /* While directory entries remain */
    631    while ((bytes_read = libssh2_sftp_readdir(list_state->directory,
    632                filename, sizeof(filename), &attributes)) > 0) {
    633
    634        char absolute_path[GUAC_COMMON_SSH_SFTP_MAX_PATH];
    635
    636        /* Skip current and parent directory entries */
    637        if (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0)
    638            continue;
    639
    640        /* Concatenate into absolute path - skip if invalid */
    641        if (!guac_ssh_append_filename(absolute_path, 
    642                    list_state->directory_name, filename)) {
    643
    644            guac_user_log(user, GUAC_LOG_DEBUG,
    645                    "Skipping filename \"%s\" - filename is invalid or "
    646                    "resulting path is too long", filename);
    647
    648            continue;
    649        }
    650
    651        /* Stat explicitly if symbolic link (might point to directory) */
    652        if (LIBSSH2_SFTP_S_ISLNK(attributes.permissions))
    653            libssh2_sftp_stat(sftp, absolute_path, &attributes);
    654
    655        /* Determine mimetype */
    656        const char* mimetype;
    657        if (LIBSSH2_SFTP_S_ISDIR(attributes.permissions))
    658            mimetype = GUAC_USER_STREAM_INDEX_MIMETYPE;
    659        else
    660            mimetype = "application/octet-stream";
    661
    662        /* Write entry, waiting for next ack if a blob is written */
    663        if (guac_common_json_write_property(user, stream,
    664                    &list_state->json_state, absolute_path, mimetype))
    665            break;
    666
    667    }
    668
    669    /* Complete JSON and cleanup at end of directory */
    670    if (bytes_read <= 0) {
    671
    672        /* Complete JSON object */
    673        guac_common_json_end_object(user, stream, &list_state->json_state);
    674        guac_common_json_flush(user, stream, &list_state->json_state);
    675
    676        /* Clean up resources */
    677        libssh2_sftp_closedir(list_state->directory);
    678        guac_mem_free(list_state);
    679
    680        /* Signal of stream */
    681        guac_protocol_send_end(user->socket, stream);
    682        guac_user_free_stream(user, stream);
    683
    684    }
    685
    686    guac_socket_flush(user->socket);
    687    return 0;
    688
    689}
    690
    691/**
    692 * Translates a stream name for the given SFTP filesystem object into the
    693 * absolute path corresponding to the actual file it represents.
    694 *
    695 * @param fullpath
    696 *     The buffer to populate with the translated path. This buffer MUST be at
    697 *     least GUAC_COMMON_SSH_SFTP_MAX_PATH bytes in size.
    698 *
    699 * @param object
    700 *     The Guacamole protocol object associated with the SFTP filesystem.
    701 *
    702 * @param name
    703 *     The name of the stream (file) to translate into an absolute path.
    704 *
    705 * @return
    706 *     Non-zero if translation succeeded, zero otherwise.
    707 */
    708static int guac_common_ssh_sftp_translate_name(char* fullpath,
    709        guac_object* object, char* name) {
    710
    711    char normalized_name[GUAC_COMMON_SSH_SFTP_MAX_PATH];
    712
    713    guac_common_ssh_sftp_filesystem* filesystem =
    714        (guac_common_ssh_sftp_filesystem*) object->data;
    715
    716    /* Normalize stream name into a path, and append to the root path */
    717    return guac_common_ssh_sftp_normalize_path(normalized_name, name)
    718        && guac_ssh_append_path(fullpath, filesystem->root_path,
    719                normalized_name);
    720
    721}
    722
    723/**
    724 * Handler for get messages. In context of SFTP and the filesystem exposed via
    725 * the Guacamole protocol, get messages request the body of a file within the
    726 * filesystem.
    727 *
    728 * @param user
    729 *     The user who sent the get message.
    730 *
    731 * @param object
    732 *     The Guacamole protocol object associated with the get request itself.
    733 *
    734 * @param name
    735 *     The name of the input stream (file) being requested.
    736 *
    737 * @return
    738 *     Zero on success, non-zero on error.
    739 */
    740static int guac_common_ssh_sftp_get_handler(guac_user* user,
    741        guac_object* object, char* name) {
    742
    743    char fullpath[GUAC_COMMON_SSH_SFTP_MAX_PATH];
    744
    745    guac_common_ssh_sftp_filesystem* filesystem =
    746        (guac_common_ssh_sftp_filesystem*) object->data;
    747
    748    LIBSSH2_SFTP* sftp = filesystem->sftp_session;
    749    LIBSSH2_SFTP_ATTRIBUTES attributes;
    750
    751    /* Translate stream name into filesystem path */
    752    if (!guac_common_ssh_sftp_translate_name(fullpath, object, name)) {
    753        guac_user_log(user, GUAC_LOG_INFO, "Unable to generate real path "
    754                "for stream \"%s\"", name);
    755        return 0;
    756    }
    757
    758    /* Attempt to read file information */
    759    if (libssh2_sftp_stat(sftp, fullpath, &attributes)) {
    760        guac_user_log(user, GUAC_LOG_INFO, "Unable to read file \"%s\"",
    761                fullpath);
    762        return 0;
    763    }
    764
    765    /* If directory, send contents of directory */
    766    if (LIBSSH2_SFTP_S_ISDIR(attributes.permissions)) {
    767
    768        /* Open as directory */
    769        LIBSSH2_SFTP_HANDLE* dir = libssh2_sftp_opendir(sftp, fullpath);
    770        if (dir == NULL) {
    771            guac_user_log(user, GUAC_LOG_INFO,
    772                    "Unable to read directory \"%s\"", fullpath);
    773            return 0;
    774        }
    775
    776        /* Init directory listing state */
    777        guac_common_ssh_sftp_ls_state* list_state =
    778            guac_mem_alloc(sizeof(guac_common_ssh_sftp_ls_state));
    779
    780        list_state->directory = dir;
    781        list_state->filesystem = filesystem;
    782
    783        int length = guac_strlcpy(list_state->directory_name, name,
    784                sizeof(list_state->directory_name));
    785
    786        /* Bail out if directory name is too long to store */
    787        if (length >= sizeof(list_state->directory_name)) {
    788            guac_user_log(user, GUAC_LOG_INFO, "Unable to read directory "
    789                    "\"%s\": Path too long", fullpath);
    790            guac_mem_free(list_state);
    791            return 0;
    792        }
    793
    794        /* Allocate stream for body */
    795        guac_stream* stream = guac_user_alloc_stream(user);
    796        stream->ack_handler = guac_common_ssh_sftp_ls_ack_handler;
    797        stream->data = list_state;
    798
    799        /* Init JSON object state */
    800        guac_common_json_begin_object(user, stream, &list_state->json_state);
    801
    802        /* Associate new stream with get request */
    803        guac_protocol_send_body(user->socket, object, stream,
    804                GUAC_USER_STREAM_INDEX_MIMETYPE, name);
    805
    806    }
    807
    808    /* Otherwise, send file contents */
    809    else {
    810
    811        /* If downloads are disabled, log and return. */
    812        if (filesystem->disable_download) {
    813            guac_user_log(user, GUAC_LOG_INFO,
    814                    "Unable to download file \"%s\", "
    815                    "file downloads have been disabled.", fullpath);
    816            return 0;
    817        }
    818        
    819        /* Open as normal file */
    820        LIBSSH2_SFTP_HANDLE* file = libssh2_sftp_open(sftp, fullpath,
    821            LIBSSH2_FXF_READ, 0);
    822        if (file == NULL) {
    823            guac_user_log(user, GUAC_LOG_INFO,
    824                    "Unable to read file \"%s\"", fullpath);
    825            return 0;
    826        }
    827
    828        /* Allocate stream for body */
    829        guac_stream* stream = guac_user_alloc_stream(user);
    830        stream->ack_handler = guac_common_ssh_sftp_ack_handler;
    831        stream->data = file;
    832
    833        /* Associate new stream with get request */
    834        guac_protocol_send_body(user->socket, object, stream,
    835                "application/octet-stream", name);
    836
    837    }
    838
    839    guac_socket_flush(user->socket);
    840    return 0;
    841}
    842
    843/**
    844 * Handler for put messages. In context of SFTP and the filesystem exposed via
    845 * the Guacamole protocol, put messages request write access to a file within
    846 * the filesystem.
    847 *
    848 * @param user
    849 *     The user who sent the put message.
    850 *
    851 * @param object
    852 *     The Guacamole protocol object associated with the put request itself.
    853 *
    854 * @param stream
    855 *     The Guacamole protocol stream along which the user will be sending
    856 *     file data.
    857 *
    858 * @param mimetype
    859 *     The mimetype of the data being send along the stream.
    860 *
    861 * @param name
    862 *     The name of the input stream (file) being requested.
    863 *
    864 * @return
    865 *     Zero on success, non-zero on error.
    866 */
    867static int guac_common_ssh_sftp_put_handler(guac_user* user,
    868        guac_object* object, guac_stream* stream, char* mimetype, char* name) {
    869
    870    char fullpath[GUAC_COMMON_SSH_SFTP_MAX_PATH];
    871
    872    guac_common_ssh_sftp_filesystem* filesystem =
    873        (guac_common_ssh_sftp_filesystem*) object->data;
    874
    875    /* Ignore upload if uploads have been disabled */
    876    if (filesystem->disable_upload) {
    877        guac_user_log(user, GUAC_LOG_WARNING, "A upload attempt has "
    878                "been blocked due to uploads being disabled, however it "
    879                "should have been blocked at a higher level. This is likely "
    880                "a bug.");
    881        guac_protocol_send_ack(user->socket, stream, "SFTP: Upload disabled",
    882                GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN);
    883        guac_socket_flush(user->socket);
    884        return 0;
    885    }
    886
    887    LIBSSH2_SFTP* sftp = filesystem->sftp_session;
    888
    889    /* Translate stream name into filesystem path */
    890    if (!guac_common_ssh_sftp_translate_name(fullpath, object, name)) {
    891        guac_user_log(user, GUAC_LOG_INFO, "Unable to generate real path "
    892                "for stream \"%s\"", name);
    893        return 0;
    894    }
    895
    896    /* Open file via SFTP */
    897    LIBSSH2_SFTP_HANDLE* file = libssh2_sftp_open(sftp, fullpath,
    898            LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_TRUNC,
    899            S_IRUSR | S_IWUSR);
    900
    901    /* Acknowledge stream if successful */
    902    if (file != NULL) {
    903        guac_user_log(user, GUAC_LOG_DEBUG, "File \"%s\" opened", fullpath);
    904        guac_protocol_send_ack(user->socket, stream, "SFTP: File opened",
    905                GUAC_PROTOCOL_STATUS_SUCCESS);
    906    }
    907
    908    /* Abort on failure */
    909    else {
    910        guac_user_log(user, GUAC_LOG_INFO,
    911                "Unable to open file \"%s\"", fullpath);
    912        guac_protocol_send_ack(user->socket, stream, "SFTP: Open failed",
    913                guac_sftp_get_status(filesystem));
    914    }
    915
    916    /* Set handlers for file stream */
    917    stream->blob_handler = guac_common_ssh_sftp_blob_handler;
    918    stream->end_handler = guac_common_ssh_sftp_end_handler;
    919
    920    /* Store file within stream */
    921    stream->data = file;
    922
    923    guac_socket_flush(user->socket);
    924    return 0;
    925}
    926
    927void* guac_common_ssh_expose_sftp_filesystem(guac_user* user, void* data) {
    928
    929    guac_common_ssh_sftp_filesystem* filesystem =
    930        (guac_common_ssh_sftp_filesystem*) data;
    931
    932    /* No need to expose if there is no filesystem or the user has left */
    933    if (user == NULL || filesystem == NULL)
    934        return NULL;
    935
    936    /* Allocate and expose filesystem object for user */
    937    return guac_common_ssh_alloc_sftp_filesystem_object(filesystem, user);
    938
    939}
    940
    941guac_object* guac_common_ssh_alloc_sftp_filesystem_object(
    942        guac_common_ssh_sftp_filesystem* filesystem, guac_user* user) {
    943
    944    /* Init filesystem */
    945    guac_object* fs_object = guac_user_alloc_object(user);
    946    fs_object->get_handler = guac_common_ssh_sftp_get_handler;
    947    
    948    /* Only handle uploads if not disabled. */
    949    if (!filesystem->disable_upload)
    950        fs_object->put_handler = guac_common_ssh_sftp_put_handler;
    951    
    952    fs_object->data = filesystem;
    953
    954    /* Send filesystem to user */
    955    guac_protocol_send_filesystem(user->socket, fs_object, filesystem->name);
    956    guac_socket_flush(user->socket);
    957
    958    return fs_object;
    959
    960}
    961
    962guac_common_ssh_sftp_filesystem* guac_common_ssh_create_sftp_filesystem(
    963        guac_common_ssh_session* session, const char* root_path,
    964        const char* name, int disable_download, int disable_upload) {
    965
    966    /* Request SFTP */
    967    LIBSSH2_SFTP* sftp_session = libssh2_sftp_init(session->session);
    968    if (sftp_session == NULL)
    969        return NULL;
    970
    971    /* Allocate data for SFTP session */
    972    guac_common_ssh_sftp_filesystem* filesystem =
    973        guac_mem_alloc(sizeof(guac_common_ssh_sftp_filesystem));
    974
    975    /* Associate SSH session with SFTP data and user */
    976    filesystem->ssh_session = session;
    977    filesystem->sftp_session = sftp_session;
    978    
    979    /* Copy over disable flags */
    980    filesystem->disable_download = disable_download;
    981    filesystem->disable_upload = disable_upload;
    982
    983    /* Normalize and store the provided root path */
    984    if (!guac_common_ssh_sftp_normalize_path(filesystem->root_path,
    985                root_path)) {
    986        guac_client_log(session->client, GUAC_LOG_WARNING, "Cannot create "
    987                "SFTP filesystem - \"%s\" is not a valid path.", root_path);
    988        guac_mem_free(filesystem);
    989        return NULL;
    990    }
    991
    992    /* Generate filesystem name from root path if no name is provided */
    993    if (name != NULL)
    994        filesystem->name = guac_strdup(name);
    995    else
    996        filesystem->name = guac_strdup(filesystem->root_path);
    997
    998    /* Initially upload files to current directory */
    999    strcpy(filesystem->upload_path, ".");
   1000
   1001    /* Return allocated filesystem */
   1002    return filesystem;
   1003
   1004}
   1005
   1006void guac_common_ssh_destroy_sftp_filesystem(
   1007        guac_common_ssh_sftp_filesystem* filesystem) {
   1008
   1009    /* Shutdown SFTP session */
   1010    libssh2_sftp_shutdown(filesystem->sftp_session);
   1011
   1012    /* Free associated memory */
   1013    guac_mem_free(filesystem->name);
   1014    guac_mem_free(filesystem);
   1015
   1016}
   1017