From c0a631fad90b1350289428329db3b4b63e2747e9 Mon Sep 17 00:00:00 2001 From: Jim Blandy Date: Fri, 6 Sep 2013 08:06:23 -0700 Subject: [PATCH] Bug 892114: Add support for Unix domain sockets to nsIServerSocket.idl and @mozilla.org/network/server-socket;1. r=mayhemer --- netwerk/base/public/nsIServerSocket.idl | 58 +- netwerk/base/public/nsISocketTransport.idl | 5 +- .../base/public/nsISocketTransportService.idl | 37 +- netwerk/base/src/nsServerSocket.cpp | 50 +- netwerk/base/src/nsSocketTransport2.cpp | 94 ++- netwerk/base/src/nsSocketTransport2.h | 5 + .../base/src/nsSocketTransportService2.cpp | 25 + netwerk/dns/DNS.cpp | 13 +- netwerk/dns/DNS.h | 3 + netwerk/test/unit/test_unix_domain.js | 533 ++++++++++++++++++ netwerk/test/unit/xpcshell.ini | 5 + testing/xpcshell/head.js | 12 + xpcom/io/nsIFile.idl | 18 +- 13 files changed, 841 insertions(+), 17 deletions(-) create mode 100644 netwerk/test/unit/test_unix_domain.js diff --git a/netwerk/base/public/nsIServerSocket.idl b/netwerk/base/public/nsIServerSocket.idl index 072eaceafbe1..74e5c80988ad 100644 --- a/netwerk/base/public/nsIServerSocket.idl +++ b/netwerk/base/public/nsIServerSocket.idl @@ -4,6 +4,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsISupports.idl" +#include "nsIFile.idl" interface nsIServerSocketListener; interface nsISocketTransport; @@ -18,7 +19,7 @@ typedef unsigned long nsServerSocketFlag; * * An interface to a server socket that can accept incoming connections. */ -[scriptable, uuid(0df6a0e2-a6b1-4d4c-b30d-f2cb093444e3)] +[scriptable, uuid(7a9c39cb-a13f-4eef-9bdf-a74301628742)] interface nsIServerSocket : nsISupports { /** @@ -94,6 +95,61 @@ interface nsIServerSocket : nsISupports */ [noscript] void initWithAddress([const] in PRNetAddrPtr aAddr, in long aBackLog); + /** + * initWithFilename + * + * This method initializes a Unix domain or "local" server socket. Such + * a socket has a name in the filesystem, like an ordinary file. To + * connect, a client supplies the socket's filename, and the usual + * permission checks on socket apply. + * + * This makes Unix domain sockets useful for communication between the + * programs being run by a specific user on a single machine: the + * operating system takes care of authentication, and the user's home + * directory or profile directory provide natural per-user rendezvous + * points. + * + * Since Unix domain sockets are always local to the machine, they are + * not affected by the nsIIOService's 'offline' flag. + * + * The system-level socket API may impose restrictions on the length of + * the filename that are stricter than those of the underlying + * filesystem. If the file name is too long, this returns + * NS_ERROR_FILE_NAME_TOO_LONG. + * + * All components of the path prefix of |aPath| must name directories; + * otherwise, this returns NS_ERROR_FILE_NOT_DIRECTORY. + * + * This call requires execute permission on all directories containing + * the one in which the socket is to be created, and write and execute + * permission on the directory itself. Otherwise, this returns + * NS_ERROR_CONNECTION_REFUSED. + * + * This call creates the socket's directory entry. There must not be + * any existing entry with the given name. If there is, this returns + * NS_ERROR_SOCKET_ADDRESS_IN_USE. + * + * On systems that don't support Unix domain sockets at all, this + * returns NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED. + * + * @param aPath nsIFile + * The file name at which the socket should be created. + * + * @param aPermissions unsigned long + * Unix-style permission bits to be applied to the new socket. + * + * Note about permissions: Linux's unix(7) man page claims that some + * BSD-derived systems ignore permissions on UNIX-domain sockets; + * NetBSD's bind(2) man page agrees, but says it does check now (dated + * 2005). POSIX has required 'connect' to fail if write permission on + * the socket itself is not granted since 2003 (Issue 6). NetBSD says + * that the permissions on the containing directory (execute) have + * always applied, so creating sockets in appropriately protected + * directories should be secure on both old and new systems. + */ + void initWithFilename(in nsIFile aPath, in unsigned long aPermissions, + in long aBacklog); + /** * close * diff --git a/netwerk/base/public/nsISocketTransport.idl b/netwerk/base/public/nsISocketTransport.idl index 0bf1b489f054..e7598002fe3c 100644 --- a/netwerk/base/public/nsISocketTransport.idl +++ b/netwerk/base/public/nsISocketTransport.idl @@ -27,12 +27,15 @@ native NetAddr(mozilla::net::NetAddr); interface nsISocketTransport : nsITransport { /** - * Get the host for the underlying socket connection. + * Get the peer's host for the underlying socket connection. + * For Unix domain sockets, this is a pathname, or the empty string for + * unnamed and abstract socket addresses. */ readonly attribute AUTF8String host; /** * Get the port for the underlying socket connection. + * For Unix domain sockets, this is zero. */ readonly attribute long port; diff --git a/netwerk/base/public/nsISocketTransportService.idl b/netwerk/base/public/nsISocketTransportService.idl index 8fc2cd67e952..cb328426b77a 100644 --- a/netwerk/base/public/nsISocketTransportService.idl +++ b/netwerk/base/public/nsISocketTransportService.idl @@ -4,6 +4,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsISupports.idl" +#include "nsIFile.idl" interface nsISocketTransport; interface nsIProxyInfo; @@ -17,7 +18,7 @@ struct PRFileDesc; [ptr] native PRFileDescPtr(PRFileDesc); [ptr] native nsASocketHandlerPtr(nsASocketHandler); -[scriptable, uuid(185B3A5D-8729-436D-9693-7BDCCB9C2216)] +[scriptable, uuid(ad56b25f-e6bb-4db3-9f7b-5b7db33fd2b1)] interface nsISocketTransportService : nsISupports { /** @@ -49,6 +50,40 @@ interface nsISocketTransportService : nsISupports in long aPort, in nsIProxyInfo aProxyInfo); + /** + * Create a transport built on a Unix domain socket, connecting to the + * given filename. + * + * Since Unix domain sockets are always local to the machine, they are + * not affected by the nsIIOService's 'offline' flag. + * + * On systems that don't support Unix domain sockets at all, this + * returns NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED. + * + * The system-level socket API may impose restrictions on the length of + * the filename that are stricter than those of the underlying + * filesystem. If the file name is too long, this returns + * NS_ERROR_FILE_NAME_TOO_LONG. + * + * The |aPath| parameter must specify an existing directory entry. + * Otherwise, this returns NS_ERROR_FILE_NOT_FOUND. + * + * The program must have search permission on all components of the + * path prefix of |aPath|, and read and write permission on |aPath| + * itself. Without such permission, this returns + * NS_ERROR_CONNECTION_REFUSED. + * + * The |aPath| parameter must refer to a unix-domain socket. Otherwise, + * this returns NS_ERROR_CONNECTION_REFUSED. (POSIX specifies + * ECONNREFUSED when "the target address was not listening for + * connections", and this is what Linux returns.) + * + * @param aPath + * The file name of the Unix domain socket to which we should + * connect. + */ + nsISocketTransport createUnixDomainTransport(in nsIFile aPath); + /** * Adds a new socket to the list of controlled sockets. * diff --git a/netwerk/base/src/nsServerSocket.cpp b/netwerk/base/src/nsServerSocket.cpp index 05a9c10a305c..95a7c4350692 100644 --- a/netwerk/base/src/nsServerSocket.cpp +++ b/netwerk/base/src/nsServerSocket.cpp @@ -173,6 +173,13 @@ nsServerSocket::OnSocketReady(PRFileDesc *fd, int16_t outFlags) PRNetAddr prClientAddr; NetAddr clientAddr; + // NSPR doesn't tell us the peer address's length (as provided by the + // 'accept' system call), so we can't distinguish between named, + // unnamed, and abstract peer addresses. Clear prClientAddr first, so + // that the path will at least be reliably empty for unnamed and + // abstract addresses, and not garbage when the peer is unnamed. + memset(&prClientAddr, 0, sizeof(prClientAddr)); + clientFD = PR_Accept(mFD, &prClientAddr, PR_INTERVAL_NO_WAIT); PRNetAddrToNetAddr(&prClientAddr, &clientAddr); if (!clientFD) @@ -230,6 +237,15 @@ nsServerSocket::OnSocketDetached(PRFileDesc *fd) void nsServerSocket::IsLocal(bool *aIsLocal) { +#if defined(XP_UNIX) || defined(XP_OS2) + // Unix-domain sockets are always local. + if (mAddr.raw.family == PR_AF_LOCAL) + { + *aIsLocal = true; + return; + } +#endif + // If bound to loopback, this server socket only accepts local connections. *aIsLocal = PR_IsNetAddrType(&mAddr, PR_IpAddrLoopback); } @@ -257,6 +273,35 @@ nsServerSocket::Init(int32_t aPort, bool aLoopbackOnly, int32_t aBackLog) return InitSpecialConnection(aPort, aLoopbackOnly ? LoopbackOnly : 0, aBackLog); } +NS_IMETHODIMP +nsServerSocket::InitWithFilename(nsIFile *aPath, uint32_t aPermissions, int32_t aBacklog) +{ +#if defined(XP_UNIX) || defined(XP_OS2) + nsresult rv; + + nsAutoCString path; + rv = aPath->GetNativePath(path); + if (NS_FAILED(rv)) + return rv; + + // Create a Unix domain PRNetAddr referring to the given path. + PRNetAddr addr; + if (path.Length() > sizeof(addr.local.path) - 1) + return NS_ERROR_FILE_NAME_TOO_LONG; + addr.local.family = PR_AF_LOCAL; + memcpy(addr.local.path, path.get(), path.Length()); + addr.local.path[path.Length()] = '\0'; + + rv = InitWithAddress(&addr, aBacklog); + if (NS_FAILED(rv)) + return rv; + + return aPath->SetPermissions(aPermissions); +#else + return NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED; +#endif +} + NS_IMETHODIMP nsServerSocket::InitSpecialConnection(int32_t aPort, nsServerSocketFlag aFlags, int32_t aBackLog) @@ -469,8 +514,11 @@ nsServerSocket::GetPort(int32_t *aResult) uint16_t port; if (mAddr.raw.family == PR_AF_INET) port = mAddr.inet.port; - else + else if (mAddr.raw.family == PR_AF_INET6) port = mAddr.ipv6.port; + else + return NS_ERROR_FAILURE; + *aResult = (int32_t) PR_ntohs(port); return NS_OK; } diff --git a/netwerk/base/src/nsSocketTransport2.cpp b/netwerk/base/src/nsSocketTransport2.cpp index 15395977159a..6c00641561be 100644 --- a/netwerk/base/src/nsSocketTransport2.cpp +++ b/netwerk/base/src/nsSocketTransport2.cpp @@ -863,20 +863,46 @@ nsSocketTransport::Init(const char **types, uint32_t typeCount, return NS_OK; } +nsresult +nsSocketTransport::InitWithFilename(const char *filename) +{ +#if defined(XP_UNIX) || defined(XP_OS2) + size_t filenameLength = strlen(filename); + + if (filenameLength > sizeof(mNetAddr.local.path) - 1) + return NS_ERROR_FILE_NAME_TOO_LONG; + + mHost.Assign(filename); + mPort = 0; + mTypeCount = 0; + + mNetAddr.local.family = AF_LOCAL; + memcpy(mNetAddr.local.path, filename, filenameLength); + mNetAddr.local.path[filenameLength] = '\0'; + mNetAddrIsSet = true; + + return NS_OK; +#else + return NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED; +#endif +} + nsresult nsSocketTransport::InitWithConnectedSocket(PRFileDesc *fd, const NetAddr *addr) { NS_ASSERTION(!mFD, "already initialized"); - char buf[kIPv6CStrBufSize]; + char buf[kNetAddrMaxCStrBufSize]; NetAddrToString(addr, buf, sizeof(buf)); mHost.Assign(buf); uint16_t port; if (addr->raw.family == AF_INET) port = addr->inet.port; - else + else if (addr->raw.family == AF_INET6) port = addr->inet6.port; + else + port = 0; mPort = ntohs(port); memcpy(&mNetAddr, addr, sizeof(NetAddr)); @@ -951,6 +977,10 @@ nsSocketTransport::ResolveHost() if (!mProxyHost.IsEmpty()) { if (!mProxyTransparent || mProxyTransparentResolvesHost) { +#if defined(XP_UNIX) || defined(XP_OS2) + NS_ABORT_IF_FALSE(!mNetAddrIsSet || mNetAddr.raw.family != AF_LOCAL, + "Unix domain sockets can't be used with proxies"); +#endif // When not resolving mHost locally, we still want to ensure that // it only contains valid characters. See bug 304904 for details. if (!net_IsValidHostName(mHost)) @@ -1014,6 +1044,11 @@ nsSocketTransport::BuildSocket(PRFileDesc *&fd, bool &proxyTransparent, bool &us rv = fd ? NS_OK : NS_ERROR_OUT_OF_MEMORY; } else { +#if defined(XP_UNIX) || defined(XP_OS2) + NS_ABORT_IF_FALSE(!mNetAddrIsSet || mNetAddr.raw.family != AF_LOCAL, + "Unix domain sockets can't be used with socket types"); +#endif + fd = nullptr; nsCOMPtr spserv = @@ -1117,9 +1152,13 @@ nsSocketTransport::InitiateSocket() nsresult rv; - if (gIOService->IsOffline() && - !IsLoopBackAddress(&mNetAddr)) - return NS_ERROR_OFFLINE; + if (gIOService->IsOffline()) { + bool isLocal; + + IsLocal(&isLocal); + if (!isLocal) + return NS_ERROR_OFFLINE; + } // // find out if it is going to be ok to attach another socket to the STS. @@ -1224,7 +1263,7 @@ nsSocketTransport::InitiateSocket() #if defined(PR_LOGGING) if (SOCKET_LOG_ENABLED()) { - char buf[kIPv6CStrBufSize]; + char buf[kNetAddrMaxCStrBufSize]; NetAddrToString(&mNetAddr, buf, sizeof(buf)); SOCKET_LOG((" trying address: %s\n", buf)); } @@ -1312,6 +1351,13 @@ nsSocketTransport::RecoverFromError() SOCKET_LOG(("nsSocketTransport::RecoverFromError [this=%p state=%x cond=%x]\n", this, mState, mCondition)); +#if defined(XP_UNIX) || defined(XP_OS2) + // Unix domain connections don't have multiple addresses to try, + // so the recovery techniques here don't apply. + if (mNetAddrIsSet && mNetAddr.raw.family == AF_LOCAL) + return false; +#endif + // can only recover from errors in these states if (mState != STATE_RESOLVING && mState != STATE_CONNECTING) return false; @@ -1516,6 +1562,7 @@ void nsSocketTransport::ReleaseFD_Locked(PRFileDesc *fd) { NS_ASSERTION(mFD == fd, "wrong fd"); + SOCKET_LOG(("JIMB: ReleaseFD_Locked: mFDref = %d\n", mFDref)); if (--mFDref == 0) { if (PR_GetCurrentThread() == gSocketThread) { @@ -1556,9 +1603,18 @@ nsSocketTransport::OnSocketEvent(uint32_t type, nsresult status, nsISupports *pa // ensure that we have created a socket, attached it, and have a // connection. // - if (mState == STATE_CLOSED) - mCondition = ResolveHost(); - else + if (mState == STATE_CLOSED) { + // Unix domain sockets are ready to connect; mNetAddr is all we + // need. Internet address families require a DNS lookup (or possibly + // several) before we can connect. +#if defined(XP_UNIX) || defined(XP_OS2) + if (mNetAddrIsSet && mNetAddr.raw.family == AF_LOCAL) + mCondition = InitiateSocket(); + else +#endif + mCondition = ResolveHost(); + + } else SOCKET_LOG((" ignoring redundant event\n")); break; @@ -1789,6 +1845,16 @@ nsSocketTransport::IsLocal(bool *aIsLocal) { { MutexAutoLock lock(mLock); + +#if defined(XP_UNIX) || defined(XP_OS2) + // Unix-domain sockets are always local. + if (mNetAddr.raw.family == PR_AF_LOCAL) + { + *aIsLocal = true; + return; + } +#endif + *aIsLocal = IsLoopBackAddress(&mNetAddr); } } @@ -2050,6 +2116,16 @@ nsSocketTransport::GetSelfAddr(NetAddr *addr) } PRNetAddr prAddr; + + // NSPR doesn't tell us the socket address's length (as provided by + // the 'getsockname' system call), so we can't distinguish between + // named, unnamed, and abstract Unix domain socket names. (Server + // sockets are never unnamed, obviously, but client sockets can use + // any kind of address.) Clear prAddr first, so that the path for + // unnamed and abstract addresses will at least be reliably empty, + // and not garbage for unnamed sockets. + memset(&prAddr, 0, sizeof(prAddr)); + nsresult rv = (PR_GetSockName(fd, &prAddr) == PR_SUCCESS) ? NS_OK : NS_ERROR_FAILURE; PRNetAddrToNetAddr(&prAddr, addr); diff --git a/netwerk/base/src/nsSocketTransport2.h b/netwerk/base/src/nsSocketTransport2.h index 90e7358816a7..96d9ec5c35e1 100644 --- a/netwerk/base/src/nsSocketTransport2.h +++ b/netwerk/base/src/nsSocketTransport2.h @@ -129,6 +129,11 @@ public: nsresult InitWithConnectedSocket(PRFileDesc *socketFD, const mozilla::net::NetAddr *addr); + // This method instructs the socket transport to open a socket + // connected to the given Unix domain address. We can only create + // unlayered, simple, stream sockets. + nsresult InitWithFilename(const char *filename); + // nsASocketHandler methods: void OnSocketReady(PRFileDesc *, int16_t outFlags); void OnSocketDetached(PRFileDesc *); diff --git a/netwerk/base/src/nsSocketTransportService2.cpp b/netwerk/base/src/nsSocketTransportService2.cpp index d2eff6f4d6a6..4caa6aa1c247 100644 --- a/netwerk/base/src/nsSocketTransportService2.cpp +++ b/netwerk/base/src/nsSocketTransportService2.cpp @@ -573,6 +573,31 @@ nsSocketTransportService::CreateTransport(const char **types, return NS_OK; } +NS_IMETHODIMP +nsSocketTransportService::CreateUnixDomainTransport(nsIFile *aPath, + nsISocketTransport **result) +{ + nsresult rv; + + NS_ENSURE_TRUE(mInitialized, NS_ERROR_NOT_INITIALIZED); + + nsAutoCString path; + rv = aPath->GetNativePath(path); + if (NS_FAILED(rv)) + return rv; + + nsRefPtr trans = new nsSocketTransport(); + if (!trans) + return NS_ERROR_OUT_OF_MEMORY; + + rv = trans->InitWithFilename(path.get()); + if (NS_FAILED(rv)) + return rv; + + trans.forget(result); + return NS_OK; +} + NS_IMETHODIMP nsSocketTransportService::GetAutodialEnabled(bool *value) { diff --git a/netwerk/dns/DNS.cpp b/netwerk/dns/DNS.cpp index 66a1f109e0b2..cf1a284f5069 100644 --- a/netwerk/dns/DNS.cpp +++ b/netwerk/dns/DNS.cpp @@ -115,9 +115,20 @@ bool NetAddrToString(const NetAddr *addr, char *buf, uint32_t bufSize) #if defined(XP_UNIX) || defined(XP_OS2) else if (addr->raw.family == AF_LOCAL) { if (bufSize < sizeof(addr->local.path)) { + // Many callers don't bother checking our return value, so + // null-terminate just in case. + if (bufSize > 0) { + buf[0] = '\0'; + } return false; } - memcpy(buf, addr->local.path, bufSize); + + // Usually, the size passed to memcpy should be the size of the + // destination. Here, we know that the source is no larger than the + // destination, so using the source's size is always safe, whereas + // using the destination's size may cause us to read off the end of the + // source. + memcpy(buf, addr->local.path, sizeof(addr->local.path)); return true; } #endif diff --git a/netwerk/dns/DNS.h b/netwerk/dns/DNS.h index 26bcf05c607a..4fe64bafb84a 100644 --- a/netwerk/dns/DNS.h +++ b/netwerk/dns/DNS.h @@ -55,9 +55,12 @@ namespace net { // Windows requires longer buffers for some reason. const int kIPv4CStrBufSize = 22; const int kIPv6CStrBufSize = 65; +const int kNetAddrMaxCStrBufSize = kIPv6CStrBufSize; #else const int kIPv4CStrBufSize = 16; const int kIPv6CStrBufSize = 46; +const int kLocalCStrBufSize = 108; +const int kNetAddrMaxCStrBufSize = kLocalCStrBufSize; #endif // This was all created at a time in which we were using NSPR for host diff --git a/netwerk/test/unit/test_unix_domain.js b/netwerk/test/unit/test_unix_domain.js new file mode 100644 index 000000000000..230170debfdf --- /dev/null +++ b/netwerk/test/unit/test_unix_domain.js @@ -0,0 +1,533 @@ +// Exercise Unix domain sockets. + +const Ci = Components.interfaces; +const Cc = Components.classes; +const CC = Components.Constructor; +const Cr = Components.results; + +const UnixServerSocket = CC("@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "initWithFilename"); + +const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1", + "nsIScriptableInputStream", + "init"); + +const IOService = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); +const socketTransportService = Cc["@mozilla.org/network/socket-transport-service;1"] + .getService(Ci.nsISocketTransportService); + +const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + +const allPermissions = parseInt("777", 8); + +function run_test() +{ + // If we're on Windows, simply check for graceful failure. + if ("@mozilla.org/windows-registry-key;1" in Cc) { + test_not_supported(); + return; + } + + add_test(test_echo); + add_test(test_name_too_long); + add_test(test_no_directory); + add_test(test_no_such_socket); + add_test(test_address_in_use); + add_test(test_file_in_way); + add_test(test_create_permission); + add_test(test_connect_permission); + add_test(test_long_socket_name); + add_test(test_keep_when_offline); + + run_next_test(); +} + +// Check that creating a Unix domain socket fails gracefully on Windows. +function test_not_supported() +{ + let socketName = do_get_tempdir(); + socketName.append('socket'); + do_print("creating socket: " + socketName.path); + + do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1), + "NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED"); + + do_check_throws_nsIException(() => socketTransportService.createUnixDomainTransport(socketName), + "NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED"); +} + +// Actually exchange data with Unix domain sockets. +function test_echo() +{ + let log = ''; + + let socketName = do_get_tempdir(); + socketName.append('socket'); + + // Create a server socket, listening for connections. + do_print("creating socket: " + socketName.path); + let server = new UnixServerSocket(socketName, allPermissions, -1); + server.asyncListen({ + onSocketAccepted: function(aServ, aTransport) { + do_print("called test_echo's onSocketAccepted"); + log += 'a'; + + do_check_eq(aServ, server); + + let connection = aTransport; + + // Check the server socket's self address. + let connectionSelfAddr = connection.getScriptableSelfAddr(); + do_check_eq(connectionSelfAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); + do_check_eq(connectionSelfAddr.address, socketName.path); + + // The client socket is anonymous, so the server transport should + // have an empty peer address. + do_check_eq(connection.host, ''); + do_check_eq(connection.port, 0); + let connectionPeerAddr = connection.getScriptablePeerAddr(); + do_check_eq(connectionPeerAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); + do_check_eq(connectionPeerAddr.address, ''); + + let serverInput = new ScriptableInputStream(connection.openInputStream(0, 0, 0)); + let serverOutput = connection.openOutputStream(0, 0, 0); + + // Receive data from the client, and send back a response. + do_check_eq(serverInput.readBytes(17), "Mervyn Murgatroyd"); + serverOutput.write("Ruthven Murgatroyd", 18); + }, + + onStopListening: function(aServ, aStatus) { + do_print("called test_echo's onStopListening"); + log += 's'; + + do_check_eq(aServ, server); + do_check_eq(log, 'acs'); + + run_next_test(); + } + }); + + // Create a client socket, and connect to the server. + let client = socketTransportService.createUnixDomainTransport(socketName); + do_check_eq(client.host, socketName.path); + do_check_eq(client.port, 0); + + let clientAsyncInput = client.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream); + let clientInput = new ScriptableInputStream(clientAsyncInput); + let clientOutput = client.openOutputStream(0, 0, 0); + + clientOutput.write("Mervyn Murgatroyd", 17); + clientAsyncInput.asyncWait(function (aStream) { + do_print("called test_echo's onInputStreamReady"); + log += 'c'; + + do_check_eq(aStream, clientAsyncInput); + + // Now that the connection has been established, we can check the + // transport's self and peer addresses. + let clientSelfAddr = client.getScriptableSelfAddr(); + do_check_eq(clientSelfAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); + do_check_eq(clientSelfAddr.address, ''); + + do_check_eq(client.host, socketName.path); // re-check, but hey + let clientPeerAddr = client.getScriptablePeerAddr(); + do_check_eq(clientPeerAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); + do_check_eq(clientPeerAddr.address, socketName.path); + + do_check_eq(clientInput.readBytes(18), "Ruthven Murgatroyd"); + + server.close(); + }, 0, 0, threadManager.currentThread); +} + +// Create client and server sockets using a path that's too long. +function test_name_too_long() +{ + let socketName = do_get_tempdir(); + // The length limits on all the systems NSPR supports are a bit past 100. + socketName.append(new Array(1000).join('x')); + + // The length must be checked before we ever make any system calls --- we + // have to create the sockaddr first --- so it's unambiguous which error + // we should get here. + + do_check_throws_nsIException(() => new UnixServerSocket(socketName, 0, -1), + "NS_ERROR_FILE_NAME_TOO_LONG"); + + // Unlike most other client socket errors, this one gets reported + // immediately, as we can't even initialize the sockaddr with the given + // name. + do_check_throws_nsIException(() => socketTransportService.createUnixDomainTransport(socketName), + "NS_ERROR_FILE_NAME_TOO_LONG"); + + run_next_test(); +} + +// Try creating a socket in a directory that doesn't exist. +function test_no_directory() +{ + let socketName = do_get_tempdir(); + socketName.append('directory-that-does-not-exist'); + socketName.append('socket'); + + do_check_throws_nsIException(() => new UnixServerSocket(socketName, 0, -1), + "NS_ERROR_FILE_NOT_FOUND"); + + run_next_test(); +} + +// Try connecting to a server socket that isn't there. +function test_no_such_socket() +{ + let socketName = do_get_tempdir(); + socketName.append('nonexistent-socket'); + + let client = socketTransportService.createUnixDomainTransport(socketName); + let clientAsyncInput = client.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream); + clientAsyncInput.asyncWait(function (aStream) { + do_print("called test_no_such_socket's onInputStreamReady"); + + do_check_eq(aStream, clientAsyncInput); + + // nsISocketTransport puts off actually creating sockets as long as + // possible, so the error in connecting doesn't actually show up until + // this point. + do_check_throws_nsIException(() => clientAsyncInput.available(), + "NS_ERROR_FILE_NOT_FOUND"); + + clientAsyncInput.close(); + client.close(Cr.NS_OK); + + run_next_test(); + }, 0, 0, threadManager.currentThread); +} + +// Creating a socket with a name that another socket is already using is an +// error. +function test_address_in_use() +{ + let socketName = do_get_tempdir(); + socketName.append('socket-in-use'); + + // Create one server socket. + let server = new UnixServerSocket(socketName, allPermissions, -1); + + // Now try to create another with the same name. + do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1), + "NS_ERROR_SOCKET_ADDRESS_IN_USE"); + + run_next_test(); +} + +// Creating a socket with a name that is already a file is an error. +function test_file_in_way() +{ + let socketName = do_get_tempdir(); + socketName.append('file_in_way'); + + // Create a file with the given name. + socketName.create(Ci.nsIFile.NORMAL_FILE_TYPE, allPermissions); + + // Try to create a socket with the same name. + do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1), + "NS_ERROR_SOCKET_ADDRESS_IN_USE"); + + // Try to create a socket under a name that uses that as a parent directory. + socketName.append('socket'); + do_check_throws_nsIException(() => new UnixServerSocket(socketName, 0, -1), + "NS_ERROR_FILE_NOT_DIRECTORY"); + + run_next_test(); +} + +// It is not permitted to create a socket in a directory which we are not +// permitted to execute, or create files in. +function test_create_permission() +{ + let dirName = do_get_tempdir(); + dirName.append('unfriendly'); + + let socketName = dirName.clone(); + socketName.append('socket'); + + // The test harness has difficulty cleaning things up if we don't make + // everything writable before we're done. + try { + // Create a directory which we are not permitted to search. + dirName.create(Ci.nsIFile.DIRECTORY_TYPE, 0); + + // Try to create a socket in that directory. Because Linux returns EACCES + // when a 'connect' fails because of a local firewall rule, + // nsIServerSocket returns NS_ERROR_CONNECTION_REFUSED in this case. + do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1), + "NS_ERROR_CONNECTION_REFUSED"); + + // Grant read and execute permission, but not write permission on the directory. + dirName.permissions = parseInt("0555", 8); + + // This should also fail; we need write permission. + do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1), + "NS_ERROR_CONNECTION_REFUSED"); + + } finally { + // Make the directory writable, so the test harness can clean it up. + dirName.permissions = allPermissions; + } + + // This should succeed, since we now have all the permissions on the + // directory we could want. + do_check_instanceof(new UnixServerSocket(socketName, allPermissions, -1), + Ci.nsIServerSocket); + + run_next_test(); +} + +// To connect to a Unix domain socket, we need search permission on the +// directories containing it, and some kind of permission or other on the +// socket itself. +function test_connect_permission() +{ + // This test involves a lot of callbacks, but they're written out so that + // the actual control flow proceeds from top to bottom. + let log = ''; + + // Create a directory which we are permitted to search - at first. + let dirName = do_get_tempdir(); + dirName.append('inhospitable'); + dirName.create(Ci.nsIFile.DIRECTORY_TYPE, allPermissions); + + let socketName = dirName.clone(); + socketName.append('socket'); + + // Create a server socket in that directory, listening for connections, + // and accessible. + let server = new UnixServerSocket(socketName, allPermissions, -1); + server.asyncListen({ onSocketAccepted: socketAccepted, onStopListening: stopListening }); + + // Make the directory unsearchable. + dirName.permissions = 0; + + let client3; + + let client1 = socketTransportService.createUnixDomainTransport(socketName); + let client1AsyncInput = client1.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream); + client1AsyncInput.asyncWait(function (aStream) { + do_print("called test_connect_permission's client1's onInputStreamReady"); + log += '1'; + + // nsISocketTransport puts off actually creating sockets as long as + // possible, so the error doesn't actually show up until this point. + do_check_throws_nsIException(() => client1AsyncInput.available(), + "NS_ERROR_CONNECTION_REFUSED"); + + client1AsyncInput.close(); + client1.close(Cr.NS_OK); + + // Make the directory searchable, but make the socket inaccessible. + dirName.permissions = allPermissions; + socketName.permissions = 0; + + let client2 = socketTransportService.createUnixDomainTransport(socketName); + let client2AsyncInput = client2.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream); + client2AsyncInput.asyncWait(function (aStream) { + do_print("called test_connect_permission's client2's onInputStreamReady"); + log += '2'; + + do_check_throws_nsIException(() => client2AsyncInput.available(), + "NS_ERROR_CONNECTION_REFUSED"); + + client2AsyncInput.close(); + client2.close(Cr.NS_OK); + + // Now make everything accessible, and try one last time. + socketName.permissions = allPermissions; + + client3 = socketTransportService.createUnixDomainTransport(socketName); + + let client3Output = client3.openOutputStream(0, 0, 0); + client3Output.write("Hanratty", 8); + + let client3AsyncInput = client3.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream); + client3AsyncInput.asyncWait(client3InputStreamReady, 0, 0, threadManager.currentThread); + }, 0, 0, threadManager.currentThread); + }, 0, 0, threadManager.currentThread); + + function socketAccepted(aServ, aTransport) { + do_print("called test_connect_permission's onSocketAccepted"); + log += 'a'; + + let serverInput = new ScriptableInputStream(aTransport.openInputStream(0, 0, 0)); + let serverOutput = aTransport.openOutputStream(0, 0, 0); + + // Receive data from the client, and send back a response. + do_check_eq(serverInput.readBytes(8), "Hanratty"); + serverOutput.write("Ferlingatti", 11); + } + + function client3InputStreamReady(aStream) { + do_print("called client3's onInputStreamReady"); + log += '3'; + + let client3Input = new ScriptableInputStream(aStream); + + do_check_eq(client3Input.readBytes(11), "Ferlingatti"); + + aStream.close(); + client3.close(Cr.NS_OK); + server.close(); + } + + function stopListening(aServ, aStatus) { + do_print("called test_connect_permission's server's stopListening"); + log += 's'; + + do_check_eq(log, '12a3s'); + + run_next_test(); + } +} + +// Creating a socket with a long filename doesn't crash. +function test_long_socket_name() +{ + let socketName = do_get_tempdir(); + socketName.append(new Array(10000).join('long')); + + // Try to create a server socket with the long name. + do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1), + "NS_ERROR_FILE_NAME_TOO_LONG"); + + // Try to connect to a socket with the long name. + do_check_throws_nsIException(() => socketTransportService.createUnixDomainTransport(socketName), + "NS_ERROR_FILE_NAME_TOO_LONG"); + + run_next_test(); +} + +// Going offline should not shut down Unix domain sockets. +function test_keep_when_offline() +{ + let log = ''; + + let socketName = do_get_tempdir(); + socketName.append('keep-when-offline'); + + // Create a listening socket. + let listener = new UnixServerSocket(socketName, allPermissions, -1); + listener.asyncListen({ onSocketAccepted: onAccepted, onStopListening: onStopListening }); + + // Connect a client socket to the listening socket. + let client = socketTransportService.createUnixDomainTransport(socketName); + let clientOutput = client.openOutputStream(0, 0, 0); + let clientInput = client.openInputStream(0, 0, 0); + clientInput.asyncWait(clientReady, 0, 0, threadManager.currentThread); + let clientScriptableInput = new ScriptableInputStream(clientInput); + + let server, serverInput, serverScriptableInput, serverOutput; + + // How many times has the server invited the client to go first? + let count = 0; + + // The server accepted connection callback. + function onAccepted(aListener, aServer) { + do_print("test_keep_when_offline: onAccepted called"); + log += 'a'; + do_check_eq(aListener, listener); + server = aServer; + + // Prepare to receive messages from the client. + serverInput = server.openInputStream(0, 0, 0); + serverInput.asyncWait(serverReady, 0, 0, threadManager.currentThread); + serverScriptableInput = new ScriptableInputStream(serverInput); + + // Start a conversation with the client. + serverOutput = server.openOutputStream(0, 0, 0); + serverOutput.write("After you, Alphonse!", 20); + count++; + } + + // The client has seen its end of the socket close. + function clientReady(aStream) { + log += 'c'; + do_print("test_keep_when_offline: clientReady called: " + log); + do_check_eq(aStream, clientInput); + + // If the connection has been closed, end the conversation and stop listening. + let available; + try { + available = clientInput.available(); + } catch (ex) { + do_check_instanceof(ex, Ci.nsIException); + do_check_eq(ex.result, Cr.NS_BASE_STREAM_CLOSED); + + do_print("client received end-of-stream; closing client output stream"); + log += ')'; + + client.close(Cr.NS_OK); + + // Now both output streams have been closed, and both input streams + // have received the close notification. Stop listening for + // connections. + listener.close(); + } + + if (available) { + // Check the message from the server. + do_check_eq(clientScriptableInput.readBytes(20), "After you, Alphonse!"); + + // Write our response to the server. + clientOutput.write("No, after you, Gaston!", 22); + + // Ask to be called again, when more input arrives. + clientInput.asyncWait(clientReady, 0, 0, threadManager.currentThread); + } + } + + function serverReady(aStream) { + log += 's'; + do_print("test_keep_when_offline: serverReady called: " + log); + do_check_eq(aStream, serverInput); + + // Check the message from the client. + do_check_eq(serverScriptableInput.readBytes(22), "No, after you, Gaston!"); + + // This should not shut things down: Unix domain sockets should + // remain open in offline mode. + if (count == 5) { + IOService.offline = true; + log += 'o'; + } + + if (count < 10) { + // Insist. + serverOutput.write("After you, Alphonse!", 20); + count++; + + // As long as the input stream is open, always ask to be called again + // when more input arrives. + serverInput.asyncWait(serverReady, 0, 0, threadManager.currentThread); + } else if (count == 10) { + // After sending ten times and receiving ten replies, we're not + // going to send any more. Close the server's output stream; the + // client's input stream should see this. + do_print("closing server transport"); + server.close(Cr.NS_OK); + log += '('; + } + } + + // We have stopped listening. + function onStopListening(aServ, aStatus) { + do_print("test_keep_when_offline: onStopListening called"); + log += 'L'; + do_check_eq(log, 'acscscscscsocscscscscs(c)L'); + + do_check_eq(aServ, listener); + do_check_eq(aStatus, Cr.NS_BINDING_ABORTED); + + run_next_test(); + } +} diff --git a/netwerk/test/unit/xpcshell.ini b/netwerk/test/unit/xpcshell.ini index 4ee78a994d86..433942d80341 100644 --- a/netwerk/test/unit/xpcshell.ini +++ b/netwerk/test/unit/xpcshell.ini @@ -2,6 +2,11 @@ head = head_channels.js head_cache.js tail = +[test_unix_domain.js] +# The xpcshell temp directory on Android doesn't seem to let us create +# Unix domain sockets. (Perhaps it's a FAT filesystem?) +skip-if = os == "android" + [test_addr_in_use_error.js] [test_304_responses.js] # Bug 675039: test hangs on Android-armv6 diff --git a/testing/xpcshell/head.js b/testing/xpcshell/head.js index 31a0af9d074a..083f01a17c43 100644 --- a/testing/xpcshell/head.js +++ b/testing/xpcshell/head.js @@ -925,6 +925,18 @@ function legible_exception(exception) } } +function do_check_instanceof(value, constructor, + stack=Components.stack.caller, todo=false) { + do_report_result(value instanceof constructor, + "value should be an instance of " + constructor.name, + stack, todo); +} + +function todo_check_instanceof(value, constructor, + stack=Components.stack.caller) { + do_check_instanceof(value, constructor, stack, true); +} + function do_test_pending(aName) { ++_tests_pending; diff --git a/xpcom/io/nsIFile.idl b/xpcom/io/nsIFile.idl index 71701c024516..effd27654f72 100644 --- a/xpcom/io/nsIFile.idl +++ b/xpcom/io/nsIFile.idl @@ -18,9 +18,21 @@ interface nsISimpleEnumerator; /** - * This is the only correct cross-platform way to specify a file. - * Strings are not such a way. If you grew up on windows or unix, you - * may think they are. Welcome to reality. + * An nsIFile is an abstract representation of a filename. It manages + * filename encoding issues, pathname component separators ('/' vs. '\\' + * vs. ':') and weird stuff like differing volumes with identical names, as + * on pre-Darwin Macintoshes. + * + * This file has long introduced itself to new hackers with this opening + * paragraph: + * + * This is the only correct cross-platform way to specify a file. + * Strings are not such a way. If you grew up on windows or unix, you + * may think they are. Welcome to reality. + * + * While taking the pose struck here to heart would be uncalled for, one + * may safely conclude that writing cross-platform code is an embittering + * experience. * * All methods with string parameters have two forms. The preferred * form operates on UCS-2 encoded characters strings. An alternate