1/* 2 * Copyright (c) 2013 Lukasz Marek <lukasz.m.luki@gmail.com> 3 * 4 * This file is part of FFmpeg. 5 * 6 * FFmpeg is free software; you can redistribute it and/or 7 * modify it under the terms of the GNU Lesser General Public 8 * License as published by the Free Software Foundation; either 9 * version 2.1 of the License, or (at your option) any later version. 10 * 11 * FFmpeg is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 * Lesser General Public License for more details. 15 * 16 * You should have received a copy of the GNU Lesser General Public 17 * License along with FFmpeg; if not, write to the Free Software 18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 */ 20 21#include <fcntl.h> 22#define LIBSSH_STATIC 23#include <libssh/sftp.h> 24#include "libavutil/avstring.h" 25#include "libavutil/opt.h" 26#include "libavutil/attributes.h" 27#include "libavformat/avio.h" 28#include "avformat.h" 29#include "internal.h" 30#include "url.h" 31 32typedef struct { 33 const AVClass *class; 34 ssh_session session; 35 sftp_session sftp; 36 sftp_file file; 37 sftp_dir dir; 38 int64_t filesize; 39 int rw_timeout; 40 int trunc; 41 char *priv_key; 42} LIBSSHContext; 43 44static av_cold int libssh_create_ssh_session(LIBSSHContext *libssh, const char* hostname, unsigned int port) 45{ 46 static const int verbosity = SSH_LOG_NOLOG; 47 48 if (!(libssh->session = ssh_new())) { 49 av_log(libssh, AV_LOG_ERROR, "SSH session creation failed: %s\n", ssh_get_error(libssh->session)); 50 return AVERROR(ENOMEM); 51 } 52 ssh_options_set(libssh->session, SSH_OPTIONS_HOST, hostname); 53 ssh_options_set(libssh->session, SSH_OPTIONS_PORT, &port); 54 ssh_options_set(libssh->session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity); 55 if (libssh->rw_timeout > 0) { 56 long timeout = libssh->rw_timeout * 1000; 57 ssh_options_set(libssh->session, SSH_OPTIONS_TIMEOUT_USEC, &timeout); 58 } 59 60 if (ssh_options_parse_config(libssh->session, NULL) < 0) { 61 av_log(libssh, AV_LOG_WARNING, "Could not parse the config file.\n"); 62 } 63 64 if (ssh_connect(libssh->session) != SSH_OK) { 65 av_log(libssh, AV_LOG_ERROR, "Connection failed: %s\n", ssh_get_error(libssh->session)); 66 return AVERROR(EIO); 67 } 68 69 return 0; 70} 71 72static av_cold int libssh_authentication(LIBSSHContext *libssh, const char *user, const char *password) 73{ 74 int authorized = 0; 75 int auth_methods; 76 77 if (user) 78 ssh_options_set(libssh->session, SSH_OPTIONS_USER, user); 79 80 if (ssh_userauth_none(libssh->session, NULL) == SSH_AUTH_SUCCESS) 81 return 0; 82 83 auth_methods = ssh_userauth_list(libssh->session, NULL); 84 85 if (auth_methods & SSH_AUTH_METHOD_PUBLICKEY) { 86 if (libssh->priv_key) { 87 ssh_string pub_key; 88 ssh_private_key priv_key; 89 int type; 90 if (!ssh_try_publickey_from_file(libssh->session, libssh->priv_key, &pub_key, &type)) { 91 priv_key = privatekey_from_file(libssh->session, libssh->priv_key, type, password); 92 if (ssh_userauth_pubkey(libssh->session, NULL, pub_key, priv_key) == SSH_AUTH_SUCCESS) { 93 av_log(libssh, AV_LOG_DEBUG, "Authentication successful with selected private key.\n"); 94 authorized = 1; 95 } 96 } else { 97 av_log(libssh, AV_LOG_DEBUG, "Invalid key is provided.\n"); 98 return AVERROR(EACCES); 99 } 100 } else if (ssh_userauth_autopubkey(libssh->session, password) == SSH_AUTH_SUCCESS) { 101 av_log(libssh, AV_LOG_DEBUG, "Authentication successful with auto selected key.\n"); 102 authorized = 1; 103 } 104 } 105 106 if (!authorized && password && (auth_methods & SSH_AUTH_METHOD_PASSWORD)) { 107 if (ssh_userauth_password(libssh->session, NULL, password) == SSH_AUTH_SUCCESS) { 108 av_log(libssh, AV_LOG_DEBUG, "Authentication successful with password.\n"); 109 authorized = 1; 110 } 111 } 112 113 if (!authorized) { 114 av_log(libssh, AV_LOG_ERROR, "Authentication failed.\n"); 115 return AVERROR(EACCES); 116 } 117 118 return 0; 119} 120 121static av_cold int libssh_create_sftp_session(LIBSSHContext *libssh) 122{ 123 if (!(libssh->sftp = sftp_new(libssh->session))) { 124 av_log(libssh, AV_LOG_ERROR, "SFTP session creation failed: %s\n", ssh_get_error(libssh->session)); 125 return AVERROR(ENOMEM); 126 } 127 128 if (sftp_init(libssh->sftp) != SSH_OK) { 129 av_log(libssh, AV_LOG_ERROR, "Error initializing sftp session: %s\n", ssh_get_error(libssh->session)); 130 return AVERROR(EIO); 131 } 132 133 return 0; 134} 135 136static av_cold int libssh_open_file(LIBSSHContext *libssh, int flags, const char *file) 137{ 138 int access; 139 140 if ((flags & AVIO_FLAG_WRITE) && (flags & AVIO_FLAG_READ)) { 141 access = O_CREAT | O_RDWR; 142 if (libssh->trunc) 143 access |= O_TRUNC; 144 } else if (flags & AVIO_FLAG_WRITE) { 145 access = O_CREAT | O_WRONLY; 146 if (libssh->trunc) 147 access |= O_TRUNC; 148 } else 149 access = O_RDONLY; 150 151 /* 0666 = -rw-rw-rw- = read+write for everyone, minus umask */ 152 if (!(libssh->file = sftp_open(libssh->sftp, file, access, 0666))) { 153 av_log(libssh, AV_LOG_ERROR, "Error opening sftp file: %s\n", ssh_get_error(libssh->session)); 154 return AVERROR(EIO); 155 } 156 157 return 0; 158} 159 160static av_cold void libssh_stat_file(LIBSSHContext *libssh) 161{ 162 sftp_attributes stat; 163 164 if (!(stat = sftp_fstat(libssh->file))) { 165 av_log(libssh, AV_LOG_WARNING, "Cannot stat remote file.\n"); 166 libssh->filesize = -1; 167 } else { 168 libssh->filesize = stat->size; 169 sftp_attributes_free(stat); 170 } 171} 172 173static av_cold int libssh_close(URLContext *h) 174{ 175 LIBSSHContext *libssh = h->priv_data; 176 if (libssh->file) { 177 sftp_close(libssh->file); 178 libssh->file = NULL; 179 } 180 if (libssh->sftp) { 181 sftp_free(libssh->sftp); 182 libssh->sftp = NULL; 183 } 184 if (libssh->session) { 185 ssh_disconnect(libssh->session); 186 ssh_free(libssh->session); 187 libssh->session = NULL; 188 } 189 return 0; 190} 191 192static av_cold int libssh_connect(URLContext *h, const char *url, char *path, size_t path_size) 193{ 194 LIBSSHContext *libssh = h->priv_data; 195 char proto[10], hostname[1024], credencials[1024]; 196 int port = 22, ret; 197 const char *user = NULL, *pass = NULL; 198 char *end = NULL; 199 200 av_url_split(proto, sizeof(proto), 201 credencials, sizeof(credencials), 202 hostname, sizeof(hostname), 203 &port, 204 path, path_size, 205 url); 206 207 if (!(*path)) 208 av_strlcpy(path, "/", path_size); 209 210 // a port of 0 will use a port from ~/.ssh/config or the default value 22 211 if (port < 0 || port > 65535) 212 port = 0; 213 214 if ((ret = libssh_create_ssh_session(libssh, hostname, port)) < 0) 215 return ret; 216 217 user = av_strtok(credencials, ":", &end); 218 pass = av_strtok(end, ":", &end); 219 220 if ((ret = libssh_authentication(libssh, user, pass)) < 0) 221 return ret; 222 223 if ((ret = libssh_create_sftp_session(libssh)) < 0) 224 return ret; 225 226 return 0; 227} 228 229static av_cold int libssh_open(URLContext *h, const char *url, int flags) 230{ 231 int ret; 232 LIBSSHContext *libssh = h->priv_data; 233 char path[MAX_URL_SIZE]; 234 235 if ((ret = libssh_connect(h, url, path, sizeof(path))) < 0) 236 goto fail; 237 238 if ((ret = libssh_open_file(libssh, flags, path)) < 0) 239 goto fail; 240 241 libssh_stat_file(libssh); 242 243 return 0; 244 245 fail: 246 libssh_close(h); 247 return ret; 248} 249 250static int64_t libssh_seek(URLContext *h, int64_t pos, int whence) 251{ 252 LIBSSHContext *libssh = h->priv_data; 253 int64_t newpos; 254 255 if (libssh->filesize == -1 && (whence == AVSEEK_SIZE || whence == SEEK_END)) { 256 av_log(h, AV_LOG_ERROR, "Error during seeking.\n"); 257 return AVERROR(EIO); 258 } 259 260 switch(whence) { 261 case AVSEEK_SIZE: 262 return libssh->filesize; 263 case SEEK_SET: 264 newpos = pos; 265 break; 266 case SEEK_CUR: 267 newpos = sftp_tell64(libssh->file) + pos; 268 break; 269 case SEEK_END: 270 newpos = libssh->filesize + pos; 271 break; 272 default: 273 return AVERROR(EINVAL); 274 } 275 276 if (newpos < 0) { 277 av_log(h, AV_LOG_ERROR, "Seeking to nagative position.\n"); 278 return AVERROR(EINVAL); 279 } 280 281 if (sftp_seek64(libssh->file, newpos)) { 282 av_log(h, AV_LOG_ERROR, "Error during seeking.\n"); 283 return AVERROR(EIO); 284 } 285 286 return newpos; 287} 288 289static int libssh_read(URLContext *h, unsigned char *buf, int size) 290{ 291 LIBSSHContext *libssh = h->priv_data; 292 int bytes_read; 293 294 if ((bytes_read = sftp_read(libssh->file, buf, size)) < 0) { 295 av_log(libssh, AV_LOG_ERROR, "Read error.\n"); 296 return AVERROR(EIO); 297 } 298 return bytes_read ? bytes_read : AVERROR_EOF; 299} 300 301static int libssh_write(URLContext *h, const unsigned char *buf, int size) 302{ 303 LIBSSHContext *libssh = h->priv_data; 304 int bytes_written; 305 306 if ((bytes_written = sftp_write(libssh->file, buf, size)) < 0) { 307 av_log(libssh, AV_LOG_ERROR, "Write error.\n"); 308 return AVERROR(EIO); 309 } 310 return bytes_written; 311} 312 313static int libssh_open_dir(URLContext *h) 314{ 315 LIBSSHContext *libssh = h->priv_data; 316 int ret; 317 char path[MAX_URL_SIZE]; 318 319 if ((ret = libssh_connect(h, h->filename, path, sizeof(path))) < 0) 320 goto fail; 321 322 if (!(libssh->dir = sftp_opendir(libssh->sftp, path))) { 323 av_log(libssh, AV_LOG_ERROR, "Error opening sftp dir: %s\n", ssh_get_error(libssh->session)); 324 ret = AVERROR(EIO); 325 goto fail; 326 } 327 328 return 0; 329 330 fail: 331 libssh_close(h); 332 return ret; 333} 334 335static int libssh_read_dir(URLContext *h, AVIODirEntry **next) 336{ 337 LIBSSHContext *libssh = h->priv_data; 338 sftp_attributes attr = NULL; 339 AVIODirEntry *entry; 340 341 *next = entry = ff_alloc_dir_entry(); 342 if (!entry) 343 return AVERROR(ENOMEM); 344 345 do { 346 if (attr) 347 sftp_attributes_free(attr); 348 attr = sftp_readdir(libssh->sftp, libssh->dir); 349 if (!attr) { 350 av_freep(next); 351 if (sftp_dir_eof(libssh->dir)) 352 return 0; 353 return AVERROR(EIO); 354 } 355 } while (!strcmp(attr->name, ".") || !strcmp(attr->name, "..")); 356 357 entry->name = av_strdup(attr->name); 358 entry->group_id = attr->gid; 359 entry->user_id = attr->uid; 360 entry->size = attr->size; 361 entry->access_timestamp = INT64_C(1000000) * attr->atime; 362 entry->modification_timestamp = INT64_C(1000000) * attr->mtime; 363 entry->filemode = attr->permissions & 0777; 364 switch(attr->type) { 365 case SSH_FILEXFER_TYPE_REGULAR: 366 entry->type = AVIO_ENTRY_FILE; 367 break; 368 case SSH_FILEXFER_TYPE_DIRECTORY: 369 entry->type = AVIO_ENTRY_DIRECTORY; 370 break; 371 case SSH_FILEXFER_TYPE_SYMLINK: 372 entry->type = AVIO_ENTRY_SYMBOLIC_LINK; 373 break; 374 case SSH_FILEXFER_TYPE_SPECIAL: 375 /* Special type includes: sockets, char devices, block devices and pipes. 376 It is probably better to return unknown type, to not confuse anybody. */ 377 case SSH_FILEXFER_TYPE_UNKNOWN: 378 default: 379 entry->type = AVIO_ENTRY_UNKNOWN; 380 } 381 sftp_attributes_free(attr); 382 return 0; 383} 384 385static int libssh_close_dir(URLContext *h) 386{ 387 LIBSSHContext *libssh = h->priv_data; 388 if (libssh->dir) 389 sftp_closedir(libssh->dir); 390 libssh->dir = NULL; 391 libssh_close(h); 392 return 0; 393} 394 395static int libssh_delete(URLContext *h) 396{ 397 int ret; 398 LIBSSHContext *libssh = h->priv_data; 399 sftp_attributes attr = NULL; 400 char path[MAX_URL_SIZE]; 401 402 if ((ret = libssh_connect(h, h->filename, path, sizeof(path))) < 0) 403 goto cleanup; 404 405 if (!(attr = sftp_stat(libssh->sftp, path))) { 406 ret = AVERROR(sftp_get_error(libssh->sftp)); 407 goto cleanup; 408 } 409 410 if (attr->type == SSH_FILEXFER_TYPE_DIRECTORY) { 411 if (sftp_rmdir(libssh->sftp, path) < 0) { 412 ret = AVERROR(sftp_get_error(libssh->sftp)); 413 goto cleanup; 414 } 415 } else { 416 if (sftp_unlink(libssh->sftp, path) < 0) { 417 ret = AVERROR(sftp_get_error(libssh->sftp)); 418 goto cleanup; 419 } 420 } 421 422 ret = 0; 423 424cleanup: 425 if (attr) 426 sftp_attributes_free(attr); 427 libssh_close(h); 428 return ret; 429} 430 431static int libssh_move(URLContext *h_src, URLContext *h_dst) 432{ 433 int ret; 434 LIBSSHContext *libssh = h_src->priv_data; 435 char path_src[MAX_URL_SIZE], path_dst[MAX_URL_SIZE]; 436 char hostname_src[1024], hostname_dst[1024]; 437 char credentials_src[1024], credentials_dst[1024]; 438 int port_src = 22, port_dst = 22; 439 440 av_url_split(NULL, 0, 441 credentials_src, sizeof(credentials_src), 442 hostname_src, sizeof(hostname_src), 443 &port_src, 444 path_src, sizeof(path_src), 445 h_src->filename); 446 447 av_url_split(NULL, 0, 448 credentials_dst, sizeof(credentials_dst), 449 hostname_dst, sizeof(hostname_dst), 450 &port_dst, 451 path_dst, sizeof(path_dst), 452 h_dst->filename); 453 454 if (strcmp(credentials_src, credentials_dst) || 455 strcmp(hostname_src, hostname_dst) || 456 port_src != port_dst) { 457 return AVERROR(EINVAL); 458 } 459 460 if ((ret = libssh_connect(h_src, h_src->filename, path_src, sizeof(path_src))) < 0) 461 goto cleanup; 462 463 if (sftp_rename(libssh->sftp, path_src, path_dst) < 0) { 464 ret = AVERROR(sftp_get_error(libssh->sftp)); 465 goto cleanup; 466 } 467 468 ret = 0; 469 470cleanup: 471 libssh_close(h_src); 472 return ret; 473} 474 475#define OFFSET(x) offsetof(LIBSSHContext, x) 476#define D AV_OPT_FLAG_DECODING_PARAM 477#define E AV_OPT_FLAG_ENCODING_PARAM 478static const AVOption options[] = { 479 {"timeout", "set timeout of socket I/O operations", OFFSET(rw_timeout), AV_OPT_TYPE_INT, {.i64 = -1}, -1, INT_MAX, D|E }, 480 {"truncate", "Truncate existing files on write", OFFSET(trunc), AV_OPT_TYPE_INT, { .i64 = 1 }, 0, 1, E }, 481 {"private_key", "set path to private key", OFFSET(priv_key), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D|E }, 482 {NULL} 483}; 484 485static const AVClass libssh_context_class = { 486 .class_name = "libssh", 487 .item_name = av_default_item_name, 488 .option = options, 489 .version = LIBAVUTIL_VERSION_INT, 490}; 491 492const URLProtocol ff_libssh_protocol = { 493 .name = "sftp", 494 .url_open = libssh_open, 495 .url_read = libssh_read, 496 .url_write = libssh_write, 497 .url_seek = libssh_seek, 498 .url_close = libssh_close, 499 .url_delete = libssh_delete, 500 .url_move = libssh_move, 501 .url_open_dir = libssh_open_dir, 502 .url_read_dir = libssh_read_dir, 503 .url_close_dir = libssh_close_dir, 504 .priv_data_size = sizeof(LIBSSHContext), 505 .priv_data_class = &libssh_context_class, 506 .flags = URL_PROTOCOL_FLAG_NETWORK, 507}; 508