ftp.c 31.6 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
} 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 },
79
    {"ftp-write-seekable", "control seekability of connection during encoding", OFFSET(write_seekable), AV_OPT_TYPE_BOOL, {.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 405
    static const int retr_codes[] = {150, 125, 0};
    int resp_code;
406

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

    s->state = DOWNLOADING;

    return 0;
}

static int ftp_store(FTPContext *s)
{
419
    char command[CONTROL_BUFFER_SIZE];
420 421
    static const int stor_codes[] = {150, 125, 0};
    int resp_code;
422

423
    snprintf(command, sizeof(command), "STOR %s\r\n", s->path);
424 425
    resp_code = ftp_send_command(s, command, stor_codes, NULL);
    if (resp_code != 125 && resp_code != 150)
Lukasz Marek's avatar
Lukasz Marek committed
426 427 428 429 430 431 432
        return AVERROR(EIO);

    s->state = UPLOADING;

    return 0;
}

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

438
    if (ftp_send_command(s, command, type_codes, NULL) != 200)
439 440 441 442 443 444 445
        return AVERROR(EIO);

    return 0;
}

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

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

    return 0;
}

456 457 458 459 460 461 462 463 464 465 466
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;
}

467
static int ftp_list_mlsd(FTPContext *s)
468 469 470 471 472 473
{
    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);
474 475 476 477 478 479 480 481 482 483 484 485
    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;
486 487 488
    return 0;
}

489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509
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;
}

510 511 512 513
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";
514
    static const int feat_codes[] = {211, 0};
515
    static const int opts_codes[] = {200, 451, 0};
516

517 518 519 520 521 522 523 524
    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;
525
    }
526

527 528 529
    return 0;
}

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

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

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

559 560 561 562 563
        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);

564 565 566 567 568 569
        if ((err = ftp_auth(s)) < 0) {
            av_log(h, AV_LOG_ERROR, "FTP authentication failed\n");
            return err;
        }

        if ((err = ftp_type(s)) < 0) {
570
            av_log(h, AV_LOG_ERROR, "Set content type failed\n");
571 572
            return err;
        }
573 574

        ftp_features(s);
575 576 577 578 579
    }
    return 0;
}

static int ftp_connect_data_connection(URLContext *h)
Lukasz Marek's avatar
Lukasz Marek committed
580 581
{
    int err;
582
    char buf[CONTROL_BUFFER_SIZE];
Lukasz Marek's avatar
Lukasz Marek committed
583 584 585 586
    AVDictionary *opts = NULL;
    FTPContext *s = h->priv_data;

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

        if (s->position)
            if ((err = ftp_restart(s, s->position)) < 0)
                return err;
Lukasz Marek's avatar
Lukasz Marek committed
608 609
    }
    s->state = READY;
Lukasz Marek's avatar
Lukasz Marek committed
610 611 612 613 614
    return 0;
}

static int ftp_abort(URLContext *h)
{
615
    static const char *command = "ABOR\r\n";
Lukasz Marek's avatar
Lukasz Marek committed
616
    int err;
617
    static const int abor_codes[] = {225, 226, 0};
618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637
    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);
638 639 640 641 642 643 644
        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;
            }
645 646 647
        }
    }

Lukasz Marek's avatar
Lukasz Marek committed
648 649 650
    return 0;
}

651
static int ftp_connect(URLContext *h, const char *url)
Lukasz Marek's avatar
Lukasz Marek committed
652
{
653
    char proto[10], path[MAX_URL_SIZE], credencials[MAX_URL_SIZE], hostname[MAX_URL_SIZE];
654
    const char *tok_user = NULL, *tok_pass = NULL;
655
    char *end = NULL, *newpath = NULL;
656
    int err;
Lukasz Marek's avatar
Lukasz Marek committed
657 658 659
    FTPContext *s = h->priv_data;

    s->state = DISCONNECTED;
660
    s->listing_method = UNKNOWN_METHOD;
Lukasz Marek's avatar
Lukasz Marek committed
661
    s->filesize = -1;
Lukasz Marek's avatar
Lukasz Marek committed
662
    s->position = 0;
663
    s->features = NULL;
Lukasz Marek's avatar
Lukasz Marek committed
664 665

    av_url_split(proto, sizeof(proto),
666
                 credencials, sizeof(credencials),
667
                 hostname, sizeof(hostname),
668
                 &s->server_control_port,
Lukasz Marek's avatar
Lukasz Marek committed
669 670 671
                 path, sizeof(path),
                 url);

672 673 674 675 676 677 678 679
    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);
680 681
    s->hostname = av_strdup(hostname);
    if (!s->hostname || !s->user || (tok_pass && !s->password)) {
682
        return AVERROR(ENOMEM);
683 684
    }

685 686
    if (s->server_control_port < 0 || s->server_control_port > 65535)
        s->server_control_port = 21;
Lukasz Marek's avatar
Lukasz Marek committed
687

688
    if ((err = ftp_connect_control_connection(h)) < 0)
689
        return err;
Lukasz Marek's avatar
Lukasz Marek committed
690

691
    if ((err = ftp_current_dir(s)) < 0)
692
        return err;
693 694 695 696 697 698

    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
699

700 701 702 703 704 705 706 707
    return 0;
}

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

708
    ff_dlog(h, "ftp protocol open\n");
709 710 711 712

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

713
    if (ftp_restart(s, 0) < 0) {
714
        h->is_streamed = 1;
715 716 717 718 719 720
    } 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
721 722

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

Lukasz Marek's avatar
Lukasz Marek committed
724 725
  fail:
    av_log(h, AV_LOG_ERROR, "FTP open failed\n");
726
    ftp_close(h);
Lukasz Marek's avatar
Lukasz Marek committed
727 728 729 730 731 732 733
    return err;
}

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

736
    ff_dlog(h, "ftp protocol seek %"PRId64" %d\n", pos, whence);
Lukasz Marek's avatar
Lukasz Marek committed
737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755

    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);
    }

756
    if (h->is_streamed)
Lukasz Marek's avatar
Lukasz Marek committed
757 758
        return AVERROR(EIO);

759 760 761 762
    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
763

764
    fake_pos = s->filesize != -1 ? FFMIN(new_pos, s->filesize) : new_pos;
765
    if (fake_pos != s->position) {
Lukasz Marek's avatar
Lukasz Marek committed
766
        if ((err = ftp_abort(h)) < 0)
Lukasz Marek's avatar
Lukasz Marek committed
767
            return err;
768
        s->position = fake_pos;
Lukasz Marek's avatar
Lukasz Marek committed
769 770 771 772
    }
    return new_pos;
}

Lukasz Marek's avatar
Lukasz Marek committed
773 774 775 776 777
static int ftp_read(URLContext *h, unsigned char *buf, int size)
{
    FTPContext *s = h->priv_data;
    int read, err, retry_done = 0;

778
    ff_dlog(h, "ftp protocol read %d bytes\n", size);
Lukasz Marek's avatar
Lukasz Marek committed
779
  retry:
Lukasz Marek's avatar
Lukasz Marek committed
780
    if (s->state == DISCONNECTED) {
Lukasz Marek's avatar
Lukasz Marek committed
781
        /* optimization */
782 783
        if (s->position >= s->filesize)
            return 0;
Lukasz Marek's avatar
Lukasz Marek committed
784 785 786
        if ((err = ftp_connect_data_connection(h)) < 0)
            return err;
    }
Lukasz Marek's avatar
Lukasz Marek committed
787
    if (s->state == READY) {
788 789
        if (s->position >= s->filesize)
            return 0;
Lukasz Marek's avatar
Lukasz Marek committed
790 791
        if ((err = ftp_retrieve(s)) < 0)
            return err;
Lukasz Marek's avatar
Lukasz Marek committed
792 793 794 795 796 797
    }
    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) {
798
                /* server will terminate, but keep current position to avoid madness */
Lukasz Marek's avatar
Lukasz Marek committed
799
                /* save position to restart from it */
800 801 802
                int64_t pos = s->position;
                if (ftp_abort(h) < 0) {
                    s->position = pos;
Lukasz Marek's avatar
Lukasz Marek committed
803
                    return AVERROR(EIO);
804 805
                }
                s->position = pos;
Lukasz Marek's avatar
Lukasz Marek committed
806 807
            }
        }
808
        if (read <= 0 && s->position < s->filesize && !h->is_streamed) {
Lukasz Marek's avatar
Lukasz Marek committed
809 810 811
            /* Server closed connection. Probably due to inactivity */
            int64_t pos = s->position;
            av_log(h, AV_LOG_INFO, "Reconnect to FTP server.\n");
812
            if ((err = ftp_abort(h)) < 0)
Lukasz Marek's avatar
Lukasz Marek committed
813 814
                return err;
            if ((err = ftp_seek(h, pos, SEEK_SET)) < 0) {
Lukasz Marek's avatar
Lukasz Marek committed
815
                av_log(h, AV_LOG_ERROR, "Position cannot be restored.\n");
Lukasz Marek's avatar
Lukasz Marek committed
816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831
                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
832
    int err;
Lukasz Marek's avatar
Lukasz Marek committed
833 834 835
    FTPContext *s = h->priv_data;
    int written;

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

Lukasz Marek's avatar
Lukasz Marek committed
838 839 840 841
    if (s->state == DISCONNECTED) {
        if ((err = ftp_connect_data_connection(h)) < 0)
            return err;
    }
Lukasz Marek's avatar
Lukasz Marek committed
842
    if (s->state == READY) {
Lukasz Marek's avatar
Lukasz Marek committed
843 844
        if ((err = ftp_store(s)) < 0)
            return err;
Lukasz Marek's avatar
Lukasz Marek committed
845 846 847 848 849 850 851 852 853 854 855 856 857 858
    }
    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
859 860
static int ftp_close(URLContext *h)
{
861 862
    FTPContext *s = h->priv_data;

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

865 866 867
    ftp_close_both_connections(s);
    av_freep(&s->user);
    av_freep(&s->password);
868 869
    av_freep(&s->hostname);
    av_freep(&s->path);
870
    av_freep(&s->features);
Lukasz Marek's avatar
Lukasz Marek committed
871 872 873 874 875 876 877 878

    return 0;
}

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

879
    ff_dlog(h, "ftp protocol get_file_handle\n");
Lukasz Marek's avatar
Lukasz Marek committed
880 881 882 883 884 885 886 887 888 889 890

    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;

891
    ff_dlog(h, "ftp protocol shutdown\n");
Lukasz Marek's avatar
Lukasz Marek committed
892 893 894 895 896 897 898

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

    return AVERROR(EIO);
}

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 928 929 930 931 932 933
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);
}

934 935 936 937 938 939 940
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)
941 942
{
    char *fact, *value;
943
    ff_dlog(NULL, "%s\n", mlsd);
944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972
    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;
}

973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990
/**
 * @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;
    }
}

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 1022 1023 1024 1025 1026 1027
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;
1028
        ret = ftp_parse_entry(h, start, *next);
1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040
        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;
1041
    av_freep(&s->dir_buffer);
1042 1043 1044 1045 1046
    ffurl_closep(&s->conn_control);
    ffurl_closep(&s->conn_data);
    return 0;
}

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 1100 1101 1102 1103 1104 1105
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;
}

1106
const URLProtocol ff_ftp_protocol = {
Lukasz Marek's avatar
Lukasz Marek committed
1107 1108 1109 1110 1111 1112 1113 1114 1115 1116
    .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,
1117 1118 1119
    .url_open_dir        = ftp_open_dir,
    .url_read_dir        = ftp_read_dir,
    .url_close_dir       = ftp_close_dir,
1120 1121
    .url_delete          = ftp_delete,
    .url_move            = ftp_move,
Lukasz Marek's avatar
Lukasz Marek committed
1122
    .flags               = URL_PROTOCOL_FLAG_NETWORK,
1123
    .default_whitelist   = "tcp",
Lukasz Marek's avatar
Lukasz Marek committed
1124
};