1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.net;
18
19import android.net.SntpClient;
20import android.util.Log;
21import libcore.util.HexEncoding;
22
23import java.io.IOException;
24import java.net.DatagramPacket;
25import java.net.DatagramSocket;
26import java.net.InetAddress;
27import java.net.SocketException;
28import java.util.Arrays;
29import junit.framework.TestCase;
30
31
32public class SntpClientTest extends TestCase {
33    private static final String TAG = "SntpClientTest";
34
35    private static final int ORIGINATE_TIME_OFFSET = 24;
36    private static final int TRANSMIT_TIME_OFFSET = 40;
37
38    private static final int NTP_MODE_SERVER = 4;
39    private static final int NTP_MODE_BROADCAST = 5;
40
41    // From tcpdump (admittedly, an NTPv4 packet):
42    //
43    // Server, Leap indicator:  (0), Stratum 2 (secondary reference), poll 6 (64s), precision -20
44    // Root Delay: 0.005447, Root dispersion: 0.002716, Reference-ID: 221.253.71.41
45    //   Reference Timestamp:  3653932102.507969856 (2015/10/15 14:08:22)
46    //   Originator Timestamp: 3653932113.576327741 (2015/10/15 14:08:33)
47    //   Receive Timestamp:    3653932113.581012725 (2015/10/15 14:08:33)
48    //   Transmit Timestamp:   3653932113.581012725 (2015/10/15 14:08:33)
49    //     Originator - Receive Timestamp:  +0.004684958
50    //     Originator - Transmit Timestamp: +0.004684958
51    private static final String WORKING_VERSION4 =
52            "240206ec" +
53            "00000165" +
54            "000000b2" +
55            "ddfd4729" +
56            "d9ca9446820a5000" +
57            "d9ca9451938a3771" +
58            "d9ca945194bd3fff" +
59            "d9ca945194bd4001";
60
61    private final SntpTestServer mServer = new SntpTestServer();
62    private final SntpClient mClient = new SntpClient();
63
64    public void testBasicWorkingSntpClientQuery() throws Exception {
65        mServer.setServerReply(HexEncoding.decode(WORKING_VERSION4.toCharArray(), false));
66        assertTrue(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500));
67        assertEquals(1, mServer.numRequestsReceived());
68        assertEquals(1, mServer.numRepliesSent());
69    }
70
71    public void testDnsResolutionFailure() throws Exception {
72        assertFalse(mClient.requestTime("ntp.server.doesnotexist.example", 5000));
73    }
74
75    public void testTimeoutFailure() throws Exception {
76        mServer.clearServerReply();
77        assertFalse(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500));
78        assertEquals(1, mServer.numRequestsReceived());
79        assertEquals(0, mServer.numRepliesSent());
80    }
81
82    public void testIgnoreLeapNoSync() throws Exception {
83        final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false);
84        reply[0] |= (byte) 0xc0;
85        mServer.setServerReply(reply);
86        assertFalse(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500));
87        assertEquals(1, mServer.numRequestsReceived());
88        assertEquals(1, mServer.numRepliesSent());
89    }
90
91    public void testAcceptOnlyServerAndBroadcastModes() throws Exception {
92        final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false);
93        for (int i = 0; i <= 7; i++) {
94            final String logMsg = "mode: " + i;
95            reply[0] &= (byte) 0xf8;
96            reply[0] |= (byte) i;
97            mServer.setServerReply(reply);
98            final boolean rval = mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500);
99            switch (i) {
100                case NTP_MODE_SERVER:
101                case NTP_MODE_BROADCAST:
102                    assertTrue(logMsg, rval);
103                    break;
104                default:
105                    assertFalse(logMsg, rval);
106                    break;
107            }
108            assertEquals(logMsg, 1, mServer.numRequestsReceived());
109            assertEquals(logMsg, 1, mServer.numRepliesSent());
110        }
111    }
112
113    public void testAcceptableStrataOnly() throws Exception {
114        final int STRATUM_MIN = 1;
115        final int STRATUM_MAX = 15;
116
117        final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false);
118        for (int i = 0; i < 256; i++) {
119            final String logMsg = "stratum: " + i;
120            reply[1] = (byte) i;
121            mServer.setServerReply(reply);
122            final boolean rval = mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500);
123            if (STRATUM_MIN <= i && i <= STRATUM_MAX) {
124                assertTrue(logMsg, rval);
125            } else {
126                assertFalse(logMsg, rval);
127            }
128            assertEquals(logMsg, 1, mServer.numRequestsReceived());
129            assertEquals(logMsg, 1, mServer.numRepliesSent());
130        }
131    }
132
133    public void testZeroTransmitTime() throws Exception {
134        final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false);
135        Arrays.fill(reply, TRANSMIT_TIME_OFFSET, TRANSMIT_TIME_OFFSET + 8, (byte) 0x00);
136        mServer.setServerReply(reply);
137        assertFalse(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500));
138        assertEquals(1, mServer.numRequestsReceived());
139        assertEquals(1, mServer.numRepliesSent());
140    }
141
142
143    private static class SntpTestServer {
144        private final Object mLock = new Object();
145        private final DatagramSocket mSocket;
146        private final InetAddress mAddress;
147        private final int mPort;
148        private byte[] mReply;
149        private int mRcvd;
150        private int mSent;
151        private Thread mListeningThread;
152
153        public SntpTestServer() {
154            mSocket = makeSocket();
155            mAddress = mSocket.getLocalAddress();
156            mPort = mSocket.getLocalPort();
157            Log.d(TAG, "testing server listening on (" + mAddress + ", " + mPort + ")");
158
159            mListeningThread = new Thread() {
160                public void run() {
161                    while (true) {
162                        byte[] buffer = new byte[512];
163                        DatagramPacket ntpMsg = new DatagramPacket(buffer, buffer.length);
164                        try {
165                            mSocket.receive(ntpMsg);
166                        } catch (IOException e) {
167                            Log.e(TAG, "datagram receive error: " + e);
168                            break;
169                        }
170                        synchronized (mLock) {
171                            mRcvd++;
172                            if (mReply == null) { continue; }
173                            // Copy transmit timestamp into originate timestamp.
174                            // TODO: bounds checking.
175                            System.arraycopy(ntpMsg.getData(), TRANSMIT_TIME_OFFSET,
176                                             mReply, ORIGINATE_TIME_OFFSET, 8);
177                            ntpMsg.setData(mReply);
178                            ntpMsg.setLength(mReply.length);
179                            try {
180                                mSocket.send(ntpMsg);
181                            } catch (IOException e) {
182                                Log.e(TAG, "datagram send error: " + e);
183                                break;
184                            }
185                            mSent++;
186                        }
187                    }
188                    mSocket.close();
189                }
190            };
191            mListeningThread.start();
192        }
193
194        private DatagramSocket makeSocket() {
195            DatagramSocket socket;
196            try {
197                socket = new DatagramSocket(0, InetAddress.getLoopbackAddress());
198            } catch (SocketException e) {
199                Log.e(TAG, "Failed to create test server socket: " + e);
200                return null;
201            }
202            return socket;
203        }
204
205        public void clearServerReply() {
206            setServerReply(null);
207        }
208
209        public void setServerReply(byte[] reply) {
210            synchronized (mLock) {
211                mReply = reply;
212                mRcvd = 0;
213                mSent = 0;
214            }
215        }
216
217        public InetAddress getAddress() { return mAddress; }
218        public int getPort() { return mPort; }
219        public int numRequestsReceived() { synchronized (mLock) { return mRcvd; } }
220        public int numRepliesSent() { synchronized (mLock) { return mSent; } }
221    }
222}
223