1 /* 2 * Copyright 2015 The gRPC Authors 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 17 package io.grpc.internal; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 21 import com.google.common.annotations.VisibleForTesting; 22 import com.google.common.base.Preconditions; 23 import com.google.common.base.Throwables; 24 import com.google.common.base.Verify; 25 import io.grpc.Attributes; 26 import io.grpc.EquivalentAddressGroup; 27 import io.grpc.NameResolver; 28 import io.grpc.Status; 29 import io.grpc.internal.SharedResourceHolder.Resource; 30 import java.io.IOException; 31 import java.lang.reflect.Constructor; 32 import java.net.InetAddress; 33 import java.net.InetSocketAddress; 34 import java.net.URI; 35 import java.net.UnknownHostException; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.Collections; 39 import java.util.HashSet; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Map.Entry; 43 import java.util.Random; 44 import java.util.Set; 45 import java.util.concurrent.ExecutorService; 46 import java.util.concurrent.atomic.AtomicReference; 47 import java.util.logging.Level; 48 import java.util.logging.Logger; 49 import javax.annotation.Nullable; 50 import javax.annotation.concurrent.GuardedBy; 51 52 /** 53 * A DNS-based {@link NameResolver}. 54 * 55 * <p>Each {@code A} or {@code AAAA} record emits an {@link EquivalentAddressGroup} in the list 56 * passed to {@link NameResolver.Listener#onAddresses(List, Attributes)} 57 * 58 * @see DnsNameResolverProvider 59 */ 60 final class DnsNameResolver extends NameResolver { 61 62 private static final Logger logger = Logger.getLogger(DnsNameResolver.class.getName()); 63 64 private static final String SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY = "clientLanguage"; 65 private static final String SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY = "percentage"; 66 private static final String SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY = "clientHostname"; 67 private static final String SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY = "serviceConfig"; 68 69 // From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md 70 static final String SERVICE_CONFIG_PREFIX = "_grpc_config="; 71 private static final Set<String> SERVICE_CONFIG_CHOICE_KEYS = 72 Collections.unmodifiableSet( 73 new HashSet<String>( 74 Arrays.asList( 75 SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY, 76 SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY, 77 SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY, 78 SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY))); 79 80 // From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md 81 private static final String SERVICE_CONFIG_NAME_PREFIX = "_grpc_config."; 82 // From https://github.com/grpc/proposal/blob/master/A5-grpclb-in-dns.md 83 private static final String GRPCLB_NAME_PREFIX = "_grpclb._tcp."; 84 85 private static final String JNDI_PROPERTY = 86 System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_jndi", "true"); 87 private static final String JNDI_LOCALHOST_PROPERTY = 88 System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_jndi_localhost", "false"); 89 private static final String JNDI_SRV_PROPERTY = 90 System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_grpclb", "false"); 91 private static final String JNDI_TXT_PROPERTY = 92 System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_service_config", "false"); 93 94 @VisibleForTesting 95 static boolean enableJndi = Boolean.parseBoolean(JNDI_PROPERTY); 96 @VisibleForTesting 97 static boolean enableJndiLocalhost = Boolean.parseBoolean(JNDI_LOCALHOST_PROPERTY); 98 @VisibleForTesting 99 static boolean enableSrv = Boolean.parseBoolean(JNDI_SRV_PROPERTY); 100 @VisibleForTesting 101 static boolean enableTxt = Boolean.parseBoolean(JNDI_TXT_PROPERTY); 102 103 104 private static final ResourceResolverFactory resourceResolverFactory = 105 getResourceResolverFactory(DnsNameResolver.class.getClassLoader()); 106 107 @VisibleForTesting 108 final ProxyDetector proxyDetector; 109 110 /** Access through {@link #getLocalHostname}. */ 111 private static String localHostname; 112 113 private final Random random = new Random(); 114 115 private volatile AddressResolver addressResolver = JdkAddressResolver.INSTANCE; 116 private final AtomicReference<ResourceResolver> resourceResolver = 117 new AtomicReference<ResourceResolver>(); 118 119 private final String authority; 120 private final String host; 121 private final int port; 122 private final Resource<ExecutorService> executorResource; 123 @GuardedBy("this") 124 private boolean shutdown; 125 @GuardedBy("this") 126 private ExecutorService executor; 127 @GuardedBy("this") 128 private boolean resolving; 129 @GuardedBy("this") 130 private Listener listener; 131 DnsNameResolver(@ullable String nsAuthority, String name, Attributes params, Resource<ExecutorService> executorResource, ProxyDetector proxyDetector)132 DnsNameResolver(@Nullable String nsAuthority, String name, Attributes params, 133 Resource<ExecutorService> executorResource, 134 ProxyDetector proxyDetector) { 135 // TODO: if a DNS server is provided as nsAuthority, use it. 136 // https://www.captechconsulting.com/blogs/accessing-the-dusty-corners-of-dns-with-java 137 this.executorResource = executorResource; 138 // Must prepend a "//" to the name when constructing a URI, otherwise it will be treated as an 139 // opaque URI, thus the authority and host of the resulted URI would be null. 140 URI nameUri = URI.create("//" + checkNotNull(name, "name")); 141 Preconditions.checkArgument(nameUri.getHost() != null, "Invalid DNS name: %s", name); 142 authority = Preconditions.checkNotNull(nameUri.getAuthority(), 143 "nameUri (%s) doesn't have an authority", nameUri); 144 host = nameUri.getHost(); 145 if (nameUri.getPort() == -1) { 146 Integer defaultPort = params.get(NameResolver.Factory.PARAMS_DEFAULT_PORT); 147 if (defaultPort != null) { 148 port = defaultPort; 149 } else { 150 throw new IllegalArgumentException( 151 "name '" + name + "' doesn't contain a port, and default port is not set in params"); 152 } 153 } else { 154 port = nameUri.getPort(); 155 } 156 this.proxyDetector = proxyDetector; 157 } 158 159 @Override getServiceAuthority()160 public final String getServiceAuthority() { 161 return authority; 162 } 163 164 @Override start(Listener listener)165 public final synchronized void start(Listener listener) { 166 Preconditions.checkState(this.listener == null, "already started"); 167 executor = SharedResourceHolder.get(executorResource); 168 this.listener = Preconditions.checkNotNull(listener, "listener"); 169 resolve(); 170 } 171 172 @Override refresh()173 public final synchronized void refresh() { 174 Preconditions.checkState(listener != null, "not started"); 175 resolve(); 176 } 177 178 private final Runnable resolutionRunnable = new Runnable() { 179 @Override 180 public void run() { 181 Listener savedListener; 182 synchronized (DnsNameResolver.this) { 183 if (shutdown) { 184 return; 185 } 186 savedListener = listener; 187 resolving = true; 188 } 189 try { 190 InetSocketAddress destination = InetSocketAddress.createUnresolved(host, port); 191 ProxyParameters proxy; 192 try { 193 proxy = proxyDetector.proxyFor(destination); 194 } catch (IOException e) { 195 savedListener.onError( 196 Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e)); 197 return; 198 } 199 if (proxy != null) { 200 EquivalentAddressGroup server = 201 new EquivalentAddressGroup( 202 new ProxySocketAddress(destination, proxy)); 203 savedListener.onAddresses(Collections.singletonList(server), Attributes.EMPTY); 204 return; 205 } 206 207 ResolutionResults resolutionResults; 208 try { 209 ResourceResolver resourceResolver = null; 210 if (shouldUseJndi(enableJndi, enableJndiLocalhost, host)) { 211 resourceResolver = getResourceResolver(); 212 } 213 resolutionResults = 214 resolveAll(addressResolver, resourceResolver, enableSrv, enableTxt, host); 215 } catch (Exception e) { 216 savedListener.onError( 217 Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e)); 218 return; 219 } 220 // Each address forms an EAG 221 List<EquivalentAddressGroup> servers = new ArrayList<>(); 222 for (InetAddress inetAddr : resolutionResults.addresses) { 223 servers.add(new EquivalentAddressGroup(new InetSocketAddress(inetAddr, port))); 224 } 225 servers.addAll(resolutionResults.balancerAddresses); 226 227 Attributes.Builder attrs = Attributes.newBuilder(); 228 if (!resolutionResults.txtRecords.isEmpty()) { 229 Map<String, Object> serviceConfig = null; 230 try { 231 for (Map<String, Object> possibleConfig : 232 parseTxtResults(resolutionResults.txtRecords)) { 233 try { 234 serviceConfig = 235 maybeChooseServiceConfig(possibleConfig, random, getLocalHostname()); 236 } catch (RuntimeException e) { 237 logger.log(Level.WARNING, "Bad service config choice " + possibleConfig, e); 238 } 239 if (serviceConfig != null) { 240 break; 241 } 242 } 243 } catch (RuntimeException e) { 244 logger.log(Level.WARNING, "Can't parse service Configs", e); 245 } 246 if (serviceConfig != null) { 247 attrs.set(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG, serviceConfig); 248 } 249 } else { 250 logger.log(Level.FINE, "No TXT records found for {0}", new Object[]{host}); 251 } 252 savedListener.onAddresses(servers, attrs.build()); 253 } finally { 254 synchronized (DnsNameResolver.this) { 255 resolving = false; 256 } 257 } 258 } 259 }; 260 261 @GuardedBy("this") resolve()262 private void resolve() { 263 if (resolving || shutdown) { 264 return; 265 } 266 executor.execute(resolutionRunnable); 267 } 268 269 @Override shutdown()270 public final synchronized void shutdown() { 271 if (shutdown) { 272 return; 273 } 274 shutdown = true; 275 if (executor != null) { 276 executor = SharedResourceHolder.release(executorResource, executor); 277 } 278 } 279 getPort()280 final int getPort() { 281 return port; 282 } 283 284 @VisibleForTesting resolveAll( AddressResolver addressResolver, @Nullable ResourceResolver resourceResolver, boolean requestSrvRecords, boolean requestTxtRecords, String name)285 static ResolutionResults resolveAll( 286 AddressResolver addressResolver, 287 @Nullable ResourceResolver resourceResolver, 288 boolean requestSrvRecords, 289 boolean requestTxtRecords, 290 String name) { 291 List<? extends InetAddress> addresses = Collections.emptyList(); 292 Exception addressesException = null; 293 List<EquivalentAddressGroup> balancerAddresses = Collections.emptyList(); 294 Exception balancerAddressesException = null; 295 List<String> txtRecords = Collections.emptyList(); 296 Exception txtRecordsException = null; 297 298 try { 299 addresses = addressResolver.resolveAddress(name); 300 } catch (Exception e) { 301 addressesException = e; 302 } 303 if (resourceResolver != null) { 304 if (requestSrvRecords) { 305 try { 306 balancerAddresses = 307 resourceResolver.resolveSrv(addressResolver, GRPCLB_NAME_PREFIX + name); 308 } catch (Exception e) { 309 balancerAddressesException = e; 310 } 311 } 312 if (requestTxtRecords) { 313 boolean balancerLookupFailedOrNotAttempted = 314 !requestSrvRecords || balancerAddressesException != null; 315 boolean dontResolveTxt = 316 (addressesException != null) && balancerLookupFailedOrNotAttempted; 317 // Only do the TXT record lookup if one of the above address resolutions succeeded. 318 if (!dontResolveTxt) { 319 try { 320 txtRecords = resourceResolver.resolveTxt(SERVICE_CONFIG_NAME_PREFIX + name); 321 } catch (Exception e) { 322 txtRecordsException = e; 323 } 324 } 325 } 326 } 327 try { 328 if (addressesException != null 329 && (balancerAddressesException != null || balancerAddresses.isEmpty())) { 330 Throwables.throwIfUnchecked(addressesException); 331 throw new RuntimeException(addressesException); 332 } 333 } finally { 334 if (addressesException != null) { 335 logger.log(Level.FINE, "Address resolution failure", addressesException); 336 } 337 if (balancerAddressesException != null) { 338 logger.log(Level.FINE, "Balancer resolution failure", balancerAddressesException); 339 } 340 if (txtRecordsException != null) { 341 logger.log(Level.FINE, "ServiceConfig resolution failure", txtRecordsException); 342 } 343 } 344 return new ResolutionResults(addresses, txtRecords, balancerAddresses); 345 } 346 347 @SuppressWarnings("unchecked") 348 @VisibleForTesting parseTxtResults(List<String> txtRecords)349 static List<Map<String, Object>> parseTxtResults(List<String> txtRecords) { 350 List<Map<String, Object>> serviceConfigs = new ArrayList<Map<String, Object>>(); 351 for (String txtRecord : txtRecords) { 352 if (txtRecord.startsWith(SERVICE_CONFIG_PREFIX)) { 353 List<Map<String, Object>> choices; 354 try { 355 Object rawChoices = JsonParser.parse(txtRecord.substring(SERVICE_CONFIG_PREFIX.length())); 356 if (!(rawChoices instanceof List)) { 357 throw new IOException("wrong type " + rawChoices); 358 } 359 List<Object> listChoices = (List<Object>) rawChoices; 360 for (Object obj : listChoices) { 361 if (!(obj instanceof Map)) { 362 throw new IOException("wrong element type " + rawChoices); 363 } 364 } 365 choices = (List<Map<String, Object>>) (List<?>) listChoices; 366 } catch (IOException e) { 367 logger.log(Level.WARNING, "Bad service config: " + txtRecord, e); 368 continue; 369 } 370 serviceConfigs.addAll(choices); 371 } else { 372 logger.log(Level.FINE, "Ignoring non service config {0}", new Object[]{txtRecord}); 373 } 374 } 375 return serviceConfigs; 376 } 377 378 @Nullable getPercentageFromChoice( Map<String, Object> serviceConfigChoice)379 private static final Double getPercentageFromChoice( 380 Map<String, Object> serviceConfigChoice) { 381 if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY)) { 382 return null; 383 } 384 return ServiceConfigUtil.getDouble(serviceConfigChoice, SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY); 385 } 386 387 @Nullable getClientLanguagesFromChoice( Map<String, Object> serviceConfigChoice)388 private static final List<String> getClientLanguagesFromChoice( 389 Map<String, Object> serviceConfigChoice) { 390 if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY)) { 391 return null; 392 } 393 return ServiceConfigUtil.checkStringList( 394 ServiceConfigUtil.getList(serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY)); 395 } 396 397 @Nullable getHostnamesFromChoice( Map<String, Object> serviceConfigChoice)398 private static final List<String> getHostnamesFromChoice( 399 Map<String, Object> serviceConfigChoice) { 400 if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY)) { 401 return null; 402 } 403 return ServiceConfigUtil.checkStringList( 404 ServiceConfigUtil.getList(serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY)); 405 } 406 407 /** 408 * Determines if a given Service Config choice applies, and if so, returns it. 409 * 410 * @see <a href="https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md"> 411 * Service Config in DNS</a> 412 * @param choice The service config choice. 413 * @return The service config object or {@code null} if this choice does not apply. 414 */ 415 @Nullable 416 @SuppressWarnings("BetaApi") // Verify isn't all that beta 417 @VisibleForTesting maybeChooseServiceConfig( Map<String, Object> choice, Random random, String hostname)418 static Map<String, Object> maybeChooseServiceConfig( 419 Map<String, Object> choice, Random random, String hostname) { 420 for (Entry<String, ?> entry : choice.entrySet()) { 421 Verify.verify(SERVICE_CONFIG_CHOICE_KEYS.contains(entry.getKey()), "Bad key: %s", entry); 422 } 423 424 List<String> clientLanguages = getClientLanguagesFromChoice(choice); 425 if (clientLanguages != null && !clientLanguages.isEmpty()) { 426 boolean javaPresent = false; 427 for (String lang : clientLanguages) { 428 if ("java".equalsIgnoreCase(lang)) { 429 javaPresent = true; 430 break; 431 } 432 } 433 if (!javaPresent) { 434 return null; 435 } 436 } 437 Double percentage = getPercentageFromChoice(choice); 438 if (percentage != null) { 439 int pct = percentage.intValue(); 440 Verify.verify(pct >= 0 && pct <= 100, "Bad percentage: %s", percentage); 441 if (random.nextInt(100) >= pct) { 442 return null; 443 } 444 } 445 List<String> clientHostnames = getHostnamesFromChoice(choice); 446 if (clientHostnames != null && !clientHostnames.isEmpty()) { 447 boolean hostnamePresent = false; 448 for (String clientHostname : clientHostnames) { 449 if (clientHostname.equals(hostname)) { 450 hostnamePresent = true; 451 break; 452 } 453 } 454 if (!hostnamePresent) { 455 return null; 456 } 457 } 458 return ServiceConfigUtil.getObject(choice, SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY); 459 } 460 461 /** 462 * Describes the results from a DNS query. 463 */ 464 @VisibleForTesting 465 static final class ResolutionResults { 466 final List<? extends InetAddress> addresses; 467 final List<String> txtRecords; 468 final List<EquivalentAddressGroup> balancerAddresses; 469 ResolutionResults( List<? extends InetAddress> addresses, List<String> txtRecords, List<EquivalentAddressGroup> balancerAddresses)470 ResolutionResults( 471 List<? extends InetAddress> addresses, 472 List<String> txtRecords, 473 List<EquivalentAddressGroup> balancerAddresses) { 474 this.addresses = Collections.unmodifiableList(checkNotNull(addresses, "addresses")); 475 this.txtRecords = Collections.unmodifiableList(checkNotNull(txtRecords, "txtRecords")); 476 this.balancerAddresses = 477 Collections.unmodifiableList(checkNotNull(balancerAddresses, "balancerAddresses")); 478 } 479 } 480 481 @VisibleForTesting setAddressResolver(AddressResolver addressResolver)482 void setAddressResolver(AddressResolver addressResolver) { 483 this.addressResolver = addressResolver; 484 } 485 486 /** 487 * {@link ResourceResolverFactory} is a factory for making resource resolvers. It supports 488 * optionally checking if the factory is available. 489 */ 490 interface ResourceResolverFactory { 491 492 /** 493 * Creates a new resource resolver. The return value is {@code null} iff 494 * {@link #unavailabilityCause()} is not null; 495 */ newResourceResolver()496 @Nullable ResourceResolver newResourceResolver(); 497 498 /** 499 * Returns the reason why the resource resolver cannot be created. The return value is 500 * {@code null} if {@link #newResourceResolver()} is suitable for use. 501 */ unavailabilityCause()502 @Nullable Throwable unavailabilityCause(); 503 } 504 505 /** 506 * AddressResolver resolves a hostname into a list of addresses. 507 */ 508 interface AddressResolver { resolveAddress(String host)509 List<InetAddress> resolveAddress(String host) throws Exception; 510 } 511 512 private enum JdkAddressResolver implements AddressResolver { 513 INSTANCE; 514 515 @Override resolveAddress(String host)516 public List<InetAddress> resolveAddress(String host) throws UnknownHostException { 517 return Collections.unmodifiableList(Arrays.asList(InetAddress.getAllByName(host))); 518 } 519 } 520 521 /** 522 * {@link ResourceResolver} is a Dns ResourceRecord resolver. 523 */ 524 interface ResourceResolver { resolveTxt(String host)525 List<String> resolveTxt(String host) throws Exception; 526 resolveSrv( AddressResolver addressResolver, String host)527 List<EquivalentAddressGroup> resolveSrv( 528 AddressResolver addressResolver, String host) throws Exception; 529 } 530 531 @Nullable getResourceResolver()532 private ResourceResolver getResourceResolver() { 533 ResourceResolver rr; 534 if ((rr = resourceResolver.get()) == null) { 535 if (resourceResolverFactory != null) { 536 assert resourceResolverFactory.unavailabilityCause() == null; 537 rr = resourceResolverFactory.newResourceResolver(); 538 } 539 } 540 return rr; 541 } 542 543 @Nullable 544 @VisibleForTesting getResourceResolverFactory(ClassLoader loader)545 static ResourceResolverFactory getResourceResolverFactory(ClassLoader loader) { 546 Class<? extends ResourceResolverFactory> jndiClazz; 547 try { 548 jndiClazz = 549 Class.forName("io.grpc.internal.JndiResourceResolverFactory", true, loader) 550 .asSubclass(ResourceResolverFactory.class); 551 } catch (ClassNotFoundException e) { 552 logger.log(Level.FINE, "Unable to find JndiResourceResolverFactory, skipping.", e); 553 return null; 554 } 555 Constructor<? extends ResourceResolverFactory> jndiCtor; 556 try { 557 jndiCtor = jndiClazz.getConstructor(); 558 } catch (Exception e) { 559 logger.log(Level.FINE, "Can't find JndiResourceResolverFactory ctor, skipping.", e); 560 return null; 561 } 562 ResourceResolverFactory rrf; 563 try { 564 rrf = jndiCtor.newInstance(); 565 } catch (Exception e) { 566 logger.log(Level.FINE, "Can't construct JndiResourceResolverFactory, skipping.", e); 567 return null; 568 } 569 if (rrf.unavailabilityCause() != null) { 570 logger.log( 571 Level.FINE, 572 "JndiResourceResolverFactory not available, skipping.", 573 rrf.unavailabilityCause()); 574 } 575 return rrf; 576 } 577 getLocalHostname()578 private static String getLocalHostname() { 579 if (localHostname == null) { 580 try { 581 localHostname = InetAddress.getLocalHost().getHostName(); 582 } catch (UnknownHostException e) { 583 throw new RuntimeException(e); 584 } 585 } 586 return localHostname; 587 } 588 589 @VisibleForTesting shouldUseJndi(boolean jndiEnabled, boolean jndiLocalhostEnabled, String target)590 static boolean shouldUseJndi(boolean jndiEnabled, boolean jndiLocalhostEnabled, String target) { 591 if (!jndiEnabled) { 592 return false; 593 } 594 if ("localhost".equalsIgnoreCase(target)) { 595 return jndiLocalhostEnabled; 596 } 597 // Check if this name looks like IPv6 598 if (target.contains(":")) { 599 return false; 600 } 601 // Check if this might be IPv4. Such addresses have no alphabetic characters. This also 602 // checks the target is empty. 603 boolean alldigits = true; 604 for (int i = 0; i < target.length(); i++) { 605 char c = target.charAt(i); 606 if (c != '.') { 607 alldigits &= (c >= '0' && c <= '9'); 608 } 609 } 610 return !alldigits; 611 } 612 } 613