package CustomDNS;
import java.lang.Exception;
import java.io.*;
import java.net.*;
import java.util.StringTokenizer;
import net.espeak.infra.cci.exception.*;
import net.espeak.jesi.*;
import ZoneRegistrarIntf;
import ZoneAuthenticatorIntf;
import CustomDNS.UpdateProtocol;
import CustomDNS.UpdateProtocol.ServerResponse;
/*************************************************************************
* TCP/IP server for our custom update protocol.
*************************************************************************
* We listen on a TCP/IP port. Clients connect to us to update their
* address records.
*/
public class UpdateServer {
// Timeout values, in milliseconds. These are pretty arbitrary. We're
// just trying to shut down unused connections promptly.
static private final int TCP_TIMEOUT = 60000;
// Instance variables.
private String zone;
private ZoneRegistrarIntf registrar;
private ZoneAuthenticatorIntf authenticator;
private short port;
/*********************************************************************
* Start a new update server.
*********************************************************************
* @param args Our command-line arguments. Pass an espeak connection
* properties file and customdns.prop.
* @see CustomDNS.Configuration
*/
public UpdateServer (String zone, short port,
ZoneRegistrarIntf registrar,
ZoneAuthenticatorIntf authenticator)
throws IOException
{
this.zone = zone;
this.registrar = registrar;
this.authenticator = authenticator;
this.port = port;
startServer();
}
// Connect the server to a TCP/IP port and listen for requests.
// TODO - We should spawn a listener thread.
private void startServer ()
throws IOException
{
ServerSocket listener = new ServerSocket(this.port);
while (true) {
final Socket socket = listener.accept();
socket.setSoTimeout(TCP_TIMEOUT);
Thread handler = new Thread(new Runnable() {
public void run () {
handleConnection(socket);
}
});
handler.start();
}
}
// Handle a single client connection.
private void handleConnection (Socket socket) {
try {
Reader rawin = new InputStreamReader(socket.getInputStream());
BufferedReader in = new BufferedReader(rawin);
Writer out = new OutputStreamWriter(socket.getOutputStream());
InetAddress remoteAddress = socket.getInetAddress();
runCommandLoop(in, out, remoteAddress);
} catch (Exception e) {
System.err.println(e.toString());
} finally {
try {
socket.close();
} catch (IOException e) {}
}
}
// End the current command and send a response code.
// We throw an exception to end the current command
// and print a response across the network. For example:
// 500 Unknown error
// Yes, this a pretty odd way of doing things. But it saves a lot
// of ugly state flags in runCommandLoop.
private void sendResponse(int code, String message)
throws ServerResponse
{
throw new ServerResponse(code, message);
}
// Run our command loop.
private void runCommandLoop (BufferedReader in, Writer out,
InetAddress remoteAddress)
throws IOException
{
String username = null;
String password = null;
UpdateProtocol.writeLine(out, "201 1.0 CustomDNS Update Server Ready");
while (true) {
try {
// Get our command.
String commandLine = in.readLine();
if (commandLine == null)
return;
StringTokenizer tokens = new StringTokenizer(commandLine);
int tokenCount = tokens.countTokens();
if (tokenCount == 0)
sendResponse(UpdateProtocol.STATUS_SYNTAX_ERROR,
"No command specified");
String command = tokens.nextToken().toUpperCase();
// Handle our command.
if (command.equals("QUIT")) {
// Bail out of our command loop.
break;
} else if (command.equals("AUTH")) {
// Parse our arguments.
if (tokenCount != 5)
sendResponse(UpdateProtocol.STATUS_SYNTAX_ERROR,
"Wrong number of arguments");
String authZone = tokens.nextToken();
String authType = tokens.nextToken().toUpperCase();
username = tokens.nextToken();
password = tokens.nextToken();
// Do some error checking.
if (!this.zone.equals(authZone))
sendResponse(UpdateProtocol.STATUS_UNKNOWN_ZONE,
"Unknown zone");
if (!authType.equals("PASS"))
sendResponse(UpdateProtocol.STATUS_UNKNOWN_AUTH_TYPE,
"Unknown authentication type");
// Send back an optimistic response.
sendResponse(UpdateProtocol.STATUS_AUTH_ACCEPTED,
"Authentication accepted");
} else if (command.equals("ADDRESS")) {
// Parse our arguments.
if (tokenCount != 3)
sendResponse(UpdateProtocol.STATUS_SYNTAX_ERROR,
"Wrong number of arguments");
String hostname = tokens.nextToken().toLowerCase();
String address = tokens.nextToken();
// Check the user's privileges.
approveUpdate(username, password, hostname);
if (address.toUpperCase().equals("AUTOMATIC")) {
// Use the address of the remote end of our
// socket.
address = remoteAddress.getHostAddress();
} else {
// Sanity-check the address field.
address = cleanUpAddress(address);
}
// Attempt to perform the update.
try {
registrar.registerAddressForHost(hostname, address);
} catch (ESInvocationException e) {
sendResponse(UpdateProtocol.STATUS_NO_UPDATE,
"Unable to update address");
}
sendResponse(UpdateProtocol.STATUS_OK,
"Address updated");
} else {
sendResponse(UpdateProtocol.STATUS_UNKNOWN_COMMAND,
"Unknown command");
}
} catch (ServerResponse e) {
UpdateProtocol.writeLine(out, e.getMessage());
}
}
UpdateProtocol.writeLine(out, "200 Goodbye");
}
// Authenticate the user.
private void approveUpdate (String user, String password, String host)
throws ServerResponse
{
if (user == null || password == null)
sendResponse(UpdateProtocol.STATUS_WRONG_STATE,
"Must use AUTH command before ADDRESS");
try {
if (!authenticator.authenticateUser(user, password, host))
sendResponse(UpdateProtocol.STATUS_NOT_ALLOWED,
"Update not allowed");
} catch (ESInvocationException e) {
sendResponse(UpdateProtocol.STATUS_COULD_NOT_AUTH,
"Couldn't peform authentication");
}
}
// Try to bash our address into something safe.
// (We don't want to put garbage into the database.)
// This has the unfortunate side effect of allowing users to supply
// hostnames as addresses, and cause us to do a real lookup.
private String cleanUpAddress (String address)
throws ServerResponse
{
String result = null;
try {
InetAddress inetaddr = InetAddress.getByName(address);
result = inetaddr.getHostAddress();
} catch (UnknownHostException e) {
sendResponse(UpdateProtocol.STATUS_MALFORMED_ADDRESS,
"Malformed address");
}
return result;
}
/*********************************************************************
* Start a new update server.
*********************************************************************
* @param args Our command-line arguments. Pass an espeak connection
* properties file and customdns.prop.
* @see CustomDNS.Configuration
*/
public static void main (String [] args) {
String appname = "CustomDNS.UpdateServer";
try {
// Parse our command line arguments, and get the configuration
// information we'll be needing.
Configuration config = Configuration.parseArguments(appname, args);
String zone = config.getProperty("customdns.updateserver.zone");
String defaultPort = Short.toString(UpdateProtocol.DEFAULT_PORT);
String portstr = config.getProperty("customdns.updateserver.port",
defaultPort);
short port = Short.parseShort(portstr);
String connfile = config.getConnectionFile();
// Connect to e-speak, and find our zone registarar and
// zone authentator.
ESConnection core = new ESConnection(connfile);
ESQuery query = new ESQuery("Name == '" + zone + "'");
String intfName = "ZoneRegistrarIntf";
ESServiceFinder finder = new ESServiceFinder(core, intfName);
ZoneRegistrarIntf reg = (ZoneRegistrarIntf) finder.find(query);
intfName = "ZoneAuthenticatorIntf";
finder = new ESServiceFinder(core, intfName);
ZoneAuthenticatorIntf auth = (ZoneAuthenticatorIntf)
finder.find(query);
// Start our server.
System.err.println("CustomDNS.UpdateServer: Starting.");
new UpdateServer(zone, port, reg, auth);
} catch (Exception e) {
System.err.println("CustomDNS.UpdateServer: " + e.toString());
System.exit(1);
}
}
}
|