ftp.c 31.2 KB
Newer Older
Lukasz Marek's avatar
Lukasz Marek committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
/*
 * Copyright (c) 2013 Lukasz Marek <lukasz.m.luki@gmail.com>
 *
 * This file is part of FFmpeg.
 *
 * FFmpeg is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * FFmpeg is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with FFmpeg; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "libavutil/avstring.h"
22
#include "libavutil/internal.h"
23
#include "libavutil/parseutils.h"
Lukasz Marek's avatar
Lukasz Marek committed
24 25 26 27
#include "avformat.h"
#include "internal.h"
#include "url.h"
#include "libavutil/opt.h"
28
#include "libavutil/bprint.h"
Lukasz Marek's avatar
Lukasz Marek committed
29 30

#define CONTROL_BUFFER_SIZE 1024
31
#define DIR_BUFFER_SIZE 4096
Lukasz Marek's avatar
Lukasz Marek committed
32 33 34 35 36 37

typedef enum {
    UNKNOWN,
    READY,
    DOWNLOADING,
    UPLOADING,
38
    LISTING_DIR,
Lukasz Marek's avatar
Lukasz Marek committed
39 40 41
    DISCONNECTED
} FTPState;

42 43 44 45 46 47
typedef enum {
    UNKNOWN_METHOD,
    NLST,
    MLSD
} FTPListingMethod;

Lukasz Marek's avatar
Lukasz Marek committed
48 49
typedef struct {
    const AVClass *class;
50 51 52 53 54
    URLContext *conn_control;                    /**< Control connection */
    URLContext *conn_data;                       /**< Data connection, NULL when not connected */
    uint8_t control_buffer[CONTROL_BUFFER_SIZE]; /**< Control connection buffer */
    uint8_t *control_buf_ptr, *control_buf_end;
    int server_data_port;                        /**< Data connection port opened by server, -1 on error. */
55
    int server_control_port;                     /**< Control connection port, default is 21 */
56
    char *hostname;                              /**< Server address. */
57 58
    char *user;                                  /**< Server user */
    char *password;                              /**< Server user's password */
59
    char *path;                                  /**< Path to resource on server. */
60 61 62 63 64 65
    int64_t filesize;                            /**< Size of file on server, -1 on error. */
    int64_t position;                            /**< Current position, calculated. */
    int rw_timeout;                              /**< Network timeout. */
    const char *anonymous_password;              /**< Password to be used for anonymous user. An email should be used. */
    int write_seekable;                          /**< Control seekability, 0 = disable, 1 = enable. */
    FTPState state;                              /**< State of data connection */
66 67
    FTPListingMethod listing_method;             /**< Called listing method */
    char *features;                              /**< List of server's features represented as raw response */
68 69 70 71
    char *dir_buffer;
    size_t dir_buffer_size;
    size_t dir_buffer_offset;
    int utf8;
Lukasz Marek's avatar
Lukasz Marek committed
72 73 74 75 76 77 78 79
} FTPContext;

#define OFFSET(x) offsetof(FTPContext, x)
#define D AV_OPT_FLAG_DECODING_PARAM
#define E AV_OPT_FLAG_ENCODING_PARAM
static const AVOption options[] = {
    {"timeout", "set timeout of socket I/O operations", OFFSET(rw_timeout), AV_OPT_TYPE_INT, {.i64 = -1}, -1, INT_MAX, D|E },
    {"ftp-write-seekable", "control seekability of connection during encoding", OFFSET(write_seekable), AV_OPT_TYPE_INT, {.i64 = 0}, 0, 1, E },
80
    {"ftp-anonymous-password", "password for anonymous login. E-mail address should be used.", OFFSET(anonymous_password), AV_OPT_TYPE_STRING, { 0 }, 0, 0, D|E },
Lukasz Marek's avatar
Lukasz Marek committed
81 82 83 84 85 86 87 88 89 90
    {NULL}
};

static const AVClass ftp_context_class = {
    .class_name     = "ftp",
    .item_name      = av_default_item_name,
    .option         = options,
    .version        = LIBAVUTIL_VERSION_INT,
};

91 92
static int ftp_close(URLContext *h);

Lukasz Marek's avatar
Lukasz Marek committed
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
static int ftp_getc(FTPContext *s)
{
    int len;
    if (s->control_buf_ptr >= s->control_buf_end) {
        len = ffurl_read(s->conn_control, s->control_buffer, CONTROL_BUFFER_SIZE);
        if (len < 0) {
            return len;
        } else if (!len) {
            return -1;
        } else {
            s->control_buf_ptr = s->control_buffer;
            s->control_buf_end = s->control_buffer + len;
        }
    }
    return *s->control_buf_ptr++;
}

static int ftp_get_line(FTPContext *s, char *line, int line_size)
{
    int ch;
    char *q = line;

    for (;;) {
        ch = ftp_getc(s);
        if (ch < 0) {
            return ch;
        }
        if (ch == '\n') {
            /* process line */
            if (q > line && q[-1] == '\r')
                q--;
            *q = '\0';
            return 0;
        } else {
            if ((q - line) < line_size - 1)
                *q++ = ch;
        }
    }
}

/*
 * This routine returns ftp server response code.
Lukasz Marek's avatar
Lukasz Marek committed
135 136
 * Server may send more than one response for a certain command.
 * First expected code is returned.
Lukasz Marek's avatar
Lukasz Marek committed
137
 */
138
static int ftp_status(FTPContext *s, char **line, const int response_codes[])
Lukasz Marek's avatar
Lukasz Marek committed
139
{
140
    int err, i, dash = 0, result = 0, code_found = 0, linesize;
Lukasz Marek's avatar
Lukasz Marek committed
141
    char buf[CONTROL_BUFFER_SIZE];
142
    AVBPrint line_buffer;
Lukasz Marek's avatar
Lukasz Marek committed
143

144 145 146 147
    if (line)
        av_bprint_init(&line_buffer, 0, AV_BPRINT_SIZE_AUTOMATIC);

    while (!code_found || dash) {
148
        if ((err = ftp_get_line(s, buf, sizeof(buf))) < 0) {
149 150
            if (line)
                av_bprint_finalize(&line_buffer, NULL);
151
            return err;
Lukasz Marek's avatar
Lukasz Marek committed
152 153 154 155
        }

        av_log(s, AV_LOG_DEBUG, "%s\n", buf);

156
        linesize = strlen(buf);
157
        err = 0;
158 159 160 161 162 163 164 165 166 167 168 169
        if (linesize >= 3) {
            for (i = 0; i < 3; ++i) {
                if (buf[i] < '0' || buf[i] > '9') {
                    err = 0;
                    break;
                }
                err *= 10;
                err += buf[i] - '0';
            }
        }

        if (!code_found) {
170 171 172 173 174 175 176 177 178 179
            if (err >= 500) {
                code_found = 1;
                result = err;
            } else
                for (i = 0; response_codes[i]; ++i) {
                    if (err == response_codes[i]) {
                        code_found = 1;
                        result = err;
                        break;
                    }
180
                }
181
        }
182 183 184 185 186 187 188 189
        if (code_found) {
            if (line)
                av_bprintf(&line_buffer, "%s\r\n", buf);
            if (linesize >= 4) {
                if (!dash && buf[3] == '-')
                    dash = err;
                else if (err == dash && buf[3] == ' ')
                    dash = 0;
190 191
            }
        }
Lukasz Marek's avatar
Lukasz Marek committed
192
    }
193 194 195

    if (line)
        av_bprint_finalize(&line_buffer, line);
Lukasz Marek's avatar
Lukasz Marek committed
196 197 198
    return result;
}

199 200 201 202 203
static int ftp_send_command(FTPContext *s, const char *command,
                            const int response_codes[], char **response)
{
    int err;

204
    ff_dlog(s, "%s", command);
205

206 207 208
    if (response)
        *response = NULL;

209 210
    if ((err = ffurl_write(s->conn_control, command, strlen(command))) < 0)
        return err;
211 212
    if (!err)
        return -1;
213 214

    /* return status */
215 216 217 218
    if (response_codes) {
        return ftp_status(s, response, response_codes);
    }
    return 0;
219 220
}

221
static void ftp_close_data_connection(FTPContext *s)
Lukasz Marek's avatar
Lukasz Marek committed
222 223 224 225 226 227
{
    ffurl_closep(&s->conn_data);
    s->position = 0;
    s->state = DISCONNECTED;
}

228 229 230 231 232 233
static void ftp_close_both_connections(FTPContext *s)
{
    ffurl_closep(&s->conn_control);
    ftp_close_data_connection(s);
}

234
static int ftp_auth(FTPContext *s)
Lukasz Marek's avatar
Lukasz Marek committed
235
{
236
    char buf[CONTROL_BUFFER_SIZE];
Lukasz Marek's avatar
Lukasz Marek committed
237
    int err;
238 239
    static const int user_codes[] = {331, 230, 0};
    static const int pass_codes[] = {230, 0};
240

241
    snprintf(buf, sizeof(buf), "USER %s\r\n", s->user);
242
    err = ftp_send_command(s, buf, user_codes, NULL);
243
    if (err == 331) {
244 245
        if (s->password) {
            snprintf(buf, sizeof(buf), "PASS %s\r\n", s->password);
246
            err = ftp_send_command(s, buf, pass_codes, NULL);
247
        } else
Lukasz Marek's avatar
Lukasz Marek committed
248
            return AVERROR(EACCES);
249
    }
250
    if (err != 230)
251
        return AVERROR(EACCES);
Lukasz Marek's avatar
Lukasz Marek committed
252 253 254 255

    return 0;
}

256 257 258 259 260 261
static int ftp_passive_mode_epsv(FTPContext *s)
{
    char *res = NULL, *start = NULL, *end = NULL;
    int i;
    static const char d = '|';
    static const char *command = "EPSV\r\n";
262
    static const int epsv_codes[] = {229, 0};
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286

    if (ftp_send_command(s, command, epsv_codes, &res) != 229 || !res)
        goto fail;

    for (i = 0; res[i]; ++i) {
        if (res[i] == '(') {
            start = res + i + 1;
        } else if (res[i] == ')') {
            end = res + i;
            break;
        }
    }
    if (!start || !end)
        goto fail;

    *end = '\0';
    if (strlen(start) < 5)
        goto fail;
    if (start[0] != d || start[1] != d || start[2] != d || end[-1] != d)
        goto fail;
    start += 3;
    end[-1] = '\0';

    s->server_data_port = atoi(start);
287
    ff_dlog(s, "Server data port: %d\n", s->server_data_port);
288 289 290 291 292 293 294 295 296 297

    av_free(res);
    return 0;

  fail:
    av_free(res);
    s->server_data_port = -1;
    return AVERROR(ENOSYS);
}

Lukasz Marek's avatar
Lukasz Marek committed
298 299
static int ftp_passive_mode(FTPContext *s)
{
300
    char *res = NULL, *start = NULL, *end = NULL;
301
    int i;
302
    static const char *command = "PASV\r\n";
303
    static const int pasv_codes[] = {227, 0};
304

305
    if (ftp_send_command(s, command, pasv_codes, &res) != 227 || !res)
Lukasz Marek's avatar
Lukasz Marek committed
306 307
        goto fail;

308
    for (i = 0; res[i]; ++i) {
Lukasz Marek's avatar
Lukasz Marek committed
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
        if (res[i] == '(') {
            start = res + i + 1;
        } else if (res[i] == ')') {
            end = res + i;
            break;
        }
    }
    if (!start || !end)
        goto fail;

    *end  = '\0';
    /* skip ip */
    if (!av_strtok(start, ",", &end)) goto fail;
    if (!av_strtok(end, ",", &end)) goto fail;
    if (!av_strtok(end, ",", &end)) goto fail;
    if (!av_strtok(end, ",", &end)) goto fail;

    /* parse port number */
    start = av_strtok(end, ",", &end);
    if (!start) goto fail;
    s->server_data_port = atoi(start) * 256;
    start = av_strtok(end, ",", &end);
    if (!start) goto fail;
    s->server_data_port += atoi(start);
333
    ff_dlog(s, "Server data port: %d\n", s->server_data_port);
Lukasz Marek's avatar
Lukasz Marek committed
334 335 336 337 338 339 340 341 342 343 344 345 346

    av_free(res);
    return 0;

  fail:
    av_free(res);
    s->server_data_port = -1;
    return AVERROR(EIO);
}

static int ftp_current_dir(FTPContext *s)
{
    char *res = NULL, *start = NULL, *end = NULL;
347
    int i;
348 349
    static const char *command = "PWD\r\n";
    static const int pwd_codes[] = {257, 0};
350

351
    if (ftp_send_command(s, command, pwd_codes, &res) != 257 || !res)
Lukasz Marek's avatar
Lukasz Marek committed
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
        goto fail;

    for (i = 0; res[i]; ++i) {
        if (res[i] == '"') {
            if (!start) {
                start = res + i + 1;
                continue;
            }
            end = res + i;
            break;
        }
    }

    if (!end)
        goto fail;

368
    *end = '\0';
369
    s->path = av_strdup(start);
Lukasz Marek's avatar
Lukasz Marek committed
370 371

    av_free(res);
372 373 374

    if (!s->path)
        return AVERROR(ENOMEM);
Lukasz Marek's avatar
Lukasz Marek committed
375 376 377 378 379 380 381 382 383
    return 0;

  fail:
    av_free(res);
    return AVERROR(EIO);
}

static int ftp_file_size(FTPContext *s)
{
384
    char command[CONTROL_BUFFER_SIZE];
Lukasz Marek's avatar
Lukasz Marek committed
385
    char *res = NULL;
386
    static const int size_codes[] = {213, 0};
387

388
    snprintf(command, sizeof(command), "SIZE %s\r\n", s->path);
389
    if (ftp_send_command(s, command, size_codes, &res) == 213 && res) {
390
        s->filesize = strtoll(&res[4], NULL, 10);
Lukasz Marek's avatar
Lukasz Marek committed
391 392 393 394 395 396 397 398 399 400 401 402
    } else {
        s->filesize = -1;
        av_free(res);
        return AVERROR(EIO);
    }

    av_free(res);
    return 0;
}

static int ftp_retrieve(FTPContext *s)
{
403
    char command[CONTROL_BUFFER_SIZE];
404
    static const int retr_codes[] = {150, 0};
405

406
    snprintf(command, sizeof(command), "RETR %s\r\n", s->path);
407
    if (ftp_send_command(s, command, retr_codes, NULL) != 150)
Lukasz Marek's avatar
Lukasz Marek committed
408 409 410 411 412 413 414 415 416
        return AVERROR(EIO);

    s->state = DOWNLOADING;

    return 0;
}

static int ftp_store(FTPContext *s)
{
417
    char command[CONTROL_BUFFER_SIZE];
418
    static const int stor_codes[] = {150, 0};
419

420
    snprintf(command, sizeof(command), "STOR %s\r\n", s->path);
421
    if (ftp_send_command(s, command, stor_codes, NULL) != 150)
Lukasz Marek's avatar
Lukasz Marek committed
422 423 424 425 426 427 428
        return AVERROR(EIO);

    s->state = UPLOADING;

    return 0;
}

Lukasz Marek's avatar
Lukasz Marek committed
429
static int ftp_type(FTPContext *s)
Lukasz Marek's avatar
Lukasz Marek committed
430
{
431
    static const char *command = "TYPE I\r\n";
432
    static const int type_codes[] = {200, 0};
433

434
    if (ftp_send_command(s, command, type_codes, NULL) != 200)
435 436 437 438 439 440 441
        return AVERROR(EIO);

    return 0;
}

static int ftp_restart(FTPContext *s, int64_t pos)
{
442
    char command[CONTROL_BUFFER_SIZE];
443
    static const int rest_codes[] = {350, 0};
444

445
    snprintf(command, sizeof(command), "REST %"PRId64"\r\n", pos);
446
    if (ftp_send_command(s, command, rest_codes, NULL) != 350)
Lukasz Marek's avatar
Lukasz Marek committed
447 448 449 450 451
        return AVERROR(EIO);

    return 0;
}

452 453 454 455 456 457 458 459 460 461 462
static int ftp_set_dir(FTPContext *s)
{
    static const int cwd_codes[] = {250, 550, 0}; /* 550 is incorrect code */
    char command[MAX_URL_SIZE];

    snprintf(command, sizeof(command), "CWD %s\r\n", s->path);
    if (ftp_send_command(s, command, cwd_codes, NULL) != 250)
        return AVERROR(EIO);
    return 0;
}

463
static int ftp_list_mlsd(FTPContext *s)
464 465 466 467 468 469
{
    static const char *command = "MLSD\r\n";
    static const int mlsd_codes[] = {150, 500, 0}; /* 500 is incorrect code */

    if (ftp_send_command(s, command, mlsd_codes, NULL) != 150)
        return AVERROR(ENOSYS);
470 471 472 473 474 475 476 477 478 479 480 481
    s->listing_method = MLSD;
    return 0;
}

static int ftp_list_nlst(FTPContext *s)
{
    static const char *command = "NLST\r\n";
    static const int nlst_codes[] = {226, 425, 426, 451, 450, 550, 0};

    if (ftp_send_command(s, command, nlst_codes, NULL) != 226)
        return AVERROR(ENOSYS);
    s->listing_method = NLST;
482 483 484
    return 0;
}

485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
static int ftp_has_feature(FTPContext *s, const char *feature_name);

static int ftp_list(FTPContext *s)
{
    int ret;
    s->state = LISTING_DIR;

    if ((ret = ftp_list_mlsd(s)) < 0)
        ret = ftp_list_nlst(s);

    return ret;
}

static int ftp_has_feature(FTPContext *s, const char *feature_name)
{
    if (!s->features)
        return 0;

    return av_stristr(s->features, feature_name) != NULL;
}

506 507 508 509
static int ftp_features(FTPContext *s)
{
    static const char *feat_command        = "FEAT\r\n";
    static const char *enable_utf8_command = "OPTS UTF8 ON\r\n";
510
    static const int feat_codes[] = {211, 0};
511
    static const int opts_codes[] = {200, 451, 0};
512

513 514 515 516 517 518 519 520
    av_freep(&s->features);
    if (ftp_send_command(s, feat_command, feat_codes, &s->features) != 211) {
        av_freep(&s->features);
    }

    if (ftp_has_feature(s, "UTF8")) {
        if (ftp_send_command(s, enable_utf8_command, opts_codes, NULL) == 200)
            s->utf8 = 1;
521
    }
522

523 524 525
    return 0;
}

526 527
static int ftp_connect_control_connection(URLContext *h)
{
528
    char buf[CONTROL_BUFFER_SIZE], *response = NULL;
529 530 531
    int err;
    AVDictionary *opts = NULL;
    FTPContext *s = h->priv_data;
532
    static const int connect_codes[] = {220, 0};
533 534 535 536 537

    if (!s->conn_control) {
        ff_url_join(buf, sizeof(buf), "tcp", NULL,
                    s->hostname, s->server_control_port, NULL);
        if (s->rw_timeout != -1) {
538
            av_dict_set_int(&opts, "timeout", s->rw_timeout, 0);
539 540
        } /* if option is not given, don't pass it and let tcp use its own default */
        err = ffurl_open(&s->conn_control, buf, AVIO_FLAG_READ_WRITE,
541
                         &h->interrupt_callback, &opts);
542 543
        av_dict_free(&opts);
        if (err < 0) {
544
            av_log(h, AV_LOG_ERROR, "Cannot open control connection\n");
545 546 547
            return err;
        }

Lukasz Marek's avatar
Lukasz Marek committed
548
        /* check if server is ready */
549
        if (ftp_status(s, ((h->flags & AVIO_FLAG_WRITE) ? &response : NULL), connect_codes) != 220) {
550
            av_log(h, AV_LOG_ERROR, "FTP server not ready for new users\n");
551
            return AVERROR(EACCES);
552 553
        }

554 555 556 557 558
        if ((h->flags & AVIO_FLAG_WRITE) && av_stristr(response, "pure-ftpd")) {
            av_log(h, AV_LOG_WARNING, "Pure-FTPd server is used as an output protocol. It is known issue this implementation may produce incorrect content and it cannot be fixed at this moment.");
        }
        av_free(response);

559 560 561 562 563 564
        if ((err = ftp_auth(s)) < 0) {
            av_log(h, AV_LOG_ERROR, "FTP authentication failed\n");
            return err;
        }

        if ((err = ftp_type(s)) < 0) {
565
            av_log(h, AV_LOG_ERROR, "Set content type failed\n");
566 567
            return err;
        }
568 569

        ftp_features(s);
570 571 572 573 574
    }
    return 0;
}

static int ftp_connect_data_connection(URLContext *h)
Lukasz Marek's avatar
Lukasz Marek committed
575 576
{
    int err;
577
    char buf[CONTROL_BUFFER_SIZE];
Lukasz Marek's avatar
Lukasz Marek committed
578 579 580 581
    AVDictionary *opts = NULL;
    FTPContext *s = h->priv_data;

    if (!s->conn_data) {
582
        /* Enter passive mode */
583 584 585 586 587
        if (ftp_passive_mode_epsv(s) < 0) {
            /* Use PASV as fallback */
            if ((err = ftp_passive_mode(s)) < 0)
                return err;
        }
588
        /* Open data connection */
Lukasz Marek's avatar
Lukasz Marek committed
589 590
        ff_url_join(buf, sizeof(buf), "tcp", NULL, s->hostname, s->server_data_port, NULL);
        if (s->rw_timeout != -1) {
591
            av_dict_set_int(&opts, "timeout", s->rw_timeout, 0);
Lukasz Marek's avatar
Lukasz Marek committed
592
        } /* if option is not given, don't pass it and let tcp use its own default */
593
        err = ffurl_open(&s->conn_data, buf, h->flags,
Lukasz Marek's avatar
Lukasz Marek committed
594 595 596 597
                         &h->interrupt_callback, &opts);
        av_dict_free(&opts);
        if (err < 0)
            return err;
Lukasz Marek's avatar
Lukasz Marek committed
598 599 600 601

        if (s->position)
            if ((err = ftp_restart(s, s->position)) < 0)
                return err;
Lukasz Marek's avatar
Lukasz Marek committed
602 603
    }
    s->state = READY;
Lukasz Marek's avatar
Lukasz Marek committed
604 605 606 607 608
    return 0;
}

static int ftp_abort(URLContext *h)
{
609
    static const char *command = "ABOR\r\n";
Lukasz Marek's avatar
Lukasz Marek committed
610
    int err;
611
    static const int abor_codes[] = {225, 226, 0};
612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631
    FTPContext *s = h->priv_data;

    /* According to RCF 959:
       "ABOR command tells the server to abort the previous FTP
       service command and any associated transfer of data."

       There are FTP server implementations that don't response
       to any commands during data transfer in passive mode (including ABOR).

       This implementation closes data connection by force.
    */

    if (ftp_send_command(s, command, NULL, NULL) < 0) {
        ftp_close_both_connections(s);
        if ((err = ftp_connect_control_connection(h)) < 0) {
            av_log(h, AV_LOG_ERROR, "Reconnect failed.\n");
            return err;
        }
    } else {
        ftp_close_data_connection(s);
632 633 634 635 636 637 638
        if (ftp_status(s, NULL, abor_codes) < 225) {
            /* wu-ftpd also closes control connection after data connection closing */
            ffurl_closep(&s->conn_control);
            if ((err = ftp_connect_control_connection(h)) < 0) {
                av_log(h, AV_LOG_ERROR, "Reconnect failed.\n");
                return err;
            }
639 640 641
        }
    }

Lukasz Marek's avatar
Lukasz Marek committed
642 643 644
    return 0;
}

645
static int ftp_connect(URLContext *h, const char *url)
Lukasz Marek's avatar
Lukasz Marek committed
646
{
647
    char proto[10], path[MAX_URL_SIZE], credencials[MAX_URL_SIZE], hostname[MAX_URL_SIZE];
648
    const char *tok_user = NULL, *tok_pass = NULL;
649
    char *end = NULL, *newpath = NULL;
650
    int err;
Lukasz Marek's avatar
Lukasz Marek committed
651 652 653
    FTPContext *s = h->priv_data;

    s->state = DISCONNECTED;
654
    s->listing_method = UNKNOWN_METHOD;
Lukasz Marek's avatar
Lukasz Marek committed
655
    s->filesize = -1;
Lukasz Marek's avatar
Lukasz Marek committed
656
    s->position = 0;
657
    s->features = NULL;
Lukasz Marek's avatar
Lukasz Marek committed
658 659

    av_url_split(proto, sizeof(proto),
660
                 credencials, sizeof(credencials),
661
                 hostname, sizeof(hostname),
662
                 &s->server_control_port,
Lukasz Marek's avatar
Lukasz Marek committed
663 664 665
                 path, sizeof(path),
                 url);

666 667 668 669 670 671 672 673
    tok_user = av_strtok(credencials, ":", &end);
    tok_pass = av_strtok(end, ":", &end);
    if (!tok_user) {
        tok_user = "anonymous";
        tok_pass = av_x_if_null(s->anonymous_password, "nopassword");
    }
    s->user = av_strdup(tok_user);
    s->password = av_strdup(tok_pass);
674 675
    s->hostname = av_strdup(hostname);
    if (!s->hostname || !s->user || (tok_pass && !s->password)) {
676
        return AVERROR(ENOMEM);
677 678
    }

679 680
    if (s->server_control_port < 0 || s->server_control_port > 65535)
        s->server_control_port = 21;
Lukasz Marek's avatar
Lukasz Marek committed
681

682
    if ((err = ftp_connect_control_connection(h)) < 0)
683
        return err;
Lukasz Marek's avatar
Lukasz Marek committed
684

685
    if ((err = ftp_current_dir(s)) < 0)
686
        return err;
687 688 689 690 691 692

    newpath = av_append_path_component(s->path, path);
    if (!newpath)
        return AVERROR(ENOMEM);
    av_free(s->path);
    s->path = newpath;
Lukasz Marek's avatar
Lukasz Marek committed
693

694 695 696 697 698 699 700 701
    return 0;
}

static int ftp_open(URLContext *h, const char *url, int flags)
{
    FTPContext *s = h->priv_data;
    int err;

702
    ff_dlog(h, "ftp protocol open\n");
703 704 705 706

    if ((err = ftp_connect(h, url)) < 0)
        goto fail;

707
    if (ftp_restart(s, 0) < 0) {
708
        h->is_streamed = 1;
709 710 711 712 713 714
    } else {
        if (ftp_file_size(s) < 0 && flags & AVIO_FLAG_READ)
            h->is_streamed = 1;
        if (s->write_seekable != 1 && flags & AVIO_FLAG_WRITE)
            h->is_streamed = 1;
    }
Lukasz Marek's avatar
Lukasz Marek committed
715 716

    return 0;
Lukasz Marek's avatar
Lukasz Marek committed
717

Lukasz Marek's avatar
Lukasz Marek committed
718 719
  fail:
    av_log(h, AV_LOG_ERROR, "FTP open failed\n");
720
    ftp_close(h);
Lukasz Marek's avatar
Lukasz Marek committed
721 722 723 724 725 726 727
    return err;
}

static int64_t ftp_seek(URLContext *h, int64_t pos, int whence)
{
    FTPContext *s = h->priv_data;
    int err;
728
    int64_t new_pos, fake_pos;
Lukasz Marek's avatar
Lukasz Marek committed
729

730
    ff_dlog(h, "ftp protocol seek %"PRId64" %d\n", pos, whence);
Lukasz Marek's avatar
Lukasz Marek committed
731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749

    switch(whence) {
    case AVSEEK_SIZE:
        return s->filesize;
    case SEEK_SET:
        new_pos = pos;
        break;
    case SEEK_CUR:
        new_pos = s->position + pos;
        break;
    case SEEK_END:
        if (s->filesize < 0)
            return AVERROR(EIO);
        new_pos = s->filesize + pos;
        break;
    default:
        return AVERROR(EINVAL);
    }

750
    if (h->is_streamed)
Lukasz Marek's avatar
Lukasz Marek committed
751 752
        return AVERROR(EIO);

753 754 755 756
    if (new_pos < 0) {
        av_log(h, AV_LOG_ERROR, "Seeking to nagative position.\n");
        return AVERROR(EINVAL);
    }
Lukasz Marek's avatar
Lukasz Marek committed
757

758
    fake_pos = s->filesize != -1 ? FFMIN(new_pos, s->filesize) : new_pos;
759
    if (fake_pos != s->position) {
Lukasz Marek's avatar
Lukasz Marek committed
760
        if ((err = ftp_abort(h)) < 0)
Lukasz Marek's avatar
Lukasz Marek committed
761
            return err;
762
        s->position = fake_pos;
Lukasz Marek's avatar
Lukasz Marek committed
763 764 765 766
    }
    return new_pos;
}

Lukasz Marek's avatar
Lukasz Marek committed
767 768 769 770 771
static int ftp_read(URLContext *h, unsigned char *buf, int size)
{
    FTPContext *s = h->priv_data;
    int read, err, retry_done = 0;

772
    ff_dlog(h, "ftp protocol read %d bytes\n", size);
Lukasz Marek's avatar
Lukasz Marek committed
773
  retry:
Lukasz Marek's avatar
Lukasz Marek committed
774
    if (s->state == DISCONNECTED) {
Lukasz Marek's avatar
Lukasz Marek committed
775
        /* optimization */
776 777
        if (s->position >= s->filesize)
            return 0;
Lukasz Marek's avatar
Lukasz Marek committed
778 779 780
        if ((err = ftp_connect_data_connection(h)) < 0)
            return err;
    }
Lukasz Marek's avatar
Lukasz Marek committed
781
    if (s->state == READY) {
782 783
        if (s->position >= s->filesize)
            return 0;
Lukasz Marek's avatar
Lukasz Marek committed
784 785
        if ((err = ftp_retrieve(s)) < 0)
            return err;
Lukasz Marek's avatar
Lukasz Marek committed
786 787 788 789 790 791
    }
    if (s->conn_data && s->state == DOWNLOADING) {
        read = ffurl_read(s->conn_data, buf, size);
        if (read >= 0) {
            s->position += read;
            if (s->position >= s->filesize) {
792
                /* server will terminate, but keep current position to avoid madness */
Lukasz Marek's avatar
Lukasz Marek committed
793
                /* save position to restart from it */
794 795 796
                int64_t pos = s->position;
                if (ftp_abort(h) < 0) {
                    s->position = pos;
Lukasz Marek's avatar
Lukasz Marek committed
797
                    return AVERROR(EIO);
798 799
                }
                s->position = pos;
Lukasz Marek's avatar
Lukasz Marek committed
800 801
            }
        }
802
        if (read <= 0 && s->position < s->filesize && !h->is_streamed) {
Lukasz Marek's avatar
Lukasz Marek committed
803 804 805
            /* Server closed connection. Probably due to inactivity */
            int64_t pos = s->position;
            av_log(h, AV_LOG_INFO, "Reconnect to FTP server.\n");
806
            if ((err = ftp_abort(h)) < 0)
Lukasz Marek's avatar
Lukasz Marek committed
807 808
                return err;
            if ((err = ftp_seek(h, pos, SEEK_SET)) < 0) {
Lukasz Marek's avatar
Lukasz Marek committed
809
                av_log(h, AV_LOG_ERROR, "Position cannot be restored.\n");
Lukasz Marek's avatar
Lukasz Marek committed
810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825
                return err;
            }
            if (!retry_done) {
                retry_done = 1;
                goto retry;
            }
        }
        return read;
    }

    av_log(h, AV_LOG_DEBUG, "FTP read failed\n");
    return AVERROR(EIO);
}

static int ftp_write(URLContext *h, const unsigned char *buf, int size)
{
Lukasz Marek's avatar
Lukasz Marek committed
826
    int err;
Lukasz Marek's avatar
Lukasz Marek committed
827 828 829
    FTPContext *s = h->priv_data;
    int written;

830
    ff_dlog(h, "ftp protocol write %d bytes\n", size);
Lukasz Marek's avatar
Lukasz Marek committed
831

Lukasz Marek's avatar
Lukasz Marek committed
832 833 834 835
    if (s->state == DISCONNECTED) {
        if ((err = ftp_connect_data_connection(h)) < 0)
            return err;
    }
Lukasz Marek's avatar
Lukasz Marek committed
836
    if (s->state == READY) {
Lukasz Marek's avatar
Lukasz Marek committed
837 838
        if ((err = ftp_store(s)) < 0)
            return err;
Lukasz Marek's avatar
Lukasz Marek committed
839 840 841 842 843 844 845 846 847 848 849 850 851 852
    }
    if (s->conn_data && s->state == UPLOADING) {
        written = ffurl_write(s->conn_data, buf, size);
        if (written > 0) {
            s->position += written;
            s->filesize = FFMAX(s->filesize, s->position);
        }
        return written;
    }

    av_log(h, AV_LOG_ERROR, "FTP write failed\n");
    return AVERROR(EIO);
}

Lukasz Marek's avatar
Lukasz Marek committed
853 854
static int ftp_close(URLContext *h)
{
855 856
    FTPContext *s = h->priv_data;

857
    ff_dlog(h, "ftp protocol close\n");
Lukasz Marek's avatar
Lukasz Marek committed
858

859 860 861
    ftp_close_both_connections(s);
    av_freep(&s->user);
    av_freep(&s->password);
862 863
    av_freep(&s->hostname);
    av_freep(&s->path);
864
    av_freep(&s->features);
Lukasz Marek's avatar
Lukasz Marek committed
865 866 867 868 869 870 871 872

    return 0;
}

static int ftp_get_file_handle(URLContext *h)
{
    FTPContext *s = h->priv_data;

873
    ff_dlog(h, "ftp protocol get_file_handle\n");
Lukasz Marek's avatar
Lukasz Marek committed
874 875 876 877 878 879 880 881 882 883 884

    if (s->conn_data)
        return ffurl_get_file_handle(s->conn_data);

    return AVERROR(EIO);
}

static int ftp_shutdown(URLContext *h, int flags)
{
    FTPContext *s = h->priv_data;

885
    ff_dlog(h, "ftp protocol shutdown\n");
Lukasz Marek's avatar
Lukasz Marek committed
886 887 888 889 890 891 892

    if (s->conn_data)
        return ffurl_shutdown(s->conn_data, flags);

    return AVERROR(EIO);
}

893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927
static int ftp_open_dir(URLContext *h)
{
    FTPContext *s = h->priv_data;
    int ret;

    if ((ret = ftp_connect(h, h->filename)) < 0)
        goto fail;
    if ((ret = ftp_set_dir(s)) < 0)
        goto fail;
    if ((ret = ftp_connect_data_connection(h)) < 0)
        goto fail;
    if ((ret = ftp_list(s)) < 0)
        goto fail;
    s->dir_buffer = av_malloc(DIR_BUFFER_SIZE);
    if (!s->dir_buffer) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }
    s->dir_buffer[0] = 0;
    if (s->conn_data && s->state == LISTING_DIR)
        return 0;
  fail:
    ffurl_closep(&s->conn_control);
    ffurl_closep(&s->conn_data);
    return ret;
}

static int64_t ftp_parse_date(const char *date)
{
    struct tm tv;
    memset(&tv, 0, sizeof(struct tm));
    av_small_strptime(date, "%Y%m%d%H%M%S", &tv);
    return INT64_C(1000000) * av_timegm(&tv);
}

928 929 930 931 932 933 934
static int ftp_parse_entry_nlst(char *line, AVIODirEntry *next)
{
    next->name = av_strdup(line);
    return 0;
}

static int ftp_parse_entry_mlsd(char *mlsd, AVIODirEntry *next)
935 936
{
    char *fact, *value;
937
    ff_dlog(NULL, "%s\n", mlsd);
938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966
    while(fact = av_strtok(mlsd, ";", &mlsd)) {
        if (fact[0] == ' ') {
            next->name = av_strdup(&fact[1]);
            continue;
        }
        fact = av_strtok(fact, "=", &value);
        if (!av_strcasecmp(fact, "type")) {
            if (!av_strcasecmp(value, "cdir") || !av_strcasecmp(value, "pdir"))
                return 1;
            if (!av_strcasecmp(value, "dir"))
                next->type = AVIO_ENTRY_DIRECTORY;
            else if (!av_strcasecmp(value, "file"))
                next->type = AVIO_ENTRY_FILE;
            else if (!av_strcasecmp(value, "OS.unix=slink:"))
                next->type = AVIO_ENTRY_SYMBOLIC_LINK;
        } else if (!av_strcasecmp(fact, "modify")) {
            next->modification_timestamp = ftp_parse_date(value);
        } else if (!av_strcasecmp(fact, "UNIX.mode")) {
            next->filemode = strtoumax(value, NULL, 8);
        } else if (!av_strcasecmp(fact, "UNIX.uid") || !av_strcasecmp(fact, "UNIX.owner"))
            next->user_id = strtoumax(value, NULL, 10);
        else if (!av_strcasecmp(fact, "UNIX.gid") || !av_strcasecmp(fact, "UNIX.group"))
            next->group_id = strtoumax(value, NULL, 10);
        else if (!av_strcasecmp(fact, "size") || !av_strcasecmp(fact, "sizd"))
            next->size = strtoll(value, NULL, 10);
    }
    return 0;
}

967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984
/**
 * @return 0 on success, negative on error, positive on entry to discard.
 */
static int ftp_parse_entry(URLContext *h, char *line, AVIODirEntry *next)
{
    FTPContext *s = h->priv_data;

    switch (s->listing_method) {
    case MLSD:
        return ftp_parse_entry_mlsd(line, next);
    case NLST:
        return ftp_parse_entry_nlst(line, next);
    case UNKNOWN_METHOD:
    default:
        return -1;
    }
}

985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021
static int ftp_read_dir(URLContext *h, AVIODirEntry **next)
{
    FTPContext *s = h->priv_data;
    char *start, *found;
    int ret, retried;

    do {
        retried = 0;
        start = s->dir_buffer + s->dir_buffer_offset;
        while (!(found = strstr(start, "\n"))) {
            if (retried)
                return AVERROR(EIO);
            s->dir_buffer_size -= s->dir_buffer_offset;
            s->dir_buffer_offset = 0;
            if (s->dir_buffer_size)
                memmove(s->dir_buffer, start, s->dir_buffer_size);
            ret = ffurl_read(s->conn_data, s->dir_buffer + s->dir_buffer_size, DIR_BUFFER_SIZE - (s->dir_buffer_size + 1));
            if (ret < 0)
                return ret;
            if (!ret) {
                *next = NULL;
                return 0;
            }
            s->dir_buffer_size += ret;
            s->dir_buffer[s->dir_buffer_size] = 0;
            start = s->dir_buffer;
            retried = 1;
        }
        s->dir_buffer_offset += (found + 1 - start);
        found[0] = 0;
        if (found > start && found[-1] == '\r')
            found[-1] = 0;

        *next = ff_alloc_dir_entry();
        if (!*next)
            return AVERROR(ENOMEM);
        (*next)->utf8 = s->utf8;
1022
        ret = ftp_parse_entry(h, start, *next);
1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034
        if (ret) {
            avio_free_directory_entry(next);
            if (ret < 0)
                return ret;
        }
    } while (ret > 0);
    return 0;
}

static int ftp_close_dir(URLContext *h)
{
    FTPContext *s = h->priv_data;
1035
    av_freep(&s->dir_buffer);
1036 1037 1038 1039 1040
    ffurl_closep(&s->conn_control);
    ffurl_closep(&s->conn_data);
    return 0;
}

1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099
static int ftp_delete(URLContext *h)
{
    FTPContext *s = h->priv_data;
    char command[MAX_URL_SIZE];
    static const int del_codes[] = {250, 421, 450, 500, 501, 502, 530, 550, 0};
    static const int rmd_codes[] = {250, 421, 500, 501, 502, 530, 550, 0};
    int ret;

    if ((ret = ftp_connect(h, h->filename)) < 0)
        goto cleanup;

    snprintf(command, sizeof(command), "DELE %s\r\n", s->path);
    if (ftp_send_command(s, command, del_codes, NULL) == 250) {
        ret = 0;
        goto cleanup;
    }

    snprintf(command, sizeof(command), "RMD %s\r\n", s->path);
    if (ftp_send_command(s, command, rmd_codes, NULL) == 250)
        ret = 0;
    else
        ret = AVERROR(EIO);

cleanup:
    ftp_close(h);
    return ret;
}

static int ftp_move(URLContext *h_src, URLContext *h_dst)
{
    FTPContext *s = h_src->priv_data;
    char command[MAX_URL_SIZE], path[MAX_URL_SIZE];
    static const int rnfr_codes[] = {350, 421, 450, 500, 501, 502, 503, 530, 0};
    static const int rnto_codes[] = {250, 421, 500, 501, 502, 503, 530, 532, 553, 0};
    int ret;

    if ((ret = ftp_connect(h_src, h_src->filename)) < 0)
        goto cleanup;

    snprintf(command, sizeof(command), "RNFR %s\r\n", s->path);
    if (ftp_send_command(s, command, rnfr_codes, NULL) != 350) {
        ret = AVERROR(EIO);
        goto cleanup;
    }

    av_url_split(0, 0, 0, 0, 0, 0, 0,
                 path, sizeof(path),
                 h_dst->filename);
    snprintf(command, sizeof(command), "RNTO %s\r\n", path);
    if (ftp_send_command(s, command, rnto_codes, NULL) == 250)
        ret = 0;
    else
        ret = AVERROR(EIO);

cleanup:
    ftp_close(h_src);
    return ret;
}

Lukasz Marek's avatar
Lukasz Marek committed
1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110
URLProtocol ff_ftp_protocol = {
    .name                = "ftp",
    .url_open            = ftp_open,
    .url_read            = ftp_read,
    .url_write           = ftp_write,
    .url_seek            = ftp_seek,
    .url_close           = ftp_close,
    .url_get_file_handle = ftp_get_file_handle,
    .url_shutdown        = ftp_shutdown,
    .priv_data_size      = sizeof(FTPContext),
    .priv_data_class     = &ftp_context_class,
1111 1112 1113
    .url_open_dir        = ftp_open_dir,
    .url_read_dir        = ftp_read_dir,
    .url_close_dir       = ftp_close_dir,
1114 1115
    .url_delete          = ftp_delete,
    .url_move            = ftp_move,
Lukasz Marek's avatar
Lukasz Marek committed
1116 1117
    .flags               = URL_PROTOCOL_FLAG_NETWORK,
};