/* * Copyright (c) 2008-2023 Jonathan Schleifer <js@nil.im> * * 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 * the packaging of this file. * * Alternatively, it may be distributed under the terms of the GNU General * Public License, either version 2 or 3, which can be found in the file * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this * file. */ #include "config.h" #include <errno.h> #ifdef HAVE_FCNTL_H # include <fcntl.h> #endif #import "OFDDPSocket.h" #import "OFDictionary.h" #import "OFNumber.h" #import "OFPair.h" #import "OFSocket.h" #import "OFSocket+Private.h" #import "OFAlreadyOpenException.h" #import "OFBindDDPSocketFailedException.h" #import "OFGetOptionFailedException.h" #import "OFInvalidArgumentException.h" #import "OFNotOpenException.h" #import "OFOutOfRangeException.h" #import "OFReadFailedException.h" #import "OFSetOptionFailedException.h" #import "OFWriteFailedException.h" #ifdef HAVE_NET_IF_H # include <net/if.h> #endif #ifdef HAVE_SYS_IOCTL_H # include <sys/ioctl.h> #endif #ifdef OF_HAVE_NETAT_APPLETALK_H # include <netat/ddp.h> # include <sys/ioctl.h> /* Unfortulately, there is no struct for the following in userland headers */ struct ATInterfaceConfig { char interfaceName[16]; unsigned int flags; struct at_addr address, router; unsigned short netStart, netEnd; at_nvestr_t zoneName; }; #endif #ifdef OF_HAVE_APPLETALK_IFCONFIG const OFAppleTalkInterfaceConfigurationKey OFAppleTalkInterfaceConfigurationNode = @"OFAppleTalkInterfaceConfigurationNode"; const OFAppleTalkInterfaceConfigurationKey OFAppleTalkInterfaceConfigurationNetwork = @"OFAppleTalkInterfaceConfigurationNetwork"; const OFAppleTalkInterfaceConfigurationKey OFAppleTalkInterfaceConfigurationPhase = @"OFAppleTalkInterfaceConfigurationPhase"; const OFAppleTalkInterfaceConfigurationKey OFAppleTalkInterfaceConfigurationNetworkRange = @"OFAppleTalkInterfaceConfigurationNetworkRange"; #endif @implementation OFDDPSocket @dynamic delegate; #ifdef OF_HAVE_APPLETALK_IFCONFIG + (void)setConfiguration: (OFAppleTalkInterfaceConfiguration)config forInterface: (OFString *)interfaceName { OFNumber *network, *node, *phase; OFPair OF_GENERIC(OFNumber *, OFNumber *) *range; int sock; struct ifreq request; struct sockaddr_at *sat; uint16_t rangeStart, rangeEnd; if (interfaceName.UTF8StringLength > IFNAMSIZ - 1) @throw [OFOutOfRangeException exception]; network = [config objectForKey: OFAppleTalkInterfaceConfigurationNetwork]; node = [config objectForKey: OFAppleTalkInterfaceConfigurationNode]; phase = [config objectForKey: OFAppleTalkInterfaceConfigurationPhase]; range = [config objectForKey: OFAppleTalkInterfaceConfigurationNetworkRange]; if (network == nil || node == nil) @throw [OFInvalidArgumentException exception]; if (phase != nil && phase.unsignedCharValue != 1 && phase.unsignedCharValue != 2) @throw [OFInvalidArgumentException exception]; # ifdef OF_MACOS if ((sock = socket(AF_APPLETALK, SOCK_RAW, 0)) < 0) # else if ((sock = socket(AF_APPLETALK, SOCK_DGRAM, 0)) < 0) # endif @throw [OFSetOptionFailedException exceptionWithObject: nil errNo: OFSocketErrNo()]; memset(&request, 0, sizeof(request)); strncpy(request.ifr_name, interfaceName.UTF8String, IFNAMSIZ - 1); sat = (struct sockaddr_at *)&request.ifr_addr; sat->sat_family = AF_APPLETALK; sat->sat_net = OFToBigEndian16(network.unsignedShortValue); sat->sat_node = node.unsignedCharValue; /* * The netrange is hidden in sat_zero and different OSes use different * struct names for it, so the portable way is setting sat_zero * directly. */ sat->sat_zero[0] = (phase != nil ? phase.unsignedCharValue : 2); if (range != nil) { rangeStart = [range.firstObject unsignedShortValue]; rangeEnd = [range.secondObject unsignedShortValue]; } else { rangeStart = rangeEnd = network.unsignedShortValue; } sat->sat_zero[2] = rangeStart >> 8; sat->sat_zero[3] = rangeStart & 0xFF; sat->sat_zero[4] = rangeEnd >> 8; sat->sat_zero[5] = rangeEnd & 0xFF; if (ioctl(sock, SIOCSIFADDR, &request) != 0) @throw [OFSetOptionFailedException exceptionWithObject: nil errNo: OFSocketErrNo()]; close(sock); } + (OFAppleTalkInterfaceConfiguration) configurationForInterface: (OFString *)interfaceName { int sock; struct ifreq request; struct sockaddr_at *sat; # ifndef OF_LINUX uint16_t rangeStart, rangeEnd; OFPair *range; # endif if (interfaceName.UTF8StringLength > IFNAMSIZ - 1) @throw [OFOutOfRangeException exception]; # ifdef OF_MACOS if ((sock = socket(AF_APPLETALK, SOCK_RAW, 0)) < 0) # else if ((sock = socket(AF_APPLETALK, SOCK_DGRAM, 0)) < 0) # endif @throw [OFGetOptionFailedException exceptionWithObject: nil errNo: OFSocketErrNo()]; memset(&request, 0, sizeof(request)); strncpy(request.ifr_name, interfaceName.UTF8String, IFNAMSIZ - 1); if (ioctl(sock, SIOCGIFADDR, &request) < 0) { int errNo = OFSocketErrNo(); /* No AppleTalk configured on this interface. */ if (errNo == EADDRNOTAVAIL) { close(sock); return nil; } @throw [OFGetOptionFailedException exceptionWithObject: nil errNo: errNo]; } sat = (struct sockaddr_at *)&request.ifr_addr; close(sock); # ifndef OF_LINUX /* * Linux currently doesn't fill out the phase or netrange. * * The netrange is hidden in sat_zero and different OSes use different * struct names for it, so the portable way is setting sat_zero * directly. */ rangeStart = sat->sat_zero[2] << 8 | sat->sat_zero[3]; rangeEnd = sat->sat_zero[4] << 8 | sat->sat_zero[5]; range = [OFPair pairWithFirstObject: [OFNumber numberWithUnsignedShort: rangeStart] secondObject: [OFNumber numberWithUnsignedShort: rangeEnd]]; # endif return [OFDictionary dictionaryWithKeysAndObjects: OFAppleTalkInterfaceConfigurationNode, [OFNumber numberWithUnsignedChar: sat->sat_node], OFAppleTalkInterfaceConfigurationNetwork, [OFNumber numberWithUnsignedShort: OFFromBigEndian16(sat->sat_net)], # ifndef OF_LINUX OFAppleTalkInterfaceConfigurationPhase, [OFNumber numberWithUnsignedChar: sat->sat_zero[0]], OFAppleTalkInterfaceConfigurationNetworkRange, range, # endif nil]; } + (void)removeConfigurationForInterface: (OFString *)interfaceName { int sock; struct ifreq request; if (interfaceName.UTF8StringLength > IFNAMSIZ - 1) @throw [OFOutOfRangeException exception]; # ifdef OF_MACOS if ((sock = socket(AF_APPLETALK, SOCK_RAW, 0)) < 0) # else if ((sock = socket(AF_APPLETALK, SOCK_DGRAM, 0)) < 0) # endif @throw [OFSetOptionFailedException exceptionWithObject: nil errNo: OFSocketErrNo()]; /* * NetBSD requires the address to be removed, while Linux ignores the * address entirely. */ memset(&request, 0, sizeof(request)); strncpy(request.ifr_name, interfaceName.UTF8String, IFNAMSIZ - 1); if (ioctl(sock, SIOCGIFADDR, &request) != 0) if (errno != EADDRNOTAVAIL) @throw [OFSetOptionFailedException exceptionWithObject: nil errNo: OFSocketErrNo()]; if (ioctl(sock, SIOCDIFADDR, &request) != 0) @throw [OFSetOptionFailedException exceptionWithObject: nil errNo: OFSocketErrNo()]; } #endif - (OFSocketAddress)bindToNetwork: (uint16_t)network node: (uint8_t)node port: (uint8_t)port protocolType: (uint8_t)protocolType { #ifdef OF_MACOS const int one = 1; struct ATInterfaceConfig config = { { 0 } }; #endif OFSocketAddress address; #if SOCK_CLOEXEC == 0 && defined(HAVE_FCNTL_H) && defined(FD_CLOEXEC) int flags; #endif if (protocolType == 0) @throw [OFInvalidArgumentException exception]; if (_socket != OFInvalidSocketHandle) @throw [OFAlreadyOpenException exceptionWithObject: self]; address = OFSocketAddressMakeAppleTalk(network, node, port); #if defined(OF_MACOS) if ((_socket = socket(address.sockaddr.at.sat_family, SOCK_RAW | SOCK_CLOEXEC, protocolType)) == OFInvalidSocketHandle) #elif defined(OF_WINDOWS) if ((_socket = socket(address.sockaddr.at.sat_family, SOCK_DGRAM | SOCK_CLOEXEC, ATPROTO_BASE + protocolType)) == OFInvalidSocketHandle) #else if ((_socket = socket(address.sockaddr.at.sat_family, SOCK_DGRAM | SOCK_CLOEXEC, 0)) == OFInvalidSocketHandle) #endif @throw [OFBindDDPSocketFailedException exceptionWithNetwork: network node: node port: port protocolType: protocolType socket: self errNo: OFSocketErrNo()]; _canBlock = true; #if SOCK_CLOEXEC == 0 && defined(HAVE_FCNTL_H) && defined(FD_CLOEXEC) if ((flags = fcntl(_socket, F_GETFD, 0)) != -1) fcntl(_socket, F_SETFD, flags | FD_CLOEXEC); #endif if (bind(_socket, (struct sockaddr *)&address.sockaddr, address.length) != 0) { int errNo = OFSocketErrNo(); closesocket(_socket); _socket = OFInvalidSocketHandle; @throw [OFBindDDPSocketFailedException exceptionWithNetwork: network node: node port: port protocolType: protocolType socket: self errNo: errNo]; } memset(&address, 0, sizeof(address)); address.family = OFSocketAddressFamilyAppleTalk; address.length = (socklen_t)sizeof(address.sockaddr); if (OFGetSockName(_socket, (struct sockaddr *)&address.sockaddr, &address.length) != 0) { int errNo = OFSocketErrNo(); closesocket(_socket); _socket = OFInvalidSocketHandle; @throw [OFBindDDPSocketFailedException exceptionWithNetwork: network node: node port: port protocolType: protocolType socket: self errNo: errNo]; } if (address.sockaddr.at.sat_family != AF_APPLETALK) { closesocket(_socket); _socket = OFInvalidSocketHandle; @throw [OFBindDDPSocketFailedException exceptionWithNetwork: network node: node port: port protocolType: protocolType socket: self errNo: EAFNOSUPPORT]; } #ifdef OF_MACOS if (setsockopt(_socket, ATPROTO_NONE, DDP_STRIPHDR, &one, sizeof(one)) != 0 || ioctl(_socket, _IOWR('a', 2, struct ATInterfaceConfig), &config) != 0) @throw [OFBindDDPSocketFailedException exceptionWithNetwork: network node: node port: port protocolType: protocolType socket: self errNo: OFSocketErrNo()]; OFSocketAddressSetAppleTalkNetwork(&address, config.address.s_net); OFSocketAddressSetAppleTalkNode(&address, config.address.s_node); #endif #if !defined(OF_MACOS) && !defined(OF_WINDOWS) _protocolType = protocolType; #endif return address; } /* * Everybody but macOS and Windows is probably using a netatalk-compatible * implementation, which includes the protocol type as the first byte of the * data instead of considering it part of the header. * * The following overrides prepend the protocol type when sending and compare * and strip it when receiving. * * Unfortunately, the downside of this is that the only way to handle receiving * a packet with the wrong protocol type is to throw an exception with errNo * ENOMSG, while macOS and Windows just filter those out in the kernel. * Returning 0 would mean this is indistinguishable from an empty packet, so it * has to be an exception. */ #if !defined(OF_MACOS) && !defined(OF_WINDOWS) - (size_t)receiveIntoBuffer: (void *)buffer length: (size_t)length sender: (OFSocketAddress *)sender { ssize_t ret; uint8_t protocolType; struct iovec iov[2] = { { &protocolType, 1 }, { buffer, length } }; struct msghdr msg = { .msg_name = (sender != NULL ? (struct sockaddr *)&sender->sockaddr : NULL), .msg_namelen = (sender != NULL ? (socklen_t)sizeof(sender->sockaddr) : 0), .msg_iov = iov, .msg_iovlen = 2 }; if (_socket == OFInvalidSocketHandle) @throw [OFNotOpenException exceptionWithObject: self]; if ((ret = recvmsg(_socket, &msg, 0)) < 0) @throw [OFReadFailedException exceptionWithObject: self requestedLength: length errNo: OFSocketErrNo()]; if (ret < 1 || protocolType != _protocolType) @throw [OFReadFailedException exceptionWithObject: self requestedLength: length errNo: ENOMSG]; if (sender != NULL) { sender->length = msg.msg_namelen; sender->family = OFSocketAddressFamilyAppleTalk; } return ret - 1; } - (void)sendBuffer: (const void *)buffer length: (size_t)length receiver: (const OFSocketAddress *)receiver { struct iovec iov[2] = { { &_protocolType, 1 }, { (void *)buffer, length } }; struct msghdr msg = { .msg_name = (struct sockaddr *)&receiver->sockaddr, .msg_namelen = receiver->length, .msg_iov = iov, .msg_iovlen = 2 }; ssize_t bytesWritten; if (_socket == OFInvalidSocketHandle) @throw [OFNotOpenException exceptionWithObject: self]; if ((bytesWritten = sendmsg(_socket, &msg, 0)) < 0) @throw [OFWriteFailedException exceptionWithObject: self requestedLength: length bytesWritten: 0 errNo: OFSocketErrNo()]; if ((size_t)bytesWritten != length + 1) { bytesWritten--; if (bytesWritten < 0) bytesWritten = 0; @throw [OFWriteFailedException exceptionWithObject: self requestedLength: length bytesWritten: bytesWritten errNo: 0]; } } #endif @end