blob: f0b981c486a6e5edcaf0b6a9cb25ee944caad4cc [file] [log] [blame]
Thomas Vachuska24c849c2014-10-27 09:53:05 -07001/*
Brian O'Connora09fe5b2017-08-03 21:12:30 -07002 * Copyright 2014-present Open Networking Foundation
Thomas Vachuska24c849c2014-10-27 09:53:05 -07003 *
Thomas Vachuska4f1a60c2014-10-28 13:39:07 -07004 * 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
Thomas Vachuska24c849c2014-10-27 09:53:05 -07007 *
Thomas Vachuska4f1a60c2014-10-28 13:39:07 -07008 * 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.
Thomas Vachuska24c849c2014-10-27 09:53:05 -070015 */
alshabib7911a052014-10-16 17:49:37 -070016package org.onlab.packet;
17
18import com.google.common.collect.Lists;
Ayaka Koshibe12c8c082015-12-08 12:48:46 -080019import com.google.common.collect.Maps;
alshabib7911a052014-10-16 17:49:37 -070020import org.apache.commons.lang.ArrayUtils;
21
22import java.nio.ByteBuffer;
Ray Milkey241b96a2014-11-17 13:08:20 -080023import java.nio.charset.StandardCharsets;
Samuel Jero31e16f52018-09-21 10:34:28 -040024import java.security.InvalidKeyException;
25import java.security.NoSuchAlgorithmException;
jaegonkim47b1b4a2017-12-04 14:08:26 +090026import java.util.Arrays;
Jonathan Hart7838c512016-06-07 15:18:22 -070027import java.util.HashMap;
alshabib7911a052014-10-16 17:49:37 -070028
Samuel Jero31e16f52018-09-21 10:34:28 -040029import javax.crypto.Mac;
30import javax.crypto.spec.SecretKeySpec;
31
Ayaka Koshibe12c8c082015-12-08 12:48:46 -080032import static org.onlab.packet.LLDPOrganizationalTLV.OUI_LENGTH;
33import static org.onlab.packet.LLDPOrganizationalTLV.SUBTYPE_LENGTH;
34
alshabib7911a052014-10-16 17:49:37 -070035/**
Ayaka Koshibe12c8c082015-12-08 12:48:46 -080036 * ONOS LLDP containing organizational TLV for ONOS device discovery.
alshabib7911a052014-10-16 17:49:37 -070037 */
38public class ONOSLLDP extends LLDP {
Charles Chan928ff8b2017-05-04 12:22:54 -070039
alshabib7911a052014-10-16 17:49:37 -070040 public static final String DEFAULT_DEVICE = "INVALID";
41 public static final String DEFAULT_NAME = "ONOS Discovery";
42
alshabib7911a052014-10-16 17:49:37 -070043
Ayaka Koshibe12c8c082015-12-08 12:48:46 -080044 protected static final byte NAME_SUBTYPE = 1;
45 protected static final byte DEVICE_SUBTYPE = 2;
46 protected static final byte DOMAIN_SUBTYPE = 3;
Samuel Jero31e16f52018-09-21 10:34:28 -040047 protected static final byte TIMESTAMP_SUBTYPE = 4;
48 protected static final byte SIG_SUBTYPE = 5;
Ayaka Koshibe12c8c082015-12-08 12:48:46 -080049
50 private static final short NAME_LENGTH = OUI_LENGTH + SUBTYPE_LENGTH;
51 private static final short DEVICE_LENGTH = OUI_LENGTH + SUBTYPE_LENGTH;
52 private static final short DOMAIN_LENGTH = OUI_LENGTH + SUBTYPE_LENGTH;
Samuel Jero31e16f52018-09-21 10:34:28 -040053 private static final short TIMESTAMP_LENGTH = OUI_LENGTH + SUBTYPE_LENGTH;
54 private static final short SIG_LENGTH = OUI_LENGTH + SUBTYPE_LENGTH;
Ayaka Koshibe12c8c082015-12-08 12:48:46 -080055
Jonathan Hart7838c512016-06-07 15:18:22 -070056 private final HashMap<Byte, LLDPOrganizationalTLV> opttlvs = Maps.newHashMap();
alshabib7911a052014-10-16 17:49:37 -070057
58 // TLV constants: type, size and subtype
59 // Organizationally specific TLV also have packet offset and contents of TLV
60 // header
61 private static final byte CHASSIS_TLV_TYPE = 1;
62 private static final byte CHASSIS_TLV_SIZE = 7;
63 private static final byte CHASSIS_TLV_SUBTYPE = 4;
64
65 private static final byte PORT_TLV_TYPE = 2;
alshabib7911a052014-10-16 17:49:37 -070066 private static final byte PORT_TLV_SUBTYPE = 2;
67
68 private static final byte TTL_TLV_TYPE = 3;
69
jaegonkim47b1b4a2017-12-04 14:08:26 +090070 private static final byte PORT_DESC_TLV_TYPE = 4;
71
alshabib7911a052014-10-16 17:49:37 -070072 private final byte[] ttlValue = new byte[] {0, 0x78};
73
Ayaka Koshibe12c8c082015-12-08 12:48:46 -080074 // Only needs to be accessed from LinkProbeFactory.
Ray Milkey88cc3432017-03-30 17:19:08 -070075 public ONOSLLDP(byte... subtype) {
alshabib7911a052014-10-16 17:49:37 -070076 super();
Ayaka Koshibe12c8c082015-12-08 12:48:46 -080077 for (byte st : subtype) {
78 opttlvs.put(st, new LLDPOrganizationalTLV());
79 }
80 // guarantee the following (name and device) TLVs exist
81 opttlvs.putIfAbsent(NAME_SUBTYPE, new LLDPOrganizationalTLV());
82 opttlvs.putIfAbsent(DEVICE_SUBTYPE, new LLDPOrganizationalTLV());
alshabib7911a052014-10-16 17:49:37 -070083 setName(DEFAULT_NAME);
84 setDevice(DEFAULT_DEVICE);
Ayaka Koshibe12c8c082015-12-08 12:48:46 -080085
Jonathan Hart7838c512016-06-07 15:18:22 -070086 setOptionalTLVList(Lists.newArrayList(opttlvs.values()));
Yuta HIGUCHI3e848a82014-11-02 20:19:42 -080087 setTtl(new LLDPTLV().setType(TTL_TLV_TYPE)
alshabib7911a052014-10-16 17:49:37 -070088 .setLength((short) ttlValue.length)
89 .setValue(ttlValue));
alshabib7911a052014-10-16 17:49:37 -070090 }
91
92 private ONOSLLDP(LLDP lldp) {
93 this.portId = lldp.getPortId();
94 this.chassisId = lldp.getChassisId();
95 this.ttl = lldp.getTtl();
96 this.optionalTLVList = lldp.getOptionalTLVList();
97 }
98
99 public void setName(String name) {
Ayaka Koshibe12c8c082015-12-08 12:48:46 -0800100 LLDPOrganizationalTLV nametlv = opttlvs.get(NAME_SUBTYPE);
101 nametlv.setLength((short) (name.length() + NAME_LENGTH));
102 nametlv.setInfoString(name);
103 nametlv.setSubType(NAME_SUBTYPE);
Charles Chan928ff8b2017-05-04 12:22:54 -0700104 nametlv.setOUI(MacAddress.ONOS.oui());
alshabib7911a052014-10-16 17:49:37 -0700105 }
106
107 public void setDevice(String device) {
Ayaka Koshibe12c8c082015-12-08 12:48:46 -0800108 LLDPOrganizationalTLV devicetlv = opttlvs.get(DEVICE_SUBTYPE);
109 devicetlv.setInfoString(device);
110 devicetlv.setLength((short) (device.length() + DEVICE_LENGTH));
111 devicetlv.setSubType(DEVICE_SUBTYPE);
Charles Chan928ff8b2017-05-04 12:22:54 -0700112 devicetlv.setOUI(MacAddress.ONOS.oui());
Ayaka Koshibe12c8c082015-12-08 12:48:46 -0800113 }
114
115 public void setDomainInfo(String domainId) {
116 LLDPOrganizationalTLV domaintlv = opttlvs.get(DOMAIN_SUBTYPE);
117 if (domaintlv == null) {
118 // maybe warn people not to set this if remote probes aren't.
119 return;
120 }
121 domaintlv.setInfoString(domainId);
122 domaintlv.setLength((short) (domainId.length() + DOMAIN_LENGTH));
123 domaintlv.setSubType(DOMAIN_SUBTYPE);
Charles Chan928ff8b2017-05-04 12:22:54 -0700124 domaintlv.setOUI(MacAddress.ONOS.oui());
alshabib7911a052014-10-16 17:49:37 -0700125 }
126
127 public void setChassisId(final ChassisId chassisId) {
128 MacAddress chassisMac = MacAddress.valueOf(chassisId.value());
129 byte[] chassis = ArrayUtils.addAll(new byte[] {CHASSIS_TLV_SUBTYPE},
Yuta HIGUCHI3e848a82014-11-02 20:19:42 -0800130 chassisMac.toBytes());
alshabib7911a052014-10-16 17:49:37 -0700131
132 LLDPTLV chassisTLV = new LLDPTLV();
133 chassisTLV.setLength(CHASSIS_TLV_SIZE);
134 chassisTLV.setType(CHASSIS_TLV_TYPE);
135 chassisTLV.setValue(chassis);
136 this.setChassisId(chassisTLV);
137 }
138
139 public void setPortId(final int portNumber) {
140 byte[] port = ArrayUtils.addAll(new byte[] {PORT_TLV_SUBTYPE},
DongRyeol Chae0c98db2018-07-12 16:17:21 +0900141 String.valueOf(portNumber).getBytes(StandardCharsets.UTF_8));
alshabib7911a052014-10-16 17:49:37 -0700142
143 LLDPTLV portTLV = new LLDPTLV();
DongRyeol Chae0c98db2018-07-12 16:17:21 +0900144 portTLV.setLength((short) port.length);
alshabib7911a052014-10-16 17:49:37 -0700145 portTLV.setType(PORT_TLV_TYPE);
146 portTLV.setValue(port);
147 this.setPortId(portTLV);
148 }
149
Samuel Jero31e16f52018-09-21 10:34:28 -0400150 public void setTimestamp(long timestamp) {
151 LLDPOrganizationalTLV tmtlv = opttlvs.get(TIMESTAMP_SUBTYPE);
152 if (tmtlv == null) {
153 return;
154 }
155 tmtlv.setInfoString(ByteBuffer.allocate(8).putLong(timestamp).array());
156 tmtlv.setLength((short) (8 + TIMESTAMP_LENGTH));
157 tmtlv.setSubType(TIMESTAMP_SUBTYPE);
158 tmtlv.setOUI(MacAddress.ONOS.oui());
159 }
160
161 public void setSig(byte[] sig) {
162 LLDPOrganizationalTLV sigtlv = opttlvs.get(SIG_SUBTYPE);
163 if (sigtlv == null) {
164 return;
165 }
166 sigtlv.setInfoString(sig);
167 sigtlv.setLength((short) (sig.length + SIG_LENGTH));
168 sigtlv.setSubType(SIG_SUBTYPE);
169 sigtlv.setOUI(MacAddress.ONOS.oui());
170 }
171
alshabib7911a052014-10-16 17:49:37 -0700172 public LLDPOrganizationalTLV getNameTLV() {
173 for (LLDPTLV tlv : this.getOptionalTLVList()) {
174 if (tlv.getType() == LLDPOrganizationalTLV.ORGANIZATIONAL_TLV_TYPE) {
175 LLDPOrganizationalTLV orgTLV = (LLDPOrganizationalTLV) tlv;
176 if (orgTLV.getSubType() == NAME_SUBTYPE) {
177 return orgTLV;
178 }
179 }
180 }
181 return null;
182 }
183
184 public LLDPOrganizationalTLV getDeviceTLV() {
185 for (LLDPTLV tlv : this.getOptionalTLVList()) {
186 if (tlv.getType() == LLDPOrganizationalTLV.ORGANIZATIONAL_TLV_TYPE) {
Samuel Jero31e16f52018-09-21 10:34:28 -0400187 LLDPOrganizationalTLV orgTLV = (LLDPOrganizationalTLV) tlv;
alshabib7911a052014-10-16 17:49:37 -0700188 if (orgTLV.getSubType() == DEVICE_SUBTYPE) {
189 return orgTLV;
190 }
191 }
192 }
193 return null;
194 }
195
Samuel Jero31e16f52018-09-21 10:34:28 -0400196 public LLDPOrganizationalTLV getTimestampTLV() {
197 for (LLDPTLV tlv : this.getOptionalTLVList()) {
198 if (tlv.getType() == LLDPOrganizationalTLV.ORGANIZATIONAL_TLV_TYPE) {
199 LLDPOrganizationalTLV orgTLV = (LLDPOrganizationalTLV) tlv;
200 if (orgTLV.getSubType() == TIMESTAMP_SUBTYPE) {
201 return orgTLV;
202 }
203 }
204 }
205 return null;
206 }
207
208 public LLDPOrganizationalTLV getSigTLV() {
209 for (LLDPTLV tlv : this.getOptionalTLVList()) {
210 if (tlv.getType() == LLDPOrganizationalTLV.ORGANIZATIONAL_TLV_TYPE) {
211 LLDPOrganizationalTLV orgTLV = (LLDPOrganizationalTLV) tlv;
212 if (orgTLV.getSubType() == SIG_SUBTYPE) {
213 return orgTLV;
214 }
215 }
216 }
217 return null;
218 }
219
Ayaka Koshibe12c8c082015-12-08 12:48:46 -0800220 /**
221 * Gets the TLV associated with remote probing. This TLV will be null if
222 * remote probing is disabled.
223 *
224 * @return A TLV containing domain ID, or null.
225 */
226 public LLDPOrganizationalTLV getDomainTLV() {
227 for (LLDPTLV tlv : this.getOptionalTLVList()) {
228 if (tlv.getType() == LLDPOrganizationalTLV.ORGANIZATIONAL_TLV_TYPE) {
229 LLDPOrganizationalTLV orgTLV = (LLDPOrganizationalTLV) tlv;
230 if (orgTLV.getSubType() == DOMAIN_SUBTYPE) {
231 return orgTLV;
232 }
233 }
234 }
235 return null;
236 }
237
alshabib7911a052014-10-16 17:49:37 -0700238 public String getNameString() {
239 LLDPOrganizationalTLV tlv = getNameTLV();
240 if (tlv != null) {
Ray Milkey241b96a2014-11-17 13:08:20 -0800241 return new String(tlv.getInfoString(), StandardCharsets.UTF_8);
alshabib7911a052014-10-16 17:49:37 -0700242 }
243 return null;
244 }
245
246 public String getDeviceString() {
247 LLDPOrganizationalTLV tlv = getDeviceTLV();
248 if (tlv != null) {
Ray Milkey241b96a2014-11-17 13:08:20 -0800249 return new String(tlv.getInfoString(), StandardCharsets.UTF_8);
alshabib7911a052014-10-16 17:49:37 -0700250 }
251 return null;
252 }
253
Ayaka Koshibe12c8c082015-12-08 12:48:46 -0800254 public String getDomainString() {
255 LLDPOrganizationalTLV tlv = getDomainTLV();
256 if (tlv != null) {
257 return new String(tlv.getInfoString(), StandardCharsets.UTF_8);
258 }
259 return null;
260 }
261
alshabib7911a052014-10-16 17:49:37 -0700262 public Integer getPort() {
263 ByteBuffer portBB = ByteBuffer.wrap(this.getPortId().getValue());
264 portBB.position(1);
DongRyeol Chae0c98db2018-07-12 16:17:21 +0900265
266 return Integer.parseInt(new String(portBB.array(),
267 portBB.position(), portBB.remaining(), StandardCharsets.UTF_8));
alshabib7911a052014-10-16 17:49:37 -0700268 }
269
Samuel Jero31e16f52018-09-21 10:34:28 -0400270 public long getTimestamp() {
271 LLDPOrganizationalTLV tlv = getTimestampTLV();
272 if (tlv != null) {
273 ByteBuffer b = ByteBuffer.allocate(8).put(tlv.getInfoString());
274 b.flip();
275 return b.getLong();
276 }
277 return 0;
278 }
279
280 public byte[] getSig() {
281 LLDPOrganizationalTLV tlv = getSigTLV();
282 if (tlv != null) {
283 return tlv.getInfoString();
284 }
285 return null;
286 }
287
alshabib7911a052014-10-16 17:49:37 -0700288 /**
289 * Given an ethernet packet, determines if this is an LLDP from
290 * ONOS and returns the device the LLDP came from.
291 * @param eth an ethernet packet
292 * @return a the lldp packet or null
293 */
294 public static ONOSLLDP parseONOSLLDP(Ethernet eth) {
295 if (eth.getEtherType() == Ethernet.TYPE_LLDP ||
296 eth.getEtherType() == Ethernet.TYPE_BSN) {
Jonathan Hart7838c512016-06-07 15:18:22 -0700297 ONOSLLDP onosLldp = new ONOSLLDP((LLDP) eth.getPayload());
alshabib7911a052014-10-16 17:49:37 -0700298 if (ONOSLLDP.DEFAULT_NAME.equals(onosLldp.getNameString())) {
299 return onosLldp;
300 }
301 }
302 return null;
303 }
Ayaka Koshibe12c8c082015-12-08 12:48:46 -0800304
305 /**
306 * Creates a link probe for link discovery/verification.
Samuel Jero31e16f52018-09-21 10:34:28 -0400307 * @deprecated since 1.15. Insecure, do not use.
Ayaka Koshibe12c8c082015-12-08 12:48:46 -0800308 *
309 * @param deviceId The device ID as a String
310 * @param chassisId The chassis ID of the device
311 * @param portNum Port number of port to send probe out of
312 * @return ONOSLLDP probe message
313 */
Samuel Jero31e16f52018-09-21 10:34:28 -0400314 @Deprecated
Ayaka Koshibe12c8c082015-12-08 12:48:46 -0800315 public static ONOSLLDP onosLLDP(String deviceId, ChassisId chassisId, int portNum) {
316 ONOSLLDP probe = new ONOSLLDP(NAME_SUBTYPE, DEVICE_SUBTYPE);
317 probe.setPortId(portNum);
318 probe.setDevice(deviceId);
319 probe.setChassisId(chassisId);
320 return probe;
321 }
jaegonkim47b1b4a2017-12-04 14:08:26 +0900322
323 /**
324 * Creates a link probe for link discovery/verification.
325 *
326 * @param deviceId The device ID as a String
327 * @param chassisId The chassis ID of the device
328 * @param portNum Port number of port to send probe out of
Samuel Jero31e16f52018-09-21 10:34:28 -0400329 * @param secret LLDP secret
330 * @return ONOSLLDP probe message
331 */
332 public static ONOSLLDP onosSecureLLDP(String deviceId, ChassisId chassisId, int portNum, String secret) {
333 ONOSLLDP probe = null;
334 if (secret == null) {
335 probe = new ONOSLLDP(NAME_SUBTYPE, DEVICE_SUBTYPE);
336 } else {
337 probe = new ONOSLLDP(NAME_SUBTYPE, DEVICE_SUBTYPE, TIMESTAMP_SUBTYPE, SIG_SUBTYPE);
338 }
339 probe.setPortId(portNum);
340 probe.setDevice(deviceId);
341 probe.setChassisId(chassisId);
342
343 if (secret != null) {
344 /* Secure Mode */
345 long ts = System.currentTimeMillis();
346 probe.setTimestamp(ts);
347 byte[] sig = createSig(deviceId, portNum, ts, secret);
348 if (sig == null) {
349 return null;
350 }
351 probe.setSig(sig);
352 sig = null;
353 }
354 return probe;
355 }
356
357 /**
358 * Creates a link probe for link discovery/verification.
359 * @deprecated since 1.15. Insecure, do not use.
360 *
361 * @param deviceId The device ID as a String
362 * @param chassisId The chassis ID of the device
363 * @param portNum Port number of port to send probe out of
jaegonkim47b1b4a2017-12-04 14:08:26 +0900364 * @param portDesc Port description of port to send probe out of
365 * @return ONOSLLDP probe message
366 */
Samuel Jero31e16f52018-09-21 10:34:28 -0400367 @Deprecated
jaegonkim47b1b4a2017-12-04 14:08:26 +0900368 public static ONOSLLDP onosLLDP(String deviceId, ChassisId chassisId, int portNum, String portDesc) {
jaegonkim47b1b4a2017-12-04 14:08:26 +0900369 ONOSLLDP probe = onosLLDP(deviceId, chassisId, portNum);
Samuel Jero31e16f52018-09-21 10:34:28 -0400370 addPortDesc(probe, portDesc);
371 return probe;
372 }
jaegonkim47b1b4a2017-12-04 14:08:26 +0900373
Samuel Jero31e16f52018-09-21 10:34:28 -0400374 /**
375 * Creates a link probe for link discovery/verification.
376 *
377 * @param deviceId The device ID as a String
378 * @param chassisId The chassis ID of the device
379 * @param portNum Port number of port to send probe out of
380 * @param portDesc Port description of port to send probe out of
381 * @param secret LLDP secret
382 * @return ONOSLLDP probe message
383 */
384 public static ONOSLLDP onosSecureLLDP(String deviceId, ChassisId chassisId, int portNum, String portDesc,
385 String secret) {
386 ONOSLLDP probe = onosSecureLLDP(deviceId, chassisId, portNum, secret);
387 addPortDesc(probe, portDesc);
388 return probe;
389 }
390
391 private static void addPortDesc(ONOSLLDP probe, String portDesc) {
jaegonkim47b1b4a2017-12-04 14:08:26 +0900392 if (portDesc != null && !portDesc.isEmpty()) {
393 byte[] bPortDesc = portDesc.getBytes(StandardCharsets.UTF_8);
394
395 if (bPortDesc.length > LLDPTLV.MAX_LENGTH) {
396 bPortDesc = Arrays.copyOf(bPortDesc, LLDPTLV.MAX_LENGTH);
397 }
398 LLDPTLV portDescTlv = new LLDPTLV()
399 .setType(PORT_DESC_TLV_TYPE)
400 .setLength((short) bPortDesc.length)
401 .setValue(bPortDesc);
402 probe.addOptionalTLV(portDescTlv);
403 }
Samuel Jero31e16f52018-09-21 10:34:28 -0400404 }
405
406 private static byte[] createSig(String deviceId, int portNum, long timestamp, String secret) {
407 byte[] pnb = ByteBuffer.allocate(8).putLong(portNum).array();
408 byte[] tmb = ByteBuffer.allocate(8).putLong(timestamp).array();
409
410 try {
411 SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
412 Mac mac = Mac.getInstance("HmacSHA256");
413 mac.init(signingKey);
414 mac.update(deviceId.getBytes());
415 mac.update(pnb);
416 mac.update(tmb);
417 byte[] sig = mac.doFinal();
418 return sig;
419 } catch (NoSuchAlgorithmException e) {
420 return null;
421 } catch (InvalidKeyException e) {
422 return null;
423 }
424 }
425
426 private static boolean verifySig(byte[] sig, String deviceId, int portNum, long timestamp, String secret) {
427 byte[] nsig = createSig(deviceId, portNum, timestamp, secret);
428 if (nsig == null) {
429 return false;
430 }
431
432 if (!ArrayUtils.isSameLength(nsig, sig)) {
433 return false;
434 }
435
436 boolean fail = false;
437 for (int i = 0; i < nsig.length; i++) {
438 if (sig[i] != nsig[i]) {
439 fail = true;
440 }
441 }
442 if (fail) {
443 return false;
444 }
445 return true;
446 }
447
448 public static boolean verify(ONOSLLDP probe, String secret, long maxDelay) {
449 if (secret == null) {
450 return true;
451 }
452
453 String deviceId = probe.getDeviceString();
454 int portNum = probe.getPort();
455 long timestamp = probe.getTimestamp();
456 byte[] sig = probe.getSig();
457
458 if (deviceId == null || sig == null) {
459 return false;
460 }
461
462 if (timestamp + maxDelay <= System.currentTimeMillis() ||
463 timestamp > System.currentTimeMillis()) {
464 return false;
465 }
466
467 return verifySig(sig, deviceId, portNum, timestamp, secret);
jaegonkim47b1b4a2017-12-04 14:08:26 +0900468 }
469
alshabib7911a052014-10-16 17:49:37 -0700470}