diff --git a/apps/time/java/build.xml b/apps/time/java/build.xml new file mode 100644 index 0000000000000000000000000000000000000000..63644343742bcbffbfff5488feade9895aa463bc --- /dev/null +++ b/apps/time/java/build.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project basedir="." default="all" name="time"> + <target name="all" depends="clean, build" /> + <target name="build" depends="builddep, jar" /> + <target name="builddep"> + <ant dir="../../../core/java/" target="build" /> + </target> + <target name="compile"> + <mkdir dir="./build" /> + <mkdir dir="./build/obj" /> + <javac srcdir="./src" debug="true" destdir="./build/obj" includes="**/*.java" classpath="../../../core/java/build/i2p.jar" /> + </target> + <target name="jar" depends="compile"> + <jar destfile="./build/timestamper.jar" basedir="./build/obj" includes="**/*.class"> + <manifest> + <attribute name="Main-Class" value="net.i2p.time.Timestamper" /> + <attribute name="Class-Path" value="i2p.jar timestamper.jar" /> + </manifest> + </jar> + </target> + <target name="javadoc"> + <mkdir dir="./build" /> + <mkdir dir="./build/javadoc" /> + <javadoc + sourcepath="./src:../../../core/java/src:../../../core/java/test" destdir="./build/javadoc" + packagenames="*" + use="true" + access="package" + splitindex="true" + windowtitle="I2P timestamper" /> + </target> + <target name="clean"> + <delete dir="./build" /> + </target> + <target name="cleandep" depends="clean"> + <ant dir="../../../core/java/" target="cleandep" /> + </target> + <target name="distclean" depends="clean"> + <ant dir="../../../core/java/" target="distclean" /> + </target> +</project> diff --git a/apps/time/java/src/net/i2p/time/NtpClient.java b/apps/time/java/src/net/i2p/time/NtpClient.java new file mode 100644 index 0000000000000000000000000000000000000000..1cef7d72128b99bacfef15dfd5b21e63fdab65da --- /dev/null +++ b/apps/time/java/src/net/i2p/time/NtpClient.java @@ -0,0 +1,138 @@ +package net.i2p.time; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.text.DecimalFormat; + + +/** + * NtpClient - an NTP client for Java. This program connects to an NTP server + * and prints the response to the console. + * + * The local clock offset calculation is implemented according to the SNTP + * algorithm specified in RFC 2030. + * + * Note that on windows platforms, the curent time-of-day timestamp is limited + * to an resolution of 10ms and adversely affects the accuracy of the results. + * + * + * This code is copyright (c) Adam Buckley 2004 + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. A HTML version of the GNU General Public License can be + * seen at http://www.gnu.org/licenses/gpl.html + * + * 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 General Public License for + * more details. + * + * @author Adam Buckley + * (minor refactoring by jrandom) + */ +public class NtpClient { + /** difference between the unix epoch and jan 1 1900 (NTP uses that) */ + private final static double SECONDS_1900_TO_EPOCH = 2208988800.0; + private final static int NTP_PORT = 123; + + /** + * Query the ntp servers, returning the current time from first one we find + * + * @return milliseconds since january 1, 1970 (UTC) + * @throws IllegalArgumentException if none of the servers are reachable + */ + public static long currentTime(String serverNames[]) { + if (serverNames == null) + throw new IllegalArgumentException("No NTP servers specified"); + for (int i = 0; i < serverNames.length; i++) { + long now = currentTime(serverNames[i]); + if (now > 0) + return now; + } + throw new IllegalArgumentException("No reachable NTP servers specified"); + } + + /** + * Query the given NTP server, returning the current internet time + * + * @return milliseconds since january 1, 1970 (UTC), or -1 on error + */ + public static long currentTime(String serverName) { + try { + // Send request + DatagramSocket socket = new DatagramSocket(); + InetAddress address = InetAddress.getByName(serverName); + byte[] buf = new NtpMessage().toByteArray(); + DatagramPacket packet = new DatagramPacket(buf, buf.length, address, NTP_PORT); + + // Set the transmit timestamp *just* before sending the packet + // ToDo: Does this actually improve performance or not? + NtpMessage.encodeTimestamp(packet.getData(), 40, + (System.currentTimeMillis()/1000.0) + + SECONDS_1900_TO_EPOCH); + + socket.send(packet); + + // Get response + packet = new DatagramPacket(buf, buf.length); + socket.receive(packet); + + // Immediately record the incoming timestamp + double destinationTimestamp = (System.currentTimeMillis()/1000.0) + SECONDS_1900_TO_EPOCH; + + // Process response + NtpMessage msg = new NtpMessage(packet.getData()); + double roundTripDelay = (destinationTimestamp-msg.originateTimestamp) - + (msg.receiveTimestamp-msg.transmitTimestamp); + double localClockOffset = ((msg.receiveTimestamp - msg.originateTimestamp) + + (msg.transmitTimestamp - destinationTimestamp)) / 2; + socket.close(); + + //System.out.println("host: " + serverName + " rtt: " + roundTripDelay + " offset: " + localClockOffset + " seconds"); + return (long)(System.currentTimeMillis() + localClockOffset*1000); + } catch (IOException ioe) { + ioe.printStackTrace(); + return -1; + } + } + + public static void main(String[] args) throws IOException { + // Process command-line args + if(args.length <= 0) { + printUsage(); + return; + // args = new String[] { "ntp1.sth.netnod.se", "ntp2.sth.netnod.se" }; + } + + long now = currentTime(args); + System.out.println("Current time: " + new java.util.Date(now)); + } + + + + /** + * Prints usage + */ + static void printUsage() { + System.out.println( + "NtpClient - an NTP client for Java.\n" + + "\n" + + "This program connects to an NTP server and prints the current time to the console.\n" + + "\n" + + "\n" + + "Usage: java NtpClient server[ server]*\n" + + "\n" + + "\n" + + "This program is copyright (c) Adam Buckley 2004 and distributed under the terms\n" + + "of the GNU General Public License. This program is distributed in the hope\n" + + "that it will be useful, but WITHOUT ANY WARRANTY; without even the implied\n" + + "warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" + + "General Public License available at http://www.gnu.org/licenses/gpl.html for\n" + + "more details."); + + } +} diff --git a/apps/time/java/src/net/i2p/time/NtpMessage.java b/apps/time/java/src/net/i2p/time/NtpMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..3e693449abe8b407f252d16028878ca58be9f45b --- /dev/null +++ b/apps/time/java/src/net/i2p/time/NtpMessage.java @@ -0,0 +1,451 @@ +package net.i2p.time; + +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + + +/** + * This class represents a NTP message, as specified in RFC 2030. The message + * format is compatible with all versions of NTP and SNTP. + * + * This class does not support the optional authentication protocol, and + * ignores the key ID and message digest fields. + * + * For convenience, this class exposes message values as native Java types, not + * the NTP-specified data formats. For example, timestamps are + * stored as doubles (as opposed to the NTP unsigned 64-bit fixed point + * format). + * + * However, the contructor NtpMessage(byte[]) and the method toByteArray() + * allow the import and export of the raw NTP message format. + * + * + * Usage example + * + * // Send message + * DatagramSocket socket = new DatagramSocket(); + * InetAddress address = InetAddress.getByName("ntp.cais.rnp.br"); + * byte[] buf = new NtpMessage().toByteArray(); + * DatagramPacket packet = new DatagramPacket(buf, buf.length, address, 123); + * socket.send(packet); + * + * // Get response + * socket.receive(packet); + * System.out.println(msg.toString()); + * + * + * This code is copyright (c) Adam Buckley 2004 + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. A HTML version of the GNU General Public License can be + * seen at http://www.gnu.org/licenses/gpl.html + * + * 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 General Public License for + * more details. + * + * + * Comments for member variables are taken from RFC2030 by David Mills, + * University of Delaware. + * + * Number format conversion code in NtpMessage(byte[] array) and toByteArray() + * inspired by http://www.pps.jussieu.fr/~jch/enseignement/reseaux/ + * NTPMessage.java which is copyright (c) 2003 by Juliusz Chroboczek + * + * @author Adam Buckley + */ +public class NtpMessage { + /** + * This is a two-bit code warning of an impending leap second to be + * inserted/deleted in the last minute of the current day. It's values + * may be as follows: + * + * Value Meaning + * ----- ------- + * 0 no warning + * 1 last minute has 61 seconds + * 2 last minute has 59 seconds) + * 3 alarm condition (clock not synchronized) + */ + public byte leapIndicator = 0; + + + /** + * This value indicates the NTP/SNTP version number. The version number + * is 3 for Version 3 (IPv4 only) and 4 for Version 4 (IPv4, IPv6 and OSI). + * If necessary to distinguish between IPv4, IPv6 and OSI, the + * encapsulating context must be inspected. + */ + public byte version = 3; + + + /** + * This value indicates the mode, with values defined as follows: + * + * Mode Meaning + * ---- ------- + * 0 reserved + * 1 symmetric active + * 2 symmetric passive + * 3 client + * 4 server + * 5 broadcast + * 6 reserved for NTP control message + * 7 reserved for private use + * + * In unicast and anycast modes, the client sets this field to 3 (client) + * in the request and the server sets it to 4 (server) in the reply. In + * multicast mode, the server sets this field to 5 (broadcast). + */ + public byte mode = 0; + + + /** + * This value indicates the stratum level of the local clock, with values + * defined as follows: + * + * Stratum Meaning + * ---------------------------------------------- + * 0 unspecified or unavailable + * 1 primary reference (e.g., radio clock) + * 2-15 secondary reference (via NTP or SNTP) + * 16-255 reserved + */ + public short stratum = 0; + + + /** + * This value indicates the maximum interval between successive messages, + * in seconds to the nearest power of two. The values that can appear in + * this field presently range from 4 (16 s) to 14 (16284 s); however, most + * applications use only the sub-range 6 (64 s) to 10 (1024 s). + */ + public byte pollInterval = 0; + + + /** + * This value indicates the precision of the local clock, in seconds to + * the nearest power of two. The values that normally appear in this field + * range from -6 for mains-frequency clocks to -20 for microsecond clocks + * found in some workstations. + */ + public byte precision = 0; + + + /** + * This value indicates the total roundtrip delay to the primary reference + * source, in seconds. Note that this variable can take on both positive + * and negative values, depending on the relative time and frequency + * offsets. The values that normally appear in this field range from + * negative values of a few milliseconds to positive values of several + * hundred milliseconds. + */ + public double rootDelay = 0; + + + /** + * This value indicates the nominal error relative to the primary reference + * source, in seconds. The values that normally appear in this field + * range from 0 to several hundred milliseconds. + */ + public double rootDispersion = 0; + + + /** + * This is a 4-byte array identifying the particular reference source. + * In the case of NTP Version 3 or Version 4 stratum-0 (unspecified) or + * stratum-1 (primary) servers, this is a four-character ASCII string, left + * justified and zero padded to 32 bits. In NTP Version 3 secondary + * servers, this is the 32-bit IPv4 address of the reference source. In NTP + * Version 4 secondary servers, this is the low order 32 bits of the latest + * transmit timestamp of the reference source. NTP primary (stratum 1) + * servers should set this field to a code identifying the external + * reference source according to the following list. If the external + * reference is one of those listed, the associated code should be used. + * Codes for sources not listed can be contrived as appropriate. + * + * Code External Reference Source + * ---- ------------------------- + * LOCL uncalibrated local clock used as a primary reference for + * a subnet without external means of synchronization + * PPS atomic clock or other pulse-per-second source + * individually calibrated to national standards + * ACTS NIST dialup modem service + * USNO USNO modem service + * PTB PTB (Germany) modem service + * TDF Allouis (France) Radio 164 kHz + * DCF Mainflingen (Germany) Radio 77.5 kHz + * MSF Rugby (UK) Radio 60 kHz + * WWV Ft. Collins (US) Radio 2.5, 5, 10, 15, 20 MHz + * WWVB Boulder (US) Radio 60 kHz + * WWVH Kaui Hawaii (US) Radio 2.5, 5, 10, 15 MHz + * CHU Ottawa (Canada) Radio 3330, 7335, 14670 kHz + * LORC LORAN-C radionavigation system + * OMEG OMEGA radionavigation system + * GPS Global Positioning Service + * GOES Geostationary Orbit Environment Satellite + */ + public byte[] referenceIdentifier = {0, 0, 0, 0}; + + + /** + * This is the time at which the local clock was last set or corrected, in + * seconds since 00:00 1-Jan-1900. + */ + public double referenceTimestamp = 0; + + + /** + * This is the time at which the request departed the client for the + * server, in seconds since 00:00 1-Jan-1900. + */ + public double originateTimestamp = 0; + + + /** + * This is the time at which the request arrived at the server, in seconds + * since 00:00 1-Jan-1900. + */ + public double receiveTimestamp = 0; + + + /** + * This is the time at which the reply departed the server for the client, + * in seconds since 00:00 1-Jan-1900. + */ + public double transmitTimestamp = 0; + + + + /** + * Constructs a new NtpMessage from an array of bytes. + */ + public NtpMessage(byte[] array) { + // See the packet format diagram in RFC 2030 for details + leapIndicator = (byte) ((array[0] >> 6) & 0x3); + version = (byte) ((array[0] >> 3) & 0x7); + mode = (byte) (array[0] & 0x7); + stratum = unsignedByteToShort(array[1]); + pollInterval = array[2]; + precision = array[3]; + + rootDelay = (array[4] * 256.0) + + unsignedByteToShort(array[5]) + + (unsignedByteToShort(array[6]) / 256.0) + + (unsignedByteToShort(array[7]) / 65536.0); + + rootDispersion = (unsignedByteToShort(array[8]) * 256.0) + + unsignedByteToShort(array[9]) + + (unsignedByteToShort(array[10]) / 256.0) + + (unsignedByteToShort(array[11]) / 65536.0); + + referenceIdentifier[0] = array[12]; + referenceIdentifier[1] = array[13]; + referenceIdentifier[2] = array[14]; + referenceIdentifier[3] = array[15]; + + referenceTimestamp = decodeTimestamp(array, 16); + originateTimestamp = decodeTimestamp(array, 24); + receiveTimestamp = decodeTimestamp(array, 32); + transmitTimestamp = decodeTimestamp(array, 40); + } + + + + /** + * Constructs a new NtpMessage in client -> server mode, and sets the + * transmit timestamp to the current time. + */ + public NtpMessage() { + // Note that all the other member variables are already set with + // appropriate default values. + this.mode = 3; + this.transmitTimestamp = (System.currentTimeMillis()/1000.0) + 2208988800.0; + } + + + + /** + * This method constructs the data bytes of a raw NTP packet. + */ + public byte[] toByteArray() { + // All bytes are automatically set to 0 + byte[] p = new byte[48]; + + p[0] = (byte) (leapIndicator << 6 | version << 3 | mode); + p[1] = (byte) stratum; + p[2] = (byte) pollInterval; + p[3] = (byte) precision; + + // root delay is a signed 16.16-bit FP, in Java an int is 32-bits + int l = (int) (rootDelay * 65536.0); + p[4] = (byte) ((l >> 24) & 0xFF); + p[5] = (byte) ((l >> 16) & 0xFF); + p[6] = (byte) ((l >> 8) & 0xFF); + p[7] = (byte) (l & 0xFF); + + // root dispersion is an unsigned 16.16-bit FP, in Java there are no + // unsigned primitive types, so we use a long which is 64-bits + long ul = (long) (rootDispersion * 65536.0); + p[8] = (byte) ((ul >> 24) & 0xFF); + p[9] = (byte) ((ul >> 16) & 0xFF); + p[10] = (byte) ((ul >> 8) & 0xFF); + p[11] = (byte) (ul & 0xFF); + + p[12] = referenceIdentifier[0]; + p[13] = referenceIdentifier[1]; + p[14] = referenceIdentifier[2]; + p[15] = referenceIdentifier[3]; + + encodeTimestamp(p, 16, referenceTimestamp); + encodeTimestamp(p, 24, originateTimestamp); + encodeTimestamp(p, 32, receiveTimestamp); + encodeTimestamp(p, 40, transmitTimestamp); + + return p; + } + + + + /** + * Returns a string representation of a NtpMessage + */ + public String toString() { + String precisionStr = new DecimalFormat("0.#E0").format(Math.pow(2, precision)); + + return "Leap indicator: " + leapIndicator + "\n" + + "Version: " + version + "\n" + + "Mode: " + mode + "\n" + + "Stratum: " + stratum + "\n" + + "Poll: " + pollInterval + "\n" + + "Precision: " + precision + " (" + precisionStr + " seconds)\n" + + "Root delay: " + new DecimalFormat("0.00").format(rootDelay*1000) + " ms\n" + + "Root dispersion: " + new DecimalFormat("0.00").format(rootDispersion*1000) + " ms\n" + + "Reference identifier: " + referenceIdentifierToString(referenceIdentifier, stratum, version) + "\n" + + "Reference timestamp: " + timestampToString(referenceTimestamp) + "\n" + + "Originate timestamp: " + timestampToString(originateTimestamp) + "\n" + + "Receive timestamp: " + timestampToString(receiveTimestamp) + "\n" + + "Transmit timestamp: " + timestampToString(transmitTimestamp); + } + + + + /** + * Converts an unsigned byte to a short. By default, Java assumes that + * a byte is signed. + */ + public static short unsignedByteToShort(byte b) { + if((b & 0x80)==0x80) + return (short) (128 + (b & 0x7f)); + else + return (short) b; + } + + + + /** + * Will read 8 bytes of a message beginning at <code>pointer</code> + * and return it as a double, according to the NTP 64-bit timestamp + * format. + */ + public static double decodeTimestamp(byte[] array, int pointer) { + double r = 0.0; + + for(int i=0; i<8; i++) { + r += unsignedByteToShort(array[pointer+i]) * Math.pow(2, (3-i)*8); + } + + return r; + } + + + + /** + * Encodes a timestamp in the specified position in the message + */ + public static void encodeTimestamp(byte[] array, int pointer, double timestamp) { + // Converts a double into a 64-bit fixed point + for(int i=0; i<8; i++) { + // 2^24, 2^16, 2^8, .. 2^-32 + double base = Math.pow(2, (3-i)*8); + + // Capture byte value + array[pointer+i] = (byte) (timestamp / base); + + // Subtract captured value from remaining total + timestamp = timestamp - (double) (unsignedByteToShort(array[pointer+i]) * base); + } + + // From RFC 2030: It is advisable to fill the non-significant + // low order bits of the timestamp with a random, unbiased + // bitstring, both to avoid systematic roundoff errors and as + // a means of loop detection and replay detection. + array[7] = (byte) (Math.random()*255.0); + } + + + + /** + * Returns a timestamp (number of seconds since 00:00 1-Jan-1900) as a + * formatted date/time string. + */ + public static String timestampToString(double timestamp) { + if(timestamp==0) return "0"; + + // timestamp is relative to 1900, utc is used by Java and is relative + // to 1970 + double utc = timestamp - (2208988800.0); + + // milliseconds + long ms = (long) (utc * 1000.0); + + // date/time + String date = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss").format(new Date(ms)); + + // fraction + double fraction = timestamp - ((long) timestamp); + String fractionSting = new DecimalFormat(".000000").format(fraction); + + return date + fractionSting; + } + + + + /** + * Returns a string representation of a reference identifier according + * to the rules set out in RFC 2030. + */ + public static String referenceIdentifierToString(byte[] ref, short stratum, byte version) { + // From the RFC 2030: + // In the case of NTP Version 3 or Version 4 stratum-0 (unspecified) + // or stratum-1 (primary) servers, this is a four-character ASCII + // string, left justified and zero padded to 32 bits. + if(stratum==0 || stratum==1) { + return new String(ref); + } + + // In NTP Version 3 secondary servers, this is the 32-bit IPv4 + // address of the reference source. + else if(version==3) { + return unsignedByteToShort(ref[0]) + "." + + unsignedByteToShort(ref[1]) + "." + + unsignedByteToShort(ref[2]) + "." + + unsignedByteToShort(ref[3]); + } + + // In NTP Version 4 secondary servers, this is the low order 32 bits + // of the latest transmit timestamp of the reference source. + else if(version==4) { + return "" + ((unsignedByteToShort(ref[0]) / 256.0) + + (unsignedByteToShort(ref[1]) / 65536.0) + + (unsignedByteToShort(ref[2]) / 16777216.0) + + (unsignedByteToShort(ref[3]) / 4294967296.0)); + } + + return ""; + } +} diff --git a/apps/time/java/src/net/i2p/time/Timestamper.java b/apps/time/java/src/net/i2p/time/Timestamper.java new file mode 100644 index 0000000000000000000000000000000000000000..d832622421986ae27b6c2bca7534e4bfdded8c41 --- /dev/null +++ b/apps/time/java/src/net/i2p/time/Timestamper.java @@ -0,0 +1,103 @@ +package net.i2p.time; + +import net.i2p.util.Log; +import net.i2p.util.I2PThread; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import java.net.URL; +import java.net.MalformedURLException; +import java.io.IOException; + +/** + * Periodically query a series of NTP servers and post the offset + * to a given URL. It tries the NTP servers in order, contacting them + * using UDP port 123, and sends the current date to the URL specified + * (specifically, URL+"&now=" + yyyyMMdd_HH:mm:ss.SSS in the UK locale). + * It does this every 5 minutes, forever. + * + * Usage: <pre> + * Timestamper URL ntpServer1[ ntpServer2]* + * </pre> + */ +public class Timestamper implements Runnable { + private static Log _log = new Log(Timestamper.class); + private String _targetURL; + private String _serverList[]; + + private int DELAY_MS = 5*60*1000; + + public Timestamper(String url, String serverNames[]) { + if (_log.shouldLog(Log.INFO)) + _log.info("Creating new timestamper pointing at " + url); + _targetURL = url; + _serverList = serverNames; + } + + public void startTimestamper() { + if (_log.shouldLog(Log.INFO)) + _log.info("Starting timestamper pointing at " + _targetURL); + I2PThread t = new I2PThread(this, "Timestamper"); + t.setPriority(I2PThread.MIN_PRIORITY); + t.start(); + } + + public void run() { + if (_log.shouldLog(Log.INFO)) + _log.info("Starting up timestamper"); + while (true) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Querying servers " + _serverList); + long now = NtpClient.currentTime(_serverList); + if (now < 0) { + _log.error("Unable to contact any of the NTP servers - network disconnect?"); + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Stamp time"); + stampTime(now); + } + try { Thread.sleep(DELAY_MS); } catch (InterruptedException ie) {} + } + } + + /** + * Send an HTTP request to a given URL specifying the current time + */ + private void stampTime(long now) { + try { + URL url = new URL(_targetURL + "&now=" + getNow(now)); + Object o = url.getContent(); + // ignore the content + } catch (MalformedURLException mue) { + _log.error("Invalid URL", mue); + } catch (IOException ioe) { + _log.error("Error stamping the time", ioe); + } + } + + private SimpleDateFormat _fmt = new SimpleDateFormat("yyyyMMdd_HH:mm:ss.SSS", Locale.UK); + private String getNow(long now) { + synchronized (_fmt) { + return _fmt.format(new Date(now)); + } + } + + public static void main(String args[]) { + if ( (args == null) || (args.length < 2) ) { + usage(); + return; + //args = new String[] { "http://dev.i2p.net:80/somePath?pass=password", "ntp1.sth.netnod.se", "ntp2.sth.netnod.se" }; + } + String servers[] = new String[args.length-1]; + System.arraycopy(args, 1, servers, 0, servers.length); + Timestamper ts = new Timestamper(args[0], servers); + ts.startTimestamper(); + } + + private static void usage() { + System.err.println("Usage: Timestamper URL ntpServer[ ntpServer]*"); + _log.error("Usage: Timestamper URL ntpServer[ ntpServer]*"); + } +} \ No newline at end of file