// Exercise Unix domain sockets.
"use strict";

var CC = Components.Constructor;

const UnixServerSocket = CC(
  "@mozilla.org/network/server-socket;1",
  "nsIServerSocket",
  "initWithFilename"
);
const UnixAbstractServerSocket = CC(
  "@mozilla.org/network/server-socket;1",
  "nsIServerSocket",
  "initWithAbstractAddress"
);

const ScriptableInputStream = CC(
  "@mozilla.org/scriptableinputstream;1",
  "nsIScriptableInputStream",
  "init"
);

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 (mozinfo.os == "win") {
    test_not_supported();
    return;
  }

  // The xpcshell temp directory on Android doesn't seem to let us create
  // Unix domain sockets. (Perhaps it's a FAT filesystem?)
  if (mozinfo.os != "android") {
    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);
  }

  if (mozinfo.os == "android" || mozinfo.os == "linux") {
    add_test(test_abstract_address_socket);
  }

  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");
  info("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.
  info("creating socket: " + socketName.path);
  let server = new UnixServerSocket(socketName, allPermissions, -1);
  server.asyncListen({
    onSocketAccepted(aServ, aTransport) {
      info("called test_echo's onSocketAccepted");
      log += "a";

      Assert.equal(aServ, server);

      let connection = aTransport;

      // Check the server socket's self address.
      let connectionSelfAddr = connection.getScriptableSelfAddr();
      Assert.equal(connectionSelfAddr.family, Ci.nsINetAddr.FAMILY_LOCAL);
      Assert.equal(connectionSelfAddr.address, socketName.path);

      // The client socket is anonymous, so the server transport should
      // have an empty peer address.
      Assert.equal(connection.host, "");
      Assert.equal(connection.port, 0);
      let connectionPeerAddr = connection.getScriptablePeerAddr();
      Assert.equal(connectionPeerAddr.family, Ci.nsINetAddr.FAMILY_LOCAL);
      Assert.equal(connectionPeerAddr.address, "");

      let serverAsyncInput = connection
        .openInputStream(0, 0, 0)
        .QueryInterface(Ci.nsIAsyncInputStream);
      let serverOutput = connection.openOutputStream(0, 0, 0);

      serverAsyncInput.asyncWait(
        function (aStream) {
          info("called test_echo's server's onInputStreamReady");
          let serverScriptableInput = new ScriptableInputStream(aStream);

          // Receive data from the client, and send back a response.
          Assert.equal(
            serverScriptableInput.readBytes(17),
            "Mervyn Murgatroyd"
          );
          info("server has read message from client");
          serverOutput.write("Ruthven Murgatroyd", 18);
          info("server has written to client");
        },
        0,
        0,
        threadManager.currentThread
      );
    },

    onStopListening(aServ, aStatus) {
      info("called test_echo's onStopListening");
      log += "s";

      Assert.equal(aServ, server);
      Assert.equal(log, "acs");

      run_next_test();
    },
  });

  // Create a client socket, and connect to the server.
  let client = socketTransportService.createUnixDomainTransport(socketName);
  Assert.equal(client.host, socketName.path);
  Assert.equal(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);
  info("client has written to server");

  clientAsyncInput.asyncWait(
    function (aStream) {
      info("called test_echo's client's onInputStreamReady");
      log += "c";

      Assert.equal(aStream, clientAsyncInput);

      // Now that the connection has been established, we can check the
      // transport's self and peer addresses.
      let clientSelfAddr = client.getScriptableSelfAddr();
      Assert.equal(clientSelfAddr.family, Ci.nsINetAddr.FAMILY_LOCAL);
      Assert.equal(clientSelfAddr.address, "");

      Assert.equal(client.host, socketName.path); // re-check, but hey
      let clientPeerAddr = client.getScriptablePeerAddr();
      Assert.equal(clientPeerAddr.family, Ci.nsINetAddr.FAMILY_LOCAL);
      Assert.equal(clientPeerAddr.address, socketName.path);

      Assert.equal(clientInput.readBytes(18), "Ruthven Murgatroyd");
      info("client has read message from server");

      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("missing");
  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) {
      info("called test_no_such_socket's onInputStreamReady");

      Assert.equal(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.
  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 () {
      info("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 () {
          info("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) {
    info("called test_connect_permission's onSocketAccepted");
    log += "a";

    let serverInput = aTransport
      .openInputStream(0, 0, 0)
      .QueryInterface(Ci.nsIAsyncInputStream);
    let serverOutput = aTransport.openOutputStream(0, 0, 0);

    serverInput.asyncWait(
      function (aStream) {
        info(
          "called test_connect_permission's socketAccepted's onInputStreamReady"
        );
        log += "i";

        // Receive data from the client, and send back a response.
        let serverScriptableInput = new ScriptableInputStream(serverInput);
        Assert.equal(serverScriptableInput.readBytes(8), "Hanratty");
        serverOutput.write("Ferlingatti", 11);
      },
      0,
      0,
      threadManager.currentThread
    );
  }

  function client3InputStreamReady(aStream) {
    info("called client3's onInputStreamReady");
    log += "3";

    let client3Input = new ScriptableInputStream(aStream);

    Assert.equal(client3Input.readBytes(11), "Ferlingatti");

    client3.close(Cr.NS_OK);
    server.close();
  }

  function stopListening(aServ, aStatus) {
    info("called test_connect_permission's server's stopListening");
    log += "s";

    Assert.equal(log, "12ai3s");

    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 });

  // 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) {
    info("test_keep_when_offline: onAccepted called");
    log += "a";
    Assert.equal(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";
    info("test_keep_when_offline: clientReady called: " + log);
    Assert.equal(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);
      Assert.equal(ex.result, Cr.NS_BASE_STREAM_CLOSED);

      info("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.
      Assert.equal(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";
    info("test_keep_when_offline: serverReady called: " + log);
    Assert.equal(aStream, serverInput);

    // Check the message from the client.
    Assert.equal(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) {
      Services.io.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.
      info("closing server transport");
      server.close(Cr.NS_OK);
      log += "(";
    }
  }

  // We have stopped listening.
  function onStopListening(aServ, aStatus) {
    info("test_keep_when_offline: onStopListening called");
    log += "L";
    Assert.equal(log, "acscscscscsocscscscscs(c)L");

    Assert.equal(aServ, listener);
    Assert.equal(aStatus, Cr.NS_BINDING_ABORTED);

    run_next_test();
  }
}

function test_abstract_address_socket() {
  const socketname = "abstractsocket";
  let server = new UnixAbstractServerSocket(socketname, -1);
  server.asyncListen({
    onSocketAccepted: (aServ, aTransport) => {
      let serverInput = aTransport
        .openInputStream(0, 0, 0)
        .QueryInterface(Ci.nsIAsyncInputStream);
      let serverOutput = aTransport.openOutputStream(0, 0, 0);

      serverInput.asyncWait(
        aStream => {
          info(
            "called test_abstract_address_socket's onSocketAccepted's onInputStreamReady"
          );

          // Receive data from the client, and send back a response.
          let serverScriptableInput = new ScriptableInputStream(serverInput);
          Assert.equal(serverScriptableInput.readBytes(9), "ping ping");
          serverOutput.write("pong", 4);
        },
        0,
        0,
        threadManager.currentThread
      );
    },
    onStopListening: (aServ, aTransport) => {},
  });

  let client =
    socketTransportService.createUnixDomainAbstractAddressTransport(socketname);
  Assert.equal(client.host, socketname);
  Assert.equal(client.port, 0);
  let clientInput = client
    .openInputStream(0, 0, 0)
    .QueryInterface(Ci.nsIAsyncInputStream);
  let clientOutput = client.openOutputStream(0, 0, 0);

  clientOutput.write("ping ping", 9);

  clientInput.asyncWait(
    aStream => {
      let clientScriptInput = new ScriptableInputStream(clientInput);
      let available = clientScriptInput.available();
      if (available) {
        Assert.equal(clientScriptInput.readBytes(4), "pong");

        client.close(Cr.NS_OK);
        server.close(Cr.NS_OK);

        run_next_test();
      }
    },
    0,
    0,
    threadManager.currentThread
  );
}
