VLCHTTPUploaderController.m 14.7 KB
Newer Older
1
/*****************************************************************************
2
 * VLCHTTPUploaderController.m
3 4
 * VLC for iOS
 *****************************************************************************
5
 * Copyright (c) 2013-2015 VideoLAN. All rights reserved.
6 7 8 9 10 11
 * $Id$
 *
 * Authors: Jean-Baptiste Kempf <jb # videolan.org>
 *          Gleb Pinigin <gpinigin # gmail.com>
 *          Felix Paul Kühne <fkuehne # videolan.org>
 *          Jean-Romain Prévost <jr # 3on.fr>
12 13
 *          Carola Nitz <caro # videolan.org>
 *          Ron Soffer <rsoffer1 # gmail.com>
14 15 16
 *
 * Refer to the COPYING file of the official project for license.
 *****************************************************************************/
17

18 19
#import "VLCHTTPUploaderController.h"
#import "VLCHTTPConnection.h"
20
#import "VLCActivityManager.h"
21
#import "HTTPServer.h"
22
#import "Reachability.h"
23

24 25 26
#import <ifaddrs.h>
#import <arpa/inet.h>

27
#if TARGET_OS_IOS
28
#import "VLC-Swift.h"
29 30 31
#import "VLCMediaFileDiscoverer.h"
#endif

32
@interface VLCHTTPUploaderController()
33
{
34 35
    NSString *_nameOfUsedNetworkInterface;
    HTTPServer *_httpServer;
36
    UIBackgroundTaskIdentifier _backgroundTaskIdentifier;
37
    Reachability *_reachability;
38 39 40

    NSTimer *_idleTimer;
    NSMutableSet<NSString *> *_playlistUploadPaths;
41
}
42 43 44
@end

@implementation VLCHTTPUploaderController
45

Felix Paul Kühne's avatar
Felix Paul Kühne committed
46 47 48 49 50 51
+ (instancetype)sharedInstance
{
    static VLCHTTPUploaderController *sharedInstance = nil;
    static dispatch_once_t pred;

    dispatch_once(&pred, ^{
52
        sharedInstance = [VLCHTTPUploaderController new];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
53 54 55 56 57
    });

    return sharedInstance;
}

58 59
- (id)init
{
60
    if (self = [super init]) {
61 62
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center addObserver:self selector:@selector(applicationDidBecomeActive:)
63
            name:UIApplicationDidBecomeActiveNotification object:nil];
64
        [center addObserver:self selector:@selector(applicationDidEnterBackground:)
65
            name:UIApplicationDidEnterBackgroundNotification object:nil];
66
        [center addObserver:self selector:@selector(netReachabilityChanged) name:kReachabilityChangedNotification object:nil];
67 68
        
        BOOL isHTTPServerOn = [[NSUserDefaults standardUserDefaults] boolForKey:kVLCSettingSaveHTTPUploadServerStatus];
69
        [self netReachabilityChanged];
70
        [self changeHTTPServerState:isHTTPServerOn];
71
        _playlistUploadPaths = [NSMutableSet set];
72
    }
73 74

    return self;
75 76 77 78
}

- (void)applicationDidBecomeActive: (NSNotification *)notification
{
79
    if (!_httpServer.isRunning)
80 81 82 83 84 85
        [self changeHTTPServerState:[[NSUserDefaults standardUserDefaults] boolForKey:kVLCSettingSaveHTTPUploadServerStatus]];

    if (_backgroundTaskIdentifier && _backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
        [[UIApplication sharedApplication] endBackgroundTask:_backgroundTaskIdentifier];
        _backgroundTaskIdentifier = 0;
    }
86 87 88 89
}

- (void)applicationDidEnterBackground: (NSNotification *)notification
{
90
    if (_httpServer.isRunning) {
91
        if (!_backgroundTaskIdentifier || _backgroundTaskIdentifier == UIBackgroundTaskInvalid) {
92
            dispatch_block_t expirationHandler = ^{
93
                [self changeHTTPServerState:NO];
94 95
                [[UIApplication sharedApplication] endBackgroundTask:self->_backgroundTaskIdentifier];
                self->_backgroundTaskIdentifier = 0;
96 97 98 99 100 101
            };
            if ([[UIApplication sharedApplication] respondsToSelector:@selector(beginBackgroundTaskWithName:expirationHandler:)]) {
                _backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"VLCUploader" expirationHandler:expirationHandler];
            } else {
                _backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:expirationHandler];
            }
102 103
        }
    }
104 105
}

106 107
- (NSString *)httpStatus
{
108
    if (_httpServer.isRunning) {
109 110 111 112 113 114 115 116 117 118 119
        if (_httpServer.listeningPort != 80) {
            return [NSString stringWithFormat:@"http://%@:%i\nhttp://%@:%i",
                    [self currentIPAddress],
                    _httpServer.listeningPort,
                    [self hostname],
                    _httpServer.listeningPort];
        } else {
            return [NSString stringWithFormat:@"http://%@\nhttp://%@",
                    [self currentIPAddress],
                    [self hostname]];
        }
120 121 122 123 124 125 126
    } else {
        return NSLocalizedString(@"HTTP_UPLOAD_SERVER_OFF", nil);
    }
}

- (BOOL)isServerRunning
{
127
    return _httpServer.isRunning;
128 129 130 131
}

- (void)netReachabilityChanged
{
132 133 134
    // find an interface to listen on
    struct ifaddrs *listOfInterfaces = NULL;
    struct ifaddrs *anInterface = NULL;
135 136
    BOOL serverWasRunning = self.isServerRunning;
    [self changeHTTPServerState:NO];
137
    _nameOfUsedNetworkInterface = nil;
138 139 140 141 142 143
    int ret = getifaddrs(&listOfInterfaces);
    if (ret == 0) {
        anInterface = listOfInterfaces;

        while (anInterface != NULL) {
            if (anInterface->ifa_addr->sa_family == AF_INET) {
144
                APLog(@"Found interface %s, address %@", anInterface->ifa_name, @(inet_ntoa(((struct sockaddr_in *)anInterface->ifa_addr)->sin_addr)));
145 146 147 148 149

                /* check for primary interface first */
                if (strncmp (anInterface->ifa_name,"en0",strlen("en0")) == 0) {
                    unsigned int flags = anInterface->ifa_flags;
                    if( (flags & 0x1) && (flags & 0x40) && !(flags & 0x8) ) {
150
                        _nameOfUsedNetworkInterface = [NSString stringWithUTF8String:anInterface->ifa_name];
151 152 153 154 155 156 157 158
                        break;
                    }
                }

                /* oh well, let's move on to the secondary interface */
                if (strncmp (anInterface->ifa_name,"en1",strlen("en1")) == 0) {
                    unsigned int flags = anInterface->ifa_flags;
                    if( (flags & 0x1) && (flags & 0x40) && !(flags & 0x8) ) {
159
                        _nameOfUsedNetworkInterface = [NSString stringWithUTF8String:anInterface->ifa_name];
160 161 162
                        break;
                    }
                }
163 164 165 166 167 168 169 170

                if (strncmp (anInterface->ifa_name,"bridge100",strlen("bridge100")) == 0) {
                    unsigned int flags = anInterface->ifa_flags;
                    if( (flags & 0x1) && (flags & 0x40) && !(flags & 0x8) ) {
                        _nameOfUsedNetworkInterface = [NSString stringWithUTF8String:anInterface->ifa_name];
                        break;
                    }
                }
171 172 173 174
            }
            anInterface = anInterface->ifa_next;
        }
    }
175
    freeifaddrs(listOfInterfaces);
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
    if (_nameOfUsedNetworkInterface == nil) {
        _isReachable = NO;
        [self changeHTTPServerState:NO];
        return;
    }
    _isReachable = YES;
    if (serverWasRunning) {
        [self changeHTTPServerState:YES];
    }
}

- (BOOL)changeHTTPServerState:(BOOL)state
{
    if (!state) {
        [_httpServer stop];
        return true;
    }

    if (_nameOfUsedNetworkInterface == nil) {
        APLog(@"No interface to listen on, server not started");
        _isReachable = NO;
        return NO;
    }

#if TARGET_OS_IOS
    // clean cache before accepting new stuff
    [self cleanCache];
#endif

    // Initialize our http server
    _httpServer = [[HTTPServer alloc] init];
207

208
    [_httpServer setInterface:_nameOfUsedNetworkInterface];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
209

210 211 212
    [_httpServer setIPv4Enabled:YES];
    [_httpServer setIPv6Enabled:[[[NSUserDefaults standardUserDefaults] objectForKey:kVLCSettingWiFiSharingIPv6] boolValue]];

213 214
    // Tell the server to broadcast its presence via Bonjour.
    // This allows browsers such as Safari to automatically discover our service.
215
    [_httpServer setType:@"_http._tcp."];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
216

217 218
    // Serve files from the standard Sites folder
    NSString *docRoot = [[[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"] stringByDeletingLastPathComponent];
219

220
    APLog(@"Setting document root: %@", docRoot);
221

222
    [_httpServer setDocumentRoot:docRoot];
223
    [_httpServer setPort:80];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
224

225
    [_httpServer setConnectionClass:[VLCHTTPConnection class]];
226

227
    NSError *error = nil;
228
    if (![_httpServer start:&error]) {
229
        if (error.code == EACCES) {
230
            APLog(@"Port forbidden by OS, trying another one");
231 232
            [_httpServer setPort:8888];
            if(![_httpServer start:&error])
233 234
                return true;
        }
235

236
        /* Address already in Use, take a random one */
237
        if (error.code == EADDRINUSE) {
238
            APLog(@"Port already in use, trying another one");
239 240
            [_httpServer setPort:0];
            if(![_httpServer start:&error])
241
                return true;
242
        }
243

244
        if (error) {
245
            APLog(@"Error starting HTTP Server: %@", error.localizedDescription);
246
            [_httpServer stop];
247
        }
248
        return false;
249
    }
250
    return true;
251 252
}

253 254 255 256 257 258
- (NSString *)currentIPAddress
{
    NSString *address = @"";
    struct ifaddrs *interfaces = NULL;
    struct ifaddrs *temp_addr = NULL;
    int success = getifaddrs(&interfaces);
259

260
    if (success != 0) {
261 262 263 264 265 266 267
        freeifaddrs(interfaces);
        return address;
    }

    temp_addr = interfaces;
    while (temp_addr != NULL) {
        if (temp_addr->ifa_addr->sa_family == AF_INET) {
268
            if([@(temp_addr->ifa_name) isEqualToString:_nameOfUsedNetworkInterface])
269
                address = @(inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr));
270
        }
271
        temp_addr = temp_addr->ifa_next;
272
    }
273

274 275 276 277
    freeifaddrs(interfaces);
    return address;
}

278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
- (NSString *)hostname
{
    char baseHostName[256];
    int success = gethostname(baseHostName, 255);
    if (success != 0)
        return nil;
    baseHostName[255] = '\0';

#if !TARGET_IPHONE_SIMULATOR
    return [NSString stringWithFormat:@"%s.local", baseHostName];
#else
    return [NSString stringWithFormat:@"%s", baseHostName];
#endif
}

293 294 295 296 297
- (NSString *)hostnamePort
{
    return [NSString stringWithFormat:@"%i", _httpServer.listeningPort];
}

298
- (void)moveFileOutOfCache:(NSString *)filepath
299 300
{
    NSString *fileName = [filepath lastPathComponent];
301 302
    NSString *libraryPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
    NSString *uploadPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)
303
                            firstObject] stringByAppendingPathComponent:kVLCHTTPUploadDirectory];
304 305 306 307 308 309

    NSString *finalFilePath = [libraryPath
                               stringByAppendingString:[filepath
                                                        stringByReplacingOccurrencesOfString:uploadPath
                                                        withString:@""]];

310 311
    NSFileManager *fileManager = [NSFileManager defaultManager];

312 313 314 315 316
    // Re-create the folder structure of the user
    if (![fileManager createDirectoryAtPath:[finalFilePath stringByDeletingLastPathComponent]
                withIntermediateDirectories:YES attributes:nil error:nil])
        APLog(@"Could not create directory at path: %@", finalFilePath);

317 318
    if ([fileManager fileExistsAtPath:finalFilePath]) {
        /* we don't want to over-write existing files, so add an integer to the file name */
319 320
        NSString *potentialFullPath;
        NSString *currentPath = [finalFilePath stringByDeletingLastPathComponent];
321 322 323
        NSString *fileExtension = [fileName pathExtension];
        NSString *rawFileName = [fileName stringByDeletingPathExtension];
        for (NSUInteger x = 1; x < 100; x++) {
324 325 326 327 328 329 330 331
            potentialFullPath = [currentPath stringByAppendingString:[NSString
                                                                      stringWithFormat:@"/%@-%lu.%@",
                                                                      rawFileName,
                                                                      (unsigned long)x,
                                                                      fileExtension]];

            if (![[NSFileManager defaultManager] fileExistsAtPath:potentialFullPath]) {
                finalFilePath = potentialFullPath;
332
                break;
333
            }
334 335 336 337 338 339
        }
    }

    NSError *error;
    [fileManager moveItemAtPath:filepath toPath:finalFilePath error:&error];
    if (error) {
340
        APLog(@"Moving received media %@ to library folder failed (%li), deleting", fileName, (long)error.code);
341 342 343
        [fileManager removeItemAtPath:filepath error:nil];
    }

344
    [[VLCMediaFileDiscoverer sharedInstance] performSelectorOnMainThread:@selector(updateMediaList) withObject:nil waitUntilDone:NO];
345 346 347
    // FIXME: Replace notifications by cleaner observers
    [[NSNotificationCenter defaultCenter] postNotificationName:NSNotification.VLCNewFileAddedNotification
                                                        object:self];
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
}


- (void)moveFileFrom:(NSString *)filepath
{
    // Check if downloaded file is a playlist in order to parse at the end of the download.
    if ([[filepath lastPathComponent] isSupportedPlaylistFormat]) {
        [_playlistUploadPaths addObject:filepath];
        return;
    }

    /* update media library when file upload was completed */
    VLCActivityManager *activityManager = [VLCActivityManager defaultManager];
    [activityManager networkActivityStopped];
    [activityManager activateIdleTimer];

    /* on tvOS, the media remains in the cache folder and will disappear from there
     * while on iOS we have persistent storage, so move it there */
#if TARGET_OS_IOS
    [self moveFileOutOfCache:filepath];
368
#endif
369 370 371 372 373 374 375 376
}

- (void)cleanCache
{
    if ([[VLCActivityManager defaultManager] haveNetworkActivity])
        return;

    NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
377 378
    NSString *uploadDirPath = [searchPaths.firstObject
                               stringByAppendingPathComponent:kVLCHTTPUploadDirectory];
379 380 381
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if ([fileManager fileExistsAtPath:uploadDirPath])
        [fileManager removeItemAtPath:uploadDirPath error:nil];
382 383
}

384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
- (void)resetIdleTimer
{
    const int timeInterval = 4;

    if (!_idleTimer)
        dispatch_async(dispatch_get_main_queue(), ^{
            self->_idleTimer = [NSTimer scheduledTimerWithTimeInterval:timeInterval
                                                                target:self
                                                              selector:@selector(idleTimerDone)
                                                              userInfo:nil
                                                               repeats:NO];
        });
    else {
        if (fabs([_idleTimer.fireDate timeIntervalSinceNow]) < timeInterval)
            [_idleTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
    }
}

- (void)idleTimerDone
{
    _idleTimer = nil;

    for (NSString *path in _playlistUploadPaths) {
        [self moveFileOutOfCache:path];
    }

    [_playlistUploadPaths removeAllObjects];
}

413
@end