ftp.c 35 KB
Newer Older
1
/*****************************************************************************
2
 * ftp.c: FTP input module
3
 *****************************************************************************
Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
4
 * Copyright (C) 2001-2006 VLC authors and VideoLAN
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
5
 * Copyright © 2006 Rémi Denis-Courmont
6
 * $Id$
7
 *
8
 * Authors: Laurent Aimar <fenrir@via.ecp.fr> - original code
9
 *          Rémi Denis-Courmont <rem # videolan.org> - EPSV support
10
 *
Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
11 12 13
 * This program 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
14 15 16 17
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
18 19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
20
 *
Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
21 22 23
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
24 25 26 27 28
 *****************************************************************************/

/*****************************************************************************
 * Preamble
 *****************************************************************************/
29 30 31 32
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

33
#include <assert.h>
34
#include <stdint.h>
35
#include <errno.h>
36

37 38
#include <vlc_common.h>
#include <vlc_plugin.h>
39
#include <vlc_access.h>
40
#include <vlc_dialog.h>
Thomas Guillem's avatar
Thomas Guillem committed
41
#include <vlc_input_item.h>
42
#include <vlc_network.h>
Rémi Duraffort's avatar
Rémi Duraffort committed
43
#include <vlc_url.h>
44
#include <vlc_tls.h>
45
#include <vlc_sout.h>
46
#include <vlc_charset.h>
47
#include <vlc_interrupt.h>
Thomas Guillem's avatar
Thomas Guillem committed
48
#include <vlc_keystore.h>
49

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
50 51 52 53
#ifndef IPPORT_FTP
# define IPPORT_FTP 21u
#endif

54 55 56 57
#ifndef IPPORT_FTPS
# define IPPORT_FTPS 990u
#endif

58
/*****************************************************************************
59
 * Module descriptor
60
 *****************************************************************************/
61 62
static int   InOpen ( vlc_object_t * );
static void  InClose( vlc_object_t * );
63
#ifdef ENABLE_SOUT
64 65
static int  OutOpen ( vlc_object_t * );
static void OutClose( vlc_object_t * );
66
#endif
67

68 69 70 71 72 73
#define USER_TEXT N_("Username")
#define USER_LONGTEXT N_("Username that will be used for the connection, " \
        "if no username is set in the URL.")
#define PASS_TEXT N_("Password")
#define PASS_LONGTEXT N_("Password that will be used for the connection, " \
        "if no username or password are set in URL.")
74
#define ACCOUNT_TEXT N_("FTP account")
75
#define ACCOUNT_LONGTEXT N_("Account that will be " \
76
    "used for the connection.")
77

Thomas Guillem's avatar
Thomas Guillem committed
78 79 80 81
#define LOGIN_DIALOG_TITLE _("FTP authentication")
#define LOGIN_DIALOG_TEXT _("Please enter a valid login and password for " \
        "the ftp connexion to %s")

82 83 84 85 86 87
vlc_module_begin ()
    set_shortname( "FTP" )
    set_description( N_("FTP input") )
    set_capability( "access", 0 )
    set_category( CAT_INPUT )
    set_subcategory( SUBCAT_INPUT_ACCESS )
88 89
    add_string( "ftp-user", NULL, USER_TEXT, USER_LONGTEXT, false )
    add_string( "ftp-pwd", NULL, PASS_TEXT, PASS_LONGTEXT, false )
90
    add_string( "ftp-account", "anonymous", ACCOUNT_TEXT,
Rémi Duraffort's avatar
Rémi Duraffort committed
91
                ACCOUNT_LONGTEXT, false )
92
    add_shortcut( "ftp", "ftps", "ftpes" )
93
    set_callbacks( InOpen, InClose )
94

95
#ifdef ENABLE_SOUT
96
    add_submodule ()
Rémi Duraffort's avatar
Rémi Duraffort committed
97 98 99 100 101
        set_shortname( "FTP" )
        set_description( N_("FTP upload output") )
        set_capability( "sout access", 0 )
        set_category( CAT_SOUT )
        set_subcategory( SUBCAT_SOUT_ACO )
102
        add_shortcut( "ftp", "ftps", "ftpes" )
Rémi Duraffort's avatar
Rémi Duraffort committed
103
        set_callbacks( OutOpen, OutClose )
104
#endif
105
vlc_module_end ()
106

107 108 109
/*****************************************************************************
 * Local prototypes
 *****************************************************************************/
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
110 111 112 113
static ssize_t Read( stream_t *, void *, size_t );
static int Seek( stream_t *, uint64_t );
static int Control( stream_t *, int, va_list );
static int DirRead( stream_t *, input_item_node_t * );
114 115 116 117
#ifdef ENABLE_SOUT
static int OutSeek( sout_access_out_t *, off_t );
static ssize_t Write( sout_access_out_t *, block_t * );
#endif
118

119
static int LoginUserPwd( vlc_object_t *, access_sys_t *,
Thomas Guillem's avatar
Thomas Guillem committed
120
                         const char *, const char *, bool * );
121 122 123 124 125 126
static void FeaturesCheck( void *, const char * );

typedef struct ftp_features_t
{
    bool b_unicode;
    bool b_authtls;
127
    bool b_mlst;
128 129 130 131 132 133 134 135 136
} ftp_features_t;

enum tls_mode_e
{
    NONE = 0,
    IMPLICIT,/* ftps */
    EXPLICIT /* ftpes */
};

137
struct access_sys_t
138
{
139
    vlc_url_t  url;
140

141 142 143
    ftp_features_t   features;
    vlc_tls_creds_t *p_creds;
    enum tls_mode_e  tlsmode;
144 145
    vlc_tls_t *cmd;
    vlc_tls_t *data;
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
146

147
    char       sz_epsv_ip[NI_MAXNUMERICHOST];
148
    bool       out;
149
    uint64_t   offset;
150
    uint64_t   size;
151
};
152 153
#define GET_OUT_SYS( p_this ) \
    ((access_sys_t *)(((sout_access_out_t *)(p_this))->p_sys))
154

155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
static int ftp_SendCommand( vlc_object_t *obj, access_sys_t *sys,
                            const char *fmt, ... )
{
    size_t fmtlen = strlen( fmt );
    char fmtbuf[fmtlen + 3];

    memcpy( fmtbuf, fmt, fmtlen );
    memcpy( fmtbuf + fmtlen, "\r\n", 3 );

    va_list args;
    char *cmd;
    int val;

    va_start( args, fmt );
    val = vasprintf( &cmd, fmtbuf, args );
    va_end( args );
    if( unlikely(val == -1) )
        return -1;

174 175 176 177 178
    if( strncmp( cmd, "PASS ", 5 ) && strncmp( cmd, "ACCT ", 5 ) )
        msg_Dbg( obj, "sending request: \"%.*s\" (%d bytes)", val-2, cmd, val );
    else
        msg_Dbg( obj, "sending request: \"%.*s XXXX\" (XX bytes)", 4, cmd );

179
    if( vlc_tls_Write( sys->cmd, cmd, val ) != val )
180 181 182 183 184 185 186 187 188 189
    {
        msg_Err( obj, "request failure" );
        val = -1;
    }
    else
        val = 0;
    free( cmd );
    return val;
}

190 191
static char *ftp_GetLine( vlc_object_t *obj, access_sys_t *sys )
{
192
    char *resp = vlc_tls_GetLine( sys->cmd );
193 194 195 196 197
    if( resp == NULL )
        msg_Err( obj, "response failure" );
    return resp;
}

198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
/* TODO support this s**t :
 RFC 959 allows the client to send certain TELNET strings at any moment,
 even in the middle of a request:

 * \377\377.
 * \377\376x where x is one byte.
 * \377\375x where x is one byte. The server is obliged to send \377\374x
 *                                immediately after reading x.
 * \377\374x where x is one byte.
 * \377\373x where x is one byte. The server is obliged to send \377\376x
 *                                immediately after reading x.
 * \377x for any other byte x.

 These strings are not part of the requests, except in the case \377\377,
 where the request contains one \377. */
213 214 215
static int ftp_RecvReply( vlc_object_t *obj, access_sys_t *sys,
                          char **restrict strp,
                          void (*cb)(void *, const char *), void *opaque )
216
{
217
    char *resp = ftp_GetLine( obj, sys );
218
    if( resp == NULL )
219
        return -1;
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236

    char *end;
    unsigned code = strtoul( resp, &end, 10 );
    if( (end - resp) != 3 || (*end != '-' && *end != ' ') )
    {
        msg_Err( obj, "malformatted response" );
        goto error;
    }
    msg_Dbg( obj, "received response: \"%s\"", resp );

    if( *end == '-' ) /* Multi-line response */
    {
        bool done;

        *end = ' ';
        do
        {
237
            char *line = ftp_GetLine( obj, sys );
238 239 240 241
            if( line == NULL )
                goto error;

            done = !strncmp( resp, line, 4 );
242 243
            if( !done )
                cb( opaque, line );
244 245 246 247 248 249 250 251 252
            free( line );
        }
        while( !done );
    }

    if( strp != NULL )
        *strp = resp;
    else
        free( resp );
253
    return code;
254 255 256 257 258
error:
    free( resp );
    return -1;
}

259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
static int ftp_RecvAnswer( vlc_object_t *obj, access_sys_t *sys,
                           int *restrict codep, char **restrict strp,
                           void (*cb)(void *, const char *), void *opaque )
{
    char *str;
    int val = ftp_RecvReply( obj, sys, &str, cb, opaque );
    if( (val / 100) == 1 )
    {   /* There can be zero or one preliminary reply per command */
        free( str );
        val = ftp_RecvReply( obj, sys, &str, cb, opaque );
    }

    if( val >= 0 )
    {
        if( codep != NULL )
            *codep = val;
        if( strp != NULL )
            *strp = str;
        else
            free( str );
        val /= 100;
    }
    else
    {
        if( codep != NULL )
            *codep = 500;
        if( strp != NULL )
            *strp = NULL;
    }
    return val;
}

291 292 293 294 295 296 297 298 299 300 301
static void DummyLine( void *data, const char *str )
{
    (void) data; (void) str;
}

static int ftp_RecvCommand( vlc_object_t *obj, access_sys_t *sys,
                            int *restrict codep, char **restrict strp )
{
    return ftp_RecvAnswer( obj, sys, codep, strp, DummyLine, NULL );
}

302 303 304 305 306 307 308 309
static int ftp_RecvCommandInit( vlc_object_t *obj, access_sys_t *sys )
{
    int val = ftp_RecvReply( obj, sys, NULL, DummyLine, NULL );
    if( val >= 0 )
        val /= 100;
    return val;
}

Thomas Guillem's avatar
Thomas Guillem committed
310
static int ftp_StartStream( vlc_object_t *, access_sys_t *, uint64_t, bool );
311
static int ftp_StopStream ( vlc_object_t *, access_sys_t * );
312

313 314
static int readTLSMode( vlc_object_t *obj, access_sys_t *p_sys,
                        const char * psz_access )
315 316 317 318 319 320 321
{
    if ( !strncmp( psz_access, "ftps", 4 ) )
        p_sys->tlsmode = IMPLICIT;
    else
    if ( !strncmp( psz_access, "ftpes", 5 ) )
        p_sys->tlsmode = EXPLICIT;
    else
322 323
    {
        p_sys->p_creds = NULL;
324
        p_sys->tlsmode = NONE;
325 326 327 328 329
        return 0;
    }

    p_sys->p_creds = vlc_tls_ClientCreate( obj );
    return (p_sys->p_creds != NULL) ? 0 : -1;
330 331
}

332
static int createCmdTLS( vlc_object_t *p_access, access_sys_t *p_sys,
333 334 335
                         const char *psz_session_name )
{
    /* TLS/SSL handshake */
336 337 338 339 340 341
    vlc_tls_t *secure = vlc_tls_ClientSessionCreate( p_sys->p_creds,
                                                     p_sys->cmd,
                                                     p_sys->url.psz_host,
                                                     psz_session_name,
                                                     NULL, NULL );
    if( secure == NULL )
342 343 344 345
    {
        msg_Err( p_access, "cannot establish FTP/TLS session on command channel" );
        return -1;
    }
346
    p_sys->cmd = secure;
347 348 349
    return 0;
}

350
static void clearCmd( access_sys_t *p_sys )
351
{
352 353 354 355 356
    if( p_sys->cmd != NULL )
    {
        vlc_tls_Close( p_sys->cmd );
        p_sys->cmd = NULL;
    }
357 358
}

Thomas Guillem's avatar
Thomas Guillem committed
359
static int Login( vlc_object_t *p_access, access_sys_t *p_sys, const char *path )
360
{
361
    int i_answer;
362

363
    /* *** Open a TCP connection with server *** */
364 365 366
    p_sys->cmd = vlc_tls_SocketOpenTCP( p_access, p_sys->url.psz_host,
                                        p_sys->url.i_port );
    if( p_sys->cmd == NULL )
367
    {
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
368
        msg_Err( p_access, "connection failed" );
Thomas Guillem's avatar
Thomas Guillem committed
369 370
        vlc_dialog_display_error( p_access, _("Network interaction failed"), "%s",
            _("VLC could not connect with the given server.") );
371
        goto error;
372
    }
373

374 375
    if ( p_sys->tlsmode == IMPLICIT ) /* FTPS Mode */
    {
376
        if ( createCmdTLS( p_access, p_sys, "ftps") < 0 )
377 378 379
            goto error;
    }

380
    while( ftp_RecvCommand( p_access, p_sys, &i_answer, NULL ) == 1 );
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
381

382 383
    if( i_answer / 100 != 2 )
    {
384
        msg_Err( p_access, "connection rejected" );
Thomas Guillem's avatar
Thomas Guillem committed
385 386
        vlc_dialog_display_error( p_access, _("Network interaction failed"), "%s",
            _("VLC's connection to the given server was rejected.") );
387
        goto error;
388 389
    }

390
    msg_Dbg( p_access, "connection accepted (%d)", i_answer );
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
391

392 393 394 395 396 397
    /* Features check first */
    if( ftp_SendCommand( p_access, p_sys, "FEAT" ) < 0
     || ftp_RecvAnswer( p_access, p_sys, NULL, NULL,
                        FeaturesCheck, &p_sys->features ) < 0 )
    {
         msg_Err( p_access, "cannot get server features" );
398
         goto error;
399 400 401 402 403 404 405 406
    }

    /* Create TLS Session */
    if( p_sys->tlsmode == EXPLICIT )
    {
        if ( ! p_sys->features.b_authtls )
        {
            msg_Err( p_access, "Server does not support TLS" );
407
            goto error;
408 409 410 411 412 413 414 415
        }

        if( ftp_SendCommand( p_access, p_sys, "AUTH TLS" ) < 0
         || ftp_RecvCommand( p_access, p_sys, &i_answer, NULL ) < 0
         || i_answer != 234 )
        {
             msg_Err( p_access, "cannot switch to TLS: server replied with code %d",
                      i_answer );
416
             goto error;
417 418
        }

419
        if( createCmdTLS( p_access, p_sys, "ftpes") < 0 )
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
        {
            goto error;
        }
    }

    if( p_sys->tlsmode != NONE )
    {
        if( ftp_SendCommand( p_access, p_sys, "PBSZ 0" ) < 0 ||
            ftp_RecvCommand( p_access, p_sys, &i_answer, NULL ) < 0 ||
            i_answer != 200 )
        {
            msg_Err( p_access, "Can't truncate Protection buffer size for TLS" );
            goto error;
        }

        if( ftp_SendCommand( p_access, p_sys, "PROT P" ) < 0 ||
            ftp_RecvCommand( p_access, p_sys, &i_answer, NULL ) < 0 ||
            i_answer != 200 )
        {
            msg_Err( p_access, "Can't set Data channel protection" );
            goto error;
        }
    }

Thomas Guillem's avatar
Thomas Guillem committed
444 445
    vlc_url_t url;
    vlc_credential credential;
Thomas Guillem's avatar
Thomas Guillem committed
446
    if( vlc_UrlParseFixup( &url, path ) != 0 )
447 448 449 450
    {
        vlc_UrlClean( &url );
        goto error;
    }
Thomas Guillem's avatar
Thomas Guillem committed
451 452 453
    vlc_credential_init( &credential, &url );
    bool b_logged = false;

454 455 456 457 458
    /* First: try credentials from url / option */
    vlc_credential_get( &credential, p_access, "ftp-user", "ftp-pwd",
                        NULL, NULL );
    do
    {
459 460 461 462 463 464 465 466 467
        const char *psz_username = credential.psz_username;

        if( psz_username == NULL ) /* use anonymous by default */
            psz_username = "anonymous";

        if( LoginUserPwd( p_access, p_sys, psz_username,
                          credential.psz_password, &b_logged ) != 0
         || b_logged )
            break;
468
    }
Thomas Guillem's avatar
Thomas Guillem committed
469 470
    while( vlc_credential_get( &credential, p_access, "ftp-user", "ftp-pwd",
                               LOGIN_DIALOG_TITLE, LOGIN_DIALOG_TEXT,
471 472
                               url.psz_host ) );

Thomas Guillem's avatar
Thomas Guillem committed
473
    if( b_logged )
474
    {
475
        vlc_credential_store( &credential, p_access );
Thomas Guillem's avatar
Thomas Guillem committed
476 477
        vlc_credential_clean( &credential );
        vlc_UrlClean( &url );
478 479
        return 0;
    }
Thomas Guillem's avatar
Thomas Guillem committed
480 481
    vlc_credential_clean( &credential );
    vlc_UrlClean( &url );
482
error:
483
    clearCmd( p_sys );
484 485 486 487
    return -1;
}

static int LoginUserPwd( vlc_object_t *p_access, access_sys_t *p_sys,
Thomas Guillem's avatar
Thomas Guillem committed
488 489
                         const char *psz_user, const char *psz_pwd,
                         bool *p_logged )
490 491 492 493 494 495 496
{
    int i_answer;

    /* Send credentials over channel */
    if( ftp_SendCommand( p_access, p_sys, "USER %s", psz_user ) < 0 ||
        ftp_RecvCommand( p_access, p_sys, &i_answer, NULL ) < 0 )
        return -1;
497

498 499 500
    switch( i_answer / 100 )
    {
        case 2:
501 502 503 504 505
            /* X.509 auth successful after AUTH TLS / RFC 2228 sec. 4 */
            if ( i_answer == 232 )
                msg_Dbg( p_access, "user accepted and authenticated" );
            else
                msg_Dbg( p_access, "user accepted" );
506 507
            break;
        case 3:
508
            msg_Dbg( p_access, "password needed" );
509

510
            if( ftp_SendCommand( p_access, p_sys, "PASS %s", psz_pwd ) < 0 ||
511
                ftp_RecvCommand( p_access, p_sys, &i_answer, NULL ) < 0 )
512
                return -1;
513

514 515 516
            switch( i_answer / 100 )
            {
                case 2:
517
                    msg_Dbg( p_access, "password accepted" );
518 519
                    break;
                case 3:
520 521
                {
                    char *psz;
522
                    msg_Dbg( p_access, "account needed" );
523
                    psz = var_InheritString( p_access, "ftp-account" );
524
                    if( ftp_SendCommand( p_access, p_sys, "ACCT %s",
525
                                         psz ) < 0 ||
526
                        ftp_RecvCommand( p_access, p_sys, &i_answer, NULL ) < 0 )
527
                    {
528
                        free( psz );
529
                        return -1;
530
                    }
531
                    free( psz );
532

533 534
                    if( i_answer / 100 != 2 )
                    {
535
                        msg_Err( p_access, "account rejected" );
Thomas Guillem's avatar
Thomas Guillem committed
536 537 538
                        vlc_dialog_display_error( p_access,
                          _("Network interaction failed"),
                          "%s", _("Your account was rejected.") );
539
                        return -1;
540
                    }
541
                    msg_Dbg( p_access, "account accepted" );
542
                    break;
543
                }
544

545
                default:
Thomas Guillem's avatar
Thomas Guillem committed
546 547 548
                    msg_Warn( p_access, "password rejected" );
                    *p_logged = false;
                    return 0;
549 550 551
            }
            break;
        default:
Thomas Guillem's avatar
Thomas Guillem committed
552 553 554
            msg_Warn( p_access, "user rejected" );
            *p_logged = false;
            return 0;
555 556
    }

Thomas Guillem's avatar
Thomas Guillem committed
557
    *p_logged = true;
558 559 560
    return 0;
}

561 562
static void FeaturesCheck( void *opaque, const char *feature )
{
563
    ftp_features_t *features = opaque;
564 565

    if( strcasestr( feature, "UTF8" ) != NULL )
566 567 568 569
        features->b_unicode = true;
    else
    if( strcasestr( feature, "AUTH TLS" ) != NULL )
        features->b_authtls = true;
570 571 572

    if( strcasestr( feature, "MLST" ) != NULL )
        features->b_mlst = true;
573 574 575 576 577 578 579 580 581 582 583
}

static const char *IsASCII( const char *str )
{
    int8_t c;
    for( const char *p = str; (c = *p) != '\0'; p++ )
        if( c < 0 )
            return NULL;
    return str;
}

Thomas Guillem's avatar
Thomas Guillem committed
584
static int Connect( vlc_object_t *p_access, access_sys_t *p_sys, const char *path )
585
{
Thomas Guillem's avatar
Thomas Guillem committed
586
    if( Login( p_access, p_sys, path ) < 0 )
587
        return -1;
588

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
589
    /* Extended passive mode */
590
    if( ftp_SendCommand( p_access, p_sys, "EPSV ALL" ) < 0 )
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
591 592
    {
        msg_Err( p_access, "cannot request extended passive mode" );
593
        goto error;
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
594 595
    }

596
    if( ftp_RecvCommand( p_access, p_sys, NULL, NULL ) == 2 )
597
    {
598 599
        int fd = vlc_tls_GetFD(p_sys->cmd);
        if( net_GetPeerAddress( fd, p_sys->sz_epsv_ip, NULL ) )
600
            goto error;
601
    }
602 603 604 605 606 607 608
    else
    {
        /* If ESPV ALL fails, we fallback to PASV.
         * We have to restart the connection in case there is a NAT that
         * understands EPSV ALL in the way, and hence won't allow PASV on
         * the initial connection.
         */
609
        msg_Info( p_access, "FTP Extended passive mode disabled" );
610
        clearCmd( p_sys );
611

Thomas Guillem's avatar
Thomas Guillem committed
612
        if( Login( p_access, p_sys, path ) )
613 614 615
            goto error;
    }

616 617
    if( p_sys->url.psz_path &&
        (p_sys->features.b_unicode ? IsUTF8 : IsASCII)(p_sys->url.psz_path) == NULL )
618 619 620
    {
        msg_Err( p_access, "unsupported path: \"%s\"", p_sys->url.psz_path );
        goto error;
621
    }
622

623 624
    /* check binary mode support */
    if( ftp_SendCommand( p_access, p_sys, "TYPE I" ) < 0 ||
625
        ftp_RecvCommand( p_access, p_sys, NULL, NULL ) != 2 )
626
    {
627
        msg_Err( p_access, "cannot set binary transfer mode" );
628
        goto error;
629 630
    }

631
    return 0;
632

633
error:
634
    clearCmd( p_sys );
635
    return -1;
636 637 638
}


639
static int parseURL( vlc_url_t *url, const char *path, enum tls_mode_e mode )
640 641
{
    if( path == NULL )
642
        return VLC_EGENERIC;
643 644 645 646 647

    /* *** Parse URL and get server addr/port and path *** */
    while( *path == '/' )
        path++;

648
    vlc_UrlParseFixup( url, path );
649 650

    if( url->psz_host == NULL || *url->psz_host == '\0' )
651
        return VLC_EGENERIC;
652 653

    if( url->i_port <= 0 )
654 655 656 657 658 659
    {
        if( mode == IMPLICIT )
            url->i_port = IPPORT_FTPS;
        else
            url->i_port = IPPORT_FTP; /* default port */
    }
660

661 662
    if( url->psz_path == NULL )
        return VLC_SUCCESS;
663
    /* FTP URLs are relative to user's default directory (RFC1738 §3.2)
664
    For absolute path use ftp://foo.bar//usr/local/etc/filename */
665
    /* FIXME: we should issue a series of CWD, one per slash */
666 667 668 669 670
    if( url->psz_path )
    {
        assert( url->psz_path[0] == '/' );
        url->psz_path++;
    }
671 672 673 674 675 676 677 678

    char *type = strstr( url->psz_path, ";type=" );
    if( type )
    {
        *type = '\0';
        if( strchr( "iI", type[6] ) == NULL )
            return VLC_EGENERIC; /* ASCII and directory not supported */
    }
679
    vlc_uri_decode( url->psz_path );
680
    return VLC_SUCCESS;
681 682 683 684 685 686 687 688
}


/****************************************************************************
 * Open: connect to ftp server and ask for file
 ****************************************************************************/
static int InOpen( vlc_object_t *p_this )
{
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
689
    stream_t     *p_access = (stream_t*)p_this;
690 691
    access_sys_t *p_sys;
    char         *psz_arg;
692
    bool          b_directory;
693 694

    /* Init p_access */
695
    p_sys = p_access->p_sys = (access_sys_t*)vlc_obj_calloc( p_this, 1, sizeof( access_sys_t ) );
Thomas Guillem's avatar
Thomas Guillem committed
696 697
    if( !p_sys )
        return VLC_ENOMEM;
698
    p_sys->data = NULL;
699
    p_sys->out = false;
700
    p_sys->offset = 0;
701
    p_sys->size = UINT64_MAX;
702

703
    if( readTLSMode( p_this, p_sys, p_access->psz_name ) )
704
        goto exit_error;
705

706
    if( parseURL( &p_sys->url, p_access->psz_url, p_sys->tlsmode ) )
707 708
        goto exit_error;

Thomas Guillem's avatar
Thomas Guillem committed
709
    if( Connect( p_this, p_sys, p_access->psz_url ) )
710 711
        goto exit_error;

712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745
    do {
        /* get size */
        if( p_sys->url.psz_path == NULL || !*p_sys->url.psz_path )
        {
            b_directory = true;
            break;
        }

        if( ftp_SendCommand( p_this, p_sys, "SIZE %s",
                             p_sys->url.psz_path ) < 0 )
            goto error;

        int val = ftp_RecvCommand( p_this, p_sys, NULL, &psz_arg );
        if( val == 2 )
        {
            b_directory = false;
            p_sys->size = atoll( &psz_arg[4] );
            free( psz_arg );
            msg_Dbg( p_access, "file size: %"PRIu64, p_sys->size );
            break;
        }
        if( val >= 0 )
            free( psz_arg );

        if( ftp_SendCommand( p_this, p_sys, "CWD %s",
                             p_sys->url.psz_path ) < 0 )
            goto error;

        if( ftp_RecvCommand( p_this, p_sys, NULL, NULL ) == 2 )
        {
            b_directory = true;
            break;
        }

746
        msg_Err( p_this, "file or directory does not exist" );
747
        goto error;
748
    } while (0);
Thomas Guillem's avatar
Thomas Guillem committed
749 750

    if( b_directory )
751
    {
Thomas Guillem's avatar
Thomas Guillem committed
752
        p_access->pf_readdir = DirRead;
753
        p_access->pf_control = access_vaDirectoryControlHelper;
754
    } else
Thomas Guillem's avatar
Thomas Guillem committed
755
        ACCESS_SET_CALLBACKS( Read, NULL, Control, Seek ); \
756

757
    /* Start the 'stream' */
Thomas Guillem's avatar
Thomas Guillem committed
758
    if( ftp_StartStream( p_this, p_sys, 0, b_directory ) < 0 )
759
    {
760
        msg_Err( p_this, "cannot retrieve file" );
761
        goto error;
762 763
    }

764
    return VLC_SUCCESS;
765

766
error:
767
    clearCmd( p_sys );
768

769
exit_error:
770
    vlc_UrlClean( &p_sys->url );
771
    vlc_tls_Delete( p_sys->p_creds );
772
    return VLC_EGENERIC;
773 774
}

775
#ifdef ENABLE_SOUT
776 777 778 779 780
static int OutOpen( vlc_object_t *p_this )
{
    sout_access_out_t *p_access = (sout_access_out_t *)p_this;
    access_sys_t      *p_sys;

781
    p_sys = vlc_obj_calloc( p_this, 1, sizeof( *p_sys ) );
782
    if( !p_sys )
783 784 785
        return VLC_ENOMEM;

    /* Init p_access */
786
    p_sys->data = NULL;
787
    p_sys->out = true;
788 789 790

    if( readTLSMode( p_this, p_sys, p_access->psz_access ) )
        goto exit_error;
791

792
    if( parseURL( &p_sys->url, p_access->psz_path, p_sys->tlsmode ) )
793
        goto exit_error;
794 795 796 797 798
    if( p_sys->url.psz_path == NULL )
    {
        msg_Err( p_this, "no filename specified" );
        goto exit_error;
    }
799

Thomas Guillem's avatar
Thomas Guillem committed
800
    if( Connect( p_this, p_sys, p_access->psz_path ) )
801 802 803
        goto exit_error;

    /* Start the 'stream' */
Thomas Guillem's avatar
Thomas Guillem committed
804
    if( ftp_StartStream( p_this, p_sys, 0, false ) < 0 )
805 806
    {
        msg_Err( p_access, "cannot store file" );
807
        clearCmd( p_sys );
808 809 810 811 812
        goto exit_error;
    }

    p_access->pf_seek = OutSeek;
    p_access->pf_write = Write;
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
813
    p_access->p_sys = (void *)p_sys;
814 815 816 817

    return VLC_SUCCESS;

exit_error:
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
818
    vlc_UrlClean( &p_sys->url );
819
    vlc_tls_Delete( p_sys->p_creds );
820 821
    return VLC_EGENERIC;
}
822
#endif
823

824 825 826
/*****************************************************************************
 * Close: free unused data structures
 *****************************************************************************/
827
static void Close( vlc_object_t *p_access, access_sys_t *p_sys )
828
{
829
    msg_Dbg( p_access, "stopping stream" );
830
    ftp_StopStream( p_access, p_sys );
831

832
    if( ftp_SendCommand( p_access, p_sys, "QUIT" ) < 0 )
833
    {
834
        msg_Warn( p_access, "cannot quit" );
835 836 837
    }
    else
    {
838
        ftp_RecvCommand( p_access, p_sys, NULL, NULL );
839
    }
840

841
    clearCmd( p_sys );
842 843

    /* free memory */
844
    vlc_UrlClean( &p_sys->url );
845
    vlc_tls_Delete( p_sys->p_creds );
846 847
}

848 849
static void InClose( vlc_object_t *p_this )
{
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
850
    Close( p_this, ((stream_t *)p_this)->p_sys);
851 852
}

853
#ifdef ENABLE_SOUT
854 855 856 857
static void OutClose( vlc_object_t *p_this )
{
    Close( p_this, GET_OUT_SYS(p_this));
}
858
#endif
859 860


861 862 863
/*****************************************************************************
 * Seek: try to go at the right place
 *****************************************************************************/
864 865
static int SeekCommon( vlc_object_t *p_access, access_sys_t *p_sys,
                       uint64_t i_pos )
866
{
867
    msg_Dbg( p_access, "seeking to %"PRIu64, i_pos );
868

869
    ftp_StopStream( p_access, p_sys );
870

871 872
    if( ftp_StartStream( p_access, p_sys, i_pos, false ) < 0 )
        return VLC_EGENERIC;
873 874 875
    return VLC_SUCCESS;
}

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
876
static int Seek( stream_t *p_access, uint64_t i_pos )
877
{
878 879
    access_sys_t *p_sys = p_access->p_sys;

880
    int val = SeekCommon( (vlc_object_t *)p_access, p_sys, i_pos );
881 882
    if( val )
        return val;
883

884
    p_sys->offset = i_pos;
885

886
    return VLC_SUCCESS;
887 888
}

889
#ifdef ENABLE_SOUT
890 891
static int OutSeek( sout_access_out_t *p_access, off_t i_pos )
{
892
    return SeekCommon((vlc_object_t *)p_access, GET_OUT_SYS(p_access), i_pos);
893
}
894
#endif
895

896 897 898
/*****************************************************************************
 * Read:
 *****************************************************************************/
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
899
static ssize_t Read( stream_t *p_access, void *p_buffer, size_t i_len )
900
{
901 902
    access_sys_t *p_sys = p_access->p_sys;

903 904
    if( p_sys->data == NULL )
        return 0;
905
    assert( !p_sys->out );
906

907
    ssize_t i_read = vlc_tls_Read( p_sys->data, p_buffer, i_len, false );
908
    if( i_read >= 0 )
909
        p_sys->offset += i_read;
910 911 912
    else if( errno != EINTR && errno != EAGAIN )
    {
        msg_Err( p_access, "receive error: %s", vlc_strerror_c(errno) );
913
        i_read = 0;
914
    }
Thomas Guillem's avatar
Thomas Guillem committed
915 916 917 918 919 920 921

    return i_read;
}

/*****************************************************************************
 * DirRead:
 *****************************************************************************/
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
922
static int DirRead (stream_t *p_access, input_item_node_t *p_current_node)
Thomas Guillem's avatar
Thomas Guillem committed
923 924
{
    access_sys_t *p_sys = p_access->p_sys;
925
    int i_ret = VLC_SUCCESS;
Thomas Guillem's avatar
Thomas Guillem committed
926

927
    assert( p_sys->data != NULL );
Thomas Guillem's avatar
Thomas Guillem committed
928 929
    assert( !p_sys->out );

930 931
    struct vlc_readdir_helper rdh;
    vlc_readdir_helper_init( &rdh, p_access, p_current_node );
932 933 934

    while (i_ret == VLC_SUCCESS)
    {
935
        char *psz_file;
936
        int type = ITEM_TYPE_UNKNOWN;
937

938
        char *psz_line = vlc_tls_GetLine( p_sys->data );
939 940 941
        if( psz_line == NULL )
            break;

942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963
        if( p_sys->features.b_mlst )
        {
            /* MLST Format is key=val;key=val...; FILENAME */
            if( strstr( psz_line, "type=dir" ) )
                type = ITEM_TYPE_DIRECTORY;
            if( strstr( psz_line, "type=file" ) )
                type = ITEM_TYPE_FILE;

            /* Get the filename or fail */
            psz_file = strchr( psz_line, ' ' );
            if( psz_file )
                psz_file++;
            else
            {
                msg_Warn( p_access, "Empty filename in MLST list" );
                free( psz_line );
                continue;
            }
        }
        else
            psz_file = psz_line;

964
        char *psz_uri;
965
        char *psz_filename = vlc_uri_encode( psz_file );
966 967
        if( psz_filename != NULL &&
            asprintf( &psz_uri, "%s://%s:%d%s%s/%s",
968 969 970 971 972
                      ( p_sys->tlsmode == NONE ) ? "ftp" :
                      ( ( p_sys->tlsmode == IMPLICIT ) ? "ftps" : "ftpes" ),
                      p_sys->url.psz_host, p_sys->url.i_port,
                      p_sys->url.psz_path ? "/" : "",
                      p_sys->url.psz_path ? p_sys->url.psz_path : "",
973
                      psz_filename ) != -1 )
974
        {
975
            i_ret = vlc_readdir_helper_additem( &rdh, psz_uri, NULL, psz_file,
976
                                                type, ITEM_NET );
977 978
            free( psz_uri );
        }
979
        free( psz_filename );
980 981 982
        free( psz_line );
    }

983
    vlc_readdir_helper_finish( &rdh, i_ret == VLC_SUCCESS );
984
    return i_ret;
985 986
}

987 988 989
/*****************************************************************************
 * Write:
 *****************************************************************************/
990
#ifdef ENABLE_SOUT
991
static ssize_t Write( sout_access_out_t *p_access, block_t *p_buffer )
992 993 994 995
{
    access_sys_t *p_sys = GET_OUT_SYS(p_access);
    size_t i_write = 0;

996
    assert( p_sys->data != NULL );
997 998 999

    while( p_buffer != NULL )
    {
1000
        block_t *p_next = p_buffer->p_next;
1001

1002
        i_write += vlc_tls_Write( p_sys->data,
1003
                                  p_buffer->p_buffer, p_buffer->i_buffer );
1004 1005 1006 1007 1008 1009 1010
        block_Release( p_buffer );

        p_buffer = p_next;
    }

    return i_write;
}
1011
#endif
1012

1013 1014 1015
/*****************************************************************************
 * Control:
 *****************************************************************************/
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
1016
static int Control( stream_t *p_access, int i_query, va_list args )
1017
{
1018
    access_sys_t *sys = p_access->p_sys;
1019 1020
    bool    *pb_bool;
    int64_t *pi_64;
1021 1022 1023

    switch( i_query )
    {
1024
        case STREAM_CAN_SEEK:
1025
            pb_bool = va_arg( args, bool * );
Thomas Guillem's avatar
Thomas Guillem committed
1026
            *pb_bool = true;
1027
            break;
1028
        case STREAM_CAN_FASTSEEK:
1029
            pb_bool = va_arg( args, bool * );
1030
            *pb_bool = false;
1031
            break;
1032
        case STREAM_CAN_PAUSE:
1033
            pb_bool = va_arg( args, bool * );
1034
            *pb_bool = true;    /* FIXME */
1035
            break;
1036
        case STREAM_CAN_CONTROL_PACE:
1037
            pb_bool = va_arg( args, bool * );
1038
            *pb_bool = true;    /* FIXME */
1039
            break;
1040
        case STREAM_GET_SIZE:
1041
            if( sys->size == UINT64_MAX )
1042
                return VLC_EGENERIC;
1043
            *va_arg( args, uint64_t * ) = sys->size;
1044
            break;
1045

1046
        case STREAM_GET_PTS_DELAY:
1047
            pi_64 = va_arg( args, int64_t * );
1048 1049
            *pi_64 = INT64_C(1000)
                   * var_InheritInteger( p_access, "network-caching" );
1050
            break;
1051

1052
        case STREAM_SET_PAUSE_STATE:
1053
            pb_bool = va_arg( args, bool * );
1054
            if ( !pb_bool )
1055
                 return Seek( p_access, sys->offset );
1056 1057 1058 1059 1060 1061 1062
            break;

        default:
            return VLC_EGENERIC;

    }
    return VLC_SUCCESS;
1063 1064
}

1065
static int ftp_StartStream( vlc_object_t *p_access, access_sys_t *p_sys,
Thomas Guillem's avatar
Thomas Guillem committed
1066
                            uint64_t i_start, bool b_directory )
1067
{
1068
    char psz_ipv4[16], *psz_ip = p_sys->sz_epsv_ip;
1069 1070 1071 1072
    int  i_answer;
    char *psz_arg, *psz_parser;
    int  i_port;

1073
    assert( p_sys->data == NULL );
1074

1075
    if( ( ftp_SendCommand( p_access, p_sys, *psz_ip ? "EPSV" : "PASV" ) < 0 )
1076
     || ( ftp_RecvCommand( p_access, p_sys, &i_answer, &psz_arg ) != 2 ) )
1077
    {
1078
        msg_Err( p_access, "cannot set passive mode" );
1079
        return VLC_EGENERIC;
1080
    }
1081

1082
    psz_parser = strchr( psz_arg, '(' );
1083
    if( psz_parser == NULL )
1084
    {
1085
        free( psz_arg );
1086
        msg_Err( p_access, "cannot parse passive mode response" );
1087
        return VLC_EGENERIC;
1088
    }
1089

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
1090
    if( *psz_ip )
1091
    {
1092
        if( sscanf( psz_parser, "(%*3c%u", &i_port ) < 1 )
1093 1094 1095 1096 1097 1098 1099 1100
        {
            free( psz_arg );
            msg_Err( p_access, "cannot parse passive mode response" );
            return VLC_EGENERIC;
        }
    }
    else
    {
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
1101
        unsigned a1, a2, a3, a4, p1, p2;
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
1102

1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115
        if( ( sscanf( psz_parser, "(%u,%u,%u,%u,%u,%u", &a1, &a2, &a3, &a4,
                      &p1, &p2 ) < 6 ) || ( a1 > 255 ) || ( a2 > 255 )
         || ( a3 > 255 ) || ( a4 > 255 ) || ( p1 > 255 ) || ( p2 > 255 ) )
        {
            free( psz_arg );
            msg_Err( p_access, "cannot parse passive mode response" );
            return VLC_EGENERIC;
        }

        sprintf( psz_ipv4, "%u.%u.%u.%u", a1, a2, a3, a4 );
        psz_ip = psz_ipv4;
        i_port = (p1 << 8) | p2;
    }
1116
    free( psz_arg );
1117

1118
    msg_Dbg( p_access, "ip:%s port:%d", psz_ip, i_port );
1119

1120
    if( ftp_SendCommand( p_access, p_sys, "TYPE I" ) < 0 ||
1121
        ftp_RecvCommand( p_access, p_sys, &i_answer, NULL ) != 2 )
1122
    {
1123
        msg_Err( p_access, "cannot set binary transfer mode" );
1124
        return VLC_EGENERIC;
1125 1126 1127 1128
    }

    if( i_start > 0 )
    {
1129
        if( ftp_SendCommand( p_access, p_sys, "REST %"PRIu64, i_start ) < 0 ||
1130
            ftp_RecvCommand( p_access, p_sys, &i_answer, NULL ) > 3 )
1131
        {
1132
            msg_Err( p_access, "cannot set restart offset" );
1133
            return VLC_EGENERIC;
1134 1135 1136
        }
    }

1137
    msg_Dbg( p_access, "waiting for data connection..." );
1138 1139
    p_sys->data = vlc_tls_SocketOpenTCP( p_access, psz_ip, i_port );
    if( p_sys->data == NULL )
1140
    {
1141
        msg_Err( p_access, "failed to connect with server" );
1142
        return VLC_EGENERIC;
1143
    }
1144
    msg_Dbg( p_access, "connection with \"%s:%d\" successful",
1145 1146
             psz_ip, i_port );

Thomas Guillem's avatar
Thomas Guillem committed
1147
    if( b_directory )
1148
    {
1149 1150
        if( p_sys->features.b_mlst &&
            ftp_SendCommand( p_access, p_sys, "MLSD" ) >= 0 &&
1151
            ftp_RecvCommandInit( p_access, p_sys ) == 1 )
1152 1153 1154 1155
        {
            msg_Dbg( p_access, "Using MLST extension to list" );
        }
        else
1156
        if( ftp_SendCommand( p_access, p_sys, "NLST" ) < 0 ||
1157
            ftp_RecvCommandInit( p_access, p_sys ) == 1 )
1158 1159
        {
            msg_Err( p_access, "cannot list directory contents" );
Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
1160
            return VLC_EGENERIC;
1161 1162 1163 1164 1165
        }
    }
    else
    {
        /* "1xx" message */
1166
        assert( p_sys->url.psz_path );
1167 1168
        if( ftp_SendCommand( p_access, p_sys, "%s %s",
                             p_sys->out ? "STOR"