//
// Copyright 2017 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

#import "OWSCensorshipConfiguration.h"
#import "OWSCountryMetadata.h"
#import "OWSError.h"
#import "OWSHTTPSecurityPolicy.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>

NS_ASSUME_NONNULL_BEGIN

NSString *const OWSFrontingHost_GoogleEgypt = @"www.google.com.eg";
NSString *const OWSFrontingHost_GoogleUAE = @"www.google.ae";
NSString *const OWSFrontingHost_GoogleOman = @"www.google.com.om";
NSString *const OWSFrontingHost_GoogleQatar = @"www.google.com.qa";
NSString *const OWSFrontingHost_GoogleUzbekistan = @"www.google.co.uz";
NSString *const OWSFrontingHost_Default = @"www.google.com";

@implementation OWSCensorshipConfiguration

// returns nil if phone number is not known to be censored
+ (nullable instancetype)censorshipConfigurationWithPhoneNumber:(NSString *)e164PhoneNumber
{
    NSString *countryCode = [self censoredCountryCodeWithPhoneNumber:e164PhoneNumber];
    if (countryCode.length == 0) {
        return nil;
    }

    return [self censorshipConfigurationWithCountryCode:countryCode];
}

// returns best censorship configuration for country code. Will return a default if one hasn't
// been specifically configured.
+ (instancetype)censorshipConfigurationWithCountryCode:(NSString *)countryCode
{
    OWSCountryMetadata *countryMetadadata = [OWSCountryMetadata countryMetadataForCountryCode:countryCode];
    OWSAssertDebug(countryMetadadata);

    NSString *_Nullable specifiedDomain = countryMetadadata.frontingDomain;
    if (specifiedDomain.length == 0) {
        return self.defaultConfiguration;
    }

    NSString *frontingURLString = [NSString stringWithFormat:@"https://%@", specifiedDomain];
    NSURL *_Nullable baseURL = [NSURL URLWithString:frontingURLString];
    if (baseURL == nil) {
        OWSFailDebug(@"baseURL was unexpectedly nil with specifiedDomain: %@", specifiedDomain);
        return self.defaultConfiguration;
    }
    OWSHTTPSecurityPolicy *securityPolicy = [self securityPolicyForDomain:specifiedDomain];
    OWSAssertDebug(securityPolicy);

    return [[OWSCensorshipConfiguration alloc] initWithDomainFrontBaseURL:baseURL securityPolicy:securityPolicy];
}

+ (instancetype)defaultConfiguration
{
    NSString *frontingURLString = [NSString stringWithFormat:@"https://%@", OWSFrontingHost_Default];
    NSURL *baseURL = [NSURL URLWithString:frontingURLString];
    OWSHTTPSecurityPolicy *securityPolicy = [self securityPolicyForDomain:OWSFrontingHost_Default];

    return [[OWSCensorshipConfiguration alloc] initWithDomainFrontBaseURL:baseURL securityPolicy:securityPolicy];
}

- (instancetype)initWithDomainFrontBaseURL:(NSURL *)domainFrontBaseURL
                            securityPolicy:(OWSHTTPSecurityPolicy *)securityPolicy
{
    OWSAssertDebug(domainFrontBaseURL);
    OWSAssertDebug(securityPolicy);

    self = [super init];
    if (!self) {
        return self;
    }

    _domainFrontBaseURL = domainFrontBaseURL;
    _domainFrontSecurityPolicy = securityPolicy;

    return self;
}

// MARK: Util

+ (NSDictionary<NSString *, NSString *> *)censoredCountryCodes
{
    // The set of countries for which domain fronting should be automatically enabled.
    //
    // If you want to use a domain front other than the default, specify the domain front
    // in OWSCountryMetadata, and ensure we have a Security Policy for that domain in
    // `securityPolicyForDomain:`
    return @{
        // Egypt
        @"+20" : @"EG",
        // Oman
        @"+968" : @"OM",
        // Qatar
        @"+974" : @"QA",
        // UAE
        @"+971" : @"AE",
        // Cuba
        @"+53" : @"CU",
    };
}

+ (BOOL)isCensoredPhoneNumber:(NSString *)e164PhoneNumber
{
    return [self censoredCountryCodeWithPhoneNumber:e164PhoneNumber].length > 0;
}

// Returns nil if the phone number is not known to be censored
+ (nullable NSString *)censoredCountryCodeWithPhoneNumber:(NSString *)e164PhoneNumber
{
    NSDictionary<NSString *, NSString *> *censoredCountryCodes = self.censoredCountryCodes;

    for (NSString *callingCode in censoredCountryCodes) {
        if ([e164PhoneNumber hasPrefix:callingCode]) {
            return censoredCountryCodes[callingCode];
        }
    }

    return nil;
}

#pragma mark - Reflector Pinning Policy

// When using censorship circumvention, we pin to the fronted domain host.
// Adding a new domain front entails adding a corresponding OWSHTTPSecurityPolicy
// and pinning to its CA.
// If the security policy requires new certificates, include them in the SSK bundle
+ (OWSHTTPSecurityPolicy *)securityPolicyForDomain:(NSString *)domain
{
    if ([domain isEqualToString:OWSFrontingHost_GoogleEgypt]) {
        return self.googlePinningPolicy;
    } else if ([domain isEqualToString:OWSFrontingHost_GoogleQatar]) {
        return self.googlePinningPolicy;
    } else if ([domain isEqualToString:OWSFrontingHost_GoogleOman]) {
        return self.googlePinningPolicy;
    } else if ([domain isEqualToString:OWSFrontingHost_GoogleUAE]) {
        return self.googlePinningPolicy;
    } else if ([domain isEqualToString:OWSFrontingHost_GoogleUzbekistan]) {
        return self.googlePinningPolicy;
    } else if ([domain isEqualToString:OWSFrontingHost_Default]) {
        return self.googlePinningPolicy;
    } else {
        OWSFailDebug(@"Unknown pinning domain.");
        return self.googlePinningPolicy;
    }
}

+ (OWSHTTPSecurityPolicy *)pinningPolicyWithCertNames:(NSArray<NSString *> *)certNames
{
    NSMutableSet<NSData *> *certificates = [NSMutableSet new];
    for (NSString *certName in certNames) {
        NSError *error;
        NSData *certData = [self certificateDataWithName:certName error:&error];
        if (error) {
            OWSFail(@"reading data for certificate: %@ failed with error: %@", certName, error);
        }

        if (!certData) {
            OWSFail(@"reading data for certificate: %@ failed with error: %@", certName, error);
        }
        [certificates addObject:certData];
    }

    return [[OWSHTTPSecurityPolicy alloc] initWithPinnedCertificates:certificates];
}

+ (nullable NSData *)certificateDataWithName:(NSString *)name error:(NSError **)error
{
    if (!name.length) {
        NSString *failureDescription = [NSString stringWithFormat:@"%@ expected name with length > 0", self.logTag];
        *error = OWSErrorMakeAssertionError(failureDescription);
        return nil;
    }

    NSBundle *bundle = [NSBundle bundleForClass:self.class];
    NSString *path = [bundle pathForResource:name ofType:@"crt"];
    if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
        NSString *failureDescription =
            [NSString stringWithFormat:@"%@ Missing certificate for name: %@", self.logTag, name];
        *error = OWSErrorMakeAssertionError(failureDescription);
        return nil;
    }

    NSData *_Nullable certData = [NSData dataWithContentsOfFile:path options:0 error:error];

    if (*error != nil) {
        OWSFailDebug(@"Failed to read cert file with path: %@", path);
        return nil;
    }

    if (certData.length == 0) {
        OWSFailDebug(@"empty certData for name: %@", name);
        return nil;
    }

    return certData;
}

+ (OWSHTTPSecurityPolicy *)googlePinningPolicy
{
    static OWSHTTPSecurityPolicy *securityPolicy = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // GIAG2 cert plus root certs from pki.goog
        NSArray<NSString *> *certNames = @[ @"GIAG2", @"GSR2", @"GSR4", @"GTSR1", @"GTSR2", @"GTSR3", @"GTSR4" ];
        securityPolicy = [self pinningPolicyWithCertNames:certNames];
    });
    return securityPolicy;
}

@end

NS_ASSUME_NONNULL_END
