/*
* Copyright (c) 2008-2024 Jonathan Schleifer <js@nil.im>
*
* All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version 3.0 only,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* version 3.0 for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* version 3.0 along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include "unistd_wrapper.h"
#import "OFDNSResolverSettings.h"
#import "OFArray.h"
#import "OFCharacterSet.h"
#import "OFDate.h"
#import "OFDictionary.h"
#ifdef OF_HAVE_FILES
# import "OFFile.h"
# import "OFFileManager.h"
#endif
#import "OFLocale.h"
#import "OFSocket+Private.h"
#import "OFString.h"
#ifdef OF_WINDOWS
# import "OFWindowsRegistryKey.h"
#endif
#import "OFInvalidFormatException.h"
#import "OFOpenItemFailedException.h"
#ifdef OF_WINDOWS
# import "OFOpenWindowsRegistryKeyFailedException.h"
#endif
#import "OFOutOfMemoryException.h"
#import "OFOutOfRangeException.h"
#import "OFUndefinedKeyException.h"
#ifdef OF_WINDOWS
# define interface struct
# include <iphlpapi.h>
# undef interface
#endif
#ifdef OF_NINTENDO_3DS
/* Newer versions of libctru started using id as a parameter name. */
# define id id_3ds
# include <3ds.h>
# undef id
#endif
#if defined(OF_AMIGAOS_M68K) || defined(OF_AMIGAOS4)
# define Class IntuitionClass
# include <proto/dos.h>
# undef Class
#endif
#ifdef OF_MORPHOS
# include <proto/rexxsyslib.h>
# include <rexx/errors.h>
# include <rexx/storage.h>
#endif
#if defined(OF_HAIKU)
# define HOSTS_PATH @"/system/settings/network/hosts"
# define RESOLV_CONF_PATH @"/system/settings/network/resolv.conf"
#else
# define HOSTS_PATH @"/etc/hosts"
# define RESOLV_CONF_PATH @"/etc/resolv.conf"
#endif
#ifndef HOST_NAME_MAX
# define HOST_NAME_MAX 255
#endif
#ifndef OF_WII
static OFString *
domainFromHostname(OFString *hostname)
{
OFString *ret;
if (hostname == nil)
return nil;
@try {
OFSocketAddressParseIP(hostname, 0);
/*
* If we are still here, the host name is a valid IP address.
* We can't use that as local domain.
*/
ret = nil;
} @catch (OFInvalidFormatException *e) {
/* Not an IP address -> we can use it if it contains a dot. */
size_t pos = [hostname rangeOfString: @"."].location;
if (pos != OFNotFound)
ret = [hostname substringFromIndex: pos + 1];
else
ret = nil;
}
return ret;
}
#endif
#if !defined(OF_WII) && !defined(OF_MORPHOS)
static OFString *
obtainHostname(void)
{
char hostname[HOST_NAME_MAX + 1];
if (gethostname(hostname, HOST_NAME_MAX + 1) != 0)
return nil;
return [OFString stringWithCString: hostname
encoding: [OFLocale encoding]];
}
#endif
#ifdef OF_AMIGAOS_M68K
static bool
assignExists(const char *assign)
{
struct DosList *list = LockDosList(LDF_ASSIGNS | LDF_READ);
bool found = (FindDosEntry(list, assign, LDF_ASSIGNS) != NULL);
UnLockDosList(LDF_ASSIGNS | LDF_READ);
return found;
}
#endif
#ifdef OF_MORPHOS
static OFString *
arexxCommand(const char *port, const char *command)
{
struct Library *RexxSysBase;
struct MsgPort *replyPort = NULL;
struct RexxMsg *msg = NULL;
if ((RexxSysBase = OpenLibrary("rexxsyslib.library", 36)) == NULL)
return nil;
@try {
struct MsgPort *rexxPort;
if ((replyPort = CreateMsgPort()) == NULL)
return nil;
if ((msg = CreateRexxMsg(replyPort, NULL, port)) == NULL)
return nil;
msg->rm_Action = RXCOMM | RXFF_RESULT;
if ((msg->rm_Args[0] = (char *)CreateArgstring(
command, strlen(command))) == NULL)
return nil;
Forbid();
if ((rexxPort = FindPort(port)) == NULL) {
Permit();
return nil;
}
PutMsg(rexxPort, &msg->rm_Node);
Permit();
WaitPort(replyPort);
GetMsg(replyPort);
if (msg->rm_Result1 != RC_OK || msg->rm_Result2 == 0)
return nil;
return [OFString stringWithCString: (char *)msg->rm_Result2
encoding: [OFLocale encoding]];
} @finally {
if (msg != NULL) {
if (msg->rm_Args[0] != NULL)
DeleteArgstring(msg->rm_Args[0]);
if (msg->rm_Result2 != 0)
DeleteArgstring((char *)msg->rm_Result2);
DeleteRexxMsg(msg);
}
if (replyPort != NULL)
DeleteMsgPort(replyPort);
CloseLibrary(RexxSysBase);
}
}
static OFArray OF_GENERIC(OFString *) *
parseNetStackArray(OFString *string)
{
if (![string hasPrefix: @"["] || ![string hasSuffix: @"]"])
return nil;
string = [string substringWithRange: OFMakeRange(1, string.length - 2)];
return [string componentsSeparatedByString: @"|"];
}
#endif
@implementation OFDNSResolverSettings
- (void)dealloc
{
[_staticHosts release];
[_nameServers release];
[_localDomain release];
[_searchDomains release];
[_lastConfigReload release];
[super dealloc];
}
- (id)copy
{
OFDNSResolverSettings *copy = [[OFDNSResolverSettings alloc] init];
@try {
copy->_staticHosts = [_staticHosts copy];
copy->_nameServers = [_nameServers copy];
copy->_localDomain = [_localDomain copy];
copy->_searchDomains = [_searchDomains copy];
copy->_timeout = _timeout;
copy->_maxAttempts = _maxAttempts;
copy->_minNumberOfDotsInAbsoluteName =
_minNumberOfDotsInAbsoluteName;
copy->_forcesTCP = _forcesTCP;
copy->_configReloadInterval = _configReloadInterval;
copy->_lastConfigReload = [_lastConfigReload copy];
} @catch (id e) {
[copy release];
@throw e;
}
return copy;
}
- (void)setDefaults
{
[_staticHosts release];
_staticHosts = nil;
[_nameServers release];
_nameServers = nil;
[_localDomain release];
_localDomain = nil;
[_searchDomains release];
_searchDomains = nil;
_timeout = 2;
_maxAttempts = 3;
_minNumberOfDotsInAbsoluteName = 1;
_forcesTCP = false;
#ifndef OF_NINTENDO_3DS
_configReloadInterval = 2;
#else
_configReloadInterval = 0;
#endif
}
#if defined(OF_HAVE_FILES) && !defined(OF_MORPHOS) && !defined(OF_NINTENDO_3DS)
- (void)parseHosts: (OFString *)path
{
void *pool = objc_autoreleasePoolPush();
OFCharacterSet *whitespaceCharacterSet =
[OFCharacterSet whitespaceCharacterSet];
OFCharacterSet *commentCharacters =
[OFCharacterSet characterSetWithCharactersInString: @"#;"];
OFMutableDictionary *staticHosts;
OFFile *file;
OFString *line;
@try {
file = [OFFile fileWithPath: path mode: @"r"];
} @catch (OFOpenItemFailedException *e) {
objc_autoreleasePoolPop(pool);
return;
}
staticHosts = [OFMutableDictionary dictionary];
while ((line =
[file readLineWithEncoding: [OFLocale encoding]]) != nil) {
OFArray *components, *hosts;
size_t pos;
OFString *address;
pos = [line indexOfCharacterFromSet: commentCharacters];
if (pos != OFNotFound)
line = [line substringToIndex: pos];
components = [line
componentsSeparatedByCharactersInSet: whitespaceCharacterSet
options: OFStringSkipEmptyComponents];
if (components.count < 2)
continue;
address = components.firstObject;
hosts = [components objectsInRange:
OFMakeRange(1, components.count - 1)];
for (OFString *host in hosts) {
OFMutableArray *addresses;
host = host.lowercaseString;
addresses = [staticHosts objectForKey: host];
if (addresses == nil) {
addresses = [OFMutableArray array];
[staticHosts setObject: addresses forKey: host];
}
[addresses addObject: address];
}
}
for (OFMutableArray *addresses in [staticHosts objectEnumerator])
[addresses makeImmutable];
[staticHosts makeImmutable];
_staticHosts = [staticHosts copy];
objc_autoreleasePoolPop(pool);
}
# ifndef OF_WINDOWS
- (void)parseResolvConfOption: (OFString *)option
{
@try {
if ([option hasPrefix: @"ndots:"]) {
unsigned long long number;
option = [option substringFromIndex: 6];
number = option.unsignedLongLongValue;
if (number > UINT_MAX)
@throw [OFOutOfRangeException exception];
_minNumberOfDotsInAbsoluteName = (unsigned int)number;
} else if ([option hasPrefix: @"timeout:"]) {
option = [option substringFromIndex: 8];
_timeout = option.unsignedLongLongValue;
} else if ([option hasPrefix: @"attempts:"]) {
unsigned long long number;
option = [option substringFromIndex: 9];
number = option.unsignedLongLongValue;
if (number > UINT_MAX)
@throw [OFOutOfRangeException exception];
_maxAttempts = (unsigned int)number;
} else if ([option hasPrefix: @"reload-period:"]) {
option = [option substringFromIndex: 14];
_configReloadInterval = option.unsignedLongLongValue;
} else if ([option isEqual: @"tcp"])
_forcesTCP = true;
} @catch (OFInvalidFormatException *e) {
}
}
- (void)parseResolvConf: (OFString *)path
{
void *pool = objc_autoreleasePoolPush();
OFCharacterSet *whitespaceCharacterSet =
[OFCharacterSet whitespaceCharacterSet];
OFCharacterSet *commentCharacters = [OFCharacterSet
characterSetWithCharactersInString: @"#;"];
OFMutableArray *nameServers = [[_nameServers mutableCopy] autorelease];
OFFile *file;
OFString *line;
@try {
file = [OFFile fileWithPath: path mode: @"r"];
} @catch (OFOpenItemFailedException *e) {
objc_autoreleasePoolPop(pool);
return;
}
if (nameServers == nil)
nameServers = [OFMutableArray array];
while ((line =
[file readLineWithEncoding: [OFLocale encoding]]) != nil) {
void *pool2 = objc_autoreleasePoolPush();
size_t pos;
OFArray *components, *arguments;
OFString *option;
pos = [line indexOfCharacterFromSet: commentCharacters];
if (pos != OFNotFound)
line = [line substringToIndex: pos];
components = [line
componentsSeparatedByCharactersInSet: whitespaceCharacterSet
options: OFStringSkipEmptyComponents];
if (components.count < 2) {
objc_autoreleasePoolPop(pool2);
continue;
}
option = [components.firstObject lowercaseString];
arguments = [components objectsInRange:
OFMakeRange(1, components.count - 1)];
if ([option isEqual: @"nameserver"]) {
if (arguments.count != 1) {
objc_autoreleasePoolPop(pool2);
continue;
}
[nameServers addObject: arguments.firstObject];
} else if ([option isEqual: @"domain"]) {
if (arguments.count != 1) {
objc_autoreleasePoolPop(pool2);
continue;
}
[_localDomain release];
_localDomain = [arguments.firstObject copy];
} else if ([option isEqual: @"search"]) {
[_searchDomains release];
_searchDomains = [arguments copy];
} else if ([option isEqual: @"options"])
for (OFString *argument in arguments)
[self parseResolvConfOption: argument];
objc_autoreleasePoolPop(pool2);
}
[nameServers makeImmutable];
[_nameServers release];
_nameServers = [nameServers copy];
objc_autoreleasePoolPop(pool);
}
# endif
#endif
#ifdef OF_WINDOWS
- (void)obtainWindowsSystemConfig
{
OFStringEncoding encoding = [OFLocale encoding];
OFMutableArray *nameServers;
/*
* We need more space than FIXED_INFO in case we have more than one
* name server, but we also want it to be properly aligned, meaning we
* can't just get a buffer of bytes. Thus, we just get space for 8.
*/
FIXED_INFO fixedInfo[8];
ULONG length = sizeof(fixedInfo);
PIP_ADDR_STRING iter;
if (GetNetworkParams(fixedInfo, &length) != ERROR_SUCCESS)
return;
nameServers = [OFMutableArray array];
for (iter = &fixedInfo->DnsServerList; iter != NULL;
iter = iter->Next) {
OFString *nameServer =
[OFString stringWithCString: iter->IpAddress.String
encoding: encoding];
if (nameServer.length > 0)
[nameServers addObject: nameServer];
}
if (nameServers.count > 0) {
[nameServers makeImmutable];
_nameServers = [nameServers copy];
}
if (fixedInfo->DomainName[0] != '\0')
_localDomain = [[OFString alloc]
initWithCString: fixedInfo->DomainName
encoding: encoding];
}
#endif
#ifdef OF_MORPHOS
- (void)obtainMorphOSSystemConfig
{
void *pool = objc_autoreleasePoolPush();
OFMutableDictionary *staticHosts;
_nameServers = [parseNetStackArray(arexxCommand("NETSTACK",
"QUERY NAMESERVERS")) copy];
_localDomain = [domainFromHostname(arexxCommand("NETSTACK",
"QUERY HOSTNAME")) copy];
_searchDomains = [parseNetStackArray(arexxCommand("NETSTACK",
"QUERY DOMAINS")) copy];
staticHosts = [OFMutableDictionary dictionary];
for (OFString *entry in parseNetStackArray(arexxCommand("NETSTACK",
"QUERY HOSTS"))) {
OFArray *components = [entry componentsSeparatedByString: @" "];
OFString *address;
OFArray *hosts;
if (components.count < 2)
continue;
address = components.firstObject;
hosts = [components objectsInRange:
OFMakeRange(1, components.count - 1)];
for (OFString *host in hosts) {
OFMutableArray *addresses;
host = host.lowercaseString;
addresses = [staticHosts objectForKey: host];
if (addresses == nil) {
addresses = [OFMutableArray array];
[staticHosts setObject: addresses forKey: host];
}
[addresses addObject: address];
}
}
for (OFMutableArray *addresses in [staticHosts objectEnumerator])
[addresses makeImmutable];
[staticHosts makeImmutable];
_staticHosts = [staticHosts copy];
objc_autoreleasePoolPop(pool);
}
#endif
#if defined(OF_AMIGAOS_M68K) || defined(OF_AMIGAOS4)
- (bool)obtainRoadshowSystemConfig
{
OFMutableArray *nameServers;
OFStringEncoding encoding;
struct List *nameServerList;
char buffer[MAXHOSTNAMELEN];
LONG hasDNSAPI;
if (SocketBaseTags(SBTM_GETREF(SBTC_HAVE_DNS_API), (ULONG)&hasDNSAPI,
TAG_END) != 0 || !hasDNSAPI)
return false;
nameServers = [OFMutableArray array];
encoding = [OFLocale encoding];
nameServerList = ObtainDomainNameServerList();
if (nameServerList == NULL)
@throw [OFOutOfMemoryException exception];
@try {
struct DomainNameServerNode *iter =
(struct DomainNameServerNode *)&nameServerList->lh_Head;
while (iter->dnsn_MinNode.mln_Succ != NULL) {
if (iter->dnsn_UseCount != 0 &&
iter->dnsn_Address != NULL) {
OFString *address = [OFString
stringWithCString: iter->dnsn_Address
encoding: encoding];
[nameServers addObject: address];
}
iter = (struct DomainNameServerNode *)
iter->dnsn_MinNode.mln_Succ;
}
} @finally {
ReleaseDomainNameServerList(nameServerList);
}
if (nameServers.count > 0) {
[nameServers makeImmutable];
_nameServers = [nameServers copy];
}
if (GetDefaultDomainName(buffer, sizeof(buffer)))
_localDomain = [[OFString alloc] initWithCString: buffer
encoding: encoding];
return true;
}
#endif
#ifdef OF_NINTENDO_3DS
- (void)obtainNintendo3DSSytemConfig
{
OFMutableArray *nameServers = [OFMutableArray array];
union {
/*
* For some unknown reason, this needs a 336 bytes buffer and
* always returns 336 bytes.
*/
char bytes[336];
SOCU_DNSTableEntry entries[2];
} buffer;
socklen_t optLen = sizeof(buffer);
if (SOCU_GetNetworkOpt(SOL_CONFIG, NETOPT_DNS_TABLE,
&buffer, &optLen) != 0)
return;
/*
* We're fine if this gets smaller in a future release (unlikely), as
* long as two entries still fit.
*/
if (optLen < sizeof(buffer.entries))
return;
for (uint_fast8_t i = 0; i < 2; i++) {
uint32_t ip = OFFromBigEndian32(buffer.entries[i].ip.s_addr);
if (ip == 0)
continue;
[nameServers addObject: [OFString stringWithFormat:
@"%u.%u.%u.%u", (ip >> 24) & 0xFF, (ip >> 16) & 0xFF,
(ip >> 8) & 0xFF, ip & 0xFF]];
}
if (nameServers.count > 0) {
[nameServers makeImmutable];
_nameServers = [nameServers copy];
}
}
#endif
- (void)reload
{
#ifdef OF_WINDOWS
OFString *path = nil;
#endif
#if (defined(OF_AMIGAOS_M68K) || defined(OF_AMIGAOS4)) && defined(OF_HAVE_FILES)
OFFileManager *fileManager = [OFFileManager defaultManager];
#endif
void *pool;
/*
* TODO: Rather than reparsing every time, check what actually changed
* (mtime) and only reset those.
*/
if (_lastConfigReload != nil && _configReloadInterval > 0 &&
_lastConfigReload.timeIntervalSinceNow < _configReloadInterval)
return;
pool = objc_autoreleasePoolPush();
[self setDefaults];
#if defined(OF_WINDOWS)
# ifdef OF_HAVE_FILES
@try {
OFWindowsRegistryKey *key;
key = [[OFWindowsRegistryKey localMachineKey]
openSubkeyAtPath: @"SYSTEM\\CurrentControlSet\\Services\\"
@"Tcpip\\Parameters"
accessRights: KEY_QUERY_VALUE
options: 0];
path = [[[key stringForValueNamed: @"DataBasePath"]
stringByAppendingPathComponent: @"hosts"]
stringByExpandingWindowsEnvironmentStrings];
} @catch (OFOpenWindowsRegistryKeyFailedException *e) {
/* Ignore */
} @catch (OFUndefinedKeyException *e) {
/* Ignore */
}
if (path != nil)
[self parseHosts: path];
# endif
[self obtainWindowsSystemConfig];
#elif defined(OF_MORPHOS)
[self obtainMorphOSSystemConfig];
#elif defined(OF_AMIGAOS_M68K) || defined(OF_AMIGAOS4)
# ifdef OF_HAVE_FILES
if (![self obtainRoadshowSystemConfig]) {
if (assignExists("AmiTCP"))
/*
* FIXME: The installer puts it there, but theoretically
* it could also be in AmiTCP:db/netdb or any of
* the files included there.
*/
[self parseResolvConf: @"AmiTCP:db/netdb-myhost"];
}
if ([fileManager fileExistsAtPath: @"DEVS:Internet/hosts"])
[self parseHosts: @"DEVS:Internet/hosts"];
else if (assignExists("AmiTCP"))
[self parseHosts: @"AmiTCP:db/hosts"];
# else
[self obtainRoadshowSystemConfig];
# endif
#elif defined(OF_NINTENDO_3DS)
[self obtainNintendo3DSSytemConfig];
#elif defined(OF_HAVE_FILES)
[self parseHosts: HOSTS_PATH];
[self parseResolvConf: RESOLV_CONF_PATH];
#endif
if (_staticHosts == nil) {
OFArray *localhost =
#ifdef OF_HAVE_IPV6
[OFArray arrayWithObjects: @"::1", @"127.0.0.1", nil];
#else
[OFArray arrayWithObject: @"127.0.0.1"];
#endif
_staticHosts = [[OFDictionary alloc]
initWithObject: localhost
forKey: @"localhost"];
}
if (_nameServers == nil)
#ifdef OF_HAVE_IPV6
_nameServers = [[OFArray alloc]
initWithObjects: @"127.0.0.1", @"::1", nil];
#else
_nameServers = [[OFArray alloc] initWithObject: @"127.0.0.1"];
#endif
#if !defined(OF_WII) && !defined(OF_MORPHOS)
if (_localDomain == nil)
_localDomain = [domainFromHostname(obtainHostname()) copy];
#endif
if (_searchDomains == nil) {
if (_localDomain != nil)
_searchDomains = [[OFArray alloc]
initWithObject: _localDomain];
else
_searchDomains = [[OFArray alloc] init];
}
objc_autoreleasePoolPop(pool);
}
@end