@@ -1,7 +1,7 @@ /* - * Copyright (c) 2008-2022 Jonathan Schleifer + * Copyright (c) 2008-2024 Jonathan Schleifer * * All rights reserved. * * This file is part of ObjFW. It may be distributed under the terms of the * Q Public License 1.0, which can be found in the file LICENSE.QPL included in @@ -16,16 +16,18 @@ #include "config.h" #import "OFApplication.h" #import "OFArray.h" #import "OFData.h" +#import "OFDate.h" #import "OFDictionary.h" #import "OFFile.h" #import "OFFileManager.h" #import "OFHTTPClient.h" #import "OFHTTPRequest.h" #import "OFHTTPResponse.h" +#import "OFIRI.h" #import "OFLocale.h" #import "OFOptionsParser.h" #ifdef OF_HAVE_PLUGINS # import "OFPlugin.h" #endif @@ -32,11 +34,10 @@ #import "OFSandbox.h" #import "OFStdIOStream.h" #import "OFSystemInfo.h" #import "OFTCPSocket.h" #import "OFTLSStream.h" -#import "OFURI.h" #ifdef HAVE_TLS_SUPPORT # import "ObjFWTLS.h" #endif @@ -48,10 +49,12 @@ #import "OFInvalidServerResponseException.h" #import "OFOpenItemFailedException.h" #import "OFOutOfRangeException.h" #import "OFReadFailedException.h" #import "OFResolveHostFailedException.h" +#import "OFSetItemAttributesFailedException.h" +#import "OFTLSHandshakeFailedException.h" #import "OFUnsupportedProtocolException.h" #import "OFWriteFailedException.h" #import "ProgressBar.h" @@ -60,12 +63,12 @@ #define KIBIBYTE (1024) @interface OFHTTP: OFObject { - OFArray OF_GENERIC(OFString *) *_URIs; - size_t _URIIndex; + OFArray OF_GENERIC(OFString *) *_IRIs; + size_t _IRIIndex; int _errorCode; OFString *_outputPath, *_currentFileName; bool _continue, _force, _detectFileName, _detectFileNameRequest; bool _detectedFileName, _quiet, _verbose, _insecure, _ignoreStatus; bool _useUnicode; @@ -77,11 +80,11 @@ OFStream *_output; unsigned long long _received, _length, _resumedFrom; ProgressBar *_progressBar; } -- (void)downloadNextURI; +- (void)downloadNextIRI; @end #ifdef HAVE_TLS_SUPPORT void _reference_to_ObjFWTLS(void) @@ -93,38 +96,37 @@ OF_APPLICATION_DELEGATE(OFHTTP) static void help(OFStream *stream, bool full, int status) { - [OFStdErr writeLine: - OF_LOCALIZED(@"usage", - @"Usage: %[prog] -[cehHmoOPqv] uri1 [uri2 ...]", + [OFStdErr writeLine: OF_LOCALIZED(@"usage", + @"Usage: %[prog] -[cehHmoOPqv] iri1 [iri2 ...]", @"prog", [OFApplication programName])]; if (full) { [stream writeString: @"\n"]; [stream writeLine: OF_LOCALIZED(@"full_usage", @"Options:\n " - @"-b --body " + @"-b --body= " @" Specify the file to send as body\n " @" " @" (- for standard input)\n " @"-c --continue " @" Continue download of existing file\n " @"-f --force " @" Force / overwrite existing file\n " @"-h --help " @" Show this help\n " - @"-H --header " + @"-H --header= " @" Add a header (e.g. X-Foo:Bar)\n " - @"-m --method " + @"-m --method= " @" Set the method of the HTTP request\n " - @"-o --output " + @"-o --output= " @" Specify output file name\n " @"-O --detect-filename" @" Do a HEAD request to detect the file name\n " - @"-P --proxy " + @"-P --proxy= " @" Specify SOCKS5 proxy\n " @"-q --quiet " @" Quiet mode (no output, except errors)\n " @"-v --verbose " @" Verbose mode (print headers)\n " @@ -369,11 +371,11 @@ void *pool = objc_autoreleasePoolPush(); method = method.uppercaseString; @try { - _method = OFHTTPRequestMethodParseName(method); + _method = OFHTTPRequestMethodParseString(method); } @catch (OFInvalidArgumentException *e) { [OFStdErr writeLine: OF_LOCALIZED(@"invalid_input_method", @"%[prog]: Invalid request method %[method]!", @"prog", [OFApplication programName], @"method", method)]; @@ -410,11 +412,11 @@ @"prog", [OFApplication programName])]; [OFApplication terminateWithStatus: 1]; } } -- (void)applicationDidFinishLaunching +- (void)applicationDidFinishLaunching: (OFNotification *)notification { OFString *outputPath; const OFOptionsParserOption options[] = { { 'b', @"body", 1, NULL, NULL }, { 'c', @"continue", 0, &_continue, NULL }, @@ -449,14 +451,15 @@ [OFApplication of_activateSandbox: sandbox]; #endif #ifndef OF_AMIGAOS - [OFLocale addLocalizationDirectory: @LOCALIZATION_DIR]; + [OFLocale addLocalizationDirectoryIRI: + [OFIRI fileIRIWithPath: @LOCALIZATION_DIR]]; #else - [OFLocale addLocalizationDirectory: - @"PROGDIR:/share/ofhttp/localization"]; + [OFLocale addLocalizationDirectoryIRI: + [OFIRI fileIRIWithPath: @"PROGDIR:/share/ofhttp/localization"]]; #endif optionsParser = [OFOptionsParser parserWithOptions: options]; while ((option = [optionsParser nextOption]) != '\0') { switch (option) { @@ -475,52 +478,52 @@ case 'P': [self setProxy: optionsParser.argument]; break; case ':': if (optionsParser.lastLongOption != nil) - [OFStdErr writeLine: - OF_LOCALIZED(@"long_argument_missing", + [OFStdErr writeLine: OF_LOCALIZED( + @"long_argument_missing", @"%[prog]: Argument for option --%[opt] " - @"missing" + @"missing", @"prog", [OFApplication programName], @"opt", optionsParser.lastLongOption)]; else { OFString *optStr = [OFString - stringWithFormat: @"%c", + stringWithFormat: @"%C", optionsParser.lastOption]; - [OFStdErr writeLine: - OF_LOCALIZED(@"argument_missing", + [OFStdErr writeLine: OF_LOCALIZED( + @"argument_missing", @"%[prog]: Argument for option -%[opt] " @"missing", @"prog", [OFApplication programName], @"opt", optStr)]; } [OFApplication terminateWithStatus: 1]; break; case '=': - [OFStdErr writeLine: - OF_LOCALIZED(@"option_takes_no_argument", + [OFStdErr writeLine: OF_LOCALIZED( + @"option_takes_no_argument", @"%[prog]: Option --%[opt] takes no argument", @"prog", [OFApplication programName], @"opt", optionsParser.lastLongOption)]; [OFApplication terminateWithStatus: 1]; break; case '?': if (optionsParser.lastLongOption != nil) - [OFStdErr writeLine: - OF_LOCALIZED(@"unknown_long_option", + [OFStdErr writeLine: OF_LOCALIZED( + @"unknown_long_option", @"%[prog]: Unknown option: --%[opt]", @"prog", [OFApplication programName], @"opt", optionsParser.lastLongOption)]; else { OFString *optStr = [OFString - stringWithFormat: @"%c", + stringWithFormat: @"%C", optionsParser.lastOption]; - [OFStdErr writeLine: - OF_LOCALIZED(@"unknown_option", + [OFStdErr writeLine: OF_LOCALIZED( + @"unknown_option", @"%[prog]: Unknown option: -%[opt]", @"prog", [OFApplication programName], @"opt", optStr)]; } @@ -544,13 +547,13 @@ sandbox.allowsUnveil = false; [OFApplication of_activateSandbox: sandbox]; #endif _outputPath = [outputPath copy]; - _URIs = [optionsParser.remainingArguments copy]; + _IRIs = [optionsParser.remainingArguments copy]; - if (_URIs.count < 1) + if (_IRIs.count < 1) help(OFStdErr, false, 1); if (_quiet && _verbose) { [OFStdErr writeLine: OF_LOCALIZED(@"quiet_xor_verbose", @"%[prog]: -q / --quiet and -v / --verbose are mutually " @@ -566,14 +569,14 @@ @"mutually exclusive!", @"prog", [OFApplication programName])]; [OFApplication terminateWithStatus: 1]; } - if (_outputPath != nil && _URIs.count > 1) { - [OFStdErr writeLine: - OF_LOCALIZED(@"output_only_with_one_uri", - @"%[prog]: Cannot use -o / --output when more than one URI " + if (_outputPath != nil && _IRIs.count > 1) { + [OFStdErr writeLine: OF_LOCALIZED( + @"output_only_with_one_iri", + @"%[prog]: Cannot use -o / --output when more than one IRI " @"has been specified!", @"prog", [OFApplication programName])]; [OFApplication terminateWithStatus: 1]; } @@ -584,11 +587,11 @@ _useUnicode = [OFSystemInfo isWindowsNT]; #else _useUnicode = ([OFLocale encoding] == OFStringEncodingUTF8); #endif - [self performSelector: @selector(downloadNextURI) afterDelay: 0]; + [self performSelector: @selector(downloadNextIRI) afterDelay: 0]; } - (void)client: (OFHTTPClient *)client didCreateTLSStream: (OFTLSStream *)stream request: (OFHTTPRequest *)request @@ -608,11 +611,11 @@ [body writeBuffer: buffer length: length]; } } - (bool)client: (OFHTTPClient *)client - shouldFollowRedirectToURI: (OFURI *)URI + shouldFollowRedirectToIRI: (OFIRI *)IRI statusCode: (short)statusCode request: (OFHTTPRequest *)request response: (OFHTTPResponse *)response { if (_verbose) { @@ -630,13 +633,13 @@ objc_autoreleasePoolPop(pool); } if (!_quiet) { if (_useUnicode) - [OFStdOut writeFormat: @"☇ %@", URI.string]; + [OFStdOut writeFormat: @"☇ %@", IRI.string]; else - [OFStdOut writeFormat: @"< %@", URI.string]; + [OFStdOut writeFormat: @"< %@", IRI.string]; } _length = 0; return true; @@ -646,11 +649,11 @@ didReadIntoBuffer: (void *)buffer length: (size_t)length exception: (id)exception { if (exception != nil) { - OFString *URI; + OFString *IRI; [_progressBar stop]; [_progressBar draw]; [_progressBar release]; _progressBar = nil; @@ -659,21 +662,21 @@ [OFStdOut writeString: @"\n "]; [OFStdOut writeLine: OF_LOCALIZED(@"download_error", @"Error!")]; } - URI = [_URIs objectAtIndex: _URIIndex - 1]; + IRI = [_IRIs objectAtIndex: _IRIIndex - 1]; [OFStdErr writeLine: OF_LOCALIZED( @"download_failed_exception", - @"%[prog]: Failed to download <%[uri]>!\n" + @"%[prog]: Failed to download <%[iri]>!\n" @" %[exception]", @"prog", [OFApplication programName], - @"uri", URI, + @"iri", IRI, @"exception", exception)]; _errorCode = 1; - [self performSelector: @selector(downloadNextURI) + [self performSelector: @selector(downloadNextIRI) afterDelay: 0]; return false; } [_output writeBuffer: buffer length: length]; @@ -691,11 +694,11 @@ [OFStdOut writeString: @"\n "]; [OFStdOut writeLine: OF_LOCALIZED(@"download_done", @"Done!")]; } - [self performSelector: @selector(downloadNextURI) + [self performSelector: @selector(downloadNextIRI) afterDelay: 0]; return false; } return true; @@ -813,40 +816,40 @@ if ([exception isKindOfClass: [OFResolveHostFailedException class]]) { if (!_quiet) [OFStdOut writeString: @"\n"]; - [OFStdErr writeLine: - OF_LOCALIZED(@"download_resolve_host_failed", - @"%[prog]: Failed to download <%[uri]>!\n" + [OFStdErr writeLine: OF_LOCALIZED( + @"download_resolve_host_failed", + @"%[prog]: Failed to download <%[iri]>!\n" @" Failed to resolve host: %[exception]", @"prog", [OFApplication programName], - @"uri", request.URI.string, + @"iri", request.IRI.string, @"exception", exception)]; } else if ([exception isKindOfClass: [OFConnectSocketFailedException class]]) { if (!_quiet) [OFStdOut writeString: @"\n"]; - [OFStdErr writeLine: - OF_LOCALIZED(@"download_failed_connection_failed", - @"%[prog]: Failed to download <%[uri]>!\n" + [OFStdErr writeLine: OF_LOCALIZED( + @"download_failed_connection_failed", + @"%[prog]: Failed to download <%[iri]>!\n" @" Connection failed: %[exception]", @"prog", [OFApplication programName], - @"uri", request.URI.string, + @"iri", request.IRI.string, @"exception", exception)]; } else if ([exception isKindOfClass: [OFInvalidServerResponseException class]]) { if (!_quiet) [OFStdOut writeString: @"\n"]; [OFStdErr writeLine: OF_LOCALIZED( @"download_failed_invalid_server_response", - @"%[prog]: Failed to download <%[uri]>!\n" + @"%[prog]: Failed to download <%[iri]>!\n" @" Invalid server response!", @"prog", [OFApplication programName], - @"uri", request.URI.string)]; + @"iri", request.IRI.string)]; } else if ([exception isKindOfClass: [OFUnsupportedProtocolException class]]) { if (!_quiet) [OFStdOut writeString: @"\n"]; @@ -855,10 +858,26 @@ @" In order to download via HTTPS, you need to " @"either build ObjFW with TLS\n" @" support or preload a library adding TLS " @"support to ObjFW!", @"prog", [OFApplication programName])]; + } else if ([exception isKindOfClass: + [OFTLSHandshakeFailedException class]]) { + OFString *error = OFTLSStreamErrorCodeDescription( + ((OFTLSHandshakeFailedException *)exception) + .errorCode); + + if (!_quiet) + [OFStdOut writeString: @"\n"]; + + [OFStdErr writeLine: OF_LOCALIZED( + @"download_failed_tls_handshake_failed", + @"%[prog]: Failed to download <%[iri]>!\n" + @" TLS handshake failed: %[error]", + @"prog", [OFApplication programName], + @"iri", request.IRI.string, + @"error", error)]; } else if ([exception isKindOfClass: [OFReadOrWriteFailedException class]]) { OFString *error = OF_LOCALIZED( @"download_failed_read_or_write_failed_any", @"Read or write failed"); @@ -879,14 +898,14 @@ @"write", @"Write failed"); [OFStdErr writeLine: OF_LOCALIZED( @"download_failed_read_or_write_failed", - @"%[prog]: Failed to download <%[uri]>!\n" + @"%[prog]: Failed to download <%[iri]>!\n" @" %[error]: %[exception]", @"prog", [OFApplication programName], - @"uri", request.URI.string, + @"iri", request.IRI.string, @"error", error, @"exception", exception)]; } else if ([exception isKindOfClass: [OFHTTPRequestFailedException class]]) { short statusCode; @@ -899,20 +918,20 @@ statusCode = response.statusCode; codeString = [OFString stringWithFormat: @"%hd %@", statusCode, OFHTTPStatusCodeString(statusCode)]; [OFStdErr writeLine: OF_LOCALIZED(@"download_failed", - @"%[prog]: Failed to download <%[uri]>!\n" + @"%[prog]: Failed to download <%[iri]>!\n" @" HTTP status code: %[code]", @"prog", [OFApplication programName], - @"uri", request.URI.string, + @"iri", request.IRI.string, @"code", codeString)]; } else @throw exception; _errorCode = 1; - [self performSelector: @selector(downloadNextURI) + [self performSelector: @selector(downloadNextIRI) afterDelay: 0]; return; } after_exception_handling: @@ -923,14 +942,14 @@ _currentFileName = [fileNameFromContentDisposition( [response.headers objectForKey: @"Content-Disposition"]) copy]; _detectedFileName = true; - /* Handle this URI on the next -[downloadNextURI] call */ - _URIIndex--; + /* Handle this IRI on the next -[downloadNextIRI] call */ + _IRIIndex--; - [self performSelector: @selector(downloadNextURI) + [self performSelector: @selector(downloadNextIRI) afterDelay: 0]; return; } if ([_outputPath isEqual: @"-"]) @@ -963,10 +982,43 @@ @"exception", e)]; _errorCode = 1; goto next; } + +#ifdef OF_LINUX + @try { + OFString *IRIString = request.IRI.string; + OFData *downloadedFromData = [OFData + dataWithItems: IRIString.UTF8String + count: IRIString.UTF8StringLength + 1]; + [[OFFileManager defaultManager] + setExtendedAttributeData: downloadedFromData + forName: @"user.ofhttp." + @"downloaded_from" + ofItemAtPath: _currentFileName]; + } @catch (OFSetItemAttributesFailedException *) { + /* Ignore */ + } +#endif + +#ifdef OF_MACOS + @try { + OFString *quarantine = [OFString stringWithFormat: + @"0000;%08" @PRIx64 @";ofhttp;", + (uint64_t)[[OFDate date] timeIntervalSince1970]]; + OFData *quarantineData = [OFData + dataWithItems: quarantine.UTF8String + count: quarantine.UTF8StringLength]; + [[OFFileManager defaultManager] + setExtendedAttributeData: quarantineData + forName: @"com.apple.quarantine" + ofItemAtPath: _currentFileName]; + } @catch (OFSetItemAttributesFailedException *e) { + /* Ignore */ + } +#endif } if (!_quiet) { _progressBar = [[ProgressBar alloc] initWithLength: _length @@ -985,47 +1037,47 @@ next: [_currentFileName release]; _currentFileName = nil; - [self performSelector: @selector(downloadNextURI) afterDelay: 0]; + [self performSelector: @selector(downloadNextIRI) afterDelay: 0]; } -- (void)downloadNextURI +- (void)downloadNextIRI { - OFString *URIString = nil; - OFURI *URI; + OFString *IRIString = nil; + OFIRI *IRI; OFMutableDictionary *clientHeaders; OFHTTPRequest *request; _received = _length = _resumedFrom = 0; if (_output != OFStdOut) [_output release]; _output = nil; - if (_URIIndex >= _URIs.count) + if (_IRIIndex >= _IRIs.count) [OFApplication terminateWithStatus: _errorCode]; @try { - URIString = [_URIs objectAtIndex: _URIIndex++]; - URI = [OFURI URIWithString: URIString]; + IRIString = [_IRIs objectAtIndex: _IRIIndex++]; + IRI = [OFIRI IRIWithString: IRIString]; } @catch (OFInvalidFormatException *e) { - [OFStdErr writeLine: OF_LOCALIZED(@"invalid_uri", - @"%[prog]: Invalid URI: <%[uri]>!", + [OFStdErr writeLine: OF_LOCALIZED(@"invalid_iri", + @"%[prog]: Invalid IRI: <%[iri]>!", @"prog", [OFApplication programName], - @"uri", URIString)]; + @"iri", IRIString)]; _errorCode = 1; goto next; } - if (![URI.scheme isEqual: @"http"] && ![URI.scheme isEqual: @"https"]) { + if (![IRI.scheme isEqual: @"http"] && ![IRI.scheme isEqual: @"https"]) { [OFStdErr writeLine: OF_LOCALIZED(@"invalid_scheme", - @"%[prog]: Invalid scheme: <%[uri]>!", + @"%[prog]: Invalid scheme: <%[iri]>!", @"prog", [OFApplication programName], - @"uri", URIString)]; + @"iri", IRIString)]; _errorCode = 1; goto next; } @@ -1032,16 +1084,16 @@ clientHeaders = [[_clientHeaders mutableCopy] autorelease]; if (_detectFileName && !_detectedFileName) { if (!_quiet) { if (_useUnicode) - [OFStdOut writeFormat: @"⠒ %@", URI.string]; + [OFStdOut writeFormat: @"⠒ %@", IRI.string]; else - [OFStdOut writeFormat: @"? %@", URI.string]; + [OFStdOut writeFormat: @"? %@", IRI.string]; } - request = [OFHTTPRequest requestWithURI: URI]; + request = [OFHTTPRequest requestWithIRI: IRI]; request.headers = clientHeaders; request.method = OFHTTPRequestMethodHead; _detectFileNameRequest = true; [_HTTPClient asyncPerformRequest: request]; @@ -1056,13 +1108,13 @@ if (_currentFileName == nil) _currentFileName = [_outputPath copy]; if (_currentFileName == nil) - _currentFileName = [URI.path.lastPathComponent copy]; + _currentFileName = [IRI.path.lastPathComponent copy]; - if ([_currentFileName isEqual: @"/"]) { + if ([_currentFileName isEqual: @"/"] || _currentFileName.length == 0) { [_currentFileName release]; _currentFileName = nil; } if (_currentFileName == nil) @@ -1078,31 +1130,31 @@ if (size > ULLONG_MAX) @throw [OFOutOfRangeException exception]; _resumedFrom = (unsigned long long)size; - range = [OFString stringWithFormat: @"bytes=%jd-", + range = [OFString stringWithFormat: @"bytes=%ju-", _resumedFrom]; [clientHeaders setObject: range forKey: @"Range"]; } @catch (OFGetItemAttributesFailedException *e) { } } if (!_quiet) { if (_useUnicode) - [OFStdOut writeFormat: @"⇣ %@", URI.string]; + [OFStdOut writeFormat: @"⇣ %@", IRI.string]; else - [OFStdOut writeFormat: @"< %@", URI.string]; + [OFStdOut writeFormat: @"< %@", IRI.string]; } - request = [OFHTTPRequest requestWithURI: URI]; + request = [OFHTTPRequest requestWithIRI: IRI]; request.headers = clientHeaders; request.method = _method; _detectFileNameRequest = false; [_HTTPClient asyncPerformRequest: request]; return; next: - [self performSelector: @selector(downloadNextURI) afterDelay: 0]; + [self performSelector: @selector(downloadNextIRI) afterDelay: 0]; } @end