ClientsToPortsGenerator.java
/*
** Module : ClientsToPortsGenerator.java
** Abstract : Utility to generate clients names to their own ports based on the directory.
**
** Copyright (c) 2014-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 SBI 20170628 First version.
** 002 EVL 20170703 Javadoc fix.
** 003 SBI 20171020 Changed to be able to generate clients to backends map based on the assinged
** hosts defined bythe directory under the following node brokers/hosts.
** 004 SBI 20200317 Added getPortName.
** 005 SBI 20230411 Changed readHostsFile to return string to index map.
** 006 SBI 20240501 Added loopback address entry to hosts file with localhost entry.
** 007 GBB 20240709 Hard-coded config names replaced by ConfigItem constants.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.main;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.*;
import java.util.Map.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.util.*;
/**
* Creates mapping clients to their ports and added corresponding settings under the webClient
* parent node.
* TODO refactor this with the other tool: SSLCertGenUtil
*/
public class ClientsToPortsGenerator
{
/** The generated mapping clients to their ports will be saved into this file */
private static final String OUTPUT_FILE = "map.clients-to-backends";
/** Valid entries for yes/no options. */
private static final String[] YES_NO = { "yes", "no" };
/** Read data from the standard input. */
private final BufferedReader reader;
/** Configuration used to initialize the directory. */
private final BootstrapConfig cfg;
/** Instance to access the directory. */
private DirectoryService ds;
/**
* Create a new utility.
*
* @param config
* Configuration for accessing the directory.
*/
public ClientsToPortsGenerator(BootstrapConfig config)
{
Console console = System.console();
if (console != null)
{
reader = new BufferedReader(console.reader());
}
else
{
reader = new BufferedReader(new InputStreamReader(System.in));
}
cfg = new BootstrapConfig(config);
if (cfg.getString("directory", "xml", "filename", null) == null)
{
throw new NullPointerException("The directory file must be specified!");
}
}
/**
* Main method to generate mapping clients to their ports. Added corresponding settings
* under the webClient parent node in the directory.
*
* @param hostsFilePath
* The string representing the file path to the hosts file
*
* @throws ConfigurationException
* In case the {@link #ds directory service} could not be initialized.
* @throws IOException
* If standard input can not be accessed.
*/
public void generate(String hostsFilePath)
throws ConfigurationException,
IOException
{
cfg.setConfigItem("directory", "xml", "must_exist", "true");
String dirFile = cfg.getString("directory", "xml", "filename", null);
System.out.println("Initializing service for directory " + dirFile + "...");
this.ds = DirectoryService.createInstance(cfg);
if (!ds.bind())
throw new RuntimeException("bind() failed");
//read output clients to ports mapping
String fileName = readLine("Enter the output file name, otherwise "
+ "the default file name will be used '" + OUTPUT_FILE + "':");
if (fileName == null || fileName.isEmpty())
{
fileName = OUTPUT_FILE;
}
String webClientRootPath = Utils.findDirectoryNodePath(ds, "webClient", "container", false);
if (webClientRootPath == null)
{
System.out.println("This container node /server/default/webClient is not found!");
return;
}
String portsRangeRootPath = Utils.findDirectoryNodePath(ds, "webClient/portsRange", "container", false);
if (portsRangeRootPath == null)
{
// add portsRange
openBatch(webClientRootPath);
String nodeId = webClientRootPath + "/portsRange";
addNode(nodeId, "container", (Attribute[]) null);
closeBatch(webClientRootPath);
}
int from = ConfigItem.CLIENT_PORT_RANGE_FROM.read(-1, ds);
if (from == -1)
{
while (true)
{
try
{
from = Integer.parseInt(readLine("Enter webClient/portsRange/from:"));
if (from <= 1024)
{
System.out.println("Enter a ports number that is greater than the upper bound"
+ " of priveledge ports number (>1024).");
continue;
}
break;
}
catch (NumberFormatException e)
{
System.out.println("Incorrect ports number.");
continue;
}
}
String nodeId = Utils.findDirectoryNodePath(ds, "webClient/portsRange/from", "integer", false);
String parentNodeId = webClientRootPath + "/portsRange";
openBatch(parentNodeId);
if (nodeId == null)
{
addNode(parentNodeId + "/from", "integer", from);
}
else
{
ds.setNodeInteger(nodeId, "value", from);
}
closeBatch(parentNodeId);
}
int to = ConfigItem.CLIENT_PORT_RANGE_TO.read(-1, ds);
if (to < from)
{
to = enterPortsRangeTo(from);
String nodeId = Utils.findDirectoryNodePath(ds, "webClient/portsRange/to", "integer", false);
String parentNodeId = webClientRootPath + "/portsRange";
openBatch(parentNodeId);
if (nodeId == null)
{
addNode(parentNodeId + "/to", "integer", to);
}
else
{
ds.setNodeInteger(nodeId, "value", to);
}
closeBatch(parentNodeId);
}
String prefix = ConfigItem.PROXY_CLIENT_NAME_PREFIX.read("");
if (prefix == null || prefix.isEmpty())
{
prefix = readLine("Enter a simple short name for webClient/portsRange/namePrefix,"
+ " otherwise the default prefix will be used 'client':");
if (prefix == null || prefix.isEmpty())
{
prefix = "client";
}
String nodeId = Utils.findDirectoryNodePath(ds, "webClient/portsRange/namePrefix", "string", false);
String parentNodeId = webClientRootPath + "/portsRange";
openBatch(parentNodeId);
if (nodeId == null)
{
addNode(parentNodeId + "/namePrefix", "string", prefix);
}
else
{
ds.setNodeString(nodeId, "value", prefix);
}
closeBatch(parentNodeId);
}
Map<String, Integer> hosts = readHostsFile(hostsFilePath);
// fill web client to backends map
try(FileOutputStream fout = new FileOutputStream(new File("./" + fileName));
PrintWriter pout = new PrintWriter(fout, true))
{
StringBuilder line = new StringBuilder();
for (Entry<String, Integer> hostEntry : hosts.entrySet())
{
for(int i = from; i <= to; i++)
{
line.append(getPortName(prefix, from, hostEntry.getValue(), i));
line.append(" ");
line.append(hostEntry.getKey()).append(":").append(i);
pout.println(line.toString());
line.setLength(0);
}
}
}
System.out.println("Done.");
}
/**
* Represents a well-defined mapping from host indices to their names.
*
* @param prefix
* The client prefix
* @param from
* The beginning value of the given port range
* @param hostIndex
* The host index
* @param port
* The port number
*
* @return The port name
*/
public static String getPortName(String prefix,
int from,
Integer hostIndex,
int port)
{
StringBuilder portName = new StringBuilder();
portName.append(prefix).append(hostIndex).append(port - from + 1);
return portName.toString();
}
private int enterPortsRangeTo(int from) throws IOException
{
int to;
while (true)
{
try
{
to = Integer.parseInt(readLine("Enter webClient/portsRange/to:"));
if (to < from)
{
System.out.println("Enter a ports number that is greater than or equal to"
+ " webClient/portsRange/from");
continue;
}
break;
}
catch (NumberFormatException e)
{
System.out.println("Incorrect ports number.");
continue;
}
}
return to;
}
/**
* Delete the specified node and all of its children.
*
* @param nodeId
* The node ID.
*
* @throws RuntimeException
* If the node or one of its children could not be deleted.
*/
private void deleteNode(String nodeId)
{
if (ds.getNodeClass(nodeId) != null)
{
String[] children = ds.enumerateNodes(nodeId);
for (int i = 0; i < children.length; i++)
{
String fullId = nodeId + "/" + children[i];
// remove previous entry
if (!ds.deleteNode(fullId))
{
throw new RuntimeException("deleteNode() failed for the " + fullId + " node");
}
}
if (!ds.deleteNode(nodeId))
{
throw new RuntimeException("deleteNode() failed for the " + nodeId + " node");
}
}
}
/**
* Add a new node to the directory, having the specified class and a single attribute
* with the same class and specified value.
*
* @param nodeId
* The node ID.
* @param cls
* The node's and attribute's class.
* @param value
* The attribute's value.
*
* @throws RuntimeException
* If the node could not be added.
*/
private void addNode(String nodeId, String cls, Object value)
{
NodeAttribute nodeAttr = ds.getClassNodeAttribute(cls, "value");
Attribute attr = new Attribute(nodeAttr, new Object[] { value });
addNode(nodeId, cls, new Attribute[] { attr });
}
/**
* Add a new node to the directory, having the specified class and attributes.
*
* @param nodeId
* The node ID.
* @param cls
* The node's class.
* @param attrs
* The node's attributes.
*
* @throws RuntimeException
* If the node could not be added.
*/
private void addNode(String nodeId, String cls, Attribute[] attrs)
{
if (!ds.addNode(nodeId, cls, attrs))
{
throw new RuntimeException("addNode() failed for the " + nodeId + " node");
}
}
/**
* Open a batch editing session for the specified node.
*
* @param node
* The node ID.
*
* @throws RuntimeException
* If the batch editing session could not be opened.
*/
private void openBatch(String node)
{
if (!ds.openBatch(node))
{
throw new RuntimeException("openBatch() failed for the " + node + " node");
}
}
/**
* Close a batch editing session.
*
* @param node
* The node ID.
*
* @throws RuntimeException
* If the batch editing session could not be closed.
*/
private void closeBatch(String node)
{
if (!ds.closeBatch(true))
throw new RuntimeException("closeBatch() failed for " + node);
}
/**
* Read a line of text using the created {@link #reader}.
*
* @param txt
* Description to be written to standard output.
*
* @return The line of text from standard input.
*
* @throws IOException
* If data could not be read.
*/
private String readLine(String txt)
throws IOException
{
System.out.print(txt);
String line = reader.readLine();
return line.trim();
}
/**
* Ask the user to enter one of the specified valid options, using the given message.
*
* @param msg
* The message shown to the user.
* @param valid
* An array of valid options.
*/
private String readOption(String msg, String[] valid)
throws IOException
{
String answer = null;
l1: do
{
answer = readLine(msg);
for (String s : valid)
{
if (answer.equalsIgnoreCase(s))
{
break l1;
}
}
}
while (true);
return answer;
}
/**
* Command line driver.
*
* @param args
* Application command line parameters. File name is the only one expected.
*
* @throws ConfigurationException
* In case the {@link #ds directory service} could not be initialized.
* @throws IOException
* If standard input can not be accessed.
*/
public static void main(String[] args)
throws ConfigurationException,
IOException
{
String dirFile = null;
if (args.length <= 1)
{
StringBuilder info = new StringBuilder("Usage: java ");
info.append(ClientsToPortsGenerator.class.getName()).append(" ");
info.append("<directory.xml>").append(" ").append("<hosts.txt>");
System.out.println(info.toString());
return;
}
dirFile = args[0];
BootstrapConfig cfg = new BootstrapConfig();
cfg.setConfigItem("directory", "backend", "type", "xml");
cfg.setConfigItem("directory", "xml", "filename", dirFile);
cfg.setConfigItem("directory", "xml", "must_exist", "true");
cfg.setServer(true);
ClientsToPortsGenerator gen = new ClientsToPortsGenerator(cfg);
gen.generate(args[1]);
}
/**
* Reads the given hosts file and returns the hosts map.
*
* @param filePath
* The path to the hosts file
*
* @return The hosts map that is filled with new data.
*
* @throws IOException
* Iff create, write or read operations applied to the target file is failed
*/
public static Map<String, Integer> readHostsFile(String filePath)
throws IOException
{
Map<String, Integer> hostsMap = new LinkedHashMap<String, Integer>();
Path p = Paths.get(filePath);
if (!p.toFile().exists())
{
FileStore fileStore = Files.getFileStore(p);
FileAttribute[] fileAttributes;
if (fileStore.supportsFileAttributeView("posix:permissions"))
{
// create with rw-r--r--
Set<PosixFilePermission> rwRules = PosixFilePermissions.fromString("rw-r--r--");
FileAttribute<Set<PosixFilePermission>> fileAttribute =
PosixFilePermissions.asFileAttribute(rwRules);
fileAttributes = new FileAttribute[1];
fileAttributes[0] = fileAttribute;
}
else
{
fileAttributes = new FileAttribute[0];
}
Files.createFile(p, fileAttributes);
// write a loopback interface first
try (BufferedWriter writer = Files.newBufferedWriter(p,
StandardCharsets.UTF_8,
StandardOpenOption.WRITE))
{
writer.write("localhost 1");
writer.newLine();
writer.write("127.0.0.1 1");
writer.newLine();
}
}
try (BufferedReader reader = Files.newBufferedReader(p, StandardCharsets.UTF_8))
{
String line;
HostsFileParser parser = new HostsFileParser();
while((line = reader.readLine()) != null)
{
if (parser.parse(line).isValidLine())
{
if (!hostsMap.containsKey(parser.getHostAddress()))
{
hostsMap.put(parser.getHostAddress(), parser.getValue());
}
}
}
}
return hostsMap;
}
/**
* Appends new host and its value to the hosts file.
*
* @param filePath
* The path to the hosts file
* @param hostNameOrIpAddress
* The new host address
* @param value
* The new host value
*
* @return True iff the host address and its value has been appended successfully, otherwise
* returns false.
*/
public static boolean appendHost(String filePath, String hostNameOrIpAddress, Integer value)
{
Path p = Paths.get(filePath);
if (!p.toFile().exists())
{
return false;
}
try (BufferedWriter writer = Files.newBufferedWriter(p,
StandardCharsets.UTF_8,
StandardOpenOption.WRITE,
StandardOpenOption.APPEND))
{
StringBuilder line = new StringBuilder();
line.append(hostNameOrIpAddress).append(" ").append(value);
writer.write(line.toString());
writer.newLine();
}
catch (IOException e)
{
return false;
}
return true;
}
/**
* Defines the hosts file parser.
*/
private static class HostsFileParser
{
/** The parsed value */
private int value;
/** The host name or IP4 address */
private String hostAddress;
/** The result if the last line is parsed successfully or not. */
private boolean validLine;
public HostsFileParser parse(String line)
{
validLine = true;
line = line.trim();
String[] parsed = line.split(" ");
if (parsed.length > 0)
{
String ip = parsed[0];
try
{
//just check the validity of the host name or ip address
InetAddress.getByName(ip);
hostAddress = ip;
int index = line.indexOf(ip);
index += ip.length();
line = line.substring(index);
line = line.trim();
try
{
value = Integer.parseInt(line);
}
catch(NumberFormatException ex)
{
validLine = false;
}
}
catch(UnknownHostException ex)
{
validLine = false;
}
}
return this;
}
/**
* Gets the last successfully parsed host value number.
*
* @return The host value number
*/
public int getValue()
{
return value;
}
/**
* Gets the last successful parsed IP4 host address.
*
* @return The IP4 host address
*/
public String getHostAddress()
{
return hostAddress;
}
/**
* Test if the parsed line is valid
*
* @return True iff the host IP4 address and its host value are retrieved successfully,
* otherwise false.
*/
public boolean isValidLine()
{
return validLine;
}
}
}