001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.net.ntp;
019
020import java.net.DatagramPacket;
021import java.net.InetAddress;
022import java.util.ArrayList;
023import java.util.List;
024
025/**
026 * Wrapper class to network time packet messages (NTP, etc.) that computes related timing info and stats.
027 */
028public class TimeInfo {
029
030    private final NtpV3Packet message;
031    private List<String> comments;
032    private Long delayMillis;
033    private Long offsetMillis;
034
035    /**
036     * time at which time message packet was received by local machine
037     */
038    private final long returnTimeMillis;
039
040    /**
041     * flag indicating that the TimeInfo details was processed and delay/offset were computed
042     */
043    private boolean detailsComputed;
044
045    /**
046     * Create TimeInfo object with raw packet message and destination time received.
047     *
048     * @param message          NTP message packet
049     * @param returnTimeMillis destination receive time
050     * @throws IllegalArgumentException if message is null
051     */
052    public TimeInfo(final NtpV3Packet message, final long returnTimeMillis) {
053        this(message, returnTimeMillis, null, true);
054    }
055
056    /**
057     * Create TimeInfo object with raw packet message and destination time received. Auto-computes details if computeDetails flag set otherwise this is delayed
058     * until computeDetails() is called. Delayed computation is for fast initialization when sub-millisecond timing is needed.
059     *
060     * @param msgPacket        NTP message packet
061     * @param returnTimeMillis destination receive time
062     * @param doComputeDetails flag to pre-compute delay/offset values
063     * @throws IllegalArgumentException if message is null
064     */
065    public TimeInfo(final NtpV3Packet msgPacket, final long returnTimeMillis, final boolean doComputeDetails) {
066        this(msgPacket, returnTimeMillis, null, doComputeDetails);
067    }
068
069    /**
070     * Create TimeInfo object with raw packet message and destination time received.
071     *
072     * @param message          NTP message packet
073     * @param returnTimeMillis destination receive time
074     * @param comments         List of errors/warnings identified during processing
075     * @throws IllegalArgumentException if message is null
076     */
077    public TimeInfo(final NtpV3Packet message, final long returnTimeMillis, final List<String> comments) {
078        this(message, returnTimeMillis, comments, true);
079    }
080
081    /**
082     * Create TimeInfo object with raw packet message and destination time received. Auto-computes details if computeDetails flag set otherwise this is delayed
083     * until computeDetails() is called. Delayed computation is for fast initialization when sub-millisecond timing is needed.
084     *
085     * @param message          NTP message packet
086     * @param returnTimeMillis destination receive time
087     * @param comments         list of comments used to store errors/warnings with message
088     * @param doComputeDetails flag to pre-compute delay/offset values
089     * @throws IllegalArgumentException if message is null
090     */
091    public TimeInfo(final NtpV3Packet message, final long returnTimeMillis, final List<String> comments, final boolean doComputeDetails) {
092        if (message == null) {
093            throw new IllegalArgumentException("message cannot be null");
094        }
095        this.returnTimeMillis = returnTimeMillis;
096        this.message = message;
097        this.comments = comments;
098        if (doComputeDetails) {
099            computeDetails();
100        }
101    }
102
103    /**
104     * Add comment (error/warning) to list of comments associated with processing of NTP parameters. If comment list not create then one will be created.
105     *
106     * @param comment the comment
107     */
108    public void addComment(final String comment) {
109        if (comments == null) {
110            comments = new ArrayList<>();
111        }
112        comments.add(comment);
113    }
114
115    /**
116     * Compute and validate details of the NTP message packet. Computed fields include the offset and delay.
117     */
118    public void computeDetails() {
119        if (detailsComputed) {
120            return; // details already computed - do nothing
121        }
122        detailsComputed = true;
123        if (comments == null) {
124            comments = new ArrayList<>();
125        }
126
127        final TimeStamp origNtpTime = message.getOriginateTimeStamp();
128        final long origTimeMillis = origNtpTime.getTime();
129
130        // Receive Time is time request received by server (t2)
131        final TimeStamp rcvNtpTime = message.getReceiveTimeStamp();
132        final long rcvTimeMillis = rcvNtpTime.getTime();
133
134        // Transmit time is time reply sent by server (t3)
135        final TimeStamp xmitNtpTime = message.getTransmitTimeStamp();
136        final long xmitTimeMillis = xmitNtpTime.getTime();
137
138        /*
139         * Round-trip network delay and local clock offset (or time drift) is calculated according to this standard NTP equation:
140         *
141         * LocalClockOffset = ((ReceiveTimestamp - OriginateTimestamp) + (TransmitTimestamp - DestinationTimestamp)) / 2
142         *
143         * equations from RFC-1305 (NTPv3) roundtrip delay = (t4 - t1) - (t3 - t2) local clock offset = ((t2 - t1) + (t3 - t4)) / 2
144         *
145         * It takes into account network delays and assumes that they are symmetrical.
146         *
147         * Note the typo in SNTP RFCs 1769/2030 which state that the delay is (T4 - T1) - (T2 - T3) with the "T2" and "T3" switched.
148         */
149        if (origNtpTime.ntpValue() == 0) {
150            // without originate time cannot determine when packet went out
151            // might be via a broadcast NTP packet...
152            if (xmitNtpTime.ntpValue() != 0) {
153                offsetMillis = Long.valueOf(xmitTimeMillis - returnTimeMillis);
154                comments.add("Error: zero orig time -- cannot compute delay");
155            } else {
156                comments.add("Error: zero orig time -- cannot compute delay/offset");
157            }
158        } else if (rcvNtpTime.ntpValue() == 0 || xmitNtpTime.ntpValue() == 0) {
159            comments.add("Warning: zero rcvNtpTime or xmitNtpTime");
160            // assert destTime >= origTime since network delay cannot be negative
161            if (origTimeMillis > returnTimeMillis) {
162                comments.add("Error: OrigTime > DestRcvTime");
163            } else {
164                // without receive or xmit time cannot figure out processing time
165                // so delay is simply the network travel time
166                delayMillis = Long.valueOf(returnTimeMillis - origTimeMillis);
167            }
168            // TODO: is offset still valid if rcvNtpTime=0 || xmitNtpTime=0 ???
169            // Could always hash origNtpTime (sendTime) but if host doesn't set it
170            // then it's an malformed ntp host anyway and we don't care?
171            // If server is in broadcast mode then we never send out a query in first place...
172            if (rcvNtpTime.ntpValue() != 0) {
173                // xmitTime is 0 just use rcv time
174                offsetMillis = Long.valueOf(rcvTimeMillis - origTimeMillis);
175            } else if (xmitNtpTime.ntpValue() != 0) {
176                // rcvTime is 0 just use xmitTime time
177                offsetMillis = Long.valueOf(xmitTimeMillis - returnTimeMillis);
178            }
179        } else {
180            long delayValueMillis = returnTimeMillis - origTimeMillis;
181            // assert xmitTime >= rcvTime: difference typically < 1ms
182            if (xmitTimeMillis < rcvTimeMillis) {
183                // server cannot send out a packet before receiving it...
184                comments.add("Error: xmitTime < rcvTime"); // time-travel not allowed
185            } else {
186                // subtract processing time from round-trip network delay
187                final long deltaMillis = xmitTimeMillis - rcvTimeMillis;
188                // in normal cases the processing delta is less than
189                // the total roundtrip network travel time.
190                if (deltaMillis <= delayValueMillis) {
191                    delayValueMillis -= deltaMillis; // delay = (t4 - t1) - (t3 - t2)
192                } else // if delta - delayValue == 1 ms then it's a round-off error
193                // e.g. delay=3ms, processing=4ms
194                if (deltaMillis - delayValueMillis == 1) {
195                    // delayValue == 0 -> local clock saw no tick change but destination clock did
196                    if (delayValueMillis != 0) {
197                        comments.add("Info: processing time > total network time by 1 ms -> assume zero delay");
198                        delayValueMillis = 0;
199                    }
200                } else {
201                    comments.add("Warning: processing time > total network time");
202                }
203            }
204            delayMillis = Long.valueOf(delayValueMillis);
205            if (origTimeMillis > returnTimeMillis) {
206                comments.add("Error: OrigTime > DestRcvTime");
207            }
208
209            offsetMillis = Long.valueOf((rcvTimeMillis - origTimeMillis + xmitTimeMillis - returnTimeMillis) / 2);
210        }
211    }
212
213    /**
214     * Compares this object against the specified object. The result is {@code true} if and only if the argument is not <code>null</code> and is a
215     * <code>TimeStamp</code> object that contains the same values as this object.
216     *
217     * @param obj the object to compare with.
218     * @return {@code true} if the objects are the same; {@code false} otherwise.
219     * @since 3.4
220     */
221    @Override
222    public boolean equals(final Object obj) {
223        if (this == obj) {
224            return true;
225        }
226        if (obj == null || getClass() != obj.getClass()) {
227            return false;
228        }
229        final TimeInfo other = (TimeInfo) obj;
230        return returnTimeMillis == other.returnTimeMillis && message.equals(other.message);
231    }
232
233    /**
234     * Get host address from message datagram if available
235     *
236     * @return host address of available otherwise null
237     * @since 3.4
238     */
239    public InetAddress getAddress() {
240        final DatagramPacket pkt = message.getDatagramPacket();
241        return pkt == null ? null : pkt.getAddress();
242    }
243
244    /**
245     * Return list of comments (if any) during processing of NTP packet.
246     *
247     * @return List or null if not yet computed
248     */
249    public List<String> getComments() {
250        return comments;
251    }
252
253    /**
254     * Get round-trip network delay. If null then could not compute the delay.
255     *
256     * @return Long or null if delay not available.
257     */
258    public Long getDelay() {
259        return delayMillis;
260    }
261
262    /**
263     * Returns NTP message packet.
264     *
265     * @return NTP message packet.
266     */
267    public NtpV3Packet getMessage() {
268        return message;
269    }
270
271    /**
272     * Get clock offset needed to adjust local clock to match remote clock. If null then could not compute the offset.
273     *
274     * @return Long or null if offset not available.
275     */
276    public Long getOffset() {
277        return offsetMillis;
278    }
279
280    /**
281     * Returns time at which time message packet was received by local machine.
282     *
283     * @return packet return time.
284     */
285    public long getReturnTime() {
286        return returnTimeMillis;
287    }
288
289    /**
290     * Computes a hash code for this object. The result is the exclusive OR of the return time and the message hash code.
291     *
292     * @return a hash code value for this object.
293     * @since 3.4
294     */
295    @Override
296    public int hashCode() {
297        final int prime = 31;
298        int result = (int) returnTimeMillis;
299        result = prime * result + message.hashCode();
300        return result;
301    }
302
303}