@@ -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 @@ -421,11 +421,11 @@ OFEnumerator OF_GENERIC(OFMutableArray *) *objectEnumerator; OFMutableArray *array; for (uint_fast16_t j = 0; j < count; j++) { OFString *name = parseName(buffer, length, i, - maxAllowedPointers); + maxAllowedPointers).lowercaseString; OFDNSClass DNSClass; OFDNSRecordType recordType; uint32_t TTL; uint16_t dataLength; OFDNSResourceRecord *record; @@ -464,10 +464,24 @@ [ret makeImmutable]; return ret; } + +static bool +containsExpiredRecord(OFDNSResponseRecords responseRecords, uint32_t age) +{ + OFEnumerator *enumerator = [responseRecords objectEnumerator]; + OFArray OF_GENERIC(OFDNSResourceRecord *) *records; + + while ((records = [enumerator nextObject]) != nil) + for (OFDNSResourceRecord *record in records) + if (record.TTL < age) + return true; + + return false; +} @implementation OFDNSResolverContext - (instancetype)initWithQuery: (OFDNSQuery *)query ID: (OFNumber *)ID settings: (OFDNSResolverSettings *)settings @@ -576,10 +590,11 @@ @try { _settings = [[OFDNSResolverSettings alloc] init]; _queries = [[OFMutableDictionary alloc] init]; _TCPQueries = [[OFMutableDictionary alloc] init]; + _cache = [[OFMutableDictionary alloc] init]; [_settings reload]; } @catch (id e) { [self release]; @throw e; @@ -599,10 +614,12 @@ [_IPv6Socket cancelAsyncRequests]; [_IPv6Socket release]; #endif [_queries release]; [_TCPQueries release]; + [_cache release]; + [_lastNameServers release]; [super dealloc]; } - (OFDictionary *)staticHosts @@ -676,18 +693,18 @@ { _settings->_minNumberOfDotsInAbsoluteName = minNumberOfDotsInAbsoluteName; } -- (bool)usesTCP -{ - return _settings->_usesTCP; -} - -- (void)setUsesTCP: (bool)usesTCP -{ - _settings->_usesTCP = usesTCP; +- (bool)forcesTCP +{ + return _settings->_forcesTCP; +} + +- (void)setForcesTCP: (bool)forcesTCP +{ + _settings->_forcesTCP = forcesTCP; } - (OFTimeInterval)configReloadInterval { return _settings->_configReloadInterval; @@ -721,11 +738,11 @@ forMode: runLoopMode]; nameServer = [context->_settings->_nameServers objectAtIndex: context->_nameServersIndex]; - if (context->_settings->_usesTCP) { + if (context->_settings->_forcesTCP) { OFEnsure(context->_TCPSocket == nil); context->_TCPSocket = [[OFTCPSocket alloc] init]; [_TCPQueries setObject: context forKey: context->_TCPSocket]; @@ -786,10 +803,49 @@ runLoopMode: runLoopMode]; [sock asyncReceiveIntoBuffer: _buffer length: bufferLength runLoopMode: runLoopMode]; } + +- (void)of_cleanUpCache +{ + OFTimeInterval now = [[OFDate date] timeIntervalSince1970]; + OFMutableArray *removeList; + + if (_lastNameServers != _settings->_nameServers && + ![_lastNameServers isEqual: _settings->_nameServers]) { + OFArray *old = _lastNameServers; + _lastNameServers = [_settings->_nameServers copy]; + [old release]; + + [_cache removeAllObjects]; + + return; + } + + if (now - _lastCacheCleanup < 1) + return; + + _lastCacheCleanup = now; + removeList = [OFMutableArray arrayWithCapacity: _cache.count]; + + for (OFDNSQuery *query in _cache) { + OFPair OF_GENERIC(OFDate *, OFDNSResponse *) *entry = + [_cache objectForKey: query]; + uint32_t age = + (uint32_t)now - [entry.firstObject timeIntervalSince1970]; + OFDNSResponse *response = entry.secondObject; + + if (containsExpiredRecord(response.answerRecords, age) || + containsExpiredRecord(response.authorityRecords, age) || + containsExpiredRecord(response.additionalRecords, age)) + [removeList addObject: query]; + } + + for (OFDNSQuery *query in removeList) + [_cache removeObjectForKey: query]; +} - (void)asyncPerformQuery: (OFDNSQuery *)query delegate: (id )delegate { [self asyncPerformQuery: query @@ -802,10 +858,40 @@ delegate: (id )delegate { void *pool = objc_autoreleasePoolPush(); OFNumber *ID; OFDNSResolverContext *context; + OFPair OF_GENERIC(OFDate *, OFDNSResponse *) *cacheEntry; + + [self of_cleanUpCache]; + + if ((cacheEntry = [_cache objectForKey: query]) != nil) { + uint32_t age = + (uint32_t)-[cacheEntry.firstObject timeIntervalSinceNow]; + OFDNSResponse *response = cacheEntry.secondObject; + + if (!containsExpiredRecord(response.answerRecords, age) && + !containsExpiredRecord(response.authorityRecords, age) && + !containsExpiredRecord(response.additionalRecords, age)) { + OFTimer *timer = [OFTimer + timerWithTimeInterval: 0 + target: delegate + selector: @selector(resolver: + didPerformQuery:response: + exception:) + object: self + object: query + object: response + object: nil + repeats: false]; + [[OFRunLoop currentRunLoop] addTimer: timer + forMode: runLoopMode]; + + objc_autoreleasePoolPop(pool); + return; + } + } /* Random, unused ID */ do { ID = [OFNumber numberWithUnsignedShort: OFRandom16()]; } while ([_queries objectForKey: ID] != nil); @@ -946,14 +1032,14 @@ /* TC */ if (buffer[2] & 0x02) { OFRunLoopMode runLoopMode; - if (context->_settings->_usesTCP) + if (context->_settings->_forcesTCP) @throw [OFTruncatedDataException exception]; - context->_settings->_usesTCP = true; + context->_settings->_forcesTCP = true; runLoopMode = [OFRunLoop currentRunLoop].currentMode; [self of_sendQueryForContext: context runLoopMode: runLoopMode]; return false; } @@ -1038,10 +1124,19 @@ } if (exception != nil) response = nil; + [self of_cleanUpCache]; + + if (response != nil) + [_cache setObject: [OFPair pairWithFirstObject: [OFDate date] + secondObject: response] + forKey: context->_query]; + else + [_cache removeObjectForKey: context->_query]; + [context->_delegate resolver: self didPerformQuery: context->_query response: response exception: exception];