diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 5d16905627..4162e1ec8b 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -5,16 +5,20 @@ name: Test PR on: pull_request: - branches: [ master ] + branches: [ 2.x ] + push: + branches: [ 2.x ] + workflow_dispatch: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + - uses: actions/checkout@v6 + - name: Set up JDK 8 + uses: actions/setup-java@v5 with: - java-version: 1.8 + java-version: 8 + distribution: 'corretto' - name: Build and test with Maven - run: mvn test -Ptest-output + run: mvn test -ntp -B -Ptest-output -DexcludedGroups=online diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..8fa953c3d2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,63 @@ +name: Release 2.x + +on: + push: + branches: + - 2.14.5 + + workflow_dispatch: + inputs: + snapshot: + description: 'Deploy SNAPSHOT' + type: boolean + default: false + +permissions: + contents: read + +jobs: + + deploy: + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-java@v5 + with: + distribution: 'corretto' + java-version: '8' + + - name: Validate SNAPSHOT version + if: inputs.snapshot == true + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + if [[ "$VERSION" != *-SNAPSHOT ]]; then + echo "::error::Version $VERSION is not a SNAPSHOT version" + exit 1 + fi + + - name: Remove old Maven Settings + run: rm -f /home/runner/.m2/settings.xml + + - name: Maven Settings + uses: s4u/maven-settings-action@v4.0.0 + with: + servers: | + [{ + "id": "central", + "username": "${{ secrets.OSSRH_USERNAME }}", + "password": "${{ secrets.OSSRH_PASSWORD }}" + }] + + - name: Import GPG + uses: crazy-max/ghaction-import-gpg@v7.0.0 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + + - name: Deploy + env: + GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: mvn -B -ntp deploy -DskipTests -Dgpg.keyname=${GPG_KEY_NAME} -Dgpg.passphrase=${GPG_PASSPHRASE} diff --git a/LICENSES/LICENSE.public-suffix-list.txt b/LICENSES/LICENSE.public-suffix-list.txt new file mode 100644 index 0000000000..625e64ae87 --- /dev/null +++ b/LICENSES/LICENSE.public-suffix-list.txt @@ -0,0 +1,13 @@ +The file client/src/main/resources/org/asynchttpclient/cookie/public_suffix_list.dat is a copy of the +ICANN section of the Public Suffix List, maintained by the Mozilla Foundation and distributed from +https://publicsuffix.org/list/public_suffix_list.dat. + +It is used to implement RFC 6265 section 5.3 step 5, which requires a cookie naming a public suffix +in its Domain attribute to be ignored. + +Only the ICANN section is included; the private section is omitted. The upstream licence header and +the section markers are preserved in the file. No rules were altered. + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/bom/pom.xml b/bom/pom.xml index 867f23157e..ff4898eb88 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -5,7 +5,7 @@ org.asynchttpclient async-http-client-project - 2.12.3 + 2.16.1 async-http-client-bom diff --git a/client/pom.xml b/client/pom.xml index 59b67c17d1..b384b7fc5d 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.12.3 + 2.16.1 4.0.0 async-http-client diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java index a761322dc3..3a22946e27 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java @@ -22,6 +22,7 @@ import io.netty.util.Timer; import org.asynchttpclient.channel.ChannelPool; import org.asynchttpclient.channel.KeepAliveStrategy; +import org.asynchttpclient.config.AsyncHttpClientConfigDefaults; import org.asynchttpclient.cookie.CookieStore; import org.asynchttpclient.filter.IOExceptionFilter; import org.asynchttpclient.filter.RequestFilter; @@ -347,6 +348,31 @@ public interface AsyncHttpClientConfig { int getIoThreadsCount(); + /** + * Indicates whether the Authorization header should be stripped during redirects to a different domain. + * + * @return true if the Authorization header should be stripped, false otherwise. + */ + default boolean isStripAuthorizationOnRedirect() { + return false; + } + + /** + * The maximum number of bytes a compressed response body may inflate to, counted over the whole + * response. Guards against a "decompression bomb": a small response that expands without bound once + * decoded. {@code 0} disables the limit. + *

+ * The literal is hardcoded rather than read through {@link AsyncHttpClientConfigDefaults}: this default + * runs for any third-party implementation of this interface, and it is reached on the event loop when + * the pipeline is built, so a property lookup plus {@code Integer.parseInt} would turn a typo in a + * configuration file into a failure on every connection rather than at startup. + * + * @return the decompressed response ceiling in bytes + */ + default int getMaxDecompressedResponseSize() { + return 268435456; + } + enum ResponseBodyPartFactory { EAGER { diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java index 7cc3e6e341..45094ed7e6 100644 --- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java +++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java @@ -214,7 +214,7 @@ public ListenableFuture executeRequest(Request request, AsyncHandler h if (!cookies.isEmpty()) { RequestBuilder requestBuilder = request.toBuilder(); for (Cookie cookie : cookies) { - requestBuilder.addOrReplaceCookie(cookie); + requestBuilder.addCookieIfUnset(cookie); } request = requestBuilder.build(); } diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java index 0f4e62c560..011402a275 100644 --- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java @@ -61,8 +61,10 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { private final boolean useLaxCookieEncoder; private final boolean disableZeroCopy; private final boolean keepEncodingHeader; + private final int maxDecompressedResponseSize; private final ProxyServerSelector proxyServerSelector; private final boolean validateResponseHeaders; + private final boolean stripAuthorizationOnRedirect; // websockets private final boolean aggregateWebSocketFrameFragments; @@ -149,10 +151,12 @@ private DefaultAsyncHttpClientConfig(// http boolean useLaxCookieEncoder, boolean disableZeroCopy, boolean keepEncodingHeader, + int maxDecompressedResponseSize, ProxyServerSelector proxyServerSelector, boolean validateResponseHeaders, boolean aggregateWebSocketFrameFragments, boolean enablewebSocketCompression, + boolean stripAuthorizationOnRedirect, // timeouts int connectTimeout, @@ -237,8 +241,10 @@ private DefaultAsyncHttpClientConfig(// http this.useLaxCookieEncoder = useLaxCookieEncoder; this.disableZeroCopy = disableZeroCopy; this.keepEncodingHeader = keepEncodingHeader; + this.maxDecompressedResponseSize = maxDecompressedResponseSize; this.proxyServerSelector = proxyServerSelector; this.validateResponseHeaders = validateResponseHeaders; + this.stripAuthorizationOnRedirect = stripAuthorizationOnRedirect; // websocket this.aggregateWebSocketFrameFragments = aggregateWebSocketFrameFragments; @@ -377,6 +383,11 @@ public boolean isKeepEncodingHeader() { return keepEncodingHeader; } + @Override + public int getMaxDecompressedResponseSize() { + return maxDecompressedResponseSize; + } + @Override public ProxyServerSelector getProxyServerSelector() { return proxyServerSelector; @@ -483,6 +494,11 @@ public boolean isValidateResponseHeaders() { return validateResponseHeaders; } + @Override + public boolean isStripAuthorizationOnRedirect() { + return stripAuthorizationOnRedirect; + } + // ssl @Override public boolean isUseOpenSsl() { @@ -709,10 +725,12 @@ public static class Builder { private boolean useLaxCookieEncoder = defaultUseLaxCookieEncoder(); private boolean disableZeroCopy = defaultDisableZeroCopy(); private boolean keepEncodingHeader = defaultKeepEncodingHeader(); + private int maxDecompressedResponseSize = defaultMaxDecompressedResponseSize(); private ProxyServerSelector proxyServerSelector; private boolean useProxySelector = defaultUseProxySelector(); private boolean useProxyProperties = defaultUseProxyProperties(); private boolean validateResponseHeaders = defaultValidateResponseHeaders(); + private boolean stripAuthorizationOnRedirect = false; // default value // websocket private boolean aggregateWebSocketFrameFragments = defaultAggregateWebSocketFrameFragments(); @@ -800,7 +818,9 @@ public Builder(AsyncHttpClientConfig config) { useLaxCookieEncoder = config.isUseLaxCookieEncoder(); disableZeroCopy = config.isDisableZeroCopy(); keepEncodingHeader = config.isKeepEncodingHeader(); + maxDecompressedResponseSize = config.getMaxDecompressedResponseSize(); proxyServerSelector = config.getProxyServerSelector(); + stripAuthorizationOnRedirect = config.isStripAuthorizationOnRedirect(); // websocket aggregateWebSocketFrameFragments = config.isAggregateWebSocketFrameFragments(); @@ -930,11 +950,27 @@ public Builder setKeepEncodingHeader(boolean keepEncodingHeader) { return this; } + /** + * Bound how far a compressed response body may inflate, in bytes. {@code 0} disables the limit. + * + * @param maxDecompressedResponseSize the decompressed response ceiling in bytes + * @return this + */ + public Builder setMaxDecompressedResponseSize(int maxDecompressedResponseSize) { + this.maxDecompressedResponseSize = maxDecompressedResponseSize; + return this; + } + public Builder setProxyServerSelector(ProxyServerSelector proxyServerSelector) { this.proxyServerSelector = proxyServerSelector; return this; } + public Builder setStripAuthorizationOnRedirect(boolean value) { + this.stripAuthorizationOnRedirect = value; + return this; + } + public Builder setValidateResponseHeaders(boolean validateResponseHeaders) { this.validateResponseHeaders = validateResponseHeaders; return this; @@ -1310,10 +1346,12 @@ public DefaultAsyncHttpClientConfig build() { useLaxCookieEncoder, disableZeroCopy, keepEncodingHeader, + maxDecompressedResponseSize, resolveProxyServerSelector(), validateResponseHeaders, aggregateWebSocketFrameFragments, enablewebSocketCompression, + stripAuthorizationOnRedirect, connectTimeout, requestTimeout, readTimeout, diff --git a/client/src/main/java/org/asynchttpclient/Realm.java b/client/src/main/java/org/asynchttpclient/Realm.java index c6324fd0b4..83b6b6102e 100644 --- a/client/src/main/java/org/asynchttpclient/Realm.java +++ b/client/src/main/java/org/asynchttpclient/Realm.java @@ -23,8 +23,8 @@ import java.nio.charset.Charset; import java.security.MessageDigest; +import java.security.SecureRandom; import java.util.Map; -import java.util.concurrent.ThreadLocalRandom; import static java.nio.charset.StandardCharsets.*; import static org.asynchttpclient.util.Assertions.assertNotNull; @@ -253,6 +253,9 @@ public enum AuthScheme { */ public static class Builder { + // cnonce must be unpredictable (RFC 7616 section 3.3), like the NTLM nonce + private static final ThreadLocal CNONCE_RANDOM = ThreadLocal.withInitial(SecureRandom::new); + private final String principal; private final String password; private AuthScheme scheme; @@ -413,11 +416,35 @@ private String parseRawQop(String rawQop) { return null; } + /** + * The scheme a parsed challenge authenticates with. + *

+ * A challenge that announces itself as Digest stays Digest even when its parameters do not parse. + * Deciding this from the presence of a nonce instead meant any Digest challenge we could not read + * became a Basic one, and answering it put the password on the wire in the clear, which is the single + * thing Digest exists to prevent. Omitting the nonce, or sending an empty one, was enough to trigger + * it. A Digest challenge with no nonce now produces no Authorization header at all, so the request + * fails rather than leaking. + */ + private static AuthScheme challengedScheme(String headerLine, String nonce) { + if (isNonEmpty(nonce)) { + return AuthScheme.DIGEST; + } + if (headerLine == null) { + return AuthScheme.BASIC; + } + int start = 0; + while (start < headerLine.length() && headerLine.charAt(start) == ' ') { + start++; + } + return headerLine.regionMatches(true, start, "Digest", 0, 6) ? AuthScheme.DIGEST : AuthScheme.BASIC; + } + public Builder parseWWWAuthenticateHeader(String headerLine) { setRealmName(match(headerLine, "realm")) .setNonce(match(headerLine, "nonce")) .setOpaque(match(headerLine, "opaque")) - .setScheme(isNonEmpty(nonce) ? AuthScheme.DIGEST : AuthScheme.BASIC); + .setScheme(challengedScheme(headerLine, nonce)); String algorithm = match(headerLine, "algorithm"); if (isNonEmpty(algorithm)) { setAlgorithm(algorithm); @@ -436,7 +463,7 @@ public Builder parseProxyAuthenticateHeader(String headerLine) { setRealmName(match(headerLine, "realm")) .setNonce(match(headerLine, "nonce")) .setOpaque(match(headerLine, "opaque")) - .setScheme(isNonEmpty(nonce) ? AuthScheme.DIGEST : AuthScheme.BASIC); + .setScheme(challengedScheme(headerLine, nonce)); String algorithm = match(headerLine, "algorithm"); if (isNonEmpty(algorithm)) { setAlgorithm(algorithm); @@ -449,7 +476,7 @@ public Builder parseProxyAuthenticateHeader(String headerLine) { private void newCnonce(MessageDigest md) { byte[] b = new byte[8]; - ThreadLocalRandom.current().nextBytes(b); + CNONCE_RANDOM.get().nextBytes(b); b = md.digest(b); cnonce = toHexString(b); } diff --git a/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java b/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java index 35c8145776..a7fae7f430 100644 --- a/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java +++ b/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java @@ -308,15 +308,31 @@ public T addCookie(Cookie cookie) { /** * Add/replace a cookie based on its name + * * @param cookie the new cookie * @return this */ public T addOrReplaceCookie(Cookie cookie) { + return maybeAddOrReplaceCookie(cookie, true); + } + + /** + * Add a cookie based on its name, if it does not exist yet. Cookies that + * are already set will be ignored. + * + * @param cookie the new cookie + * @return this + */ + public T addCookieIfUnset(Cookie cookie) { + return maybeAddOrReplaceCookie(cookie, false); + } + + private T maybeAddOrReplaceCookie(Cookie cookie, boolean allowReplace) { String cookieKey = cookie.name(); boolean replace = false; int index = 0; lazyInitCookies(); - for (Cookie c : this.cookies) { + for (Cookie c : cookies) { if (c.name().equals(cookieKey)) { replace = true; break; @@ -324,10 +340,11 @@ public T addOrReplaceCookie(Cookie cookie) { index++; } - if (replace) - this.cookies.set(index, cookie); - else - this.cookies.add(cookie); + if (!replace) { + cookies.add(cookie); + } else if (allowReplace) { + cookies.set(index, cookie); + } return asDerivedType(); } diff --git a/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java b/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java index 14dcec3bfd..daaa1e83b7 100644 --- a/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java +++ b/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java @@ -67,6 +67,7 @@ public final class AsyncHttpClientConfigDefaults { public static final String WEBSOCKET_MAX_BUFFER_SIZE_CONFIG = "webSocketMaxBufferSize"; public static final String WEBSOCKET_MAX_FRAME_SIZE_CONFIG = "webSocketMaxFrameSize"; public static final String KEEP_ENCODING_HEADER_CONFIG = "keepEncodingHeader"; + public static final String MAX_DECOMPRESSED_RESPONSE_SIZE_CONFIG = "maxDecompressedResponseSize"; public static final String SHUTDOWN_QUIET_PERIOD_CONFIG = "shutdownQuietPeriod"; public static final String SHUTDOWN_TIMEOUT_CONFIG = "shutdownTimeout"; public static final String USE_NATIVE_TRANSPORT_CONFIG = "useNativeTransport"; @@ -282,6 +283,10 @@ public static boolean defaultKeepEncodingHeader() { return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getBoolean(ASYNC_CLIENT_CONFIG_ROOT + KEEP_ENCODING_HEADER_CONFIG); } + public static int defaultMaxDecompressedResponseSize() { + return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + MAX_DECOMPRESSED_RESPONSE_SIZE_CONFIG); + } + public static int defaultShutdownQuietPeriod() { return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + SHUTDOWN_QUIET_PERIOD_CONFIG); } diff --git a/client/src/main/java/org/asynchttpclient/cookie/PublicSuffixList.java b/client/src/main/java/org/asynchttpclient/cookie/PublicSuffixList.java new file mode 100644 index 0000000000..66d0ef4d6b --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/cookie/PublicSuffixList.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient.cookie; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Collections; +import java.util.Locale; +import java.util.HashSet; +import java.util.Set; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * The ICANN section of the Mozilla Public Suffix List, used to decide whether a cookie {@code Domain} + * attribute names a registry rather than a site. + * + *

RFC 6265 Section 5.3 step 5 requires rejecting a {@code Domain} that is a public suffix, and that + * rule cannot be approximated: {@code co.uk} has a dot like any ordinary domain, so counting labels does + * not distinguish a registry from a site. Without the list a host under {@code co.uk} can set a cookie for + * {@code co.uk} itself and every other host under that suffix receives it. + * + *

Only the ICANN section is bundled. The private section describes organisations that let others + * register names beneath them, which is a weaker property than a registry and not what step 5 is about. + * + *

Matching lowercases with {@link Locale#ROOT}. The default locale would be wrong here in a way that + * matters: under Turkish, {@code "INFO".toLowerCase()} is not {@code info}, so the check would answer + * false for every I-initial suffix and fail open exactly where it is meant to hold. + * + *

The list is data and goes stale as registries change. A suffix added upstream after this release is + * not recognised until the bundled copy is refreshed, so this narrows the exposure rather than closing it + * for all time. If the resource cannot be read the check reports nothing as a public suffix, leaving + * behaviour as it was rather than rejecting cookies that used to work. + */ +public final class PublicSuffixList { + + private static final Logger LOGGER = LoggerFactory.getLogger(PublicSuffixList.class); + private static final String RESOURCE = "/org/asynchttpclient/cookie/public_suffix_list.dat"; + + private static final Set EXACT; + private static final Set WILDCARD; + private static final Set EXCEPTIONS; + + static { + Set exact = new HashSet<>(8192); + Set wildcard = new HashSet<>(32); + Set exceptions = new HashSet<>(16); + try (InputStream in = PublicSuffixList.class.getResourceAsStream(RESOURCE)) { + if (in == null) { + LOGGER.warn("Public suffix list {} is missing; a cookie Domain naming a public suffix " + + "cannot be rejected", RESOURCE); + } else { + BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8)); + String line; + while ((line = reader.readLine()) != null) { + String rule = line.trim(); + if (rule.isEmpty() || rule.startsWith("//")) { + continue; + } + if (rule.charAt(0) == '!') { + exceptions.add(rule.substring(1).toLowerCase(Locale.ROOT)); + } else if (rule.startsWith("*.")) { + wildcard.add(rule.substring(2).toLowerCase(Locale.ROOT)); + } else { + exact.add(rule.toLowerCase(Locale.ROOT)); + } + } + } + } catch (IOException e) { + LOGGER.warn("Could not read the public suffix list; a cookie Domain naming a public suffix " + + "cannot be rejected", e); + } + EXACT = Collections.unmodifiableSet(exact); + WILDCARD = Collections.unmodifiableSet(wildcard); + EXCEPTIONS = Collections.unmodifiableSet(exceptions); + } + + private PublicSuffixList() { + } + + /** + * Whether {@code domain} is a public suffix, and so may not be the {@code Domain} of a cookie. + * + * @param domain a hostname, without a leading dot + */ + public static boolean isPublicSuffix(String domain) { + if (domain == null || domain.isEmpty()) { + return false; + } + String candidate = domain.toLowerCase(Locale.ROOT); + if (candidate.charAt(candidate.length() - 1) == '.') { + candidate = candidate.substring(0, candidate.length() - 1); + } + // An exception rule names something that IS registrable despite matching a wildcard above it. + if (EXCEPTIONS.contains(candidate)) { + return false; + } + if (EXACT.contains(candidate)) { + return true; + } + // A wildcard rule such as *.ck makes every direct child of ck a suffix, so the candidate is one + // when its parent carries the rule. + int dot = candidate.indexOf('.'); + return dot > 0 && WILDCARD.contains(candidate.substring(dot + 1)); + } +} diff --git a/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java b/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java index 8cdc29f45e..25c07f3d13 100644 --- a/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java +++ b/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java @@ -154,6 +154,11 @@ private boolean hasCookieExpired(Cookie cookie, long whenCreated) { return false; } + // rfc6265#section-5.1.3 + private boolean domainsMatch(String cookieDomain, String requestDomain) { + return requestDomain.equals(cookieDomain) || requestDomain.endsWith('.' + cookieDomain); + } + // rfc6265#section-5.1.4 private boolean pathsMatch(String cookiePath, String requestPath) { return Objects.equals(cookiePath, requestPath) || @@ -164,6 +169,26 @@ private void add(String requestDomain, String requestPath, Cookie cookie) { AbstractMap.SimpleEntry pair = cookieDomain(cookie.domain(), requestDomain); String keyDomain = pair.getKey(); boolean hostOnly = pair.getValue(); + + // rfc6265#section-5.3 step 6: ignore a cookie whose Domain attribute is not + // domain-matched by the request host, otherwise a host can plant cookies for + // unrelated domains (cookie tossing). + if (!hostOnly && !domainsMatch(keyDomain, requestDomain)) { + return; + } + + // rfc6265#section-5.3 step 5: a Domain naming a public suffix must not be honoured. Step 6 above only + // asks whether the request host sits under the Domain, which evil.co.uk setting Domain=co.uk + // satisfies, so on its own it still lets one site plant a cookie every other site under that registry + // receives. Label counting cannot stand in: co.uk has a dot like any other domain. + // + // The step also keeps a cookie whose Domain equals the request host, as a host-only cookie. Dropping + // it would break ordinary single-label hosts: dev, app, box and cloud are ICANN suffixes as well as + // the short names Docker Compose and Kubernetes hand out. + if (!hostOnly && PublicSuffixList.isPublicSuffix(keyDomain) && !keyDomain.equals(requestDomain)) { + return; + } + String keyPath = cookiePath(cookie.path(), requestPath); CookieKey key = new CookieKey(cookie.name().toLowerCase(), keyPath); diff --git a/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java b/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java index 1eb99f11ba..ea1f2066c6 100644 --- a/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java +++ b/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java @@ -16,14 +16,25 @@ import org.slf4j.LoggerFactory; import java.io.File; -import java.io.FileNotFoundException; import java.io.OutputStream; +import java.nio.channels.Channels; +import java.nio.file.FileSystems; import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.EnumSet; import java.util.Map; import java.util.Scanner; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.StandardOpenOption.CREATE_NEW; +import static java.nio.file.StandardOpenOption.WRITE; import static org.asynchttpclient.util.MiscUtils.closeSilently; /** @@ -34,8 +45,19 @@ public class PropertiesBasedResumableProcessor implements ResumableAsyncHandler. private final static Logger log = LoggerFactory.getLogger(PropertiesBasedResumableProcessor.class); private final static File TMP = new File(System.getProperty("java.io.tmpdir"), "ahc"); private final static String storeName = "ResumableAsyncHandler.properties"; + private final static boolean POSIX = FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); + private final static FileAttribute[] DIR_ATTRIBUTES = ownerOnlyAttributes("rwx------"); + private final static FileAttribute[] FILE_ATTRIBUTES = ownerOnlyAttributes("rw-------"); + private final static Set CREATE_OPTIONS = EnumSet.of(WRITE, CREATE_NEW); + private final ConcurrentHashMap properties = new ConcurrentHashMap<>(); + private static FileAttribute[] ownerOnlyAttributes(String permissions) { + return POSIX + ? new FileAttribute[]{PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString(permissions))} + : new FileAttribute[0]; + } + private static String append(Map.Entry e) { return e.getKey() + '=' + e.getValue() + '\n'; } @@ -67,18 +89,18 @@ public void save(Map map) { OutputStream os = null; try { - if (!TMP.exists() && !TMP.mkdirs()) { - throw new IllegalStateException("Unable to create directory: " + TMP.getAbsolutePath()); - } - File f = new File(TMP, storeName); - if (!f.exists() && !f.createNewFile()) { - throw new IllegalStateException("Unable to create temp file: " + f.getAbsolutePath()); - } - if (!f.canWrite()) { - throw new IllegalStateException(); + Path dir = TMP.toPath(); + if (!Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS)) { + Files.createDirectory(dir, DIR_ATTRIBUTES); } - os = Files.newOutputStream(f.toPath()); + // The store sits at a fixed path in the shared temp directory and holds the URLs being downloaded, + // so it is recreated here with owner-only permissions instead of being written through whatever is + // already at that path. CREATE_NEW after the delete fails rather than opens if another local user + // re-plants a file or a symlink in between. + Path f = dir.resolve(storeName); + Files.deleteIfExists(f); + os = Channels.newOutputStream(Files.newByteChannel(f, CREATE_OPTIONS, FILE_ATTRIBUTES)); for (Map.Entry e : properties.entrySet()) { os.write(append(e).getBytes(UTF_8)); } @@ -97,7 +119,8 @@ public void save(Map map) { public Map load() { Scanner scan = null; try { - scan = new Scanner(new File(TMP, storeName), UTF_8.name()); + // NOFOLLOW_LINKS: refuse to read the state back through a symlink planted at the predictable path + scan = new Scanner(Files.newInputStream(new File(TMP, storeName).toPath(), LinkOption.NOFOLLOW_LINKS), UTF_8.name()); scan.useDelimiter("[=\n]"); String key; @@ -108,7 +131,7 @@ public Map load() { properties.put(key, Long.valueOf(value)); } log.debug("Loading previous download state {}", properties.toString()); - } catch (FileNotFoundException ex) { + } catch (NoSuchFileException ex) { log.debug("Missing {}", storeName); } catch (Throwable ex) { // Survive any exceptions diff --git a/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java b/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java index dddebfff41..4174bffd9e 100755 --- a/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java +++ b/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java @@ -14,6 +14,7 @@ package org.asynchttpclient.netty; import io.netty.channel.Channel; +import io.netty.handler.codec.http.HttpMethod; import org.asynchttpclient.AsyncHandler; import org.asynchttpclient.ListenableFuture; import org.asynchttpclient.Realm; @@ -80,7 +81,9 @@ public final class NettyResponseFuture implements ListenableFuture { private final long start = unpreciseMillisTime(); private final ChannelPoolPartitioning connectionPoolPartitioning; private final ConnectionSemaphore connectionSemaphore; - private final ProxyServer proxyServer; + // Not final: a filter replay can retarget this future at a different origin, reached through a + // different proxy or none at all. getPartitionKey() reads it, so it has to move with the target. + private ProxyServer proxyServer; private final int maxRetry; private final CompletableFuture future = new CompletableFuture<>(); public Throwable pendingException; @@ -116,6 +119,7 @@ public final class NettyResponseFuture implements ListenableFuture { private boolean headersAlreadyWrittenOnContinue; private boolean dontWriteBodyBecauseExpectContinue; private boolean allowConnect; + private boolean tunnelEstablished; private Realm realm; private Realm proxyRealm; @@ -306,6 +310,15 @@ public Uri getUri() { return targetRequest.getUri(); } + /** + * Points this future at the proxy serving its current target. Only a replay onto a different origin + * needs this: leaving the previous origin's proxy in place leaves the connection pool partition key + * naming a route the future no longer takes. + */ + public void setProxyServer(ProxyServer proxyServer) { + this.proxyServer = proxyServer; + } + public ProxyServer getProxyServer() { return proxyServer; } @@ -338,9 +351,33 @@ public final NettyRequest getNettyRequest() { } public final void setNettyRequest(NettyRequest nettyRequest) { + if (nettyRequest != null && nettyRequest.getHttpRequest().method() == HttpMethod.CONNECT) { + // A new tunnel attempt is starting, so whatever an earlier CONNECT established no longer holds. + // Only ConnectSuccessInterceptor may set this back to true. Keeping the invariant here rather than + // at the call site means no future path can attach a CONNECT while leaving the flag stale. + tunnelEstablished = false; + } this.nettyRequest = nettyRequest; } + /** + * Whether a CONNECT on the channel this exchange is using actually succeeded, i.e. whether the socket is + * a tunnel to the origin rather than a plaintext hop to the proxy. Set only by + * {@link org.asynchttpclient.netty.handler.intercept.ConnectSuccessInterceptor}. The fact that the last + * request sent was a CONNECT proves nothing on its own: it is equally true when the proxy REJECTED the + * CONNECT, and treating that as an established tunnel puts the origin's credentials on a socket the + * proxy is still reading in the clear. + * + * @return true if a CONNECT has been answered successfully for the current channel + */ + public boolean isTunnelEstablished() { + return tunnelEstablished; + } + + public void setTunnelEstablished(boolean tunnelEstablished) { + this.tunnelEstablished = tunnelEstablished; + } + public final AsyncHandler getAsyncHandler() { return asyncHandler; } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java index b93dfb380e..35747456b8 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java @@ -23,7 +23,7 @@ import io.netty.channel.kqueue.KQueueEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.codec.http.HttpClientCodec; -import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder; import io.netty.handler.codec.http.websocketx.WebSocket08FrameEncoder; import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator; @@ -46,8 +46,10 @@ import org.asynchttpclient.netty.NettyResponseFuture; import org.asynchttpclient.netty.OnLastHttpContentCallback; import org.asynchttpclient.netty.handler.AsyncHttpClientHandler; +import org.asynchttpclient.netty.handler.BoundedHttpContentDecompressor; import org.asynchttpclient.netty.handler.HttpHandler; import org.asynchttpclient.netty.handler.WebSocketHandler; +import org.asynchttpclient.netty.request.NettyRequest; import org.asynchttpclient.netty.request.NettyRequestSender; import org.asynchttpclient.netty.ssl.DefaultSslEngineFactory; import org.asynchttpclient.proxy.ProxyServer; @@ -253,16 +255,44 @@ protected void initChannel(Channel ch) { }); } - private HttpContentDecompressor newHttpContentDecompressor() { - if (config.isKeepEncodingHeader()) - return new HttpContentDecompressor() { - @Override - protected String getTargetContentEncoding(String contentEncoding) { - return contentEncoding; - } - }; - else - return new HttpContentDecompressor(); + /** + * A decompressor whose output is counted over the whole response, failing the request once it exceeds + * {@link AsyncHttpClientConfig#getMaxDecompressedResponseSize()}, so a small but highly compressible body + * cannot inflate without bound. Netty's own {@code maxAllocation} parameter is deliberately left at 0 - + * see {@link BoundedHttpContentDecompressor} for why it neither bounds a response nor is safe to set. + */ + private BoundedHttpContentDecompressor newHttpContentDecompressor() { + return new BoundedHttpContentDecompressor(config.isKeepEncodingHeader(), config.getMaxDecompressedResponseSize()); + } + + /** + * A channel is pooled under a partition key that names the target origin, and + * {@code NettyRequestSender.sendRequestThroughProxy} takes a channel polled under such a key to be a + * tunnel that is already up: it sends the ORIGIN request on it rather than a CONNECT. A channel whose + * last request was a CONNECT is exactly the opposite, because a CONNECT the proxy DID accept is consumed + * by {@link org.asynchttpclient.netty.handler.intercept.ConnectSuccessInterceptor}, which takes over the + * channel and never lets the exchange reach the pool. So reaching here with a CONNECT in hand means the + * proxy refused the tunnel and the socket is still a plaintext hop to it - which must be closed, not + * pooled, or the next exchange for that origin sends the origin request, Authorization header included, + * down a hop the proxy is still reading in the clear. + */ + private static boolean isRefusedTunnel(NettyResponseFuture future) { + NettyRequest nettyRequest = future.getNettyRequest(); + return nettyRequest != null && nettyRequest.getHttpRequest().method() == HttpMethod.CONNECT; + } + + public final void tryToOfferChannelToPool(Channel channel, NettyResponseFuture future, boolean keepAlive, Object partitionKey) { + tryToOfferChannelToPool(channel, future.getAsyncHandler(), keepAlive, partitionKey, isRefusedTunnel(future)); + } + + private void tryToOfferChannelToPool(Channel channel, AsyncHandler asyncHandler, boolean keepAlive, Object partitionKey, + boolean refusedTunnel) { + if (refusedTunnel) { + LOGGER.debug("Not offering channel {} to the pool: the CONNECT on it was never established", channel); + closeChannel(channel); + return; + } + tryToOfferChannelToPool(channel, asyncHandler, keepAlive, partitionKey); } public final void tryToOfferChannelToPool(Channel channel, AsyncHandler asyncHandler, boolean keepAlive, Object partitionKey) { @@ -290,6 +320,14 @@ public Channel poll(Uri uri, String virtualHost, ProxyServer proxy, ChannelPoolP Object partitionKey = connectionPoolPartitioning.getPartitionKey(uri, virtualHost, proxy); return channelPool.poll(partitionKey); } + /** + * Polls with a partition key the caller already derived, so the caller can scope it by the + * authenticated identity. See {@link PrincipalScopedPartitionKey}. + */ + public Channel poll(Object partitionKey) { + return channelPool.poll(partitionKey); + } + public void removeAll(Channel connection) { channelPool.removeAll(connection); @@ -467,15 +505,21 @@ public void upgradePipelineForWebSockets(ChannelPipeline pipeline) { private OnLastHttpContentCallback newDrainCallback(final NettyResponseFuture future, final Channel channel, final boolean keepAlive, final Object partitionKey) { + // Sampled here rather than in call(), for the same reason keepAlive and partitionKey are: callers hand + // the drain over and immediately move the future on to the NEXT request, so by the time the last chunk + // arrives the future no longer describes the response being drained off this channel. + final boolean refusedTunnel = isRefusedTunnel(future); + return new OnLastHttpContentCallback(future) { public void call() { - tryToOfferChannelToPool(channel, future.getAsyncHandler(), keepAlive, partitionKey); + tryToOfferChannelToPool(channel, future.getAsyncHandler(), keepAlive, partitionKey, refusedTunnel); } }; } public void drainChannelAndOffer(Channel channel, NettyResponseFuture future) { - drainChannelAndOffer(channel, future, future.isKeepAlive(), future.getPartitionKey()); + drainChannelAndOffer(channel, future, future.isKeepAlive(), + PrincipalScopedPartitionKey.scope(future.getPartitionKey(), future.getRealm())); } public void drainChannelAndOffer(Channel channel, NettyResponseFuture future, boolean keepAlive, Object partitionKey) { diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/PrincipalScopedPartitionKey.java b/client/src/main/java/org/asynchttpclient/netty/channel/PrincipalScopedPartitionKey.java new file mode 100644 index 0000000000..0f4e3277fb --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/netty/channel/PrincipalScopedPartitionKey.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient.netty.channel; + +import org.asynchttpclient.Realm; + +import java.util.Objects; + +/** + * Channel-pool partition key for schemes that authenticate the connection rather than the request. + * + *

NTLM and Negotiate complete a handshake once and the server then treats every later request arriving + * on that socket as coming from the identity that authenticated it. The regular partition key describes + * only where the connection goes, so a socket one principal authenticated could be handed to a request + * belonging to another, and the server would serve it as the first principal. Nothing on the wire shows the + * identity changed, because the second request carries no authentication headers of its own. + * + *

Folding the principal into the key keeps those connections separated. Basic and Digest need no such + * thing: they authenticate each request and their credentials travel with it. + * + *

If a site that offers a connection to the pool and a site that polls for one ever disagree about + * whether to scope, the poll simply misses and a new connection is opened. That costs reuse, never + * correctness, which is the right way round for this to fail. + */ +public final class PrincipalScopedPartitionKey { + + private final Object baseKey; + private final Realm.AuthScheme scheme; + private final String principal; + + private PrincipalScopedPartitionKey(Object baseKey, Realm.AuthScheme scheme, String principal) { + this.baseKey = baseKey; + this.scheme = scheme; + this.principal = principal; + } + + /** + * Wraps {@code baseKey} with the authenticated identity when {@code realm} uses a scheme that + * authenticates the connection, and returns {@code baseKey} unchanged otherwise. Every site that + * derives a pool key must apply this the same way. + */ + public static Object scope(Object baseKey, Realm realm) { + if (realm == null || realm.getPrincipal() == null || !authenticatesTheConnection(realm.getScheme())) { + return baseKey; + } + return new PrincipalScopedPartitionKey(baseKey, realm.getScheme(), realm.getPrincipal()); + } + + private static boolean authenticatesTheConnection(Realm.AuthScheme scheme) { + return scheme == Realm.AuthScheme.NTLM + || scheme == Realm.AuthScheme.KERBEROS + || scheme == Realm.AuthScheme.SPNEGO; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PrincipalScopedPartitionKey that = (PrincipalScopedPartitionKey) o; + return Objects.equals(baseKey, that.baseKey) + && scheme == that.scheme + && Objects.equals(principal, that.principal); + } + + @Override + public int hashCode() { + return 31 * (31 * Objects.hashCode(baseKey) + Objects.hashCode(scheme)) + Objects.hashCode(principal); + } + + @Override + public String toString() { + // The principal is a username, not a secret, and the existing keys print their host and proxy. + return "PrincipalScopedPartitionKey(baseKey=" + baseKey + ", scheme=" + scheme + ", principal=" + principal + ')'; + } +} diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java index ec158673f0..9fa7ee091f 100755 --- a/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java @@ -29,6 +29,7 @@ import org.asynchttpclient.netty.OnLastHttpContentCallback; import org.asynchttpclient.netty.channel.ChannelManager; import org.asynchttpclient.netty.channel.Channels; +import org.asynchttpclient.netty.channel.PrincipalScopedPartitionKey; import org.asynchttpclient.netty.future.StackTraceInspector; import org.asynchttpclient.netty.handler.intercept.Interceptors; import org.asynchttpclient.netty.request.NettyRequestSender; @@ -234,7 +235,10 @@ void finishUpdate(NettyResponseFuture future, Channel channel, boolean close) if (close) { channelManager.closeChannel(channel); } else { - channelManager.tryToOfferChannelToPool(channel, future.getAsyncHandler(), true, future.getPartitionKey()); + // A connection authenticated by NTLM or Negotiate is filed under the identity that + // authenticated it, so no other principal can draw it. + channelManager.tryToOfferChannelToPool(channel, future, true, + PrincipalScopedPartitionKey.scope(future.getPartitionKey(), future.getRealm())); } try { diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/BoundedHttpContentDecompressor.java b/client/src/main/java/org/asynchttpclient/netty/handler/BoundedHttpContentDecompressor.java new file mode 100644 index 0000000000..02bb314bdf --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/netty/handler/BoundedHttpContentDecompressor.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.handler; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.compression.DecompressionException; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.util.ReferenceCountUtil; + +/** + * An {@link HttpContentDecompressor} that bounds how far a response body may inflate, guarding against a + * decompression bomb: a small, highly compressible response that expands without limit once decoded and + * exhausts the client's heap. + * + *

The bound is cumulative over one response body, and that is the whole point. Netty's + * own {@code maxAllocation} constructor parameter is not a substitute: it reaches {@code ZlibDecoder} as + * the maxCapacity of the buffer produced by one {@code decode()} call, with no counter spanning + * calls. Since the HTTP codec hands the decompressor at most + * {@link org.asynchttpclient.AsyncHttpClientConfig#getHttpClientCodecMaxChunkSize()} bytes at a time + * (8 KiB by default), a body at any ordinary compression ratio never reaches it however large the total + * becomes. Setting a non-zero {@code maxAllocation} is also actively harmful here: Netty 4.1's + * {@code HttpContentDecompressor.newContentDecoder} forwards it to {@code new BrotliDecoder(int)}, whose + * single-argument form is an input buffer size, and {@code BrotliDecoder.handlerAdded} eagerly + * allocates a native {@code DecoderJNI.Wrapper} of exactly that size - so a 256 MiB "ceiling" would cost + * 256 MiB of direct memory for every {@code Content-Encoding: br} response, empty body included. This + * class leaves {@code maxAllocation} at Netty's default of 0 and counts the bytes itself. + * + *

The counter is installed inside the decoder Netty builds per response, at the end of that + * embedded pipeline, rather than downstream of this handler. {@code HttpContentDecoder} publishes inflated + * content by firing it at its own outer {@link ChannelHandlerContext} from a forwarder appended to that + * same embedded pipeline, so an override of {@code decode} would observe nothing. Hooking here also gets + * the scope right for free: a decoder exists only for a body actually being inflated, and only for one + * response, so an unencoded body is never counted (bounding those would fail large plain downloads the + * caller asked for) and nothing has to be reset between responses on a pooled connection. + * + *

Not shareable: it holds the state of one connection's decoder. + */ +public class BoundedHttpContentDecompressor extends HttpContentDecompressor { + + private final boolean keepEncodingHeader; + /** + * Ceiling in bytes for a single response body; a non-positive value disables the check. + */ + private final long maxDecompressedBytes; + + public BoundedHttpContentDecompressor(boolean keepEncodingHeader, long maxDecompressedBytes) { + // Deliberately the no-arg super: see the class javadoc for why maxAllocation must stay 0. + super(); + this.keepEncodingHeader = keepEncodingHeader; + this.maxDecompressedBytes = maxDecompressedBytes; + } + + @Override + protected EmbeddedChannel newContentDecoder(String contentEncoding) throws Exception { + EmbeddedChannel decoder = super.newContentDecoder(contentEncoding); + if (decoder != null && maxDecompressedBytes > 0) { + // Appended before HttpContentDecoder appends its own forwarder, so this sits between the + // decompressor and the forwarder and sees every inflated buffer before it leaves the decoder. + decoder.pipeline().addLast(new DecompressedSizeLimit()); + } + return decoder; + } + + @Override + protected String getTargetContentEncoding(String contentEncoding) throws Exception { + // keepEncodingHeader leaves the response advertising the encoding it arrived with, for callers that + // inspect it; the default rewrites it to "identity" because the body downstream is no longer encoded. + return keepEncodingHeader ? contentEncoding : super.getTargetContentEncoding(contentEncoding); + } + + /** + * Counts the inflated output of the response this decoder was created for, and fails the exchange once + * it passes the ceiling. + */ + private final class DecompressedSizeLimit extends ChannelInboundHandlerAdapter { + + private long decompressedBytes; + private boolean exceeded; + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (exceeded) { + // Already failed. Tearing the decoder down calls finishAndReleaseAll(), which re-runs it over + // whatever is left cumulated and would throw a second time out of cleanup; drop the leftovers + // quietly instead so the caller sees the exception raised below. + ReferenceCountUtil.release(msg); + return; + } + + if (msg instanceof ByteBuf) { + decompressedBytes += ((ByteBuf) msg).readableBytes(); + if (decompressedBytes > maxDecompressedBytes) { + exceeded = true; + ReferenceCountUtil.release(msg); + // EmbeddedChannel rethrows this out of the writeInbound driving the decoder, inside + // HttpContentDecoder.decode; being a DecoderException it reaches AsyncHttpClientHandler's + // exceptionCaught unwrapped, which fails the request and closes the connection. + throw new DecompressionException("Decompressed response body exceeds the maximum of " + + maxDecompressedBytes + " bytes (" + decompressedBytes + " bytes decompressed so far)"); + } + } + + ctx.fireChannelRead(msg); + } + } +} diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java index dddaeb34cb..447b903e5a 100755 --- a/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java @@ -28,6 +28,7 @@ import org.asynchttpclient.netty.channel.ChannelManager; import org.asynchttpclient.netty.channel.Channels; import org.asynchttpclient.netty.request.NettyRequestSender; +import org.asynchttpclient.util.HttpConstants.ResponseStatusCodes; import java.io.IOException; import java.net.InetSocketAddress; @@ -40,8 +41,17 @@ public HttpHandler(AsyncHttpClientConfig config, ChannelManager channelManager, } private boolean abortAfterHandlingStatus(AsyncHandler handler, + HttpMethod httpMethod, NettyResponseStatus status) throws Exception { - return handler.onStatusReceived(status) == State.ABORT; + // For a non-200 response to a CONNECT the tunnel was NOT established: the socket is still a plaintext + // hop terminated by the proxy. It has to be either closed or reused by sending the CONNECT again, and + // closing is the simpler of the two. Letting the exchange run on would instead reach handleChunk, where + // close is only `!future.isKeepAlive()` - i.e. whatever the proxy chose to say - and the still-plaintext + // socket would be offered to the pool under a partition key that names the secured origin. The next + // exchange polls it and, believing a channel under that key to be a tunnel, sends the ORIGIN request + // down it, Authorization header included. + return handler.onStatusReceived(status) == State.ABORT + || httpMethod == HttpMethod.CONNECT && status.getStatusCode() != ResponseStatusCodes.OK_200; } private boolean abortAfterHandlingHeaders(AsyncHandler handler, @@ -75,7 +85,7 @@ private void handleHttpResponse(final HttpResponse response, final Channel chann HttpHeaders responseHeaders = response.headers(); if (!interceptors.exitAfterIntercept(channel, future, handler, response, status, responseHeaders)) { - boolean abort = abortAfterHandlingStatus(handler, status) || // + boolean abort = abortAfterHandlingStatus(handler, httpRequest.method(), status) || // abortAfterHandlingHeaders(handler, responseHeaders) || // abortAfterHandlingReactiveStreams(channel, future, handler); diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java index 533322f4bd..c36f314b44 100755 --- a/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java @@ -67,6 +67,7 @@ private void upgrade(Channel channel, NettyResponseFuture future, WebSocketUp String key = getAcceptKey(future.getNettyRequest().getHttpRequest().headers().get(SEC_WEBSOCKET_KEY)); if (accept == null || !accept.equals(key)) { requestSender.abort(channel, future, new IOException("Invalid challenge. Actual: " + accept + ". Expected: " + key)); + return; } // set back the future so the protocol gets notified of frames @@ -99,6 +100,16 @@ private void abort(Channel channel, NettyResponseFuture future, WebSocketUpgr public void handleRead(Channel channel, NettyResponseFuture future, Object e) throws Exception { if (e instanceof HttpResponse) { + // Unlike HttpHandler, this check cannot guard the whole method: a successful upgrade completes the + // future (see upgrade() below) and the channel then keeps serving frames, so isDone() is true for + // all normal WebSocket traffic. It belongs on the upgrade response alone, where a 101 arriving just + // as a request timeout aborted the future would otherwise still run upgrade() and deliver onOpen - + // and the rest of the WebSocket lifecycle - after onThrowable had already fired. + if (future.isDone()) { + channelManager.closeChannel(channel); + return; + } + HttpResponse response = (HttpResponse) e; if (logger.isDebugEnabled()) { HttpRequest httpRequest = future.getNettyRequest().getHttpRequest(); diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java index eb2e98e36f..681e425d9f 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java @@ -45,6 +45,10 @@ public boolean exitAfterHandlingConnect(Channel channel, if (future.isKeepAlive()) future.attachChannel(channel, true); + // The single place this may be set. From here on the socket is a tunnel to the origin, so the origin's + // credentials may travel on it; until this point it is a plaintext hop to the proxy. + future.setTunnelEstablished(true); + Uri requestUri = request.getUri(); LOGGER.debug("Connecting to proxy {} for scheme {}", proxyServer, requestUri.getScheme()); diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java index 134213f60a..8b52427bf7 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java @@ -67,11 +67,21 @@ public boolean exitAfterIntercept(Channel channel, ProxyServer proxyServer = future.getProxyServer(); int statusCode = response.status().code(); Request request = future.getCurrentRequest(); - Realm realm = request.getRealm() != null ? request.getRealm() : config.getRealm(); + // The realm carried by the exchange (seeded from the request or the config when the exchange + // started, and reset to null by Redirect30xInterceptor on a cross-origin or scheme-downgrade + // redirect). Re-deriving from config.getRealm() here re-attaches the client-wide credentials to a + // redirect target whose auth was just stripped, leaking them to a different origin that answers 401. + Realm realm = future.getRealm(); + + // A CONNECT is addressed to the PROXY and travels in the clear, so its response is the proxy's, not + // the origin's - which changes both what may be stored from it and who may act on it. + boolean connectRequest = httpRequest.method() == HttpMethod.CONNECT; // This MUST BE called before Redirect30xInterceptor because latter assumes cookie store is already updated CookieStore cookieStore = config.getCookieStore(); - if (cookieStore != null) { + if (cookieStore != null && !connectRequest) { + // Skipped on a CONNECT: currentRequest is the ORIGIN request, so a Set-Cookie in the proxy's answer + // would be filed against the URI of an origin the request never reached. for (String cookieStr : responseHeaders.getAll(SET_COOKIE)) { Cookie c = cookieDecoder.decode(cookieStr); if (c != null) { @@ -85,6 +95,21 @@ public boolean exitAfterIntercept(Channel channel, return true; } + // Only two answers to a CONNECT may be acted on: a 200 that establishes the tunnel, and a 407 asking + // the proxy realm for credentials. Everything else - 401, 3xx, 100 - must NOT reach the origin-request + // interceptors, which rebuild the exchange as the ORIGIN request with setReuseChannel(true). That put + // the origin's Authorization on a socket still terminated by the proxy in plaintext, and let a hostile + // or compromised proxy solicit it with nothing more than a 401 or a 302. + if (connectRequest) { + if (statusCode == PROXY_AUTHENTICATION_REQUIRED_407) { + return proxyUnauthorized407Interceptor.exitAfterHandling407(channel, future, response, request, proxyServer, httpRequest); + } + if (statusCode == OK_200) { + return connectSuccessInterceptor.exitAfterHandlingConnect(channel, future, request, proxyServer); + } + return false; + } + if (statusCode == UNAUTHORIZED_401) { return unauthorized401Interceptor.exitAfterHandling401(channel, future, response, request, realm, httpRequest); @@ -97,9 +122,6 @@ public boolean exitAfterIntercept(Channel channel, } else if (Redirect30xInterceptor.REDIRECT_STATUSES.contains(statusCode)) { return redirect30xInterceptor.exitAfterHandlingRedirect(channel, future, response, request, statusCode, realm); - } else if (httpRequest.method() == HttpMethod.CONNECT && statusCode == OK_200) { - return connectSuccessInterceptor.exitAfterHandlingConnect(channel, future, request, proxyServer); - } return false; } diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java index 57436e9ae5..d087b670bb 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java @@ -58,10 +58,10 @@ public boolean exitAfterHandling407(Channel channel, ProxyServer proxyServer, HttpRequest httpRequest) { - if (future.isAndSetInProxyAuth(true)) { - LOGGER.info("Can't handle 407 as auth was already performed"); - return false; - } + // The three questions below all decide whether the PROXY wrote this response, and none of them may be + // asked after isAndSetInProxyAuth: that latch is the exchange's one shot at proxy authentication, and + // burning it on a 407 we then decline makes a later, legitimate proxy challenge on the same exchange + // fail with "auth was already performed" when no proxy auth was ever performed at all. Realm proxyRealm = future.getProxyRealm(); @@ -70,6 +70,32 @@ public boolean exitAfterHandling407(Channel channel, return false; } + // A SOCKS proxy tunnels at the transport layer and never speaks HTTP, so a 407 arriving over one was + // written by the ORIGIN. Answering it mints proxy credentials for the origin: the NTLM and + // Kerberos/SPNEGO branches below write Proxy-Authorization straight onto the request headers, which + // newNettyRequest then copies verbatim, bypassing the proxy-type gate that guards the preemptive path. + if (proxyServer == null || !proxyServer.getProxyType().isHttp()) { + LOGGER.debug("Can't handle 407: not an HTTP proxy, so the 407 came from the origin"); + return false; + } + + // Being an HTTP proxy only says the proxy CAN write a 407; it does not say it wrote this one. A CONNECT + // is addressed to the proxy, so its 407 is the proxy's. Anything else on a tunnelled socket reaches the + // ORIGIN, and answering the origin's 407 hands it the proxy's credentials. The tunnel may have been + // established by this exchange (tunnelEstablished) or inherited with a channel taken from the pool, in + // which case the flag is false but the target still implies one: behind an HTTP proxy a secured or + // WebSocket target is only ever reached through a CONNECT. + if (httpRequest.method() != HttpMethod.CONNECT + && (future.isTunnelEstablished() || request.getUri().isSecured() || request.getUri().isWebSocket())) { + LOGGER.debug("Can't handle 407: it arrived through a tunnel, so it came from the origin"); + return false; + } + + if (future.isAndSetInProxyAuth(true)) { + LOGGER.info("Can't handle 407 as auth was already performed"); + return false; + } + List proxyAuthHeaders = response.headers().getAll(PROXY_AUTHENTICATE); if (proxyAuthHeaders.isEmpty()) { diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java index a2ddbd9467..ce292128ad 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java @@ -27,6 +27,7 @@ import org.asynchttpclient.handler.MaxRedirectException; import org.asynchttpclient.netty.NettyResponseFuture; import org.asynchttpclient.netty.channel.ChannelManager; +import org.asynchttpclient.netty.channel.PrincipalScopedPartitionKey; import org.asynchttpclient.netty.request.NettyRequestSender; import org.asynchttpclient.uri.Uri; import org.slf4j.Logger; @@ -62,11 +63,13 @@ public class Redirect30xInterceptor { private final AsyncHttpClientConfig config; private final NettyRequestSender requestSender; private final MaxRedirectException maxRedirectException; + private final boolean stripAuthorizationOnRedirect; Redirect30xInterceptor(ChannelManager channelManager, AsyncHttpClientConfig config, NettyRequestSender requestSender) { this.channelManager = channelManager; this.config = config; this.requestSender = requestSender; + this.stripAuthorizationOnRedirect = config.isStripAuthorizationOnRedirect(); maxRedirectException = unknownStackTrace(new MaxRedirectException("Maximum redirect reached: " + config.getMaxRedirects()), Redirect30xInterceptor.class, "exitAfterHandlingRedirect"); } @@ -92,15 +95,37 @@ public boolean exitAfterHandlingRedirect(Channel channel, && !originalMethod.equals(OPTIONS) && !originalMethod.equals(HEAD) && (statusCode == MOVED_PERMANENTLY_301 || statusCode == SEE_OTHER_303 || (statusCode == FOUND_302 && !config.isStrict302Handling())); boolean keepBody = statusCode == TEMPORARY_REDIRECT_307 || statusCode == PERMANENT_REDIRECT_308 || (statusCode == FOUND_302 && config.isStrict302Handling()); + HttpHeaders responseHeaders = response.headers(); + String location = responseHeaders.get(LOCATION); + Uri newUri = Uri.create(future.getUri(), location); + LOGGER.debug("Redirecting to {}", newUri); + + boolean sameBase = request.getUri().isSameBase(newUri); + boolean schemeDowngrade = request.getUri().isSecured() && !newUri.isSecured(); + boolean stripAuth = !sameBase || schemeDowngrade || stripAuthorizationOnRedirect; + + if (stripAuth && (request.getRealm() != null + || request.getHeaders().contains(AUTHORIZATION) + || request.getHeaders().contains(COOKIE))) { + LOGGER.debug("Stripping credentials on redirect to {}", newUri); + } + final RequestBuilder requestBuilder = new RequestBuilder(switchToGet ? GET : originalMethod) .setChannelPoolPartitioning(request.getChannelPoolPartitioning()) .setFollowRedirect(true) .setLocalAddress(request.getLocalAddress()) .setNameResolver(request.getNameResolver()) .setProxyServer(request.getProxyServer()) - .setRealm(request.getRealm()) + .setRealm(stripAuth ? null : request.getRealm()) .setRequestTimeout(request.getRequestTimeout()); + if (stripAuth) { + // Clear both realms on the future so NettyRequestFactory cannot regenerate + // Authorization or Proxy-Authorization headers on the redirected request. + future.setRealm(null); + future.setProxyRealm(null); + } + if (keepBody) { requestBuilder.setCharset(request.getCharset()); if (isNonEmpty(request.getFormParams())) @@ -118,29 +143,26 @@ else if (isNonEmpty(request.getBodyParts())) { } } - requestBuilder.setHeaders(propagatedHeaders(request, realm, keepBody)); + requestBuilder.setHeaders(propagatedHeaders(request, realm, keepBody, stripAuth)); // in case of a redirect from HTTP to HTTPS, future // attributes might change final boolean initialConnectionKeepAlive = future.isKeepAlive(); - final Object initialPartitionKey = future.getPartitionKey(); - - HttpHeaders responseHeaders = response.headers(); - String location = responseHeaders.get(LOCATION); - Uri newUri = Uri.create(future.getUri(), location); - LOGGER.debug("Redirecting to {}", newUri); + // Scoped like every other offer: an NTLM or Negotiate connection must stay with the identity + // that authenticated it, and a same-host redirect during NTLM is the ordinary case. + final Object initialPartitionKey = PrincipalScopedPartitionKey.scope( + future.getPartitionKey(), future.getRealm()); CookieStore cookieStore = config.getCookieStore(); if (cookieStore != null) { // Update request's cookies assuming that cookie store is already updated by Interceptors List cookies = cookieStore.get(newUri); if (!cookies.isEmpty()) - for (Cookie cookie : cookies) - requestBuilder.addOrReplaceCookie(cookie); + for (Cookie cookie : cookieStore.get(newUri)) { + requestBuilder.addCookieIfUnset(cookie); + } } - boolean sameBase = request.getUri().isSameBase(newUri); - if (sameBase) { // we can only assume the virtual host is still valid if the baseUrl is the same requestBuilder.setVirtualHost(request.getVirtualHost()); @@ -173,7 +195,7 @@ else if (isNonEmpty(request.getBodyParts())) { return false; } - private HttpHeaders propagatedHeaders(Request request, Realm realm, boolean keepBody) { + private HttpHeaders propagatedHeaders(Request request, Realm realm, boolean keepBody, boolean stripAuthorization) { HttpHeaders headers = request.getHeaders() .remove(HOST) @@ -183,7 +205,13 @@ private HttpHeaders propagatedHeaders(Request request, Realm realm, boolean keep headers.remove(CONTENT_TYPE); } - if (realm != null && realm.getScheme() == AuthScheme.NTLM) { + if (stripAuthorization) { + // Cookie is dropped only on the security boundary; the URI-scoped CookieStore re-adds + // any cookies that legitimately match the new target after this method returns. + headers.remove(AUTHORIZATION) + .remove(PROXY_AUTHORIZATION) + .remove(COOKIE); + } else if (realm != null && realm.getScheme() == AuthScheme.NTLM) { headers.remove(AUTHORIZATION) .remove(PROXY_AUTHORIZATION); } diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java index 4cfee06cd6..8556cd5d14 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java @@ -199,10 +199,20 @@ public NettyRequest newNettyRequest(Request request, boolean performConnectReque headers.set(HOST, virtualHost != null ? virtualHost : hostHeader(uri)); } - // don't override authorization but append - addAuthorizationHeader(headers, perRequestAuthorizationHeader(request, realm)); - // only set proxy auth on request over plain HTTP, or when performing CONNECT - if (!uri.isSecured() || connect) { + // don't override authorization but append. Skip it on a CONNECT: that request is sent to the + // proxy in the clear to open the tunnel, so the origin Authorization would be exposed to the + // proxy. It is added to the tunneled request, which is built separately once the tunnel is up. + if (!connect) { + addAuthorizationHeader(headers, perRequestAuthorizationHeader(request, realm)); + } + // Only set proxy auth on a request sent to an HTTP proxy: either over plain HTTP (the origin request + // carries an absolute URI straight to the proxy) or on a CONNECT (sent to the proxy to open the + // tunnel). A SOCKS proxy tunnels at the transport layer, so the request reaches the ORIGIN - a + // Proxy-Authorization header would leak the proxy credentials to the origin. Guard on the proxy type. + // A ws:// request is tunnelled through CONNECT the same way wss:// is (see NettyRequestSender's + // needConnect check), so the upgrade request that follows also reaches the origin, not the proxy; + // exclude it from the plain-HTTP branch the same way wss:// already is. + if ((connect || (!uri.isSecured() && !uri.isWebSocket())) && proxyServer != null && proxyServer.getProxyType().isHttp()) { setProxyAuthorizationHeader(headers, perRequestProxyAuthorizationHeader(request, proxyRealm)); } @@ -224,9 +234,12 @@ private String requestUri(Uri uri, ProxyServer proxyServer, boolean connect) { // proxy tunnelling, connect need host and explicit port return uri.getAuthority(); - } else if (proxyServer != null && !uri.isSecured() && proxyServer.getProxyType().isHttp()) { - // proxy over HTTP, need full url - return uri.toUrl(); + } else if (proxyServer != null && !uri.isSecured() && !uri.isWebSocket() && proxyServer.getProxyType().isHttp()) { + // proxy over HTTP, need full url, minus the userinfo: this request line is sent to the proxy in the + // clear and RFC 9110 section 4.2.4 forbids userinfo in a generated request target. A ws:// request is + // tunnelled through CONNECT, so its upgrade request reaches the origin and takes the origin-form + // request target below + return uri.toUrlWithoutUserInfo(); } else { // direct connection to target host or tunnel already connected: only path and query diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java index aed08b7a70..0adcf72759 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -35,6 +35,7 @@ import org.asynchttpclient.netty.OnLastHttpContentCallback; import org.asynchttpclient.netty.SimpleFutureListener; import org.asynchttpclient.netty.channel.*; +import org.asynchttpclient.netty.channel.PrincipalScopedPartitionKey; import org.asynchttpclient.netty.handler.StreamedResponsePublisher; import org.asynchttpclient.netty.timeout.TimeoutsHolder; import org.asynchttpclient.proxy.ProxyServer; @@ -117,6 +118,11 @@ public ListenableFuture sendRequest(final Request request, private boolean isConnectAlreadyDone(Request request, NettyResponseFuture future) { return future != null + // The last request having been a CONNECT proves nothing: it is equally true when the proxy + // REJECTED it with 401, 407 or a redirect. ConnectSuccessInterceptor never ran in that case, + // so the socket is still a plaintext hop to the proxy. Skipping the CONNECT here would send + // the ORIGIN request - Authorization header and all - straight down it. + && future.isTunnelEstablished() && future.getNettyRequest() != null && future.getNettyRequest().getHttpRequest().method() == HttpMethod.CONNECT && !request.getMethod().equals(CONNECT); @@ -283,8 +289,20 @@ private ListenableFuture sendRequestWithNewChannel(Request request, } Realm realm = future.getRealm(); Realm proxyRealm = future.getProxyRealm(); - requestFactory.addAuthorizationHeader(headers, perConnectionAuthorizationHeader(request, proxy, realm)); - requestFactory.setProxyAuthorizationHeader(headers, perConnectionProxyAuthorizationHeader(request, proxyRealm)); + // On the tunnel path this is the CONNECT request, sent to the proxy in the clear before the TLS + // tunnel exists. Preemptive NTLM/Kerberos/SPNEGO realms attach their header here rather than in + // the factory, so skip it on CONNECT to keep the origin credentials off the plaintext hop. They + // travel on the tunneled request once the tunnel is up. + if (future.getNettyRequest().getHttpRequest().method() != HttpMethod.CONNECT) { + requestFactory.addAuthorizationHeader(headers, perConnectionAuthorizationHeader(request, proxy, realm)); + } + // Preemptive per-connection proxy auth (NTLM/Kerberos/SPNEGO) belongs only on a request sent to an + // HTTP proxy. Over a SOCKS proxy the transport tunnels this request to the ORIGIN, so attaching + // Proxy-Authorization here would leak the proxy credentials to the origin. Guard on the proxy type, + // mirroring the per-request gate in NettyRequestFactory. + if (proxy != null && proxy.getProxyType().isHttp()) { + requestFactory.setProxyAuthorizationHeader(headers, perConnectionProxyAuthorizationHeader(request, proxyRealm)); + } future.setInAuth(realm != null && realm.isUsePreemptiveAuth() && realm.getScheme() != AuthScheme.NTLM); future.setInProxyAuth( @@ -587,7 +605,14 @@ private Channel pollPooledChannel(Request request, ProxyServer proxy, AsyncHandl Uri uri = request.getUri(); String virtualHost = request.getVirtualHost(); - final Channel channel = channelManager.poll(uri, virtualHost, proxy, request.getChannelPoolPartitioning()); + // Scope the pool lookup by the authenticated identity: NTLM and Negotiate authenticate the socket, + // not the request, so a connection one principal completed the handshake on must not be handed to + // another. A disagreement with the offer side costs a pool miss, never a wrong reuse. + // Resolved as newNettyRequestAndResponseFuture resolves it, so the poll and the offer agree. + Realm realm = request.getRealm() != null ? request.getRealm() : config.getRealm(); + Object partitionKey = PrincipalScopedPartitionKey.scope( + request.getChannelPoolPartitioning().getPartitionKey(uri, virtualHost, proxy), realm); + final Channel channel = channelManager.poll(partitionKey); if (channel != null) { LOGGER.debug("Using pooled Channel '{}' for '{}' to '{}'", channel, request.getMethod(), uri); @@ -612,7 +637,28 @@ public void replayRequest(final NettyResponseFuture future, FilterContext fc, return; } - channelManager.drainChannelAndOffer(channel, future); + // A replay may target a different origin than the one this future was built for; failover onto a + // second host is the documented use of a ResponseFilter. Everything keyed off the future's TARGET has + // to move with it or it keeps describing the previous origin: the connection pool partition key, and + // the credentials, which must not follow the request to a host they were not meant for. + Uri previousUri = future.getTargetRequest().getUri(); + Uri newUri = newRequest.getUri(); + boolean sameBase = previousUri.isSameBase(newUri); + boolean schemeDowngrade = previousUri.isSecured() && !newUri.isSecured(); + // Sampled while the future still describes the old origin: that is what the drained channel is + // connected to, so that is the key it has to be filed under. + Object initialPartitionKey = PrincipalScopedPartitionKey.scope( + future.getPartitionKey(), future.getRealm()); + + if (!sameBase || schemeDowngrade) { + future.setRealm(newRequest.getRealm()); + future.setProxyRealm(null); + } + // The proxy is the other half of the pool key and the replay may resolve to a different one, or none. + future.setProxyServer(getProxyServer(config, newRequest)); + future.setTargetRequest(newRequest); + + channelManager.drainChannelAndOffer(channel, future, future.isKeepAlive(), initialPartitionKey); sendNextRequest(newRequest, future); } diff --git a/client/src/main/java/org/asynchttpclient/uri/Uri.java b/client/src/main/java/org/asynchttpclient/uri/Uri.java index 19986dcfc3..580c495a73 100644 --- a/client/src/main/java/org/asynchttpclient/uri/Uri.java +++ b/client/src/main/java/org/asynchttpclient/uri/Uri.java @@ -149,6 +149,30 @@ public String toUrl() { return url; } + /** + * Same as {@link #toUrl()} but without the deprecated userinfo subcomponent. RFC 9110 section 4.2.4: a + * sender MUST NOT generate the userinfo subcomponent (and its "@" delimiter) when an origin-form or + * absolute-form URI reference is generated for a request target, so this is what belongs on the wire when + * an absolute-form target is required. {@link #toUrl()} keeps the userinfo for the caller-visible URL. + * + * @return [scheme]://[hostname](:[port])/path(?[query]) + */ + public String toUrlWithoutUserInfo() { + if (userInfo == null) { + return toUrl(); + } + + StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder(); + sb.append(scheme).append("://").append(host); + if (port != -1) + sb.append(':').append(port); + if (path != null) + sb.append(path); + if (query != null) + sb.append('?').append(query); + return sb.toString(); + } + /** * @return [scheme]://[hostname](:[port])/path. Port is omitted if it matches the scheme's default one. */ diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java index 00d69af7d2..fe3c694e20 100644 --- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java @@ -55,7 +55,13 @@ private static String computeBasicAuthentication(String principal, String passwo public static String computeRealmURI(Uri uri, boolean useAbsoluteURI, boolean omitQuery) { if (useAbsoluteURI) { - return omitQuery && MiscUtils.isNonEmpty(uri.getQuery()) ? uri.withNewQuery(null).toUrl() : uri.toUrl(); + // Absolute form, so this string goes on the wire as the Digest uri="..." parameter, and the digest + // itself is computed over it. RFC 9110 section 4.2.4 forbids generating the userinfo subcomponent in + // a request target, and Digest is used precisely on hops where the password must not travel in the + // clear - toUrl() would render "user:secret@" straight into the header. Use the userinfo-free form. + return omitQuery && MiscUtils.isNonEmpty(uri.getQuery()) + ? uri.withNewQuery(null).toUrlWithoutUserInfo() + : uri.toUrlWithoutUserInfo(); } else { String path = uri.getNonEmptyPath(); return omitQuery || !MiscUtils.isNonEmpty(uri.getQuery()) ? path : path + "?" + uri.getQuery(); @@ -155,6 +161,17 @@ public static String perRequestProxyAuthorizationHeader(Request request, Realm p return proxyAuthorization; } + /** + * The service the origin realm's Negotiate token is minted for. It is always the origin, even when a + * proxy is configured: building it against the proxy host produced a service ticket for the proxy's SPN, + * a confused deputy where the origin's credential is delivered to, and only usable by, the proxy, while + * origin authentication fails. The proxy realm has its own path, in + * perConnectionProxyAuthorizationHeader, and is unaffected. + */ + static String negotiateHost(Request request) { + return request.getVirtualHost() != null ? request.getVirtualHost() : request.getUri().getHost(); + } + public static String perConnectionAuthorizationHeader(Request request, ProxyServer proxyServer, Realm realm) { String authorizationHeader = null; @@ -166,13 +183,7 @@ public static String perConnectionAuthorizationHeader(Request request, ProxyServ break; case KERBEROS: case SPNEGO: - String host; - if (proxyServer != null) - host = proxyServer.getHost(); - else if (request.getVirtualHost() != null) - host = request.getVirtualHost(); - else - host = request.getUri().getHost(); + String host = negotiateHost(request); try { authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance( diff --git a/client/src/main/java/org/asynchttpclient/util/HttpUtils.java b/client/src/main/java/org/asynchttpclient/util/HttpUtils.java index 779dba9c78..5930d31d7e 100644 --- a/client/src/main/java/org/asynchttpclient/util/HttpUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/HttpUtils.java @@ -23,8 +23,8 @@ import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.security.SecureRandom; import java.util.List; -import java.util.concurrent.ThreadLocalRandom; import static java.nio.charset.StandardCharsets.*; @@ -114,9 +114,16 @@ private static String extractContentTypeAttribute(String contentType, String att // The pool of ASCII chars to be used for generating a multipart boundary. private static byte[] MULTIPART_CHARS = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes(US_ASCII); + // The boundary is what separates parts whose content is not escaped, so it must be unpredictable, like + // the cnonce in Realm. It is also the single largest window a peer gets onto the generator: 30 to 40 + // consecutive nextInt(64) outputs, echoed verbatim in the Content-Type header and in the body. With a + // non-cryptographic generator that over-determines the state, so a boundary observed on one request + // predicts every subsequent draw from the same thread - including the Digest cnonce. + private static final ThreadLocal BOUNDARY_RANDOM = ThreadLocal.withInitial(SecureRandom::new); + // a random size from 30 to 40 public static byte[] computeMultipartBoundary() { - ThreadLocalRandom random = ThreadLocalRandom.current(); + SecureRandom random = BOUNDARY_RANDOM.get(); byte[] bytes = new byte[random.nextInt(11) + 30]; for (int i = 0; i < bytes.length; i++) { bytes[i] = MULTIPART_CHARS[random.nextInt(MULTIPART_CHARS.length)]; diff --git a/client/src/main/java/org/asynchttpclient/ws/WebSocketUtils.java b/client/src/main/java/org/asynchttpclient/ws/WebSocketUtils.java index 31bd8f5c2a..4861ac7b92 100644 --- a/client/src/main/java/org/asynchttpclient/ws/WebSocketUtils.java +++ b/client/src/main/java/org/asynchttpclient/ws/WebSocketUtils.java @@ -13,8 +13,7 @@ */ package org.asynchttpclient.ws; -import io.netty.util.internal.ThreadLocalRandom; - +import java.security.SecureRandom; import java.util.Base64; import static java.nio.charset.StandardCharsets.US_ASCII; @@ -23,12 +22,14 @@ public final class WebSocketUtils { private static final String MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + // RFC 6455 section 10.3 requires the handshake nonce to come from a strong source of entropy: it is + // what proves the 101 was produced by a peer that saw this request. Netty's ThreadLocalRandom is a + // 48-bit LCG, so an off-path party who can guess the key can precompute a valid Sec-WebSocket-Accept. + private static final ThreadLocal KEY_RANDOM = ThreadLocal.withInitial(SecureRandom::new); + public static String getWebSocketKey() { byte[] nonce = new byte[16]; - ThreadLocalRandom random = ThreadLocalRandom.current(); - for (int i = 0; i < nonce.length; i++) { - nonce[i] = (byte) random.nextInt(256); - } + KEY_RANDOM.get().nextBytes(nonce); return Base64.getEncoder().encodeToString(nonce); } diff --git a/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties b/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties index 62bc177726..014dc5d613 100644 --- a/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties +++ b/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties @@ -46,6 +46,7 @@ org.asynchttpclient.chunkedFileChunkSize=8192 org.asynchttpclient.webSocketMaxBufferSize=128000000 org.asynchttpclient.webSocketMaxFrameSize=10240 org.asynchttpclient.keepEncodingHeader=false +org.asynchttpclient.maxDecompressedResponseSize=268435456 org.asynchttpclient.shutdownQuietPeriod=2000 org.asynchttpclient.shutdownTimeout=15000 org.asynchttpclient.useNativeTransport=false diff --git a/client/src/main/resources/org/asynchttpclient/cookie/public_suffix_list.dat b/client/src/main/resources/org/asynchttpclient/cookie/public_suffix_list.dat new file mode 100644 index 0000000000..0827ebab62 --- /dev/null +++ b/client/src/main/resources/org/asynchttpclient/cookie/public_suffix_list.dat @@ -0,0 +1,11255 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Please pull this list from, and only from https://publicsuffix.org/list/public_suffix_list.dat, +// rather than any other VCS sites. Pulling from any other URL is not guaranteed to be supported. + +// Instructions on pulling and using this list can be found at https://publicsuffix.org/list/. + +// ===BEGIN ICANN DOMAINS=== + +// ac : http://nic.ac/rules.htm +ac +com.ac +edu.ac +gov.ac +mil.ac +net.ac +org.ac + +// ad : https://www.iana.org/domains/root/db/ad.html +// Confirmed by Amadeu Abril i Abril (CORE) 2024-11-17 +ad + +// ae : https://www.iana.org/domains/root/db/ae.html +ae +ac.ae +co.ae +gov.ae +mil.ae +net.ae +org.ae +sch.ae + +// aero : https://information.aero/registration/policies/dmp +aero +// 2LDs +airline.aero +airport.aero +// 2LDs (currently not accepting registration, seemingly never have) +// As of 2024-07, these are marked as reserved for potential 3LD +// registrations (clause 11 "allocated subdomains" in the 2006 TLD +// policy), but the relevant industry partners have not opened them up +// for registration. Current status can be determined from the TLD's +// policy document: 2LDs that are open for registration must list +// their policy in the TLD's policy. Any 2LD without such a policy is +// not open for registrations. +accident-investigation.aero +accident-prevention.aero +aerobatic.aero +aeroclub.aero +aerodrome.aero +agents.aero +air-surveillance.aero +air-traffic-control.aero +aircraft.aero +airtraffic.aero +ambulance.aero +association.aero +author.aero +ballooning.aero +broker.aero +caa.aero +cargo.aero +catering.aero +certification.aero +championship.aero +charter.aero +civilaviation.aero +club.aero +conference.aero +consultant.aero +consulting.aero +control.aero +council.aero +crew.aero +design.aero +dgca.aero +educator.aero +emergency.aero +engine.aero +engineer.aero +entertainment.aero +equipment.aero +exchange.aero +express.aero +federation.aero +flight.aero +freight.aero +fuel.aero +gliding.aero +government.aero +groundhandling.aero +group.aero +hanggliding.aero +homebuilt.aero +insurance.aero +journal.aero +journalist.aero +leasing.aero +logistics.aero +magazine.aero +maintenance.aero +marketplace.aero +media.aero +microlight.aero +modelling.aero +navigation.aero +parachuting.aero +paragliding.aero +passenger-association.aero +pilot.aero +press.aero +production.aero +recreation.aero +repbody.aero +res.aero +research.aero +rotorcraft.aero +safety.aero +scientist.aero +services.aero +show.aero +skydiving.aero +software.aero +student.aero +taxi.aero +trader.aero +trading.aero +trainer.aero +union.aero +workinggroup.aero +works.aero + +// af : https://www.nic.af/domain-price +af +com.af +edu.af +gov.af +net.af +org.af + +// ag : http://www.nic.ag/prices.htm +ag +co.ag +com.ag +net.ag +nom.ag +org.ag + +// ai : http://nic.com.ai/ +ai +com.ai +net.ai +off.ai +org.ai + +// al : http://www.ert.gov.al/ert_alb/faq_det.html?Id=31 +al +com.al +edu.al +gov.al +mil.al +net.al +org.al + +// am : https://www.amnic.net/policy/en/Policy_EN.pdf +// Confirmed by ISOC AM 2024-11-18 +am +co.am +com.am +commune.am +net.am +org.am + +// ao : https://www.iana.org/domains/root/db/ao.html +// https://www.dns.ao/ao/ +ao +co.ao +ed.ao +edu.ao +gov.ao +gv.ao +it.ao +og.ao +org.ao +pb.ao + +// aq : https://www.iana.org/domains/root/db/aq.html +aq + +// ar : https://nic.ar/es/nic-argentina/normativa +ar +bet.ar +com.ar +coop.ar +edu.ar +gob.ar +gov.ar +int.ar +mil.ar +musica.ar +mutual.ar +net.ar +org.ar +seg.ar +senasa.ar +tur.ar + +// arpa : https://www.iana.org/domains/root/db/arpa.html +// Confirmed by registry 2008-06-18 +arpa +e164.arpa +home.arpa +in-addr.arpa +ip6.arpa +iris.arpa +uri.arpa +urn.arpa + +// as : https://www.iana.org/domains/root/db/as.html +as +gov.as + +// asia : https://www.iana.org/domains/root/db/asia.html +asia + +// at : https://www.iana.org/domains/root/db/at.html +// Confirmed by registry 2008-06-17 +at +ac.at +sth.ac.at +co.at +gv.at +or.at + +// au : https://www.iana.org/domains/root/db/au.html +// https://www.auda.org.au/ +// Confirmed by registry 2025-07-16 +au +// 2LDs +asn.au +com.au +edu.au +gov.au +id.au +net.au +org.au +// Historic 2LDs (closed to new registration, but sites still exist) +conf.au +oz.au +// CGDNs : https://www.auda.org.au/au-domain-names/the-different-au-domain-names/state-and-territory-domain-names/ +act.au +nsw.au +nt.au +qld.au +sa.au +tas.au +vic.au +wa.au +// 3LDs +act.edu.au +catholic.edu.au +// eq.edu.au - Removed at the request of the Queensland Department of Education +nsw.edu.au +nt.edu.au +qld.edu.au +sa.edu.au +tas.edu.au +vic.edu.au +wa.edu.au +// act.gov.au - Bug 984824 - Removed at request of Greg Tankard +// nsw.gov.au - Bug 547985 - Removed at request of +// nt.gov.au - Bug 940478 - Removed at request of Greg Connors +qld.gov.au +sa.gov.au +tas.gov.au +vic.gov.au +wa.gov.au +// 4LDs +// education.tas.edu.au - Removed at the request of the Department of Education Tasmania +// schools.nsw.edu.au - Removed at the request of the New South Wales Department of Education. + +// aw : https://www.iana.org/domains/root/db/aw.html +aw +com.aw + +// ax : https://www.iana.org/domains/root/db/ax.html +ax + +// az : https://www.iana.org/domains/root/db/az.html +// Confirmed via https://whois.az/?page_id=10 2024-12-11 +az +biz.az +co.az +com.az +edu.az +gov.az +info.az +int.az +mil.az +name.az +net.az +org.az +pp.az +// No longer available for registration, however domains exist as of 2024-12-11 +// see https://whois.az/?page_id=783 +pro.az + +// ba : https://www.iana.org/domains/root/db/ba.html +ba +com.ba +edu.ba +gov.ba +mil.ba +net.ba +org.ba + +// bb : https://www.iana.org/domains/root/db/bb.html +bb +biz.bb +co.bb +com.bb +edu.bb +gov.bb +info.bb +net.bb +org.bb +store.bb +tv.bb + +// bd : https://www.iana.org/domains/root/db/bd.html +// Confirmed by registry +bd +ac.bd +ai.bd +co.bd +com.bd +edu.bd +gov.bd +id.bd +info.bd +it.bd +mil.bd +net.bd +org.bd +sch.bd +tv.bd + +// be : https://www.iana.org/domains/root/db/be.html +// Confirmed by registry 2008-06-08 +be +ac.be + +// bf : https://www.iana.org/domains/root/db/bf.html +bf +gov.bf + +// bg : https://www.iana.org/domains/root/db/bg.html +// https://www.register.bg/user/static/rules/en/index.html +bg +0.bg +1.bg +2.bg +3.bg +4.bg +5.bg +6.bg +7.bg +8.bg +9.bg +a.bg +b.bg +c.bg +d.bg +e.bg +f.bg +g.bg +h.bg +i.bg +j.bg +k.bg +l.bg +m.bg +n.bg +o.bg +p.bg +q.bg +r.bg +s.bg +t.bg +u.bg +v.bg +w.bg +x.bg +y.bg +z.bg + +// bh : https://www.iana.org/domains/root/db/bh.html +bh +com.bh +edu.bh +gov.bh +net.bh +org.bh + +// bi : https://www.iana.org/domains/root/db/bi.html +// http://whois.nic.bi/ +bi +co.bi +com.bi +edu.bi +or.bi +org.bi + +// biz : https://www.iana.org/domains/root/db/biz.html +biz + +// bj : https://nic.bj/bj-suffixes.txt +// Submitted by registry +bj +africa.bj +agro.bj +architectes.bj +assur.bj +avocats.bj +co.bj +com.bj +eco.bj +econo.bj +edu.bj +info.bj +loisirs.bj +money.bj +net.bj +org.bj +ote.bj +restaurant.bj +resto.bj +tourism.bj +univ.bj + +// bm : https://www.bermudanic.bm/domain-registration/index.php +bm +com.bm +edu.bm +gov.bm +net.bm +org.bm + +// bn : http://www.bnnic.bn/faqs +bn +com.bn +edu.bn +gov.bn +net.bn +org.bn + +// bo : https://nic.bo +// Confirmed by registry 2024-11-19 +bo +com.bo +edu.bo +gob.bo +int.bo +mil.bo +net.bo +org.bo +tv.bo +web.bo +// Social Domains +academia.bo +agro.bo +arte.bo +blog.bo +bolivia.bo +ciencia.bo +cooperativa.bo +democracia.bo +deporte.bo +ecologia.bo +economia.bo +empresa.bo +indigena.bo +industria.bo +info.bo +medicina.bo +movimiento.bo +musica.bo +natural.bo +nombre.bo +noticias.bo +patria.bo +plurinacional.bo +politica.bo +profesional.bo +pueblo.bo +revista.bo +salud.bo +tecnologia.bo +tksat.bo +transporte.bo +wiki.bo + +// br : http://registro.br/dominio/categoria.html +// Submitted by registry +br +9guacu.br +abc.br +adm.br +adv.br +agr.br +aju.br +am.br +anani.br +aparecida.br +api.br +app.br +arq.br +art.br +ato.br +b.br +barueri.br +belem.br +bet.br +bhz.br +bib.br +bio.br +blog.br +bmd.br +boavista.br +bsb.br +campinagrande.br +campinas.br +caxias.br +cim.br +cng.br +cnt.br +com.br +contagem.br +coop.br +coz.br +cri.br +cuiaba.br +curitiba.br +def.br +des.br +det.br +dev.br +ecn.br +eco.br +edu.br +emp.br +enf.br +eng.br +esp.br +etc.br +eti.br +far.br +feira.br +flog.br +floripa.br +fm.br +fnd.br +fortal.br +fot.br +foz.br +fst.br +g12.br +geo.br +ggf.br +goiania.br +gov.br +// gov.br 26 states + df https://en.wikipedia.org/wiki/States_of_Brazil +ac.gov.br +al.gov.br +am.gov.br +ap.gov.br +ba.gov.br +ce.gov.br +df.gov.br +es.gov.br +go.gov.br +ma.gov.br +mg.gov.br +ms.gov.br +mt.gov.br +pa.gov.br +pb.gov.br +pe.gov.br +pi.gov.br +pr.gov.br +rj.gov.br +rn.gov.br +ro.gov.br +rr.gov.br +rs.gov.br +sc.gov.br +se.gov.br +sp.gov.br +to.gov.br +gru.br +ia.br +imb.br +ind.br +inf.br +jab.br +jampa.br +jdf.br +joinville.br +jor.br +jus.br +leg.br +leilao.br +lel.br +log.br +londrina.br +macapa.br +maceio.br +manaus.br +maringa.br +mat.br +med.br +mil.br +morena.br +mp.br +mus.br +natal.br +net.br +niteroi.br +*.nom.br +not.br +ntr.br +odo.br +ong.br +org.br +osasco.br +palmas.br +poa.br +ppg.br +pro.br +psc.br +psi.br +pvh.br +qsl.br +radio.br +rec.br +recife.br +rep.br +ribeirao.br +rio.br +riobranco.br +riopreto.br +salvador.br +sampa.br +santamaria.br +santoandre.br +saobernardo.br +saogonca.br +seg.br +sjc.br +slg.br +slz.br +social.br +sorocaba.br +srv.br +taxi.br +tc.br +tec.br +teo.br +the.br +tmp.br +trd.br +tur.br +tv.br +udi.br +vet.br +vix.br +vlog.br +wiki.br +xyz.br +zlg.br + +// bs : http://www.nic.bs/rules.html +bs +com.bs +edu.bs +gov.bs +net.bs +org.bs + +// bt : https://www.iana.org/domains/root/db/bt.html +bt +com.bt +edu.bt +gov.bt +net.bt +org.bt + +// bv : No registrations at this time. +// Submitted by registry +bv + +// bw : https://www.iana.org/domains/root/db/bw.html +// https://nic.net.bw/bw-name-structure +bw +ac.bw +co.bw +gov.bw +net.bw +org.bw + +// by : https://www.iana.org/domains/root/db/by.html +// http://tld.by/rules_2006_en.html +// list of other 2nd level tlds ? +by +gov.by +mil.by +// Official information does not indicate that com.by is a reserved +// second-level domain, but it's being used as one (see www.google.com.by and +// www.yahoo.com.by, for example), so we list it here for safety's sake. +com.by +// http://hoster.by/ +of.by + +// bz : https://www.iana.org/domains/root/db/bz.html +// http://www.belizenic.bz/ +bz +co.bz +com.bz +edu.bz +gov.bz +net.bz +org.bz + +// ca : https://www.iana.org/domains/root/db/ca.html +ca +// ca geographical names +ab.ca +bc.ca +mb.ca +nb.ca +nf.ca +nl.ca +ns.ca +nt.ca +nu.ca +on.ca +pe.ca +qc.ca +sk.ca +yk.ca +// gc.ca: https://en.wikipedia.org/wiki/.gc.ca +// see also: http://registry.gc.ca/en/SubdomainFAQ +gc.ca + +// cat : https://www.iana.org/domains/root/db/cat.html +cat + +// cc : https://www.iana.org/domains/root/db/cc.html +cc + +// cd : https://www.iana.org/domains/root/db/cd.html +// https://www.nic.cd +cd +gov.cd + +// cf : https://www.iana.org/domains/root/db/cf.html +cf + +// cg : https://www.iana.org/domains/root/db/cg.html +cg + +// ch : https://www.iana.org/domains/root/db/ch.html +ch + +// ci : https://www.iana.org/domains/root/db/ci.html +ci +ac.ci +aéroport.ci +asso.ci +co.ci +com.ci +ed.ci +edu.ci +go.ci +gouv.ci +int.ci +net.ci +or.ci +org.ci + +// ck : https://www.iana.org/domains/root/db/ck.html +*.ck +!www.ck + +// cl : https://www.nic.cl +// Confirmed by .CL registry +cl +co.cl +gob.cl +gov.cl +mil.cl + +// cm : https://www.iana.org/domains/root/db/cm.html plus bug 981927 +cm +co.cm +com.cm +gov.cm +net.cm + +// cn : https://www.iana.org/domains/root/db/cn.html +// Submitted by registry +cn +ac.cn +com.cn +edu.cn +gov.cn +mil.cn +net.cn +org.cn +公司.cn +網絡.cn +网络.cn +// cn geographic names +ah.cn +bj.cn +cq.cn +fj.cn +gd.cn +gs.cn +gx.cn +gz.cn +ha.cn +hb.cn +he.cn +hi.cn +hk.cn +hl.cn +hn.cn +jl.cn +js.cn +jx.cn +ln.cn +mo.cn +nm.cn +nx.cn +qh.cn +sc.cn +sd.cn +sh.cn +sn.cn +sx.cn +tj.cn +tw.cn +xj.cn +xz.cn +yn.cn +zj.cn + +// co : https://www.iana.org/domains/root/db/co.html +// https://www.cointernet.com.co/como-funciona-un-dominio-restringido +// Confirmed by registry 2024-11-18 +co +com.co +edu.co +gov.co +mil.co +net.co +nom.co +org.co + +// com : https://www.iana.org/domains/root/db/com.html +com + +// coop : https://www.iana.org/domains/root/db/coop.html +coop + +// cr : https://nic.cr/capitulo-1-registro-de-un-nombre-de-dominio/ +cr +ac.cr +co.cr +ed.cr +fi.cr +go.cr +or.cr +sa.cr + +// cu : https://www.iana.org/domains/root/db/cu.html +cu +com.cu +edu.cu +gob.cu +inf.cu +nat.cu +net.cu +org.cu + +// cv : https://www.iana.org/domains/root/db/cv.html +// https://ola.cv/domain-extensions-under-cv/ +// Confirmed by registry 2024-11-26 +cv +com.cv +edu.cv +id.cv +int.cv +net.cv +nome.cv +org.cv +publ.cv + +// cw : https://www.uoc.cw/cw-registry +// Confirmed by registry 2024-11-19 +cw +com.cw +edu.cw +net.cw +org.cw + +// cx : https://www.iana.org/domains/root/db/cx.html +// list of other 2nd level tlds ? +cx +gov.cx + +// cy : http://www.nic.cy/ +// Submitted by Panayiotou Fotia +// https://nic.cy/wp-content/uploads/2024/01/Create-Request-for-domain-name-registration-1.pdf +cy +ac.cy +biz.cy +com.cy +ekloges.cy +gov.cy +ltd.cy +mil.cy +net.cy +org.cy +press.cy +pro.cy +tm.cy + +// cz : https://www.iana.org/domains/root/db/cz.html +// Confirmed by registry 2025-08-06 +cz +gov.cz + +// de : https://www.iana.org/domains/root/db/de.html +// Confirmed by registry (with technical +// reservations) 2008-07-01 +de + +// dj : https://www.iana.org/domains/root/db/dj.html +dj + +// dk : https://www.iana.org/domains/root/db/dk.html +// Confirmed by registry 2008-06-17 +dk + +// dm : https://www.iana.org/domains/root/db/dm.html +// https://nic.dm/policies/pdf/DMRulesandGuidelines2024v1.pdf +// Confirmed by registry 2024-11-19 +dm +co.dm +com.dm +edu.dm +gov.dm +net.dm +org.dm + +// do : https://www.iana.org/domains/root/db/do.html +do +art.do +com.do +edu.do +gob.do +gov.do +mil.do +net.do +org.do +sld.do +web.do + +// dz : http://www.nic.dz/images/pdf_nic/charte.pdf +dz +art.dz +asso.dz +com.dz +edu.dz +gov.dz +net.dz +org.dz +pol.dz +soc.dz +tm.dz + +// ec : https://www.nic.ec/ +// Submitted by registry +ec +abg.ec +adm.ec +agron.ec +arqt.ec +art.ec +bar.ec +chef.ec +com.ec +cont.ec +cpa.ec +cue.ec +dent.ec +dgn.ec +disco.ec +doc.ec +edu.ec +eng.ec +esm.ec +fin.ec +fot.ec +gal.ec +gob.ec +gov.ec +gye.ec +ibr.ec +info.ec +k12.ec +lat.ec +loj.ec +med.ec +mil.ec +mktg.ec +mon.ec +net.ec +ntr.ec +odont.ec +org.ec +pro.ec +prof.ec +psic.ec +psiq.ec +pub.ec +rio.ec +rrpp.ec +sal.ec +tech.ec +tul.ec +tur.ec +uio.ec +vet.ec +xxx.ec + +// edu : https://www.iana.org/domains/root/db/edu.html +edu + +// ee : https://www.internet.ee/domains/general-domains-and-procedure-for-registration-of-sub-domains-under-general-domains +ee +aip.ee +com.ee +edu.ee +fie.ee +gov.ee +lib.ee +med.ee +org.ee +pri.ee +riik.ee + +// eg : https://www.iana.org/domains/root/db/eg.html +// https://domain.eg/en/domain-rules/subdomain-names-types/ +eg +ac.eg +com.eg +edu.eg +eun.eg +gov.eg +info.eg +me.eg +mil.eg +name.eg +net.eg +org.eg +sci.eg +sport.eg +tv.eg + +// er : https://www.iana.org/domains/root/db/er.html +*.er + +// es : https://www.dominios.es/en +es +com.es +edu.es +gob.es +nom.es +org.es + +// et : https://www.iana.org/domains/root/db/et.html +et +biz.et +com.et +edu.et +gov.et +info.et +name.et +net.et +org.et + +// eu : https://www.iana.org/domains/root/db/eu.html +eu + +// fi : https://www.iana.org/domains/root/db/fi.html +fi +// aland.fi : https://www.iana.org/domains/root/db/ax.html +// This domain is being phased out in favor of .ax. As there are still many +// domains under aland.fi, we still keep it on the list until aland.fi is +// completely removed. +aland.fi + +// fj : https://www.iana.org/domains/root/db/fj.html +fj +ac.fj +biz.fj +com.fj +edu.fj +gov.fj +id.fj +info.fj +mil.fj +name.fj +net.fj +org.fj +pro.fj + +// fk : https://www.iana.org/domains/root/db/fk.html +*.fk + +// fm : https://www.iana.org/domains/root/db/fm.html +fm +com.fm +edu.fm +net.fm +org.fm + +// fo : https://www.iana.org/domains/root/db/fo.html +fo + +// fr : https://www.afnic.fr/ https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +fr +asso.fr +com.fr +gouv.fr +nom.fr +prd.fr +tm.fr +// Other SLDs now selfmanaged out of AFNIC range. Former "domaines sectoriels", still registration suffixes +avoues.fr +cci.fr +greta.fr +huissier-justice.fr + +// ga : https://www.iana.org/domains/root/db/ga.html +ga + +// gb : This registry is effectively dormant +// Submitted by registry +gb + +// gd : https://www.iana.org/domains/root/db/gd.html +gd +edu.gd +gov.gd + +// ge : https://nic.ge/en/administrator/the-ge-domain-regulations +// Confirmed by registry 2024-11-20 +ge +com.ge +edu.ge +gov.ge +net.ge +org.ge +pvt.ge +school.ge + +// gf : https://www.iana.org/domains/root/db/gf.html +gf + +// gg : https://www.channelisles.net/register-1/register-direct +// Confirmed by registry 2013-11-28 +gg +co.gg +net.gg +org.gg + +// gh : https://www.iana.org/domains/root/db/gh.html +// https://www.nic.gh/ +// Although domains directly at second level are not possible at the moment, +// they have been possible for some time and may come back. +gh +biz.gh +com.gh +edu.gh +gov.gh +mil.gh +net.gh +org.gh + +// gi : http://www.nic.gi/rules.html +gi +com.gi +edu.gi +gov.gi +ltd.gi +mod.gi +org.gi + +// gl : https://www.iana.org/domains/root/db/gl.html +// http://nic.gl +gl +co.gl +com.gl +edu.gl +net.gl +org.gl + +// gm : http://www.nic.gm/htmlpages%5Cgm-policy.htm +gm + +// gn : http://psg.com/dns/gn/gn.txt +// Submitted by registry +gn +ac.gn +com.gn +edu.gn +gov.gn +net.gn +org.gn + +// gov : https://www.iana.org/domains/root/db/gov.html +gov + +// gp : http://www.nic.gp/index.php?lang=en +gp +asso.gp +com.gp +edu.gp +mobi.gp +net.gp +org.gp + +// gq : https://www.iana.org/domains/root/db/gq.html +gq + +// gr : https://www.iana.org/domains/root/db/gr.html +// Submitted by registry +gr +com.gr +edu.gr +gov.gr +net.gr +org.gr + +// gs : https://www.iana.org/domains/root/db/gs.html +gs + +// gt : https://www.gt/sitio/registration_policy.php?lang=en +gt +com.gt +edu.gt +gob.gt +ind.gt +mil.gt +net.gt +org.gt + +// gu : http://gadao.gov.gu/register.html +// University of Guam : https://www.uog.edu +// Submitted by uognoc@triton.uog.edu +gu +com.gu +edu.gu +gov.gu +guam.gu +info.gu +net.gu +org.gu +web.gu + +// gw : https://www.iana.org/domains/root/db/gw.html +// gw : https://nic.gw/regras/ +gw + +// gy : https://www.iana.org/domains/root/db/gy.html +// http://registry.gy/ +gy +co.gy +com.gy +edu.gy +gov.gy +net.gy +org.gy + +// hk : https://www.hkirc.hk +// Submitted by registry +hk +com.hk +edu.hk +gov.hk +idv.hk +net.hk +org.hk +个人.hk +個人.hk +公司.hk +政府.hk +敎育.hk +教育.hk +箇人.hk +組織.hk +組织.hk +網絡.hk +網络.hk +组織.hk +组织.hk +网絡.hk +网络.hk + +// hm : https://www.iana.org/domains/root/db/hm.html +hm + +// hn : https://www.iana.org/domains/root/db/hn.html +hn +com.hn +edu.hn +gob.hn +mil.hn +net.hn +org.hn + +// hr : http://www.dns.hr/documents/pdf/HRTLD-regulations.pdf +hr +com.hr +from.hr +iz.hr +name.hr + +// ht : http://www.nic.ht/info/charte.cfm +ht +adult.ht +art.ht +asso.ht +com.ht +coop.ht +edu.ht +firm.ht +gouv.ht +info.ht +med.ht +net.ht +org.ht +perso.ht +pol.ht +pro.ht +rel.ht +shop.ht + +// hu : https://www.iana.org/domains/root/db/hu.html +// Confirmed by registry 2008-06-12 +hu +2000.hu +agrar.hu +bolt.hu +casino.hu +city.hu +co.hu +erotica.hu +erotika.hu +film.hu +forum.hu +games.hu +hotel.hu +info.hu +ingatlan.hu +jogasz.hu +konyvelo.hu +lakas.hu +media.hu +news.hu +org.hu +priv.hu +reklam.hu +sex.hu +shop.hu +sport.hu +suli.hu +szex.hu +tm.hu +tozsde.hu +utazas.hu +video.hu + +// id : https://www.iana.org/domains/root/db/id.html +id +ac.id +biz.id +co.id +desa.id +go.id +kop.id +mil.id +my.id +net.id +or.id +ponpes.id +sch.id +web.id +// xn--9tfky.id (.id, Und-Bali) +ᬩᬮᬶ.id + +// ie : https://www.iana.org/domains/root/db/ie.html +ie +gov.ie + +// il : http://www.isoc.org.il/domains/ +// see also: https://en.isoc.org.il/il-cctld/registration-rules +// ISOC-IL (operated by .il Registry) +il +ac.il +co.il +gov.il +idf.il +k12.il +muni.il +net.il +org.il +// xn--4dbrk0ce ("Israel", Hebrew) : IL +ישראל +// xn--4dbgdty6c.xn--4dbrk0ce. +אקדמיה.ישראל +// xn--5dbhl8d.xn--4dbrk0ce. +ישוב.ישראל +// xn--8dbq2a.xn--4dbrk0ce. +צהל.ישראל +// xn--hebda8b.xn--4dbrk0ce. +ממשל.ישראל + +// im : https://www.nic.im/ +// Submitted by registry +im +ac.im +co.im +ltd.co.im +plc.co.im +com.im +net.im +org.im +tt.im +tv.im + +// in : https://www.iana.org/domains/root/db/in.html +// see also: https://registry.in/policies +// Please note, that nic.in is not an official eTLD, but used by most +// government institutions. +// Confirmed by Gaurav Kansal 2025-11-06 +in +5g.in +6g.in +ac.in +ai.in +am.in +bank.in +bihar.in +biz.in +business.in +ca.in +cn.in +co.in +com.in +coop.in +cs.in +delhi.in +dr.in +edu.in +er.in +fin.in +firm.in +gen.in +gov.in +gujarat.in +ind.in +info.in +int.in +internet.in +io.in +me.in +mil.in +net.in +nic.in +org.in +pg.in +post.in +pro.in +res.in +travel.in +tv.in +uk.in +up.in +us.in + +// info : https://www.iana.org/domains/root/db/info.html +info + +// int : https://www.iana.org/domains/root/db/int.html +// Confirmed by registry 2008-06-18 +int +eu.int + +// io : http://www.nic.io/rules.htm +io +co.io +com.io +edu.io +gov.io +mil.io +net.io +nom.io +org.io + +// iq : http://www.cmc.iq/english/iq/iqregister1.htm +iq +com.iq +edu.iq +gov.iq +mil.iq +net.iq +org.iq + +// ir : http://www.nic.ir/Terms_and_Conditions_ir,_Appendix_1_Domain_Rules +// Also see http://www.nic.ir/Internationalized_Domain_Names +// Two .ir entries added at request of , 2010-04-16 +ir +ac.ir +co.ir +gov.ir +id.ir +net.ir +org.ir +sch.ir +// xn--mgba3a4f16a.ir (.ir, Persian YEH) +ایران.ir +// xn--mgba3a4fra.ir (.ir, Arabic YEH) +ايران.ir + +// is : http://www.isnic.is/domain/rules.php +// Confirmed by registry 2024-11-17 +is + +// it : https://www.iana.org/domains/root/db/it.html +// https://www.nic.it/ +it +edu.it +gov.it +// Regions (3.3.1) +// https://www.nic.it/en/manage-your-it/forms-and-docs -> "Assignment and Management of domain names" +abr.it +abruzzo.it +aosta-valley.it +aostavalley.it +bas.it +basilicata.it +cal.it +calabria.it +cam.it +campania.it +emilia-romagna.it +emiliaromagna.it +emr.it +friuli-v-giulia.it +friuli-ve-giulia.it +friuli-vegiulia.it +friuli-venezia-giulia.it +friuli-veneziagiulia.it +friuli-vgiulia.it +friuliv-giulia.it +friulive-giulia.it +friulivegiulia.it +friulivenezia-giulia.it +friuliveneziagiulia.it +friulivgiulia.it +fvg.it +laz.it +lazio.it +lig.it +liguria.it +lom.it +lombardia.it +lombardy.it +lucania.it +mar.it +marche.it +mol.it +molise.it +piedmont.it +piemonte.it +pmn.it +pug.it +puglia.it +sar.it +sardegna.it +sardinia.it +sic.it +sicilia.it +sicily.it +taa.it +tos.it +toscana.it +trentin-sud-tirol.it +trentin-süd-tirol.it +trentin-sudtirol.it +trentin-südtirol.it +trentin-sued-tirol.it +trentin-suedtirol.it +trentino.it +trentino-a-adige.it +trentino-aadige.it +trentino-alto-adige.it +trentino-altoadige.it +trentino-s-tirol.it +trentino-stirol.it +trentino-sud-tirol.it +trentino-süd-tirol.it +trentino-sudtirol.it +trentino-südtirol.it +trentino-sued-tirol.it +trentino-suedtirol.it +trentinoa-adige.it +trentinoaadige.it +trentinoalto-adige.it +trentinoaltoadige.it +trentinos-tirol.it +trentinostirol.it +trentinosud-tirol.it +trentinosüd-tirol.it +trentinosudtirol.it +trentinosüdtirol.it +trentinosued-tirol.it +trentinosuedtirol.it +trentinsud-tirol.it +trentinsüd-tirol.it +trentinsudtirol.it +trentinsüdtirol.it +trentinsued-tirol.it +trentinsuedtirol.it +tuscany.it +umb.it +umbria.it +val-d-aosta.it +val-daosta.it +vald-aosta.it +valdaosta.it +valle-aosta.it +valle-d-aosta.it +valle-daosta.it +valleaosta.it +valled-aosta.it +valledaosta.it +vallee-aoste.it +vallée-aoste.it +vallee-d-aoste.it +vallée-d-aoste.it +valleeaoste.it +valléeaoste.it +valleedaoste.it +valléedaoste.it +vao.it +vda.it +ven.it +veneto.it +// Provinces (3.3.2) +ag.it +agrigento.it +al.it +alessandria.it +alto-adige.it +altoadige.it +an.it +ancona.it +andria-barletta-trani.it +andria-trani-barletta.it +andriabarlettatrani.it +andriatranibarletta.it +ao.it +aosta.it +aoste.it +ap.it +aq.it +aquila.it +ar.it +arezzo.it +ascoli-piceno.it +ascolipiceno.it +asti.it +at.it +av.it +avellino.it +ba.it +balsan.it +balsan-sudtirol.it +balsan-südtirol.it +balsan-suedtirol.it +bari.it +barletta-trani-andria.it +barlettatraniandria.it +belluno.it +benevento.it +bergamo.it +bg.it +bi.it +biella.it +bl.it +bn.it +bo.it +bologna.it +bolzano.it +bolzano-altoadige.it +bozen.it +bozen-sudtirol.it +bozen-südtirol.it +bozen-suedtirol.it +br.it +brescia.it +brindisi.it +bs.it +bt.it +bulsan.it +bulsan-sudtirol.it +bulsan-südtirol.it +bulsan-suedtirol.it +bz.it +ca.it +cagliari.it +caltanissetta.it +campidano-medio.it +campidanomedio.it +campobasso.it +carbonia-iglesias.it +carboniaiglesias.it +carrara-massa.it +carraramassa.it +caserta.it +catania.it +catanzaro.it +cb.it +ce.it +cesena-forli.it +cesena-forlì.it +cesenaforli.it +cesenaforlì.it +ch.it +chieti.it +ci.it +cl.it +cn.it +co.it +como.it +cosenza.it +cr.it +cremona.it +crotone.it +cs.it +ct.it +cuneo.it +cz.it +dell-ogliastra.it +dellogliastra.it +en.it +enna.it +fc.it +fe.it +fermo.it +ferrara.it +fg.it +fi.it +firenze.it +florence.it +fm.it +foggia.it +forli-cesena.it +forlì-cesena.it +forlicesena.it +forlìcesena.it +fr.it +frosinone.it +ge.it +genoa.it +genova.it +go.it +gorizia.it +gr.it +grosseto.it +iglesias-carbonia.it +iglesiascarbonia.it +im.it +imperia.it +is.it +isernia.it +kr.it +la-spezia.it +laquila.it +laspezia.it +latina.it +lc.it +le.it +lecce.it +lecco.it +li.it +livorno.it +lo.it +lodi.it +lt.it +lu.it +lucca.it +macerata.it +mantova.it +massa-carrara.it +massacarrara.it +matera.it +mb.it +mc.it +me.it +medio-campidano.it +mediocampidano.it +messina.it +mi.it +milan.it +milano.it +mn.it +mo.it +modena.it +monza.it +monza-brianza.it +monza-e-della-brianza.it +monzabrianza.it +monzaebrianza.it +monzaedellabrianza.it +ms.it +mt.it +na.it +naples.it +napoli.it +no.it +novara.it +nu.it +nuoro.it +og.it +ogliastra.it +olbia-tempio.it +olbiatempio.it +or.it +oristano.it +ot.it +pa.it +padova.it +padua.it +palermo.it +parma.it +pavia.it +pc.it +pd.it +pe.it +perugia.it +pesaro-urbino.it +pesarourbino.it +pescara.it +pg.it +pi.it +piacenza.it +pisa.it +pistoia.it +pn.it +po.it +pordenone.it +potenza.it +pr.it +prato.it +pt.it +pu.it +pv.it +pz.it +ra.it +ragusa.it +ravenna.it +rc.it +re.it +reggio-calabria.it +reggio-emilia.it +reggiocalabria.it +reggioemilia.it +rg.it +ri.it +rieti.it +rimini.it +rm.it +rn.it +ro.it +roma.it +rome.it +rovigo.it +sa.it +salerno.it +sassari.it +savona.it +si.it +siena.it +siracusa.it +so.it +sondrio.it +sp.it +sr.it +ss.it +südtirol.it +suedtirol.it +sv.it +ta.it +taranto.it +te.it +tempio-olbia.it +tempioolbia.it +teramo.it +terni.it +tn.it +to.it +torino.it +tp.it +tr.it +trani-andria-barletta.it +trani-barletta-andria.it +traniandriabarletta.it +tranibarlettaandria.it +trapani.it +trento.it +treviso.it +trieste.it +ts.it +turin.it +tv.it +ud.it +udine.it +urbino-pesaro.it +urbinopesaro.it +va.it +varese.it +vb.it +vc.it +ve.it +venezia.it +venice.it +verbania.it +vercelli.it +verona.it +vi.it +vibo-valentia.it +vibovalentia.it +vicenza.it +viterbo.it +vr.it +vs.it +vt.it +vv.it + +// je : https://www.iana.org/domains/root/db/je.html +// Confirmed by registry 2013-11-28 +je +co.je +net.je +org.je + +// jm : http://www.com.jm/register.html +*.jm + +// jo : https://www.dns.jo/JoFamily.aspx +// Confirmed by registry 2024-11-17 +jo +agri.jo +ai.jo +com.jo +edu.jo +eng.jo +fm.jo +gov.jo +mil.jo +net.jo +org.jo +per.jo +phd.jo +sch.jo +tv.jo + +// jobs : https://www.iana.org/domains/root/db/jobs.html +jobs + +// jp : https://www.iana.org/domains/root/db/jp.html +// http://jprs.co.jp/en/jpdomain.html +// Confirmed by registry 2024-11-22 +jp +// jp organizational type names +ac.jp +ad.jp +co.jp +ed.jp +go.jp +gr.jp +lg.jp +ne.jp +or.jp +// jp prefecture type names +aichi.jp +akita.jp +aomori.jp +chiba.jp +ehime.jp +fukui.jp +fukuoka.jp +fukushima.jp +gifu.jp +gunma.jp +hiroshima.jp +hokkaido.jp +hyogo.jp +ibaraki.jp +ishikawa.jp +iwate.jp +kagawa.jp +kagoshima.jp +kanagawa.jp +kochi.jp +kumamoto.jp +kyoto.jp +mie.jp +miyagi.jp +miyazaki.jp +nagano.jp +nagasaki.jp +nara.jp +niigata.jp +oita.jp +okayama.jp +okinawa.jp +osaka.jp +saga.jp +saitama.jp +shiga.jp +shimane.jp +shizuoka.jp +tochigi.jp +tokushima.jp +tokyo.jp +tottori.jp +toyama.jp +wakayama.jp +yamagata.jp +yamaguchi.jp +yamanashi.jp +三重.jp +京都.jp +佐賀.jp +兵庫.jp +北海道.jp +千葉.jp +和歌山.jp +埼玉.jp +大分.jp +大阪.jp +奈良.jp +宮城.jp +宮崎.jp +富山.jp +山口.jp +山形.jp +山梨.jp +岐阜.jp +岡山.jp +岩手.jp +島根.jp +広島.jp +徳島.jp +愛媛.jp +愛知.jp +新潟.jp +東京.jp +栃木.jp +沖縄.jp +滋賀.jp +熊本.jp +石川.jp +神奈川.jp +福井.jp +福岡.jp +福島.jp +秋田.jp +群馬.jp +茨城.jp +長崎.jp +長野.jp +青森.jp +静岡.jp +香川.jp +高知.jp +鳥取.jp +鹿児島.jp +// jp geographic type names +// http://jprs.jp/doc/rule/saisoku-1.html +// 2024-11-22: JPRS confirmed that jp geographic type names no longer accept new registrations. +// Once all existing registrations expire (marking full discontinuation), these suffixes +// will be removed from the PSL. +*.kawasaki.jp +!city.kawasaki.jp +*.kitakyushu.jp +!city.kitakyushu.jp +*.kobe.jp +!city.kobe.jp +*.nagoya.jp +!city.nagoya.jp +*.sapporo.jp +!city.sapporo.jp +*.sendai.jp +!city.sendai.jp +*.yokohama.jp +!city.yokohama.jp +// 4th level registration +aisai.aichi.jp +ama.aichi.jp +anjo.aichi.jp +asuke.aichi.jp +chiryu.aichi.jp +chita.aichi.jp +fuso.aichi.jp +gamagori.aichi.jp +handa.aichi.jp +hazu.aichi.jp +hekinan.aichi.jp +higashiura.aichi.jp +ichinomiya.aichi.jp +inazawa.aichi.jp +inuyama.aichi.jp +isshiki.aichi.jp +iwakura.aichi.jp +kanie.aichi.jp +kariya.aichi.jp +kasugai.aichi.jp +kira.aichi.jp +kiyosu.aichi.jp +komaki.aichi.jp +konan.aichi.jp +kota.aichi.jp +mihama.aichi.jp +miyoshi.aichi.jp +nishio.aichi.jp +nisshin.aichi.jp +obu.aichi.jp +oguchi.aichi.jp +oharu.aichi.jp +okazaki.aichi.jp +owariasahi.aichi.jp +seto.aichi.jp +shikatsu.aichi.jp +shinshiro.aichi.jp +shitara.aichi.jp +tahara.aichi.jp +takahama.aichi.jp +tobishima.aichi.jp +toei.aichi.jp +togo.aichi.jp +tokai.aichi.jp +tokoname.aichi.jp +toyoake.aichi.jp +toyohashi.aichi.jp +toyokawa.aichi.jp +toyone.aichi.jp +toyota.aichi.jp +tsushima.aichi.jp +yatomi.aichi.jp +akita.akita.jp +daisen.akita.jp +fujisato.akita.jp +gojome.akita.jp +hachirogata.akita.jp +happou.akita.jp +higashinaruse.akita.jp +honjo.akita.jp +honjyo.akita.jp +ikawa.akita.jp +kamikoani.akita.jp +kamioka.akita.jp +katagami.akita.jp +kazuno.akita.jp +kitaakita.akita.jp +kosaka.akita.jp +kyowa.akita.jp +misato.akita.jp +mitane.akita.jp +moriyoshi.akita.jp +nikaho.akita.jp +noshiro.akita.jp +odate.akita.jp +oga.akita.jp +ogata.akita.jp +semboku.akita.jp +yokote.akita.jp +yurihonjo.akita.jp +aomori.aomori.jp +gonohe.aomori.jp +hachinohe.aomori.jp +hashikami.aomori.jp +hiranai.aomori.jp +hirosaki.aomori.jp +itayanagi.aomori.jp +kuroishi.aomori.jp +misawa.aomori.jp +mutsu.aomori.jp +nakadomari.aomori.jp +noheji.aomori.jp +oirase.aomori.jp +owani.aomori.jp +rokunohe.aomori.jp +sannohe.aomori.jp +shichinohe.aomori.jp +shingo.aomori.jp +takko.aomori.jp +towada.aomori.jp +tsugaru.aomori.jp +tsuruta.aomori.jp +abiko.chiba.jp +asahi.chiba.jp +chonan.chiba.jp +chosei.chiba.jp +choshi.chiba.jp +chuo.chiba.jp +funabashi.chiba.jp +futtsu.chiba.jp +hanamigawa.chiba.jp +ichihara.chiba.jp +ichikawa.chiba.jp +ichinomiya.chiba.jp +inzai.chiba.jp +isumi.chiba.jp +kamagaya.chiba.jp +kamogawa.chiba.jp +kashiwa.chiba.jp +katori.chiba.jp +katsuura.chiba.jp +kimitsu.chiba.jp +kisarazu.chiba.jp +kozaki.chiba.jp +kujukuri.chiba.jp +kyonan.chiba.jp +matsudo.chiba.jp +midori.chiba.jp +mihama.chiba.jp +minamiboso.chiba.jp +mobara.chiba.jp +mutsuzawa.chiba.jp +nagara.chiba.jp +nagareyama.chiba.jp +narashino.chiba.jp +narita.chiba.jp +noda.chiba.jp +oamishirasato.chiba.jp +omigawa.chiba.jp +onjuku.chiba.jp +otaki.chiba.jp +sakae.chiba.jp +sakura.chiba.jp +shimofusa.chiba.jp +shirako.chiba.jp +shiroi.chiba.jp +shisui.chiba.jp +sodegaura.chiba.jp +sosa.chiba.jp +tako.chiba.jp +tateyama.chiba.jp +togane.chiba.jp +tohnosho.chiba.jp +tomisato.chiba.jp +urayasu.chiba.jp +yachimata.chiba.jp +yachiyo.chiba.jp +yokaichiba.chiba.jp +yokoshibahikari.chiba.jp +yotsukaido.chiba.jp +ainan.ehime.jp +honai.ehime.jp +ikata.ehime.jp +imabari.ehime.jp +iyo.ehime.jp +kamijima.ehime.jp +kihoku.ehime.jp +kumakogen.ehime.jp +masaki.ehime.jp +matsuno.ehime.jp +matsuyama.ehime.jp +namikata.ehime.jp +niihama.ehime.jp +ozu.ehime.jp +saijo.ehime.jp +seiyo.ehime.jp +shikokuchuo.ehime.jp +tobe.ehime.jp +toon.ehime.jp +uchiko.ehime.jp +uwajima.ehime.jp +yawatahama.ehime.jp +echizen.fukui.jp +eiheiji.fukui.jp +fukui.fukui.jp +ikeda.fukui.jp +katsuyama.fukui.jp +mihama.fukui.jp +minamiechizen.fukui.jp +obama.fukui.jp +ohi.fukui.jp +ono.fukui.jp +sabae.fukui.jp +sakai.fukui.jp +takahama.fukui.jp +tsuruga.fukui.jp +wakasa.fukui.jp +ashiya.fukuoka.jp +buzen.fukuoka.jp +chikugo.fukuoka.jp +chikuho.fukuoka.jp +chikujo.fukuoka.jp +chikushino.fukuoka.jp +chikuzen.fukuoka.jp +chuo.fukuoka.jp +dazaifu.fukuoka.jp +fukuchi.fukuoka.jp +hakata.fukuoka.jp +higashi.fukuoka.jp +hirokawa.fukuoka.jp +hisayama.fukuoka.jp +iizuka.fukuoka.jp +inatsuki.fukuoka.jp +kaho.fukuoka.jp +kasuga.fukuoka.jp +kasuya.fukuoka.jp +kawara.fukuoka.jp +keisen.fukuoka.jp +koga.fukuoka.jp +kurate.fukuoka.jp +kurogi.fukuoka.jp +kurume.fukuoka.jp +minami.fukuoka.jp +miyako.fukuoka.jp +miyama.fukuoka.jp +miyawaka.fukuoka.jp +mizumaki.fukuoka.jp +munakata.fukuoka.jp +nakagawa.fukuoka.jp +nakama.fukuoka.jp +nishi.fukuoka.jp +nogata.fukuoka.jp +ogori.fukuoka.jp +okagaki.fukuoka.jp +okawa.fukuoka.jp +oki.fukuoka.jp +omuta.fukuoka.jp +onga.fukuoka.jp +onojo.fukuoka.jp +oto.fukuoka.jp +saigawa.fukuoka.jp +sasaguri.fukuoka.jp +shingu.fukuoka.jp +shinyoshitomi.fukuoka.jp +shonai.fukuoka.jp +soeda.fukuoka.jp +sue.fukuoka.jp +tachiarai.fukuoka.jp +tagawa.fukuoka.jp +takata.fukuoka.jp +toho.fukuoka.jp +toyotsu.fukuoka.jp +tsuiki.fukuoka.jp +ukiha.fukuoka.jp +umi.fukuoka.jp +usui.fukuoka.jp +yamada.fukuoka.jp +yame.fukuoka.jp +yanagawa.fukuoka.jp +yukuhashi.fukuoka.jp +aizubange.fukushima.jp +aizumisato.fukushima.jp +aizuwakamatsu.fukushima.jp +asakawa.fukushima.jp +bandai.fukushima.jp +date.fukushima.jp +fukushima.fukushima.jp +furudono.fukushima.jp +futaba.fukushima.jp +hanawa.fukushima.jp +higashi.fukushima.jp +hirata.fukushima.jp +hirono.fukushima.jp +iitate.fukushima.jp +inawashiro.fukushima.jp +ishikawa.fukushima.jp +iwaki.fukushima.jp +izumizaki.fukushima.jp +kagamiishi.fukushima.jp +kaneyama.fukushima.jp +kawamata.fukushima.jp +kitakata.fukushima.jp +kitashiobara.fukushima.jp +koori.fukushima.jp +koriyama.fukushima.jp +kunimi.fukushima.jp +miharu.fukushima.jp +mishima.fukushima.jp +namie.fukushima.jp +nango.fukushima.jp +nishiaizu.fukushima.jp +nishigo.fukushima.jp +okuma.fukushima.jp +omotego.fukushima.jp +ono.fukushima.jp +otama.fukushima.jp +samegawa.fukushima.jp +shimogo.fukushima.jp +shirakawa.fukushima.jp +showa.fukushima.jp +soma.fukushima.jp +sukagawa.fukushima.jp +taishin.fukushima.jp +tamakawa.fukushima.jp +tanagura.fukushima.jp +tenei.fukushima.jp +yabuki.fukushima.jp +yamato.fukushima.jp +yamatsuri.fukushima.jp +yanaizu.fukushima.jp +yugawa.fukushima.jp +anpachi.gifu.jp +ena.gifu.jp +gifu.gifu.jp +ginan.gifu.jp +godo.gifu.jp +gujo.gifu.jp +hashima.gifu.jp +hichiso.gifu.jp +hida.gifu.jp +higashishirakawa.gifu.jp +ibigawa.gifu.jp +ikeda.gifu.jp +kakamigahara.gifu.jp +kani.gifu.jp +kasahara.gifu.jp +kasamatsu.gifu.jp +kawaue.gifu.jp +kitagata.gifu.jp +mino.gifu.jp +minokamo.gifu.jp +mitake.gifu.jp +mizunami.gifu.jp +motosu.gifu.jp +nakatsugawa.gifu.jp +ogaki.gifu.jp +sakahogi.gifu.jp +seki.gifu.jp +sekigahara.gifu.jp +shirakawa.gifu.jp +tajimi.gifu.jp +takayama.gifu.jp +tarui.gifu.jp +toki.gifu.jp +tomika.gifu.jp +wanouchi.gifu.jp +yamagata.gifu.jp +yaotsu.gifu.jp +yoro.gifu.jp +annaka.gunma.jp +chiyoda.gunma.jp +fujioka.gunma.jp +higashiagatsuma.gunma.jp +isesaki.gunma.jp +itakura.gunma.jp +kanna.gunma.jp +kanra.gunma.jp +katashina.gunma.jp +kawaba.gunma.jp +kiryu.gunma.jp +kusatsu.gunma.jp +maebashi.gunma.jp +meiwa.gunma.jp +midori.gunma.jp +minakami.gunma.jp +naganohara.gunma.jp +nakanojo.gunma.jp +nanmoku.gunma.jp +numata.gunma.jp +oizumi.gunma.jp +ora.gunma.jp +ota.gunma.jp +shibukawa.gunma.jp +shimonita.gunma.jp +shinto.gunma.jp +showa.gunma.jp +takasaki.gunma.jp +takayama.gunma.jp +tamamura.gunma.jp +tatebayashi.gunma.jp +tomioka.gunma.jp +tsukiyono.gunma.jp +tsumagoi.gunma.jp +ueno.gunma.jp +yoshioka.gunma.jp +asaminami.hiroshima.jp +daiwa.hiroshima.jp +etajima.hiroshima.jp +fuchu.hiroshima.jp +fukuyama.hiroshima.jp +hatsukaichi.hiroshima.jp +higashihiroshima.hiroshima.jp +hongo.hiroshima.jp +jinsekikogen.hiroshima.jp +kaita.hiroshima.jp +kui.hiroshima.jp +kumano.hiroshima.jp +kure.hiroshima.jp +mihara.hiroshima.jp +miyoshi.hiroshima.jp +naka.hiroshima.jp +onomichi.hiroshima.jp +osakikamijima.hiroshima.jp +otake.hiroshima.jp +saka.hiroshima.jp +sera.hiroshima.jp +seranishi.hiroshima.jp +shinichi.hiroshima.jp +shobara.hiroshima.jp +takehara.hiroshima.jp +abashiri.hokkaido.jp +abira.hokkaido.jp +aibetsu.hokkaido.jp +akabira.hokkaido.jp +akkeshi.hokkaido.jp +asahikawa.hokkaido.jp +ashibetsu.hokkaido.jp +ashoro.hokkaido.jp +assabu.hokkaido.jp +atsuma.hokkaido.jp +bibai.hokkaido.jp +biei.hokkaido.jp +bifuka.hokkaido.jp +bihoro.hokkaido.jp +biratori.hokkaido.jp +chippubetsu.hokkaido.jp +chitose.hokkaido.jp +date.hokkaido.jp +ebetsu.hokkaido.jp +embetsu.hokkaido.jp +eniwa.hokkaido.jp +erimo.hokkaido.jp +esan.hokkaido.jp +esashi.hokkaido.jp +fukagawa.hokkaido.jp +fukushima.hokkaido.jp +furano.hokkaido.jp +furubira.hokkaido.jp +haboro.hokkaido.jp +hakodate.hokkaido.jp +hamatonbetsu.hokkaido.jp +hidaka.hokkaido.jp +higashikagura.hokkaido.jp +higashikawa.hokkaido.jp +hiroo.hokkaido.jp +hokuryu.hokkaido.jp +hokuto.hokkaido.jp +honbetsu.hokkaido.jp +horokanai.hokkaido.jp +horonobe.hokkaido.jp +ikeda.hokkaido.jp +imakane.hokkaido.jp +ishikari.hokkaido.jp +iwamizawa.hokkaido.jp +iwanai.hokkaido.jp +kamifurano.hokkaido.jp +kamikawa.hokkaido.jp +kamishihoro.hokkaido.jp +kamisunagawa.hokkaido.jp +kamoenai.hokkaido.jp +kayabe.hokkaido.jp +kembuchi.hokkaido.jp +kikonai.hokkaido.jp +kimobetsu.hokkaido.jp +kitahiroshima.hokkaido.jp +kitami.hokkaido.jp +kiyosato.hokkaido.jp +koshimizu.hokkaido.jp +kunneppu.hokkaido.jp +kuriyama.hokkaido.jp +kuromatsunai.hokkaido.jp +kushiro.hokkaido.jp +kutchan.hokkaido.jp +kyowa.hokkaido.jp +mashike.hokkaido.jp +matsumae.hokkaido.jp +mikasa.hokkaido.jp +minamifurano.hokkaido.jp +mombetsu.hokkaido.jp +moseushi.hokkaido.jp +mukawa.hokkaido.jp +muroran.hokkaido.jp +naie.hokkaido.jp +nakagawa.hokkaido.jp +nakasatsunai.hokkaido.jp +nakatombetsu.hokkaido.jp +nanae.hokkaido.jp +nanporo.hokkaido.jp +nayoro.hokkaido.jp +nemuro.hokkaido.jp +niikappu.hokkaido.jp +niki.hokkaido.jp +nishiokoppe.hokkaido.jp +noboribetsu.hokkaido.jp +numata.hokkaido.jp +obihiro.hokkaido.jp +obira.hokkaido.jp +oketo.hokkaido.jp +okoppe.hokkaido.jp +otaru.hokkaido.jp +otobe.hokkaido.jp +otofuke.hokkaido.jp +otoineppu.hokkaido.jp +oumu.hokkaido.jp +ozora.hokkaido.jp +pippu.hokkaido.jp +rankoshi.hokkaido.jp +rebun.hokkaido.jp +rikubetsu.hokkaido.jp +rishiri.hokkaido.jp +rishirifuji.hokkaido.jp +saroma.hokkaido.jp +sarufutsu.hokkaido.jp +shakotan.hokkaido.jp +shari.hokkaido.jp +shibecha.hokkaido.jp +shibetsu.hokkaido.jp +shikabe.hokkaido.jp +shikaoi.hokkaido.jp +shimamaki.hokkaido.jp +shimizu.hokkaido.jp +shimokawa.hokkaido.jp +shinshinotsu.hokkaido.jp +shintoku.hokkaido.jp +shiranuka.hokkaido.jp +shiraoi.hokkaido.jp +shiriuchi.hokkaido.jp +sobetsu.hokkaido.jp +sunagawa.hokkaido.jp +taiki.hokkaido.jp +takasu.hokkaido.jp +takikawa.hokkaido.jp +takinoue.hokkaido.jp +teshikaga.hokkaido.jp +tobetsu.hokkaido.jp +tohma.hokkaido.jp +tomakomai.hokkaido.jp +tomari.hokkaido.jp +toya.hokkaido.jp +toyako.hokkaido.jp +toyotomi.hokkaido.jp +toyoura.hokkaido.jp +tsubetsu.hokkaido.jp +tsukigata.hokkaido.jp +urakawa.hokkaido.jp +urausu.hokkaido.jp +uryu.hokkaido.jp +utashinai.hokkaido.jp +wakkanai.hokkaido.jp +wassamu.hokkaido.jp +yakumo.hokkaido.jp +yoichi.hokkaido.jp +aioi.hyogo.jp +akashi.hyogo.jp +ako.hyogo.jp +amagasaki.hyogo.jp +aogaki.hyogo.jp +asago.hyogo.jp +ashiya.hyogo.jp +awaji.hyogo.jp +fukusaki.hyogo.jp +goshiki.hyogo.jp +harima.hyogo.jp +himeji.hyogo.jp +ichikawa.hyogo.jp +inagawa.hyogo.jp +itami.hyogo.jp +kakogawa.hyogo.jp +kamigori.hyogo.jp +kamikawa.hyogo.jp +kasai.hyogo.jp +kasuga.hyogo.jp +kawanishi.hyogo.jp +miki.hyogo.jp +minamiawaji.hyogo.jp +nishinomiya.hyogo.jp +nishiwaki.hyogo.jp +ono.hyogo.jp +sanda.hyogo.jp +sannan.hyogo.jp +sasayama.hyogo.jp +sayo.hyogo.jp +shingu.hyogo.jp +shinonsen.hyogo.jp +shiso.hyogo.jp +sumoto.hyogo.jp +taishi.hyogo.jp +taka.hyogo.jp +takarazuka.hyogo.jp +takasago.hyogo.jp +takino.hyogo.jp +tamba.hyogo.jp +tatsuno.hyogo.jp +toyooka.hyogo.jp +yabu.hyogo.jp +yashiro.hyogo.jp +yoka.hyogo.jp +yokawa.hyogo.jp +ami.ibaraki.jp +asahi.ibaraki.jp +bando.ibaraki.jp +chikusei.ibaraki.jp +daigo.ibaraki.jp +fujishiro.ibaraki.jp +hitachi.ibaraki.jp +hitachinaka.ibaraki.jp +hitachiomiya.ibaraki.jp +hitachiota.ibaraki.jp +ibaraki.ibaraki.jp +ina.ibaraki.jp +inashiki.ibaraki.jp +itako.ibaraki.jp +iwama.ibaraki.jp +joso.ibaraki.jp +kamisu.ibaraki.jp +kasama.ibaraki.jp +kashima.ibaraki.jp +kasumigaura.ibaraki.jp +koga.ibaraki.jp +miho.ibaraki.jp +mito.ibaraki.jp +moriya.ibaraki.jp +naka.ibaraki.jp +namegata.ibaraki.jp +oarai.ibaraki.jp +ogawa.ibaraki.jp +omitama.ibaraki.jp +ryugasaki.ibaraki.jp +sakai.ibaraki.jp +sakuragawa.ibaraki.jp +shimodate.ibaraki.jp +shimotsuma.ibaraki.jp +shirosato.ibaraki.jp +sowa.ibaraki.jp +suifu.ibaraki.jp +takahagi.ibaraki.jp +tamatsukuri.ibaraki.jp +tokai.ibaraki.jp +tomobe.ibaraki.jp +tone.ibaraki.jp +toride.ibaraki.jp +tsuchiura.ibaraki.jp +tsukuba.ibaraki.jp +uchihara.ibaraki.jp +ushiku.ibaraki.jp +yachiyo.ibaraki.jp +yamagata.ibaraki.jp +yawara.ibaraki.jp +yuki.ibaraki.jp +anamizu.ishikawa.jp +hakui.ishikawa.jp +hakusan.ishikawa.jp +kaga.ishikawa.jp +kahoku.ishikawa.jp +kanazawa.ishikawa.jp +kawakita.ishikawa.jp +komatsu.ishikawa.jp +nakanoto.ishikawa.jp +nanao.ishikawa.jp +nomi.ishikawa.jp +nonoichi.ishikawa.jp +noto.ishikawa.jp +shika.ishikawa.jp +suzu.ishikawa.jp +tsubata.ishikawa.jp +tsurugi.ishikawa.jp +uchinada.ishikawa.jp +wajima.ishikawa.jp +fudai.iwate.jp +fujisawa.iwate.jp +hanamaki.iwate.jp +hiraizumi.iwate.jp +hirono.iwate.jp +ichinohe.iwate.jp +ichinoseki.iwate.jp +iwaizumi.iwate.jp +iwate.iwate.jp +joboji.iwate.jp +kamaishi.iwate.jp +kanegasaki.iwate.jp +karumai.iwate.jp +kawai.iwate.jp +kitakami.iwate.jp +kuji.iwate.jp +kunohe.iwate.jp +kuzumaki.iwate.jp +miyako.iwate.jp +mizusawa.iwate.jp +morioka.iwate.jp +ninohe.iwate.jp +noda.iwate.jp +ofunato.iwate.jp +oshu.iwate.jp +otsuchi.iwate.jp +rikuzentakata.iwate.jp +shiwa.iwate.jp +shizukuishi.iwate.jp +sumita.iwate.jp +tanohata.iwate.jp +tono.iwate.jp +yahaba.iwate.jp +yamada.iwate.jp +ayagawa.kagawa.jp +higashikagawa.kagawa.jp +kanonji.kagawa.jp +kotohira.kagawa.jp +manno.kagawa.jp +marugame.kagawa.jp +mitoyo.kagawa.jp +naoshima.kagawa.jp +sanuki.kagawa.jp +tadotsu.kagawa.jp +takamatsu.kagawa.jp +tonosho.kagawa.jp +uchinomi.kagawa.jp +utazu.kagawa.jp +zentsuji.kagawa.jp +akune.kagoshima.jp +amami.kagoshima.jp +hioki.kagoshima.jp +isa.kagoshima.jp +isen.kagoshima.jp +izumi.kagoshima.jp +kagoshima.kagoshima.jp +kanoya.kagoshima.jp +kawanabe.kagoshima.jp +kinko.kagoshima.jp +kouyama.kagoshima.jp +makurazaki.kagoshima.jp +matsumoto.kagoshima.jp +minamitane.kagoshima.jp +nakatane.kagoshima.jp +nishinoomote.kagoshima.jp +satsumasendai.kagoshima.jp +soo.kagoshima.jp +tarumizu.kagoshima.jp +yusui.kagoshima.jp +aikawa.kanagawa.jp +atsugi.kanagawa.jp +ayase.kanagawa.jp +chigasaki.kanagawa.jp +ebina.kanagawa.jp +fujisawa.kanagawa.jp +hadano.kanagawa.jp +hakone.kanagawa.jp +hiratsuka.kanagawa.jp +isehara.kanagawa.jp +kaisei.kanagawa.jp +kamakura.kanagawa.jp +kiyokawa.kanagawa.jp +matsuda.kanagawa.jp +minamiashigara.kanagawa.jp +miura.kanagawa.jp +nakai.kanagawa.jp +ninomiya.kanagawa.jp +odawara.kanagawa.jp +oi.kanagawa.jp +oiso.kanagawa.jp +sagamihara.kanagawa.jp +samukawa.kanagawa.jp +tsukui.kanagawa.jp +yamakita.kanagawa.jp +yamato.kanagawa.jp +yokosuka.kanagawa.jp +yugawara.kanagawa.jp +zama.kanagawa.jp +zushi.kanagawa.jp +aki.kochi.jp +geisei.kochi.jp +hidaka.kochi.jp +higashitsuno.kochi.jp +ino.kochi.jp +kagami.kochi.jp +kami.kochi.jp +kitagawa.kochi.jp +kochi.kochi.jp +mihara.kochi.jp +motoyama.kochi.jp +muroto.kochi.jp +nahari.kochi.jp +nakamura.kochi.jp +nankoku.kochi.jp +nishitosa.kochi.jp +niyodogawa.kochi.jp +ochi.kochi.jp +okawa.kochi.jp +otoyo.kochi.jp +otsuki.kochi.jp +sakawa.kochi.jp +sukumo.kochi.jp +susaki.kochi.jp +tosa.kochi.jp +tosashimizu.kochi.jp +toyo.kochi.jp +tsuno.kochi.jp +umaji.kochi.jp +yasuda.kochi.jp +yusuhara.kochi.jp +amakusa.kumamoto.jp +arao.kumamoto.jp +aso.kumamoto.jp +choyo.kumamoto.jp +gyokuto.kumamoto.jp +kamiamakusa.kumamoto.jp +kikuchi.kumamoto.jp +kumamoto.kumamoto.jp +mashiki.kumamoto.jp +mifune.kumamoto.jp +minamata.kumamoto.jp +minamioguni.kumamoto.jp +nagasu.kumamoto.jp +nishihara.kumamoto.jp +oguni.kumamoto.jp +ozu.kumamoto.jp +sumoto.kumamoto.jp +takamori.kumamoto.jp +uki.kumamoto.jp +uto.kumamoto.jp +yamaga.kumamoto.jp +yamato.kumamoto.jp +yatsushiro.kumamoto.jp +ayabe.kyoto.jp +fukuchiyama.kyoto.jp +higashiyama.kyoto.jp +ide.kyoto.jp +ine.kyoto.jp +joyo.kyoto.jp +kameoka.kyoto.jp +kamo.kyoto.jp +kita.kyoto.jp +kizu.kyoto.jp +kumiyama.kyoto.jp +kyotamba.kyoto.jp +kyotanabe.kyoto.jp +kyotango.kyoto.jp +maizuru.kyoto.jp +minami.kyoto.jp +minamiyamashiro.kyoto.jp +miyazu.kyoto.jp +muko.kyoto.jp +nagaokakyo.kyoto.jp +nakagyo.kyoto.jp +nantan.kyoto.jp +oyamazaki.kyoto.jp +sakyo.kyoto.jp +seika.kyoto.jp +tanabe.kyoto.jp +uji.kyoto.jp +ujitawara.kyoto.jp +wazuka.kyoto.jp +yamashina.kyoto.jp +yawata.kyoto.jp +asahi.mie.jp +inabe.mie.jp +ise.mie.jp +kameyama.mie.jp +kawagoe.mie.jp +kiho.mie.jp +kisosaki.mie.jp +kiwa.mie.jp +komono.mie.jp +kumano.mie.jp +kuwana.mie.jp +matsusaka.mie.jp +meiwa.mie.jp +mihama.mie.jp +minamiise.mie.jp +misugi.mie.jp +miyama.mie.jp +nabari.mie.jp +shima.mie.jp +suzuka.mie.jp +tado.mie.jp +taiki.mie.jp +taki.mie.jp +tamaki.mie.jp +toba.mie.jp +tsu.mie.jp +udono.mie.jp +ureshino.mie.jp +watarai.mie.jp +yokkaichi.mie.jp +furukawa.miyagi.jp +higashimatsushima.miyagi.jp +ishinomaki.miyagi.jp +iwanuma.miyagi.jp +kakuda.miyagi.jp +kami.miyagi.jp +kawasaki.miyagi.jp +marumori.miyagi.jp +matsushima.miyagi.jp +minamisanriku.miyagi.jp +misato.miyagi.jp +murata.miyagi.jp +natori.miyagi.jp +ogawara.miyagi.jp +ohira.miyagi.jp +onagawa.miyagi.jp +osaki.miyagi.jp +rifu.miyagi.jp +semine.miyagi.jp +shibata.miyagi.jp +shichikashuku.miyagi.jp +shikama.miyagi.jp +shiogama.miyagi.jp +shiroishi.miyagi.jp +tagajo.miyagi.jp +taiwa.miyagi.jp +tome.miyagi.jp +tomiya.miyagi.jp +wakuya.miyagi.jp +watari.miyagi.jp +yamamoto.miyagi.jp +zao.miyagi.jp +aya.miyazaki.jp +ebino.miyazaki.jp +gokase.miyazaki.jp +hyuga.miyazaki.jp +kadogawa.miyazaki.jp +kawaminami.miyazaki.jp +kijo.miyazaki.jp +kitagawa.miyazaki.jp +kitakata.miyazaki.jp +kitaura.miyazaki.jp +kobayashi.miyazaki.jp +kunitomi.miyazaki.jp +kushima.miyazaki.jp +mimata.miyazaki.jp +miyakonojo.miyazaki.jp +miyazaki.miyazaki.jp +morotsuka.miyazaki.jp +nichinan.miyazaki.jp +nishimera.miyazaki.jp +nobeoka.miyazaki.jp +saito.miyazaki.jp +shiiba.miyazaki.jp +shintomi.miyazaki.jp +takaharu.miyazaki.jp +takanabe.miyazaki.jp +takazaki.miyazaki.jp +tsuno.miyazaki.jp +achi.nagano.jp +agematsu.nagano.jp +anan.nagano.jp +aoki.nagano.jp +asahi.nagano.jp +azumino.nagano.jp +chikuhoku.nagano.jp +chikuma.nagano.jp +chino.nagano.jp +fujimi.nagano.jp +hakuba.nagano.jp +hara.nagano.jp +hiraya.nagano.jp +iida.nagano.jp +iijima.nagano.jp +iiyama.nagano.jp +iizuna.nagano.jp +ikeda.nagano.jp +ikusaka.nagano.jp +ina.nagano.jp +karuizawa.nagano.jp +kawakami.nagano.jp +kiso.nagano.jp +kisofukushima.nagano.jp +kitaaiki.nagano.jp +komagane.nagano.jp +komoro.nagano.jp +matsukawa.nagano.jp +matsumoto.nagano.jp +miasa.nagano.jp +minamiaiki.nagano.jp +minamimaki.nagano.jp +minamiminowa.nagano.jp +minowa.nagano.jp +miyada.nagano.jp +miyota.nagano.jp +mochizuki.nagano.jp +nagano.nagano.jp +nagawa.nagano.jp +nagiso.nagano.jp +nakagawa.nagano.jp +nakano.nagano.jp +nozawaonsen.nagano.jp +obuse.nagano.jp +ogawa.nagano.jp +okaya.nagano.jp +omachi.nagano.jp +omi.nagano.jp +ookuwa.nagano.jp +ooshika.nagano.jp +otaki.nagano.jp +otari.nagano.jp +sakae.nagano.jp +sakaki.nagano.jp +saku.nagano.jp +sakuho.nagano.jp +shimosuwa.nagano.jp +shinanomachi.nagano.jp +shiojiri.nagano.jp +suwa.nagano.jp +suzaka.nagano.jp +takagi.nagano.jp +takamori.nagano.jp +takayama.nagano.jp +tateshina.nagano.jp +tatsuno.nagano.jp +togakushi.nagano.jp +togura.nagano.jp +tomi.nagano.jp +ueda.nagano.jp +wada.nagano.jp +yamagata.nagano.jp +yamanouchi.nagano.jp +yasaka.nagano.jp +yasuoka.nagano.jp +chijiwa.nagasaki.jp +futsu.nagasaki.jp +goto.nagasaki.jp +hasami.nagasaki.jp +hirado.nagasaki.jp +iki.nagasaki.jp +isahaya.nagasaki.jp +kawatana.nagasaki.jp +kuchinotsu.nagasaki.jp +matsuura.nagasaki.jp +nagasaki.nagasaki.jp +obama.nagasaki.jp +omura.nagasaki.jp +oseto.nagasaki.jp +saikai.nagasaki.jp +sasebo.nagasaki.jp +seihi.nagasaki.jp +shimabara.nagasaki.jp +shinkamigoto.nagasaki.jp +togitsu.nagasaki.jp +tsushima.nagasaki.jp +unzen.nagasaki.jp +ando.nara.jp +gose.nara.jp +heguri.nara.jp +higashiyoshino.nara.jp +ikaruga.nara.jp +ikoma.nara.jp +kamikitayama.nara.jp +kanmaki.nara.jp +kashiba.nara.jp +kashihara.nara.jp +katsuragi.nara.jp +kawai.nara.jp +kawakami.nara.jp +kawanishi.nara.jp +koryo.nara.jp +kurotaki.nara.jp +mitsue.nara.jp +miyake.nara.jp +nara.nara.jp +nosegawa.nara.jp +oji.nara.jp +ouda.nara.jp +oyodo.nara.jp +sakurai.nara.jp +sango.nara.jp +shimoichi.nara.jp +shimokitayama.nara.jp +shinjo.nara.jp +soni.nara.jp +takatori.nara.jp +tawaramoto.nara.jp +tenkawa.nara.jp +tenri.nara.jp +uda.nara.jp +yamatokoriyama.nara.jp +yamatotakada.nara.jp +yamazoe.nara.jp +yoshino.nara.jp +aga.niigata.jp +agano.niigata.jp +gosen.niigata.jp +itoigawa.niigata.jp +izumozaki.niigata.jp +joetsu.niigata.jp +kamo.niigata.jp +kariwa.niigata.jp +kashiwazaki.niigata.jp +minamiuonuma.niigata.jp +mitsuke.niigata.jp +muika.niigata.jp +murakami.niigata.jp +myoko.niigata.jp +nagaoka.niigata.jp +niigata.niigata.jp +ojiya.niigata.jp +omi.niigata.jp +sado.niigata.jp +sanjo.niigata.jp +seiro.niigata.jp +seirou.niigata.jp +sekikawa.niigata.jp +shibata.niigata.jp +tagami.niigata.jp +tainai.niigata.jp +tochio.niigata.jp +tokamachi.niigata.jp +tsubame.niigata.jp +tsunan.niigata.jp +uonuma.niigata.jp +yahiko.niigata.jp +yoita.niigata.jp +yuzawa.niigata.jp +beppu.oita.jp +bungoono.oita.jp +bungotakada.oita.jp +hasama.oita.jp +hiji.oita.jp +himeshima.oita.jp +hita.oita.jp +kamitsue.oita.jp +kokonoe.oita.jp +kuju.oita.jp +kunisaki.oita.jp +kusu.oita.jp +oita.oita.jp +saiki.oita.jp +taketa.oita.jp +tsukumi.oita.jp +usa.oita.jp +usuki.oita.jp +yufu.oita.jp +akaiwa.okayama.jp +asakuchi.okayama.jp +bizen.okayama.jp +hayashima.okayama.jp +ibara.okayama.jp +kagamino.okayama.jp +kasaoka.okayama.jp +kibichuo.okayama.jp +kumenan.okayama.jp +kurashiki.okayama.jp +maniwa.okayama.jp +misaki.okayama.jp +nagi.okayama.jp +niimi.okayama.jp +nishiawakura.okayama.jp +okayama.okayama.jp +satosho.okayama.jp +setouchi.okayama.jp +shinjo.okayama.jp +shoo.okayama.jp +soja.okayama.jp +takahashi.okayama.jp +tamano.okayama.jp +tsuyama.okayama.jp +wake.okayama.jp +yakage.okayama.jp +aguni.okinawa.jp +ginowan.okinawa.jp +ginoza.okinawa.jp +gushikami.okinawa.jp +haebaru.okinawa.jp +higashi.okinawa.jp +hirara.okinawa.jp +iheya.okinawa.jp +ishigaki.okinawa.jp +ishikawa.okinawa.jp +itoman.okinawa.jp +izena.okinawa.jp +kadena.okinawa.jp +kin.okinawa.jp +kitadaito.okinawa.jp +kitanakagusuku.okinawa.jp +kumejima.okinawa.jp +kunigami.okinawa.jp +minamidaito.okinawa.jp +motobu.okinawa.jp +nago.okinawa.jp +naha.okinawa.jp +nakagusuku.okinawa.jp +nakijin.okinawa.jp +nanjo.okinawa.jp +nishihara.okinawa.jp +ogimi.okinawa.jp +okinawa.okinawa.jp +onna.okinawa.jp +shimoji.okinawa.jp +taketomi.okinawa.jp +tarama.okinawa.jp +tokashiki.okinawa.jp +tomigusuku.okinawa.jp +tonaki.okinawa.jp +urasoe.okinawa.jp +uruma.okinawa.jp +yaese.okinawa.jp +yomitan.okinawa.jp +yonabaru.okinawa.jp +yonaguni.okinawa.jp +zamami.okinawa.jp +abeno.osaka.jp +chihayaakasaka.osaka.jp +chuo.osaka.jp +daito.osaka.jp +fujiidera.osaka.jp +habikino.osaka.jp +hannan.osaka.jp +higashiosaka.osaka.jp +higashisumiyoshi.osaka.jp +higashiyodogawa.osaka.jp +hirakata.osaka.jp +ibaraki.osaka.jp +ikeda.osaka.jp +izumi.osaka.jp +izumiotsu.osaka.jp +izumisano.osaka.jp +kadoma.osaka.jp +kaizuka.osaka.jp +kanan.osaka.jp +kashiwara.osaka.jp +katano.osaka.jp +kawachinagano.osaka.jp +kishiwada.osaka.jp +kita.osaka.jp +kumatori.osaka.jp +matsubara.osaka.jp +minato.osaka.jp +minoh.osaka.jp +misaki.osaka.jp +moriguchi.osaka.jp +neyagawa.osaka.jp +nishi.osaka.jp +nose.osaka.jp +osakasayama.osaka.jp +sakai.osaka.jp +sayama.osaka.jp +sennan.osaka.jp +settsu.osaka.jp +shijonawate.osaka.jp +shimamoto.osaka.jp +suita.osaka.jp +tadaoka.osaka.jp +taishi.osaka.jp +tajiri.osaka.jp +takaishi.osaka.jp +takatsuki.osaka.jp +tondabayashi.osaka.jp +toyonaka.osaka.jp +toyono.osaka.jp +yao.osaka.jp +ariake.saga.jp +arita.saga.jp +fukudomi.saga.jp +genkai.saga.jp +hamatama.saga.jp +hizen.saga.jp +imari.saga.jp +kamimine.saga.jp +kanzaki.saga.jp +karatsu.saga.jp +kashima.saga.jp +kitagata.saga.jp +kitahata.saga.jp +kiyama.saga.jp +kouhoku.saga.jp +kyuragi.saga.jp +nishiarita.saga.jp +ogi.saga.jp +omachi.saga.jp +ouchi.saga.jp +saga.saga.jp +shiroishi.saga.jp +taku.saga.jp +tara.saga.jp +tosu.saga.jp +yoshinogari.saga.jp +arakawa.saitama.jp +asaka.saitama.jp +chichibu.saitama.jp +fujimi.saitama.jp +fujimino.saitama.jp +fukaya.saitama.jp +hanno.saitama.jp +hanyu.saitama.jp +hasuda.saitama.jp +hatogaya.saitama.jp +hatoyama.saitama.jp +hidaka.saitama.jp +higashichichibu.saitama.jp +higashimatsuyama.saitama.jp +honjo.saitama.jp +ina.saitama.jp +iruma.saitama.jp +iwatsuki.saitama.jp +kamiizumi.saitama.jp +kamikawa.saitama.jp +kamisato.saitama.jp +kasukabe.saitama.jp +kawagoe.saitama.jp +kawaguchi.saitama.jp +kawajima.saitama.jp +kazo.saitama.jp +kitamoto.saitama.jp +koshigaya.saitama.jp +kounosu.saitama.jp +kuki.saitama.jp +kumagaya.saitama.jp +matsubushi.saitama.jp +minano.saitama.jp +misato.saitama.jp +miyashiro.saitama.jp +miyoshi.saitama.jp +moroyama.saitama.jp +nagatoro.saitama.jp +namegawa.saitama.jp +niiza.saitama.jp +ogano.saitama.jp +ogawa.saitama.jp +ogose.saitama.jp +okegawa.saitama.jp +omiya.saitama.jp +otaki.saitama.jp +ranzan.saitama.jp +ryokami.saitama.jp +saitama.saitama.jp +sakado.saitama.jp +satte.saitama.jp +sayama.saitama.jp +shiki.saitama.jp +shiraoka.saitama.jp +soka.saitama.jp +sugito.saitama.jp +toda.saitama.jp +tokigawa.saitama.jp +tokorozawa.saitama.jp +tsurugashima.saitama.jp +urawa.saitama.jp +warabi.saitama.jp +yashio.saitama.jp +yokoze.saitama.jp +yono.saitama.jp +yorii.saitama.jp +yoshida.saitama.jp +yoshikawa.saitama.jp +yoshimi.saitama.jp +aisho.shiga.jp +gamo.shiga.jp +higashiomi.shiga.jp +hikone.shiga.jp +koka.shiga.jp +konan.shiga.jp +kosei.shiga.jp +koto.shiga.jp +kusatsu.shiga.jp +maibara.shiga.jp +moriyama.shiga.jp +nagahama.shiga.jp +nishiazai.shiga.jp +notogawa.shiga.jp +omihachiman.shiga.jp +otsu.shiga.jp +ritto.shiga.jp +ryuoh.shiga.jp +takashima.shiga.jp +takatsuki.shiga.jp +torahime.shiga.jp +toyosato.shiga.jp +yasu.shiga.jp +akagi.shimane.jp +ama.shimane.jp +gotsu.shimane.jp +hamada.shimane.jp +higashiizumo.shimane.jp +hikawa.shimane.jp +hikimi.shimane.jp +izumo.shimane.jp +kakinoki.shimane.jp +masuda.shimane.jp +matsue.shimane.jp +misato.shimane.jp +nishinoshima.shimane.jp +ohda.shimane.jp +okinoshima.shimane.jp +okuizumo.shimane.jp +shimane.shimane.jp +tamayu.shimane.jp +tsuwano.shimane.jp +unnan.shimane.jp +yakumo.shimane.jp +yasugi.shimane.jp +yatsuka.shimane.jp +arai.shizuoka.jp +atami.shizuoka.jp +fuji.shizuoka.jp +fujieda.shizuoka.jp +fujikawa.shizuoka.jp +fujinomiya.shizuoka.jp +fukuroi.shizuoka.jp +gotemba.shizuoka.jp +haibara.shizuoka.jp +hamamatsu.shizuoka.jp +higashiizu.shizuoka.jp +ito.shizuoka.jp +iwata.shizuoka.jp +izu.shizuoka.jp +izunokuni.shizuoka.jp +kakegawa.shizuoka.jp +kannami.shizuoka.jp +kawanehon.shizuoka.jp +kawazu.shizuoka.jp +kikugawa.shizuoka.jp +kosai.shizuoka.jp +makinohara.shizuoka.jp +matsuzaki.shizuoka.jp +minamiizu.shizuoka.jp +mishima.shizuoka.jp +morimachi.shizuoka.jp +nishiizu.shizuoka.jp +numazu.shizuoka.jp +omaezaki.shizuoka.jp +shimada.shizuoka.jp +shimizu.shizuoka.jp +shimoda.shizuoka.jp +shizuoka.shizuoka.jp +susono.shizuoka.jp +yaizu.shizuoka.jp +yoshida.shizuoka.jp +ashikaga.tochigi.jp +bato.tochigi.jp +haga.tochigi.jp +ichikai.tochigi.jp +iwafune.tochigi.jp +kaminokawa.tochigi.jp +kanuma.tochigi.jp +karasuyama.tochigi.jp +kuroiso.tochigi.jp +mashiko.tochigi.jp +mibu.tochigi.jp +moka.tochigi.jp +motegi.tochigi.jp +nasu.tochigi.jp +nasushiobara.tochigi.jp +nikko.tochigi.jp +nishikata.tochigi.jp +nogi.tochigi.jp +ohira.tochigi.jp +ohtawara.tochigi.jp +oyama.tochigi.jp +sakura.tochigi.jp +sano.tochigi.jp +shimotsuke.tochigi.jp +shioya.tochigi.jp +takanezawa.tochigi.jp +tochigi.tochigi.jp +tsuga.tochigi.jp +ujiie.tochigi.jp +utsunomiya.tochigi.jp +yaita.tochigi.jp +aizumi.tokushima.jp +anan.tokushima.jp +ichiba.tokushima.jp +itano.tokushima.jp +kainan.tokushima.jp +komatsushima.tokushima.jp +matsushige.tokushima.jp +mima.tokushima.jp +minami.tokushima.jp +miyoshi.tokushima.jp +mugi.tokushima.jp +nakagawa.tokushima.jp +naruto.tokushima.jp +sanagochi.tokushima.jp +shishikui.tokushima.jp +tokushima.tokushima.jp +wajiki.tokushima.jp +adachi.tokyo.jp +akiruno.tokyo.jp +akishima.tokyo.jp +aogashima.tokyo.jp +arakawa.tokyo.jp +bunkyo.tokyo.jp +chiyoda.tokyo.jp +chofu.tokyo.jp +chuo.tokyo.jp +edogawa.tokyo.jp +fuchu.tokyo.jp +fussa.tokyo.jp +hachijo.tokyo.jp +hachioji.tokyo.jp +hamura.tokyo.jp +higashikurume.tokyo.jp +higashimurayama.tokyo.jp +higashiyamato.tokyo.jp +hino.tokyo.jp +hinode.tokyo.jp +hinohara.tokyo.jp +inagi.tokyo.jp +itabashi.tokyo.jp +katsushika.tokyo.jp +kita.tokyo.jp +kiyose.tokyo.jp +kodaira.tokyo.jp +koganei.tokyo.jp +kokubunji.tokyo.jp +komae.tokyo.jp +koto.tokyo.jp +kouzushima.tokyo.jp +kunitachi.tokyo.jp +machida.tokyo.jp +meguro.tokyo.jp +minato.tokyo.jp +mitaka.tokyo.jp +mizuho.tokyo.jp +musashimurayama.tokyo.jp +musashino.tokyo.jp +nakano.tokyo.jp +nerima.tokyo.jp +ogasawara.tokyo.jp +okutama.tokyo.jp +ome.tokyo.jp +oshima.tokyo.jp +ota.tokyo.jp +setagaya.tokyo.jp +shibuya.tokyo.jp +shinagawa.tokyo.jp +shinjuku.tokyo.jp +suginami.tokyo.jp +sumida.tokyo.jp +tachikawa.tokyo.jp +taito.tokyo.jp +tama.tokyo.jp +toshima.tokyo.jp +chizu.tottori.jp +hino.tottori.jp +kawahara.tottori.jp +koge.tottori.jp +kotoura.tottori.jp +misasa.tottori.jp +nanbu.tottori.jp +nichinan.tottori.jp +sakaiminato.tottori.jp +tottori.tottori.jp +wakasa.tottori.jp +yazu.tottori.jp +yonago.tottori.jp +asahi.toyama.jp +fuchu.toyama.jp +fukumitsu.toyama.jp +funahashi.toyama.jp +himi.toyama.jp +imizu.toyama.jp +inami.toyama.jp +johana.toyama.jp +kamiichi.toyama.jp +kurobe.toyama.jp +nakaniikawa.toyama.jp +namerikawa.toyama.jp +nanto.toyama.jp +nyuzen.toyama.jp +oyabe.toyama.jp +taira.toyama.jp +takaoka.toyama.jp +tateyama.toyama.jp +toga.toyama.jp +tonami.toyama.jp +toyama.toyama.jp +unazuki.toyama.jp +uozu.toyama.jp +yamada.toyama.jp +arida.wakayama.jp +aridagawa.wakayama.jp +gobo.wakayama.jp +hashimoto.wakayama.jp +hidaka.wakayama.jp +hirogawa.wakayama.jp +inami.wakayama.jp +iwade.wakayama.jp +kainan.wakayama.jp +kamitonda.wakayama.jp +katsuragi.wakayama.jp +kimino.wakayama.jp +kinokawa.wakayama.jp +kitayama.wakayama.jp +koya.wakayama.jp +koza.wakayama.jp +kozagawa.wakayama.jp +kudoyama.wakayama.jp +kushimoto.wakayama.jp +mihama.wakayama.jp +misato.wakayama.jp +nachikatsuura.wakayama.jp +shingu.wakayama.jp +shirahama.wakayama.jp +taiji.wakayama.jp +tanabe.wakayama.jp +wakayama.wakayama.jp +yuasa.wakayama.jp +yura.wakayama.jp +asahi.yamagata.jp +funagata.yamagata.jp +higashine.yamagata.jp +iide.yamagata.jp +kahoku.yamagata.jp +kaminoyama.yamagata.jp +kaneyama.yamagata.jp +kawanishi.yamagata.jp +mamurogawa.yamagata.jp +mikawa.yamagata.jp +murayama.yamagata.jp +nagai.yamagata.jp +nakayama.yamagata.jp +nanyo.yamagata.jp +nishikawa.yamagata.jp +obanazawa.yamagata.jp +oe.yamagata.jp +oguni.yamagata.jp +ohkura.yamagata.jp +oishida.yamagata.jp +sagae.yamagata.jp +sakata.yamagata.jp +sakegawa.yamagata.jp +shinjo.yamagata.jp +shirataka.yamagata.jp +shonai.yamagata.jp +takahata.yamagata.jp +tendo.yamagata.jp +tozawa.yamagata.jp +tsuruoka.yamagata.jp +yamagata.yamagata.jp +yamanobe.yamagata.jp +yonezawa.yamagata.jp +yuza.yamagata.jp +abu.yamaguchi.jp +hagi.yamaguchi.jp +hikari.yamaguchi.jp +hofu.yamaguchi.jp +iwakuni.yamaguchi.jp +kudamatsu.yamaguchi.jp +mitou.yamaguchi.jp +nagato.yamaguchi.jp +oshima.yamaguchi.jp +shimonoseki.yamaguchi.jp +shunan.yamaguchi.jp +tabuse.yamaguchi.jp +tokuyama.yamaguchi.jp +toyota.yamaguchi.jp +ube.yamaguchi.jp +yuu.yamaguchi.jp +chuo.yamanashi.jp +doshi.yamanashi.jp +fuefuki.yamanashi.jp +fujikawa.yamanashi.jp +fujikawaguchiko.yamanashi.jp +fujiyoshida.yamanashi.jp +hayakawa.yamanashi.jp +hokuto.yamanashi.jp +ichikawamisato.yamanashi.jp +kai.yamanashi.jp +kofu.yamanashi.jp +koshu.yamanashi.jp +kosuge.yamanashi.jp +minami-alps.yamanashi.jp +minobu.yamanashi.jp +nakamichi.yamanashi.jp +nanbu.yamanashi.jp +narusawa.yamanashi.jp +nirasaki.yamanashi.jp +nishikatsura.yamanashi.jp +oshino.yamanashi.jp +otsuki.yamanashi.jp +showa.yamanashi.jp +tabayama.yamanashi.jp +tsuru.yamanashi.jp +uenohara.yamanashi.jp +yamanakako.yamanashi.jp +yamanashi.yamanashi.jp + +// ke : http://www.kenic.or.ke/index.php/en/ke-domains/ke-domains +ke +ac.ke +co.ke +go.ke +info.ke +me.ke +mobi.ke +ne.ke +or.ke +sc.ke + +// kg : http://www.domain.kg/dmn_n.html +kg +com.kg +edu.kg +gov.kg +mil.kg +net.kg +org.kg + +// kh : http://www.mptc.gov.kh/dns_registration.htm +*.kh + +// ki : https://www.iana.org/domains/root/db/ki.html +ki +biz.ki +com.ki +edu.ki +gov.ki +info.ki +net.ki +org.ki + +// km : https://www.iana.org/domains/root/db/km.html +// http://www.domaine.km/documents/charte.doc +km +ass.km +com.km +edu.km +gov.km +mil.km +nom.km +org.km +prd.km +tm.km +// These are only mentioned as proposed suggestions at domaine.km, but +// https://www.iana.org/domains/root/db/km.html says they're available for registration: +asso.km +coop.km +gouv.km +medecin.km +notaires.km +pharmaciens.km +presse.km +veterinaire.km + +// kn : https://www.iana.org/domains/root/db/kn.html +// http://www.dot.kn/domainRules.html +kn +edu.kn +gov.kn +net.kn +org.kn + +// kp : http://www.kcce.kp/en_index.php +kp +com.kp +edu.kp +gov.kp +org.kp +rep.kp +tra.kp + +// kr : https://www.iana.org/domains/root/db/kr.html +// see also: https://krnic.kisa.or.kr/jsp/infoboard/law/domBylawsReg.jsp +kr +ac.kr +ai.kr +co.kr +es.kr +go.kr +hs.kr +io.kr +it.kr +kg.kr +me.kr +mil.kr +ms.kr +ne.kr +or.kr +pe.kr +re.kr +sc.kr +// kr geographical names +busan.kr +chungbuk.kr +chungnam.kr +daegu.kr +daejeon.kr +gangwon.kr +gwangju.kr +gyeongbuk.kr +gyeonggi.kr +gyeongnam.kr +incheon.kr +jeju.kr +jeonbuk.kr +jeonnam.kr +seoul.kr +ulsan.kr + +// kw : https://www.nic.kw/policies/ +// Confirmed by registry +kw +com.kw +edu.kw +emb.kw +gov.kw +ind.kw +net.kw +org.kw + +// ky : http://www.icta.ky/da_ky_reg_dom.php +// Confirmed by registry 2008-06-17 +ky +com.ky +edu.ky +net.ky +org.ky + +// kz : https://www.iana.org/domains/root/db/kz.html +// see also: http://www.nic.kz/rules/index.jsp +kz +com.kz +edu.kz +gov.kz +mil.kz +net.kz +org.kz + +// la : https://www.iana.org/domains/root/db/la.html +// Submitted by registry +la +com.la +edu.la +gov.la +info.la +int.la +net.la +org.la +per.la + +// lb : https://www.iana.org/domains/root/db/lb.html +// Submitted by registry +lb +com.lb +edu.lb +gov.lb +net.lb +org.lb + +// lc : https://www.iana.org/domains/root/db/lc.html +// see also: http://www.nic.lc/rules.htm +lc +co.lc +com.lc +edu.lc +gov.lc +net.lc +org.lc + +// li : https://www.iana.org/domains/root/db/li.html +li + +// lk : https://www.iana.org/domains/root/db/lk.html +lk +ac.lk +assn.lk +com.lk +edu.lk +gov.lk +grp.lk +hotel.lk +int.lk +ltd.lk +net.lk +ngo.lk +org.lk +sch.lk +soc.lk +web.lk + +// lr : http://psg.com/dns/lr/lr.txt +// Submitted by registry +lr +com.lr +edu.lr +gov.lr +net.lr +org.lr + +// ls : http://www.nic.ls/ +// Confirmed by registry +ls +ac.ls +biz.ls +co.ls +edu.ls +gov.ls +info.ls +net.ls +org.ls +sc.ls + +// lt : https://www.iana.org/domains/root/db/lt.html +lt +// gov.lt : http://www.gov.lt/index_en.php +gov.lt + +// lu : http://www.dns.lu/en/ +lu + +// lv : https://www.iana.org/domains/root/db/lv.html +lv +asn.lv +com.lv +conf.lv +edu.lv +gov.lv +id.lv +mil.lv +net.lv +org.lv + +// ly : http://www.nic.ly/regulations.php +ly +com.ly +edu.ly +gov.ly +id.ly +med.ly +net.ly +org.ly +plc.ly +sch.ly + +// ma : https://www.iana.org/domains/root/db/ma.html +// http://www.anrt.ma/fr/admin/download/upload/file_fr782.pdf +ma +ac.ma +co.ma +gov.ma +net.ma +org.ma +press.ma + +// mc : http://www.nic.mc/ +mc +asso.mc +tm.mc + +// md : https://www.iana.org/domains/root/db/md.html +md + +// me : https://www.iana.org/domains/root/db/me.html +me +ac.me +co.me +edu.me +gov.me +its.me +net.me +org.me +priv.me + +// mg : https://nic.mg +mg +co.mg +com.mg +edu.mg +gov.mg +mil.mg +nom.mg +org.mg +prd.mg + +// mh : https://www.iana.org/domains/root/db/mh.html +mh + +// mil : https://www.iana.org/domains/root/db/mil.html +mil + +// mk : https://www.iana.org/domains/root/db/mk.html +// see also: http://dns.marnet.net.mk/postapka.php +mk +com.mk +edu.mk +gov.mk +inf.mk +name.mk +net.mk +org.mk + +// ml : https://www.iana.org/domains/root/db/ml.html +// Confirmed by Boubacar NDIAYE 2024-12-31 +ml +ac.ml +art.ml +asso.ml +com.ml +edu.ml +gouv.ml +gov.ml +info.ml +inst.ml +net.ml +org.ml +pr.ml +presse.ml + +// mm : https://www.iana.org/domains/root/db/mm.html +*.mm + +// mn : https://www.iana.org/domains/root/db/mn.html +mn +edu.mn +gov.mn +org.mn + +// mo : http://www.monic.net.mo/ +mo +com.mo +edu.mo +gov.mo +net.mo +org.mo + +// mobi : https://www.iana.org/domains/root/db/mobi.html +mobi + +// mp : http://www.dot.mp/ +// Confirmed by registry 2008-06-17 +mp + +// mq : https://www.iana.org/domains/root/db/mq.html +mq + +// mr : https://www.iana.org/domains/root/db/mr.html +mr +gov.mr + +// ms : https://www.iana.org/domains/root/db/ms.html +ms +com.ms +edu.ms +gov.ms +net.ms +org.ms + +// mt : https://www.nic.org.mt/go/policy +// Submitted by registry +mt +com.mt +edu.mt +net.mt +org.mt + +// mu : https://www.iana.org/domains/root/db/mu.html +mu +ac.mu +co.mu +com.mu +gov.mu +net.mu +or.mu +org.mu + +// museum : https://welcome.museum/wp-content/uploads/2018/05/20180525-Registration-Policy-MUSEUM-EN_VF-2.pdf https://welcome.museum/buy-your-dot-museum-2/ +museum + +// mv : https://www.iana.org/domains/root/db/mv.html +// "mv" included because, contra Wikipedia, google.mv exists. +mv +aero.mv +biz.mv +com.mv +coop.mv +edu.mv +gov.mv +info.mv +int.mv +mil.mv +museum.mv +name.mv +net.mv +org.mv +pro.mv + +// mw : http://www.registrar.mw/ +mw +ac.mw +biz.mw +co.mw +com.mw +coop.mw +edu.mw +gov.mw +int.mw +net.mw +org.mw + +// mx : http://www.nic.mx/ +// Submitted by registry +mx +com.mx +edu.mx +gob.mx +net.mx +org.mx + +// my : http://www.mynic.my/ +// Available strings: https://mynic.my/resources/domains/buying-a-domain/ +my +biz.my +com.my +edu.my +gov.my +mil.my +name.my +net.my +org.my + +// mz : http://www.uem.mz/ +// Submitted by registry +mz +ac.mz +adv.mz +co.mz +edu.mz +gov.mz +mil.mz +net.mz +org.mz + +// na : http://www.na-nic.com.na/ +na +alt.na +co.na +com.na +gov.na +net.na +org.na + +// name : http://www.nic.name/ +// Regarding 2LDs: https://github.com/publicsuffix/list/issues/2306 +name + +// nc : http://www.cctld.nc/ +nc +asso.nc +nom.nc + +// ne : https://www.iana.org/domains/root/db/ne.html +ne + +// net : https://www.iana.org/domains/root/db/net.html +net + +// nf : https://www.iana.org/domains/root/db/nf.html +nf +arts.nf +com.nf +firm.nf +info.nf +net.nf +other.nf +per.nf +rec.nf +store.nf +web.nf + +// ng : http://www.nira.org.ng/index.php/join-us/register-ng-domain/189-nira-slds +ng +com.ng +edu.ng +gov.ng +i.ng +mil.ng +mobi.ng +name.ng +net.ng +org.ng +sch.ng + +// ni : http://www.nic.ni/ +ni +ac.ni +biz.ni +co.ni +com.ni +edu.ni +gob.ni +in.ni +info.ni +int.ni +mil.ni +net.ni +nom.ni +org.ni +web.ni + +// nl : https://www.iana.org/domains/root/db/nl.html +// https://www.sidn.nl/ +nl + +// no : https://www.norid.no/en/om-domenenavn/regelverk-for-no/ +// Norid geographical second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-b/ +// Norid category second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-c/ +// Norid category second-level domains managed by parties other than Norid : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-d/ +// RSS feed: https://teknisk.norid.no/en/feed/ +no +// Norid category second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-c/ +fhs.no +folkebibl.no +fylkesbibl.no +idrett.no +museum.no +priv.no +vgs.no +// Norid category second-level domains managed by parties other than Norid : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-d/ +dep.no +herad.no +kommune.no +mil.no +stat.no +// Norid geographical second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-b/ +// counties +aa.no +ah.no +bu.no +fm.no +hl.no +hm.no +jan-mayen.no +mr.no +nl.no +nt.no +of.no +ol.no +oslo.no +rl.no +sf.no +st.no +svalbard.no +tm.no +tr.no +va.no +vf.no +// primary and lower secondary schools per county +gs.aa.no +gs.ah.no +gs.bu.no +gs.fm.no +gs.hl.no +gs.hm.no +gs.jan-mayen.no +gs.mr.no +gs.nl.no +gs.nt.no +gs.of.no +gs.ol.no +gs.oslo.no +gs.rl.no +gs.sf.no +gs.st.no +gs.svalbard.no +gs.tm.no +gs.tr.no +gs.va.no +gs.vf.no +// cities +akrehamn.no +åkrehamn.no +algard.no +ålgård.no +arna.no +bronnoysund.no +brønnøysund.no +brumunddal.no +bryne.no +drobak.no +drøbak.no +egersund.no +fetsund.no +floro.no +florø.no +fredrikstad.no +hokksund.no +honefoss.no +hønefoss.no +jessheim.no +jorpeland.no +jørpeland.no +kirkenes.no +kopervik.no +krokstadelva.no +langevag.no +langevåg.no +leirvik.no +mjondalen.no +mjøndalen.no +mo-i-rana.no +mosjoen.no +mosjøen.no +nesoddtangen.no +orkanger.no +osoyro.no +osøyro.no +raholt.no +råholt.no +sandnessjoen.no +sandnessjøen.no +skedsmokorset.no +slattum.no +spjelkavik.no +stathelle.no +stavern.no +stjordalshalsen.no +stjørdalshalsen.no +tananger.no +tranby.no +vossevangen.no +// communities +aarborte.no +aejrie.no +afjord.no +åfjord.no +agdenes.no +nes.akershus.no +aknoluokta.no +ákŋoluokta.no +al.no +ål.no +alaheadju.no +álaheadju.no +alesund.no +ålesund.no +alstahaug.no +alta.no +áltá.no +alvdal.no +amli.no +åmli.no +amot.no +åmot.no +andasuolo.no +andebu.no +andoy.no +andøy.no +ardal.no +årdal.no +aremark.no +arendal.no +ås.no +aseral.no +åseral.no +asker.no +askim.no +askoy.no +askøy.no +askvoll.no +asnes.no +åsnes.no +audnedaln.no +aukra.no +aure.no +aurland.no +aurskog-holand.no +aurskog-høland.no +austevoll.no +austrheim.no +averoy.no +averøy.no +badaddja.no +bådåddjå.no +bærum.no +bahcavuotna.no +báhcavuotna.no +bahccavuotna.no +báhccavuotna.no +baidar.no +báidár.no +bajddar.no +bájddar.no +balat.no +bálát.no +balestrand.no +ballangen.no +balsfjord.no +bamble.no +bardu.no +barum.no +batsfjord.no +båtsfjord.no +bearalvahki.no +bearalváhki.no +beardu.no +beiarn.no +berg.no +bergen.no +berlevag.no +berlevåg.no +bievat.no +bievát.no +bindal.no +birkenes.no +bjerkreim.no +bjugn.no +bodo.no +bodø.no +bokn.no +bomlo.no +bømlo.no +bremanger.no +bronnoy.no +brønnøy.no +budejju.no +nes.buskerud.no +bygland.no +bykle.no +cahcesuolo.no +čáhcesuolo.no +davvenjarga.no +davvenjárga.no +davvesiida.no +deatnu.no +dielddanuorri.no +divtasvuodna.no +divttasvuotna.no +donna.no +dønna.no +dovre.no +drammen.no +drangedal.no +dyroy.no +dyrøy.no +eid.no +eidfjord.no +eidsberg.no +eidskog.no +eidsvoll.no +eigersund.no +elverum.no +enebakk.no +engerdal.no +etne.no +etnedal.no +evenassi.no +evenášši.no +evenes.no +evje-og-hornnes.no +farsund.no +fauske.no +fedje.no +fet.no +finnoy.no +finnøy.no +fitjar.no +fjaler.no +fjell.no +fla.no +flå.no +flakstad.no +flatanger.no +flekkefjord.no +flesberg.no +flora.no +folldal.no +forde.no +førde.no +forsand.no +fosnes.no +fræna.no +frana.no +frei.no +frogn.no +froland.no +frosta.no +froya.no +frøya.no +fuoisku.no +fuossko.no +fusa.no +fyresdal.no +gaivuotna.no +gáivuotna.no +galsa.no +gálsá.no +gamvik.no +gangaviika.no +gáŋgaviika.no +gaular.no +gausdal.no +giehtavuoatna.no +gildeskal.no +gildeskål.no +giske.no +gjemnes.no +gjerdrum.no +gjerstad.no +gjesdal.no +gjovik.no +gjøvik.no +gloppen.no +gol.no +gran.no +grane.no +granvin.no +gratangen.no +grimstad.no +grong.no +grue.no +gulen.no +guovdageaidnu.no +ha.no +hå.no +habmer.no +hábmer.no +hadsel.no +hægebostad.no +hagebostad.no +halden.no +halsa.no +hamar.no +hamaroy.no +hammarfeasta.no +hámmárfeasta.no +hammerfest.no +hapmir.no +hápmir.no +haram.no +hareid.no +harstad.no +hasvik.no +hattfjelldal.no +haugesund.no +os.hedmark.no +valer.hedmark.no +våler.hedmark.no +hemne.no +hemnes.no +hemsedal.no +hitra.no +hjartdal.no +hjelmeland.no +hobol.no +hobøl.no +hof.no +hol.no +hole.no +holmestrand.no +holtalen.no +holtålen.no +os.hordaland.no +hornindal.no +horten.no +hoyanger.no +høyanger.no +hoylandet.no +høylandet.no +hurdal.no +hurum.no +hvaler.no +hyllestad.no +ibestad.no +inderoy.no +inderøy.no +iveland.no +ivgu.no +jevnaker.no +jolster.no +jølster.no +jondal.no +kafjord.no +kåfjord.no +karasjohka.no +kárášjohka.no +karasjok.no +karlsoy.no +karmoy.no +karmøy.no +kautokeino.no +klabu.no +klæbu.no +klepp.no +kongsberg.no +kongsvinger.no +kraanghke.no +kråanghke.no +kragero.no +kragerø.no +kristiansand.no +kristiansund.no +krodsherad.no +krødsherad.no +kvæfjord.no +kvænangen.no +kvafjord.no +kvalsund.no +kvam.no +kvanangen.no +kvinesdal.no +kvinnherad.no +kviteseid.no +kvitsoy.no +kvitsøy.no +laakesvuemie.no +lærdal.no +lahppi.no +láhppi.no +lardal.no +larvik.no +lavagis.no +lavangen.no +leangaviika.no +leaŋgaviika.no +lebesby.no +leikanger.no +leirfjord.no +leka.no +leksvik.no +lenvik.no +lerdal.no +lesja.no +levanger.no +lier.no +lierne.no +lillehammer.no +lillesand.no +lindas.no +lindås.no +lindesnes.no +loabat.no +loabát.no +lodingen.no +lødingen.no +lom.no +loppa.no +lorenskog.no +lørenskog.no +loten.no +løten.no +lund.no +lunner.no +luroy.no +lurøy.no +luster.no +lyngdal.no +lyngen.no +malatvuopmi.no +málatvuopmi.no +malselv.no +målselv.no +malvik.no +mandal.no +marker.no +marnardal.no +masfjorden.no +masoy.no +måsøy.no +matta-varjjat.no +mátta-várjjat.no +meland.no +meldal.no +melhus.no +meloy.no +meløy.no +meraker.no +meråker.no +midsund.no +midtre-gauldal.no +moareke.no +moåreke.no +modalen.no +modum.no +molde.no +heroy.more-og-romsdal.no +sande.more-og-romsdal.no +herøy.møre-og-romsdal.no +sande.møre-og-romsdal.no +moskenes.no +moss.no +muosat.no +muosát.no +naamesjevuemie.no +nååmesjevuemie.no +nærøy.no +namdalseid.no +namsos.no +namsskogan.no +nannestad.no +naroy.no +narviika.no +narvik.no +naustdal.no +navuotna.no +návuotna.no +nedre-eiker.no +nesna.no +nesodden.no +nesseby.no +nesset.no +nissedal.no +nittedal.no +nord-aurdal.no +nord-fron.no +nord-odal.no +norddal.no +nordkapp.no +bo.nordland.no +bø.nordland.no +heroy.nordland.no +herøy.nordland.no +nordre-land.no +nordreisa.no +nore-og-uvdal.no +notodden.no +notteroy.no +nøtterøy.no +odda.no +oksnes.no +øksnes.no +omasvuotna.no +oppdal.no +oppegard.no +oppegård.no +orkdal.no +orland.no +ørland.no +orskog.no +ørskog.no +orsta.no +ørsta.no +osen.no +osteroy.no +osterøy.no +valer.ostfold.no +våler.østfold.no +ostre-toten.no +østre-toten.no +overhalla.no +ovre-eiker.no +øvre-eiker.no +oyer.no +øyer.no +oygarden.no +øygarden.no +oystre-slidre.no +øystre-slidre.no +porsanger.no +porsangu.no +porsáŋgu.no +porsgrunn.no +rade.no +råde.no +radoy.no +radøy.no +rælingen.no +rahkkeravju.no +ráhkkerávju.no +raisa.no +ráisa.no +rakkestad.no +ralingen.no +rana.no +randaberg.no +rauma.no +rendalen.no +rennebu.no +rennesoy.no +rennesøy.no +rindal.no +ringebu.no +ringerike.no +ringsaker.no +risor.no +risør.no +rissa.no +roan.no +rodoy.no +rødøy.no +rollag.no +romsa.no +romskog.no +rømskog.no +roros.no +røros.no +rost.no +røst.no +royken.no +røyken.no +royrvik.no +røyrvik.no +ruovat.no +rygge.no +salangen.no +salat.no +sálat.no +sálát.no +saltdal.no +samnanger.no +sandefjord.no +sandnes.no +sandoy.no +sandøy.no +sarpsborg.no +sauda.no +sauherad.no +sel.no +selbu.no +selje.no +seljord.no +siellak.no +sigdal.no +siljan.no +sirdal.no +skanit.no +skánit.no +skanland.no +skånland.no +skaun.no +skedsmo.no +ski.no +skien.no +skierva.no +skiervá.no +skiptvet.no +skjak.no +skjåk.no +skjervoy.no +skjervøy.no +skodje.no +smola.no +smøla.no +snaase.no +snåase.no +snasa.no +snåsa.no +snillfjord.no +snoasa.no +sogndal.no +sogne.no +søgne.no +sokndal.no +sola.no +solund.no +somna.no +sømna.no +sondre-land.no +søndre-land.no +songdalen.no +sor-aurdal.no +sør-aurdal.no +sor-fron.no +sør-fron.no +sor-odal.no +sør-odal.no +sor-varanger.no +sør-varanger.no +sorfold.no +sørfold.no +sorreisa.no +sørreisa.no +sortland.no +sorum.no +sørum.no +spydeberg.no +stange.no +stavanger.no +steigen.no +steinkjer.no +stjordal.no +stjørdal.no +stokke.no +stor-elvdal.no +stord.no +stordal.no +storfjord.no +strand.no +stranda.no +stryn.no +sula.no +suldal.no +sund.no +sunndal.no +surnadal.no +sveio.no +svelvik.no +sykkylven.no +tana.no +bo.telemark.no +bø.telemark.no +time.no +tingvoll.no +tinn.no +tjeldsund.no +tjome.no +tjøme.no +tokke.no +tolga.no +tonsberg.no +tønsberg.no +torsken.no +træna.no +trana.no +tranoy.no +tranøy.no +troandin.no +trogstad.no +trøgstad.no +tromsa.no +tromso.no +tromsø.no +trondheim.no +trysil.no +tvedestrand.no +tydal.no +tynset.no +tysfjord.no +tysnes.no +tysvær.no +tysvar.no +ullensaker.no +ullensvang.no +ulvik.no +unjarga.no +unjárga.no +utsira.no +vaapste.no +vadso.no +vadsø.no +værøy.no +vaga.no +vågå.no +vagan.no +vågan.no +vagsoy.no +vågsøy.no +vaksdal.no +valle.no +vang.no +vanylven.no +vardo.no +vardø.no +varggat.no +várggát.no +varoy.no +vefsn.no +vega.no +vegarshei.no +vegårshei.no +vennesla.no +verdal.no +verran.no +vestby.no +sande.vestfold.no +vestnes.no +vestre-slidre.no +vestre-toten.no +vestvagoy.no +vestvågøy.no +vevelstad.no +vik.no +vikna.no +vindafjord.no +voagat.no +volda.no +voss.no + +// np : http://www.mos.com.np/register.html +*.np + +// nr : http://cenpac.net.nr/dns/index.html +// Submitted by registry +nr +biz.nr +com.nr +edu.nr +gov.nr +info.nr +net.nr +org.nr + +// nu : https://www.iana.org/domains/root/db/nu.html +nu + +// nz : https://www.iana.org/domains/root/db/nz.html +// Submitted by registry +nz +ac.nz +co.nz +cri.nz +geek.nz +gen.nz +govt.nz +health.nz +iwi.nz +kiwi.nz +maori.nz +māori.nz +mil.nz +net.nz +org.nz +parliament.nz +school.nz + +// om : https://www.iana.org/domains/root/db/om.html +om +co.om +com.om +edu.om +gov.om +med.om +museum.om +net.om +org.om +pro.om + +// onion : https://tools.ietf.org/html/rfc7686 +onion + +// org : https://www.iana.org/domains/root/db/org.html +org + +// pa : http://www.nic.pa/ +// Some additional second level "domains" resolve directly as hostnames, such as +// pannet.pa, so we add a rule for "pa". +pa +abo.pa +ac.pa +com.pa +edu.pa +gob.pa +ing.pa +med.pa +net.pa +nom.pa +org.pa +sld.pa + +// pe : https://www.nic.pe/InformeFinalComision.pdf +pe +com.pe +edu.pe +gob.pe +mil.pe +net.pe +nom.pe +org.pe + +// pf : http://www.gobin.info/domainname/formulaire-pf.pdf +pf +com.pf +edu.pf +org.pf + +// pg : https://www.iana.org/domains/root/db/pg.html +*.pg + +// ph : https://www.iana.org/domains/root/db/ph.html +// Submitted by registry +ph +com.ph +edu.ph +gov.ph +i.ph +mil.ph +net.ph +ngo.ph +org.ph + +// pk : https://pk5.pknic.net.pk/pk5/msgNamepk.PK +// Contact Email: staff@pknic.net.pk +pk +ac.pk +biz.pk +com.pk +edu.pk +fam.pk +gkp.pk +gob.pk +gog.pk +gok.pk +gop.pk +gos.pk +gov.pk +net.pk +org.pk +web.pk + +// pl : https://www.dns.pl/en/ +// Confirmed by registry 2024-11-18 +pl +com.pl +net.pl +org.pl +// pl functional domains : https://www.dns.pl/en/list_of_functional_domain_names +agro.pl +aid.pl +atm.pl +auto.pl +biz.pl +edu.pl +gmina.pl +gsm.pl +info.pl +mail.pl +media.pl +miasta.pl +mil.pl +nieruchomosci.pl +nom.pl +pc.pl +powiat.pl +priv.pl +realestate.pl +rel.pl +sex.pl +shop.pl +sklep.pl +sos.pl +szkola.pl +targi.pl +tm.pl +tourism.pl +travel.pl +turystyka.pl +// Government domains : https://www.dns.pl/informacje_o_rejestracji_domen_gov_pl +// In accordance with the .gov.pl Domain Name Regulations : https://www.dns.pl/regulamin_gov_pl +gov.pl +ap.gov.pl +griw.gov.pl +ic.gov.pl +is.gov.pl +kmpsp.gov.pl +konsulat.gov.pl +kppsp.gov.pl +kwp.gov.pl +kwpsp.gov.pl +mup.gov.pl +mw.gov.pl +oia.gov.pl +oirm.gov.pl +oke.gov.pl +oow.gov.pl +oschr.gov.pl +oum.gov.pl +pa.gov.pl +pinb.gov.pl +piw.gov.pl +po.gov.pl +pr.gov.pl +psp.gov.pl +psse.gov.pl +pup.gov.pl +rzgw.gov.pl +sa.gov.pl +sdn.gov.pl +sko.gov.pl +so.gov.pl +sr.gov.pl +starostwo.gov.pl +ug.gov.pl +ugim.gov.pl +um.gov.pl +umig.gov.pl +upow.gov.pl +uppo.gov.pl +us.gov.pl +uw.gov.pl +uzs.gov.pl +wif.gov.pl +wiih.gov.pl +winb.gov.pl +wios.gov.pl +witd.gov.pl +wiw.gov.pl +wkz.gov.pl +wsa.gov.pl +wskr.gov.pl +wsse.gov.pl +wuoz.gov.pl +wzmiuw.gov.pl +zp.gov.pl +zpisdn.gov.pl +// pl regional domains : https://www.dns.pl/en/list_of_regional_domain_names +augustow.pl +babia-gora.pl +bedzin.pl +beskidy.pl +bialowieza.pl +bialystok.pl +bielawa.pl +bieszczady.pl +boleslawiec.pl +bydgoszcz.pl +bytom.pl +cieszyn.pl +czeladz.pl +czest.pl +dlugoleka.pl +elblag.pl +elk.pl +glogow.pl +gniezno.pl +gorlice.pl +grajewo.pl +ilawa.pl +jaworzno.pl +jelenia-gora.pl +jgora.pl +kalisz.pl +karpacz.pl +kartuzy.pl +kaszuby.pl +katowice.pl +kazimierz-dolny.pl +kepno.pl +ketrzyn.pl +klodzko.pl +kobierzyce.pl +kolobrzeg.pl +konin.pl +konskowola.pl +kutno.pl +lapy.pl +lebork.pl +legnica.pl +lezajsk.pl +limanowa.pl +lomza.pl +lowicz.pl +lubin.pl +lukow.pl +malbork.pl +malopolska.pl +mazowsze.pl +mazury.pl +mielec.pl +mielno.pl +mragowo.pl +naklo.pl +nowaruda.pl +nysa.pl +olawa.pl +olecko.pl +olkusz.pl +olsztyn.pl +opoczno.pl +opole.pl +ostroda.pl +ostroleka.pl +ostrowiec.pl +ostrowwlkp.pl +pila.pl +pisz.pl +podhale.pl +podlasie.pl +polkowice.pl +pomorskie.pl +pomorze.pl +prochowice.pl +pruszkow.pl +przeworsk.pl +pulawy.pl +radom.pl +rawa-maz.pl +rybnik.pl +rzeszow.pl +sanok.pl +sejny.pl +skoczow.pl +slask.pl +slupsk.pl +sosnowiec.pl +stalowa-wola.pl +starachowice.pl +stargard.pl +suwalki.pl +swidnica.pl +swiebodzin.pl +swinoujscie.pl +szczecin.pl +szczytno.pl +tarnobrzeg.pl +tgory.pl +turek.pl +tychy.pl +ustka.pl +walbrzych.pl +warmia.pl +warszawa.pl +waw.pl +wegrow.pl +wielun.pl +wlocl.pl +wloclawek.pl +wodzislaw.pl +wolomin.pl +wroclaw.pl +zachpomor.pl +zagan.pl +zarow.pl +zgora.pl +zgorzelec.pl + +// pm : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +pm + +// pn : https://www.iana.org/domains/root/db/pn.html +pn +co.pn +edu.pn +gov.pn +net.pn +org.pn + +// post : https://www.iana.org/domains/root/db/post.html +post + +// pr : http://www.nic.pr/index.asp?f=1 +pr +biz.pr +com.pr +edu.pr +gov.pr +info.pr +isla.pr +name.pr +net.pr +org.pr +pro.pr +// these aren't mentioned on nic.pr, but on https://www.iana.org/domains/root/db/pr.html +ac.pr +est.pr +prof.pr + +// pro : http://registry.pro/get-pro +pro +aaa.pro +aca.pro +acct.pro +avocat.pro +bar.pro +cpa.pro +eng.pro +jur.pro +law.pro +med.pro +recht.pro + +// ps : https://www.iana.org/domains/root/db/ps.html +// http://www.nic.ps/registration/policy.html#reg +ps +com.ps +edu.ps +gov.ps +net.ps +org.ps +plo.ps +sec.ps + +// pt : https://www.dns.pt/en/domain/pt-terms-and-conditions-registration-rules/ +pt +com.pt +edu.pt +gov.pt +int.pt +net.pt +nome.pt +org.pt +publ.pt + +// pw : https://www.iana.org/domains/root/db/pw.html +// Confirmed by registry in private correspondence with @dnsguru 2024-12-09 +pw +gov.pw + +// py : https://www.iana.org/domains/root/db/py.html +// Submitted by registry +py +com.py +coop.py +edu.py +gov.py +mil.py +net.py +org.py + +// qa : http://domains.qa/en/ +qa +com.qa +edu.qa +gov.qa +mil.qa +name.qa +net.qa +org.qa +sch.qa + +// re : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +// Confirmed by registry 2024-11-18 +re +// Closed for registration on 2013-03-15 but domains are still maintained +asso.re +com.re + +// ro : http://www.rotld.ro/ +ro +arts.ro +com.ro +firm.ro +info.ro +nom.ro +nt.ro +org.ro +rec.ro +store.ro +tm.ro +www.ro + +// rs : https://www.rnids.rs/en/domains/national-domains +rs +ac.rs +co.rs +edu.rs +gov.rs +in.rs +org.rs + +// ru : https://cctld.ru/files/pdf/docs/en/rules_ru-rf.pdf +// Submitted by George Georgievsky +ru + +// rw : https://www.iana.org/domains/root/db/rw.html +rw +ac.rw +co.rw +coop.rw +gov.rw +mil.rw +net.rw +org.rw + +// sa : http://www.nic.net.sa/ +sa +com.sa +edu.sa +gov.sa +med.sa +net.sa +org.sa +pub.sa +sch.sa + +// sb : http://www.sbnic.net.sb/ +// Submitted by registry +sb +com.sb +edu.sb +gov.sb +net.sb +org.sb + +// sc : http://www.nic.sc/ +sc +com.sc +edu.sc +gov.sc +net.sc +org.sc + +// sd : https://www.iana.org/domains/root/db/sd.html +// Submitted by registry +sd +com.sd +edu.sd +gov.sd +info.sd +med.sd +net.sd +org.sd +tv.sd + +// se : https://www.iana.org/domains/root/db/se.html +// https://data.internetstiftelsen.se/barred_domains_list.txt -> Second level domains & Sub-domains +// Confirmed by Registry Services 2024-11-20 +se +a.se +ac.se +b.se +bd.se +brand.se +c.se +d.se +e.se +f.se +fh.se +fhsk.se +fhv.se +g.se +h.se +i.se +k.se +komforb.se +kommunalforbund.se +komvux.se +l.se +lanbib.se +m.se +n.se +naturbruksgymn.se +o.se +org.se +p.se +parti.se +pp.se +press.se +r.se +s.se +t.se +tm.se +u.se +w.se +x.se +y.se +z.se + +// sg : https://www.sgnic.sg/domain-registration/sg-categories-rules +// Confirmed by registry 2024-11-19 +sg +com.sg +edu.sg +gov.sg +net.sg +org.sg + +// sh : http://nic.sh/rules.htm +sh +com.sh +gov.sh +mil.sh +net.sh +org.sh + +// si : https://www.iana.org/domains/root/db/si.html +si + +// sj : No registrations at this time. +// Submitted by registry +sj + +// sk : https://www.iana.org/domains/root/db/sk.html +// https://sk-nic.sk/ +sk +org.sk + +// sl : http://www.nic.sl +// Submitted by registry +sl +com.sl +edu.sl +gov.sl +net.sl +org.sl + +// sm : https://www.iana.org/domains/root/db/sm.html +sm + +// sn : https://www.iana.org/domains/root/db/sn.html +sn +art.sn +com.sn +edu.sn +gouv.sn +org.sn +univ.sn + +// so : http://sonic.so/policies/ +so +com.so +edu.so +gov.so +me.so +net.so +org.so + +// sr : https://www.iana.org/domains/root/db/sr.html +sr + +// ss : https://registry.nic.ss/ +// Submitted by registry +ss +biz.ss +co.ss +com.ss +edu.ss +gov.ss +me.ss +net.ss +org.ss +sch.ss + +// st : http://www.nic.st/html/policyrules/ +st +co.st +com.st +consulado.st +edu.st +embaixada.st +mil.st +net.st +org.st +principe.st +saotome.st +store.st + +// su : https://www.iana.org/domains/root/db/su.html +su + +// sv : https://www.iana.org/domains/root/db/sv.html +sv +com.sv +edu.sv +gob.sv +org.sv +red.sv + +// sx : https://www.iana.org/domains/root/db/sx.html +// Submitted by registry +sx +gov.sx + +// sy : https://www.iana.org/domains/root/db/sy.html +sy +com.sy +edu.sy +gov.sy +mil.sy +net.sy +org.sy + +// sz : https://www.iana.org/domains/root/db/sz.html +// http://www.sispa.org.sz/ +sz +ac.sz +co.sz +org.sz + +// tc : https://www.iana.org/domains/root/db/tc.html +tc + +// td : https://www.iana.org/domains/root/db/td.html +td + +// tel : https://www.iana.org/domains/root/db/tel.html +// http://www.telnic.org/ +tel + +// tf : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +tf + +// tg : https://www.iana.org/domains/root/db/tg.html +// http://www.nic.tg/ +tg + +// th : https://www.iana.org/domains/root/db/th.html +// Submitted by registry +th +ac.th +co.th +go.th +in.th +mi.th +net.th +or.th + +// tj : http://www.nic.tj/policy.html +tj +ac.tj +biz.tj +co.tj +com.tj +edu.tj +go.tj +gov.tj +int.tj +mil.tj +name.tj +net.tj +nic.tj +org.tj +test.tj +web.tj + +// tk : https://www.iana.org/domains/root/db/tk.html +tk + +// tl : https://www.iana.org/domains/root/db/tl.html +tl +gov.tl + +// tm : https://www.nic.tm/local.html +// Confirmed by registry 2024-11-19 +tm +co.tm +com.tm +edu.tm +gov.tm +mil.tm +net.tm +nom.tm +org.tm + +// tn : http://www.registre.tn/fr/ +// https://whois.ati.tn/ +tn +com.tn +ens.tn +fin.tn +gov.tn +ind.tn +info.tn +intl.tn +mincom.tn +nat.tn +net.tn +org.tn +perso.tn +tourism.tn + +// to : https://www.iana.org/domains/root/db/to.html +// Submitted by registry +to +com.to +edu.to +gov.to +mil.to +net.to +org.to + +// tr : https://nic.tr/ +// https://nic.tr/forms/eng/policies.pdf +// https://nic.tr/index.php?USRACTN=PRICELST +tr +av.tr +bbs.tr +bel.tr +biz.tr +com.tr +dr.tr +edu.tr +gen.tr +gov.tr +info.tr +k12.tr +kep.tr +mil.tr +name.tr +net.tr +org.tr +pol.tr +tel.tr +tsk.tr +tv.tr +web.tr +// Used by Northern Cyprus +nc.tr +// Used by government agencies of Northern Cyprus +gov.nc.tr + +// tt : https://www.nic.tt/ +// Confirmed by registry 2024-11-19 +tt +biz.tt +co.tt +com.tt +edu.tt +gov.tt +info.tt +mil.tt +name.tt +net.tt +org.tt +pro.tt + +// tv : https://www.iana.org/domains/root/db/tv.html +// Not listing any 2LDs as reserved since none seem to exist in practice, +// Wikipedia notwithstanding. +tv + +// tw : https://www.iana.org/domains/root/db/tw.html +// https://twnic.tw/dnservice_catag.php +// Confirmed by registry 2024-11-26 +tw +club.tw +com.tw +ebiz.tw +edu.tw +game.tw +gov.tw +idv.tw +mil.tw +net.tw +org.tw + +// tz : http://www.tznic.or.tz/index.php/domains +// Submitted by registry +tz +ac.tz +co.tz +go.tz +hotel.tz +info.tz +me.tz +mil.tz +mobi.tz +ne.tz +or.tz +sc.tz +tv.tz + +// ua : https://hostmaster.ua/policy/?ua +// Submitted by registry +ua +// ua 2LD +com.ua +edu.ua +gov.ua +in.ua +net.ua +org.ua +// ua geographic names +// https://hostmaster.ua/2ld/ +cherkassy.ua +cherkasy.ua +chernigov.ua +chernihiv.ua +chernivtsi.ua +chernovtsy.ua +ck.ua +cn.ua +cr.ua +crimea.ua +cv.ua +dn.ua +dnepropetrovsk.ua +dnipropetrovsk.ua +donetsk.ua +dp.ua +if.ua +ivano-frankivsk.ua +kh.ua +kharkiv.ua +kharkov.ua +kherson.ua +khmelnitskiy.ua +khmelnytskyi.ua +kiev.ua +kirovograd.ua +km.ua +kr.ua +kropyvnytskyi.ua +krym.ua +ks.ua +kv.ua +kyiv.ua +lg.ua +lt.ua +lugansk.ua +luhansk.ua +lutsk.ua +lv.ua +lviv.ua +mk.ua +mykolaiv.ua +nikolaev.ua +od.ua +odesa.ua +odessa.ua +pl.ua +poltava.ua +rivne.ua +rovno.ua +rv.ua +sb.ua +sebastopol.ua +sevastopol.ua +sm.ua +sumy.ua +te.ua +ternopil.ua +uz.ua +uzhgorod.ua +uzhhorod.ua +vinnica.ua +vinnytsia.ua +vn.ua +volyn.ua +yalta.ua +zakarpattia.ua +zaporizhzhe.ua +zaporizhzhia.ua +zhitomir.ua +zhytomyr.ua +zp.ua +zt.ua + +// ug : https://www.registry.co.ug/ +// https://www.registry.co.ug, https://whois.co.ug +// Confirmed by registry 2025-01-20 +ug +ac.ug +co.ug +com.ug +edu.ug +go.ug +gov.ug +mil.ug +ne.ug +or.ug +org.ug +sc.ug +us.ug + +// uk : https://www.iana.org/domains/root/db/uk.html +// Submitted by registry +uk +ac.uk +co.uk +gov.uk +ltd.uk +me.uk +net.uk +nhs.uk +org.uk +plc.uk +police.uk +*.sch.uk + +// us : https://www.iana.org/domains/root/db/us.html +// Confirmed via the .us zone file by William Harrison 2024-12-10 +us +dni.us +isa.us +nsn.us +// Geographic Names +ak.us +al.us +ar.us +as.us +az.us +ca.us +co.us +ct.us +dc.us +de.us +fl.us +ga.us +gu.us +hi.us +ia.us +id.us +il.us +in.us +ks.us +ky.us +la.us +ma.us +md.us +me.us +mi.us +mn.us +mo.us +ms.us +mt.us +nc.us +nd.us +ne.us +nh.us +nj.us +nm.us +nv.us +ny.us +oh.us +ok.us +or.us +pa.us +pr.us +ri.us +sc.us +sd.us +tn.us +tx.us +ut.us +va.us +vi.us +vt.us +wa.us +wi.us +wv.us +wy.us +// The registrar notes several more specific domains available in each state, +// such as state.*.us, dst.*.us, etc., but resolution of these is somewhat +// haphazard; in some states these domains resolve as addresses, while in others +// only subdomains are available, or even nothing at all. We include the +// most common ones where it's clear that different sites are different +// entities. +k12.ak.us +k12.al.us +k12.ar.us +k12.as.us +k12.az.us +k12.ca.us +k12.co.us +k12.ct.us +k12.dc.us +k12.fl.us +k12.ga.us +k12.gu.us +// k12.hi.us - Bug 614565 - Hawaii has a state-wide DOE login +k12.ia.us +k12.id.us +k12.il.us +k12.in.us +k12.ks.us +k12.ky.us +k12.la.us +k12.ma.us +k12.md.us +k12.me.us +k12.mi.us +k12.mn.us +k12.mo.us +k12.ms.us +k12.mt.us +k12.nc.us +// k12.nd.us - Bug 1028347 - Removed at request of Travis Rosso +k12.ne.us +k12.nh.us +k12.nj.us +k12.nm.us +k12.nv.us +k12.ny.us +k12.oh.us +k12.ok.us +k12.or.us +k12.pa.us +k12.pr.us +// k12.ri.us - Removed at request of Kim Cournoyer +k12.sc.us +// k12.sd.us - Bug 934131 - Removed at request of James Booze +k12.tn.us +k12.tx.us +k12.ut.us +k12.va.us +k12.vi.us +k12.vt.us +k12.wa.us +k12.wi.us +// k12.wv.us - Bug 947705 - Removed at request of Verne Britton +cc.ak.us +lib.ak.us +cc.al.us +lib.al.us +cc.ar.us +lib.ar.us +cc.as.us +lib.as.us +cc.az.us +lib.az.us +cc.ca.us +lib.ca.us +cc.co.us +lib.co.us +cc.ct.us +lib.ct.us +cc.dc.us +lib.dc.us +cc.de.us +cc.fl.us +lib.fl.us +cc.ga.us +lib.ga.us +cc.gu.us +lib.gu.us +cc.hi.us +lib.hi.us +cc.ia.us +lib.ia.us +cc.id.us +lib.id.us +cc.il.us +lib.il.us +cc.in.us +lib.in.us +cc.ks.us +lib.ks.us +cc.ky.us +lib.ky.us +cc.la.us +lib.la.us +cc.ma.us +lib.ma.us +cc.md.us +lib.md.us +cc.me.us +lib.me.us +cc.mi.us +lib.mi.us +cc.mn.us +lib.mn.us +cc.mo.us +lib.mo.us +cc.ms.us +cc.mt.us +lib.mt.us +cc.nc.us +lib.nc.us +cc.nd.us +lib.nd.us +cc.ne.us +lib.ne.us +cc.nh.us +lib.nh.us +cc.nj.us +lib.nj.us +cc.nm.us +lib.nm.us +cc.nv.us +lib.nv.us +cc.ny.us +lib.ny.us +cc.oh.us +lib.oh.us +cc.ok.us +lib.ok.us +cc.or.us +lib.or.us +cc.pa.us +lib.pa.us +cc.pr.us +lib.pr.us +cc.ri.us +lib.ri.us +cc.sc.us +lib.sc.us +cc.sd.us +lib.sd.us +cc.tn.us +lib.tn.us +cc.tx.us +lib.tx.us +cc.ut.us +lib.ut.us +cc.va.us +lib.va.us +cc.vi.us +lib.vi.us +cc.vt.us +lib.vt.us +cc.wa.us +lib.wa.us +cc.wi.us +lib.wi.us +cc.wv.us +cc.wy.us +k12.wy.us +// lib.wv.us - Bug 941670 - Removed at request of Larry W Arnold +lib.wy.us +// k12.ma.us contains school districts in Massachusetts. The 4LDs are +// managed independently except for private (PVT), charter (CHTR) and +// parochial (PAROCH) schools. Those are delegated directly to the +// 5LD operators. +chtr.k12.ma.us +paroch.k12.ma.us +pvt.k12.ma.us +// Merit Network, Inc. maintains the registry for =~ /(k12|cc|lib).mi.us/ and the following +// see also: https://domreg.merit.edu : domreg@merit.edu +// see also: whois -h whois.domreg.merit.edu help +ann-arbor.mi.us +cog.mi.us +dst.mi.us +eaton.mi.us +gen.mi.us +mus.mi.us +tec.mi.us +washtenaw.mi.us + +// uy : http://www.nic.org.uy/ +uy +com.uy +edu.uy +gub.uy +mil.uy +net.uy +org.uy + +// uz : http://www.reg.uz/ +uz +co.uz +com.uz +net.uz +org.uz + +// va : https://www.iana.org/domains/root/db/va.html +va + +// vc : https://www.iana.org/domains/root/db/vc.html +// Submitted by registry +vc +com.vc +edu.vc +gov.vc +mil.vc +net.vc +org.vc + +// ve : https://registro.nic.ve/ +// https://nic.ve/site/user-agreement -> under "III. Clasificación de Nombres de Dominio" +// Submitted by registry nic@nic.ve and nicve@conatel.gob.ve +ve +arts.ve +bib.ve +co.ve +com.ve +e12.ve +edu.ve +emprende.ve +firm.ve +gob.ve +gov.ve +ia.ve +info.ve +int.ve +mil.ve +net.ve +nom.ve +org.ve +rar.ve +rec.ve +store.ve +tec.ve +web.ve + +// vg : https://www.iana.org/domains/root/db/vg.html +// Confirmed by registry 2025-01-10 +vg +edu.vg + +// vi : https://www.iana.org/domains/root/db/vi.html +vi +co.vi +com.vi +k12.vi +net.vi +org.vi + +// vn : https://www.vnnic.vn/en/domain/cctld-vn +// https://vnnic.vn/sites/default/files/tailieu/vn.cctld.domains.txt +vn +ac.vn +ai.vn +biz.vn +com.vn +edu.vn +gov.vn +health.vn +id.vn +info.vn +int.vn +io.vn +name.vn +net.vn +org.vn +pro.vn + +// vn geographical names +angiang.vn +bacgiang.vn +backan.vn +baclieu.vn +bacninh.vn +baria-vungtau.vn +bentre.vn +binhdinh.vn +binhduong.vn +binhphuoc.vn +binhthuan.vn +camau.vn +cantho.vn +caobang.vn +daklak.vn +daknong.vn +danang.vn +dienbien.vn +dongnai.vn +dongthap.vn +gialai.vn +hagiang.vn +haiduong.vn +haiphong.vn +hanam.vn +hanoi.vn +hatinh.vn +haugiang.vn +hoabinh.vn +hungyen.vn +khanhhoa.vn +kiengiang.vn +kontum.vn +laichau.vn +lamdong.vn +langson.vn +laocai.vn +longan.vn +namdinh.vn +nghean.vn +ninhbinh.vn +ninhthuan.vn +phutho.vn +phuyen.vn +quangbinh.vn +quangnam.vn +quangngai.vn +quangninh.vn +quangtri.vn +soctrang.vn +sonla.vn +tayninh.vn +thaibinh.vn +thainguyen.vn +thanhhoa.vn +thanhphohochiminh.vn +thuathienhue.vn +tiengiang.vn +travinh.vn +tuyenquang.vn +vinhlong.vn +vinhphuc.vn +yenbai.vn + +// vu : https://www.iana.org/domains/root/db/vu.html +// http://www.vunic.vu/ +vu +com.vu +edu.vu +net.vu +org.vu + +// wf : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +wf + +// ws : https://www.iana.org/domains/root/db/ws.html +// http://samoanic.ws/index.dhtml +ws +com.ws +edu.ws +gov.ws +net.ws +org.ws + +// yt : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +yt + +// IDN ccTLDs +// When submitting patches, please maintain a sort by ISO 3166 ccTLD, then +// U-label, and follow this format: +// // A-Label ("", [, variant info]) : +// // [sponsoring org] +// U-Label + +// xn--mgbaam7a8h ("Emerat", Arabic) : AE +// http://nic.ae/english/arabicdomain/rules.jsp +امارات + +// xn--y9a3aq ("hye", Armenian) : AM +// ISOC AM (operated by .am Registry) +հայ + +// xn--54b7fta0cc ("Bangla", Bangla) : BD +বাংলা + +// xn--90ae ("bg", Bulgarian) : BG +бг + +// xn--mgbcpq6gpa1a ("albahrain", Arabic) : BH +البحرين + +// xn--90ais ("bel", Belarusian/Russian Cyrillic) : BY +// Operated by .by registry +бел + +// xn--fiqs8s ("Zhongguo/China", Chinese, Simplified) : CN +// CNNIC +// https://www.cnnic.cn/11/192/index.html +中国 + +// xn--fiqz9s ("Zhongguo/China", Chinese, Traditional) : CN +// CNNIC +// https://www.cnnic.com.cn/AU/MediaC/Announcement/201609/t20160905_54470.htm +中國 + +// xn--lgbbat1ad8j ("Algeria/Al Jazair", Arabic) : DZ +الجزائر + +// xn--wgbh1c ("Egypt/Masr", Arabic) : EG +// http://www.dotmasr.eg/ +مصر + +// xn--e1a4c ("eu", Cyrillic) : EU +// https://eurid.eu +ею + +// xn--qxa6a ("eu", Greek) : EU +// https://eurid.eu +ευ + +// xn--mgbah1a3hjkrd ("Mauritania", Arabic) : MR +موريتانيا + +// xn--node ("ge", Georgian Mkhedruli) : GE +გე + +// xn--qxam ("el", Greek) : GR +// Hellenic Ministry of Infrastructure, Transport, and Networks +ελ + +// xn--j6w193g ("Hong Kong", Chinese) : HK +// https://www.hkirc.hk +// Submitted by registry +// https://www.hkirc.hk/content.jsp?id=30#!/34 +香港 +個人.香港 +公司.香港 +政府.香港 +教育.香港 +組織.香港 +網絡.香港 + +// xn--2scrj9c ("Bharat", Kannada) : IN +// India +ಭಾರತ + +// xn--3hcrj9c ("Bharat", Oriya) : IN +// India +ଭାରତ + +// xn--45br5cyl ("Bharatam", Assamese) : IN +// India +ভাৰত + +// xn--h2breg3eve ("Bharatam", Sanskrit) : IN +// India +भारतम् + +// xn--h2brj9c8c ("Bharot", Santali) : IN +// India +भारोत + +// xn--mgbgu82a ("Bharat", Sindhi) : IN +// India +ڀارت + +// xn--rvc1e0am3e ("Bharatam", Malayalam) : IN +// India +ഭാരതം + +// xn--h2brj9c ("Bharat", Devanagari) : IN +// India +भारत + +// xn--mgbbh1a ("Bharat", Kashmiri) : IN +// India +بارت + +// xn--mgbbh1a71e ("Bharat", Arabic) : IN +// India +بھارت + +// xn--fpcrj9c3d ("Bharat", Telugu) : IN +// India +భారత్ + +// xn--gecrj9c ("Bharat", Gujarati) : IN +// India +ભારત + +// xn--s9brj9c ("Bharat", Gurmukhi) : IN +// India +ਭਾਰਤ + +// xn--45brj9c ("Bharat", Bengali) : IN +// India +ভারত + +// xn--xkc2dl3a5ee0h ("India", Tamil) : IN +// India +இந்தியா + +// xn--mgba3a4f16a ("Iran", Persian) : IR +ایران + +// xn--mgba3a4fra ("Iran", Arabic) : IR +ايران + +// xn--mgbtx2b ("Iraq", Arabic) : IQ +// Communications and Media Commission +عراق + +// xn--mgbayh7gpa ("al-Ordon", Arabic) : JO +// National Information Technology Center (NITC) +// Royal Scientific Society, Al-Jubeiha +الاردن + +// xn--3e0b707e ("Republic of Korea", Hangul) : KR +한국 + +// xn--80ao21a ("Kaz", Kazakh) : KZ +қаз + +// xn--q7ce6a ("Lao", Lao) : LA +ລາວ + +// xn--fzc2c9e2c ("Lanka", Sinhalese-Sinhala) : LK +// https://nic.lk +ලංකා + +// xn--xkc2al3hye2a ("Ilangai", Tamil) : LK +// https://nic.lk +இலங்கை + +// xn--mgbc0a9azcg ("Morocco/al-Maghrib", Arabic) : MA +المغرب + +// xn--d1alf ("mkd", Macedonian) : MK +// MARnet +мкд + +// xn--l1acc ("mon", Mongolian) : MN +мон + +// xn--mix891f ("Macao", Chinese, Traditional) : MO +// MONIC / HNET Asia (Registry Operator for .mo) +澳門 + +// xn--mix082f ("Macao", Chinese, Simplified) : MO +澳门 + +// xn--mgbx4cd0ab ("Malaysia", Malay) : MY +مليسيا + +// xn--mgb9awbf ("Oman", Arabic) : OM +عمان + +// xn--mgbai9azgqp6j ("Pakistan", Urdu/Arabic) : PK +پاکستان + +// xn--mgbai9a5eva00b ("Pakistan", Urdu/Arabic, variant) : PK +پاكستان + +// xn--ygbi2ammx ("Falasteen", Arabic) : PS +// The Palestinian National Internet Naming Authority (PNINA) +// http://www.pnina.ps +فلسطين + +// xn--90a3ac ("srb", Cyrillic) : RS +// https://www.rnids.rs/en/domains/national-domains +срб +ак.срб +обр.срб +од.срб +орг.срб +пр.срб +упр.срб + +// xn--p1ai ("rf", Russian-Cyrillic) : RU +// https://cctld.ru/files/pdf/docs/en/rules_ru-rf.pdf +// Submitted by George Georgievsky +рф + +// xn--wgbl6a ("Qatar", Arabic) : QA +// http://www.ict.gov.qa/ +قطر + +// xn--mgberp4a5d4ar ("AlSaudiah", Arabic) : SA +// http://www.nic.net.sa/ +السعودية + +// xn--mgberp4a5d4a87g ("AlSaudiah", Arabic, variant): SA +السعودیة + +// xn--mgbqly7c0a67fbc ("AlSaudiah", Arabic, variant) : SA +السعودیۃ + +// xn--mgbqly7cvafr ("AlSaudiah", Arabic, variant) : SA +السعوديه + +// xn--mgbpl2fh ("sudan", Arabic) : SD +// Operated by .sd registry +سودان + +// xn--yfro4i67o Singapore ("Singapore", Chinese) : SG +新加坡 + +// xn--clchc0ea0b2g2a9gcd ("Singapore", Tamil) : SG +சிங்கப்பூர் + +// xn--ogbpf8fl ("Syria", Arabic) : SY +سورية + +// xn--mgbtf8fl ("Syria", Arabic, variant) : SY +سوريا + +// xn--o3cw4h ("Thai", Thai) : TH +// http://www.thnic.co.th +ไทย +ทหาร.ไทย +ธุรกิจ.ไทย +เน็ต.ไทย +รัฐบาล.ไทย +ศึกษา.ไทย +องค์กร.ไทย + +// xn--pgbs0dh ("Tunisia", Arabic) : TN +// http://nic.tn +تونس + +// xn--kpry57d ("Taiwan", Chinese, Traditional) : TW +// https://twnic.tw/dnservice_catag.php +台灣 + +// xn--kprw13d ("Taiwan", Chinese, Simplified) : TW +// http://www.twnic.net/english/dn/dn_07a.htm +台湾 + +// xn--nnx388a ("Taiwan", Chinese, variant) : TW +臺灣 + +// xn--j1amh ("ukr", Cyrillic) : UA +укр + +// xn--mgb2ddes ("AlYemen", Arabic) : YE +اليمن + +// xxx : http://icmregistry.com +xxx + +// ye : http://www.y.net.ye/services/domain_name.htm +ye +com.ye +edu.ye +gov.ye +mil.ye +net.ye +org.ye + +// za : https://www.iana.org/domains/root/db/za.html +ac.za +agric.za +alt.za +co.za +edu.za +gov.za +grondar.za +law.za +mil.za +net.za +ngo.za +nic.za +nis.za +nom.za +org.za +school.za +tm.za +web.za + +// zm : https://zicta.zm/ +// Submitted by registry +zm +ac.zm +biz.zm +co.zm +com.zm +edu.zm +gov.zm +info.zm +mil.zm +net.zm +org.zm +sch.zm + +// zw : https://www.potraz.gov.zw/ +// Confirmed by registry 2017-01-25 +zw +ac.zw +co.zw +gov.zw +mil.zw +org.zw + +// newGTLDs + +// List of new gTLDs imported from https://www.icann.org/resources/registries/gtlds/v2/gtlds.json on 2026-01-29T15:35:06Z +// This list is auto-generated, don't edit it manually. +// aaa : American Automobile Association, Inc. +// https://www.iana.org/domains/root/db/aaa.html +aaa + +// aarp : AARP +// https://www.iana.org/domains/root/db/aarp.html +aarp + +// abb : ABB Ltd +// https://www.iana.org/domains/root/db/abb.html +abb + +// abbott : Abbott Laboratories, Inc. +// https://www.iana.org/domains/root/db/abbott.html +abbott + +// abbvie : AbbVie Inc. +// https://www.iana.org/domains/root/db/abbvie.html +abbvie + +// abc : Disney Enterprises, Inc. +// https://www.iana.org/domains/root/db/abc.html +abc + +// able : Able Inc. +// https://www.iana.org/domains/root/db/able.html +able + +// abogado : Registry Services, LLC +// https://www.iana.org/domains/root/db/abogado.html +abogado + +// abudhabi : Abu Dhabi Systems and Information Centre +// https://www.iana.org/domains/root/db/abudhabi.html +abudhabi + +// academy : Binky Moon, LLC +// https://www.iana.org/domains/root/db/academy.html +academy + +// accenture : Accenture plc +// https://www.iana.org/domains/root/db/accenture.html +accenture + +// accountant : dot Accountant Limited +// https://www.iana.org/domains/root/db/accountant.html +accountant + +// accountants : Binky Moon, LLC +// https://www.iana.org/domains/root/db/accountants.html +accountants + +// aco : ACO Severin Ahlmann GmbH & Co. KG +// https://www.iana.org/domains/root/db/aco.html +aco + +// actor : Dog Beach, LLC +// https://www.iana.org/domains/root/db/actor.html +actor + +// ads : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/ads.html +ads + +// adult : ICM Registry AD LLC +// https://www.iana.org/domains/root/db/adult.html +adult + +// aeg : Aktiebolaget Electrolux +// https://www.iana.org/domains/root/db/aeg.html +aeg + +// aetna : Aetna Life Insurance Company +// https://www.iana.org/domains/root/db/aetna.html +aetna + +// afl : Australian Football League +// https://www.iana.org/domains/root/db/afl.html +afl + +// africa : ZA Central Registry NPC trading as Registry.Africa +// https://www.iana.org/domains/root/db/africa.html +africa + +// agakhan : Fondation Aga Khan (Aga Khan Foundation) +// https://www.iana.org/domains/root/db/agakhan.html +agakhan + +// agency : Binky Moon, LLC +// https://www.iana.org/domains/root/db/agency.html +agency + +// aig : American International Group, Inc. +// https://www.iana.org/domains/root/db/aig.html +aig + +// airbus : Airbus S.A.S. +// https://www.iana.org/domains/root/db/airbus.html +airbus + +// airforce : Dog Beach, LLC +// https://www.iana.org/domains/root/db/airforce.html +airforce + +// airtel : Bharti Airtel Limited +// https://www.iana.org/domains/root/db/airtel.html +airtel + +// akdn : Fondation Aga Khan (Aga Khan Foundation) +// https://www.iana.org/domains/root/db/akdn.html +akdn + +// alibaba : Alibaba Group Holding Limited +// https://www.iana.org/domains/root/db/alibaba.html +alibaba + +// alipay : Alibaba Group Holding Limited +// https://www.iana.org/domains/root/db/alipay.html +alipay + +// allfinanz : Allfinanz Deutsche Vermögensberatung Aktiengesellschaft +// https://www.iana.org/domains/root/db/allfinanz.html +allfinanz + +// allstate : Allstate Fire and Casualty Insurance Company +// https://www.iana.org/domains/root/db/allstate.html +allstate + +// ally : Ally Financial Inc. +// https://www.iana.org/domains/root/db/ally.html +ally + +// alsace : Region Grand Est +// https://www.iana.org/domains/root/db/alsace.html +alsace + +// alstom : ALSTOM +// https://www.iana.org/domains/root/db/alstom.html +alstom + +// amazon : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/amazon.html +amazon + +// americanexpress : American Express Travel Related Services Company, Inc. +// https://www.iana.org/domains/root/db/americanexpress.html +americanexpress + +// americanfamily : AmFam, Inc. +// https://www.iana.org/domains/root/db/americanfamily.html +americanfamily + +// amex : American Express Travel Related Services Company, Inc. +// https://www.iana.org/domains/root/db/amex.html +amex + +// amfam : AmFam, Inc. +// https://www.iana.org/domains/root/db/amfam.html +amfam + +// amica : Amica Mutual Insurance Company +// https://www.iana.org/domains/root/db/amica.html +amica + +// amsterdam : Gemeente Amsterdam +// https://www.iana.org/domains/root/db/amsterdam.html +amsterdam + +// analytics : Campus IP LLC +// https://www.iana.org/domains/root/db/analytics.html +analytics + +// android : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/android.html +android + +// anquan : Beijing Qihu Keji Co., Ltd. +// https://www.iana.org/domains/root/db/anquan.html +anquan + +// anz : Australia and New Zealand Banking Group Limited +// https://www.iana.org/domains/root/db/anz.html +anz + +// aol : Yahoo Inc. +// https://www.iana.org/domains/root/db/aol.html +aol + +// apartments : Binky Moon, LLC +// https://www.iana.org/domains/root/db/apartments.html +apartments + +// app : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/app.html +app + +// apple : Apple Inc. +// https://www.iana.org/domains/root/db/apple.html +apple + +// aquarelle : Aquarelle.com +// https://www.iana.org/domains/root/db/aquarelle.html +aquarelle + +// arab : League of Arab States +// https://www.iana.org/domains/root/db/arab.html +arab + +// aramco : Aramco Services Company +// https://www.iana.org/domains/root/db/aramco.html +aramco + +// archi : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/archi.html +archi + +// army : Dog Beach, LLC +// https://www.iana.org/domains/root/db/army.html +army + +// art : UK Creative Ideas Limited +// https://www.iana.org/domains/root/db/art.html +art + +// arte : Association Relative à la Télévision Européenne G.E.I.E. +// https://www.iana.org/domains/root/db/arte.html +arte + +// asda : Asda Stores Limited +// https://www.iana.org/domains/root/db/asda.html +asda + +// associates : Binky Moon, LLC +// https://www.iana.org/domains/root/db/associates.html +associates + +// athleta : The Gap, Inc. +// https://www.iana.org/domains/root/db/athleta.html +athleta + +// attorney : Dog Beach, LLC +// https://www.iana.org/domains/root/db/attorney.html +attorney + +// auction : Dog Beach, LLC +// https://www.iana.org/domains/root/db/auction.html +auction + +// audi : AUDI Aktiengesellschaft +// https://www.iana.org/domains/root/db/audi.html +audi + +// audible : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/audible.html +audible + +// audio : XYZ.COM LLC +// https://www.iana.org/domains/root/db/audio.html +audio + +// auspost : Australian Postal Corporation +// https://www.iana.org/domains/root/db/auspost.html +auspost + +// author : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/author.html +author + +// auto : XYZ.COM LLC +// https://www.iana.org/domains/root/db/auto.html +auto + +// autos : XYZ.COM LLC +// https://www.iana.org/domains/root/db/autos.html +autos + +// aws : AWS Registry LLC +// https://www.iana.org/domains/root/db/aws.html +aws + +// axa : AXA Group Operations SAS +// https://www.iana.org/domains/root/db/axa.html +axa + +// azure : Microsoft Corporation +// https://www.iana.org/domains/root/db/azure.html +azure + +// baby : XYZ.COM LLC +// https://www.iana.org/domains/root/db/baby.html +baby + +// baidu : Baidu, Inc. +// https://www.iana.org/domains/root/db/baidu.html +baidu + +// banamex : Citigroup Inc. +// https://www.iana.org/domains/root/db/banamex.html +banamex + +// band : Dog Beach, LLC +// https://www.iana.org/domains/root/db/band.html +band + +// bank : fTLD Registry Services LLC +// https://www.iana.org/domains/root/db/bank.html +bank + +// bar : Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable +// https://www.iana.org/domains/root/db/bar.html +bar + +// barcelona : Municipi de Barcelona +// https://www.iana.org/domains/root/db/barcelona.html +barcelona + +// barclaycard : Barclays Bank PLC +// https://www.iana.org/domains/root/db/barclaycard.html +barclaycard + +// barclays : Barclays Bank PLC +// https://www.iana.org/domains/root/db/barclays.html +barclays + +// barefoot : Gallo Vineyards, Inc. +// https://www.iana.org/domains/root/db/barefoot.html +barefoot + +// bargains : Binky Moon, LLC +// https://www.iana.org/domains/root/db/bargains.html +bargains + +// baseball : MLB Advanced Media DH, LLC +// https://www.iana.org/domains/root/db/baseball.html +baseball + +// basketball : Fédération Internationale de Basketball (FIBA) +// https://www.iana.org/domains/root/db/basketball.html +basketball + +// bauhaus : Werkhaus GmbH +// https://www.iana.org/domains/root/db/bauhaus.html +bauhaus + +// bayern : Bayern Connect GmbH +// https://www.iana.org/domains/root/db/bayern.html +bayern + +// bbc : British Broadcasting Corporation +// https://www.iana.org/domains/root/db/bbc.html +bbc + +// bbt : BB&T Corporation +// https://www.iana.org/domains/root/db/bbt.html +bbt + +// bbva : BANCO BILBAO VIZCAYA ARGENTARIA, S.A. +// https://www.iana.org/domains/root/db/bbva.html +bbva + +// bcg : The Boston Consulting Group, Inc. +// https://www.iana.org/domains/root/db/bcg.html +bcg + +// bcn : Municipi de Barcelona +// https://www.iana.org/domains/root/db/bcn.html +bcn + +// beats : Beats Electronics, LLC +// https://www.iana.org/domains/root/db/beats.html +beats + +// beauty : XYZ.COM LLC +// https://www.iana.org/domains/root/db/beauty.html +beauty + +// beer : Registry Services, LLC +// https://www.iana.org/domains/root/db/beer.html +beer + +// berlin : dotBERLIN GmbH & Co. KG +// https://www.iana.org/domains/root/db/berlin.html +berlin + +// best : BestTLD Pty Ltd +// https://www.iana.org/domains/root/db/best.html +best + +// bestbuy : BBY Solutions, Inc. +// https://www.iana.org/domains/root/db/bestbuy.html +bestbuy + +// bet : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/bet.html +bet + +// bharti : Bharti Enterprises (Holding) Private Limited +// https://www.iana.org/domains/root/db/bharti.html +bharti + +// bible : American Bible Society +// https://www.iana.org/domains/root/db/bible.html +bible + +// bid : dot Bid Limited +// https://www.iana.org/domains/root/db/bid.html +bid + +// bike : Binky Moon, LLC +// https://www.iana.org/domains/root/db/bike.html +bike + +// bing : Microsoft Corporation +// https://www.iana.org/domains/root/db/bing.html +bing + +// bingo : Binky Moon, LLC +// https://www.iana.org/domains/root/db/bingo.html +bingo + +// bio : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/bio.html +bio + +// black : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/black.html +black + +// blackfriday : Registry Services, LLC +// https://www.iana.org/domains/root/db/blackfriday.html +blackfriday + +// blockbuster : Dish DBS Corporation +// https://www.iana.org/domains/root/db/blockbuster.html +blockbuster + +// blog : Knock Knock WHOIS There, LLC +// https://www.iana.org/domains/root/db/blog.html +blog + +// bloomberg : Bloomberg IP Holdings LLC +// https://www.iana.org/domains/root/db/bloomberg.html +bloomberg + +// blue : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/blue.html +blue + +// bms : Bristol-Myers Squibb Company +// https://www.iana.org/domains/root/db/bms.html +bms + +// bmw : Bayerische Motoren Werke Aktiengesellschaft +// https://www.iana.org/domains/root/db/bmw.html +bmw + +// bnpparibas : BNP Paribas +// https://www.iana.org/domains/root/db/bnpparibas.html +bnpparibas + +// boats : XYZ.COM LLC +// https://www.iana.org/domains/root/db/boats.html +boats + +// boehringer : Boehringer Ingelheim International GmbH +// https://www.iana.org/domains/root/db/boehringer.html +boehringer + +// bofa : Bank of America Corporation +// https://www.iana.org/domains/root/db/bofa.html +bofa + +// bom : Núcleo de Informação e Coordenação do Ponto BR - NIC.br +// https://www.iana.org/domains/root/db/bom.html +bom + +// bond : ShortDot SA +// https://www.iana.org/domains/root/db/bond.html +bond + +// boo : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/boo.html +boo + +// book : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/book.html +book + +// booking : Booking.com B.V. +// https://www.iana.org/domains/root/db/booking.html +booking + +// bosch : Robert Bosch GMBH +// https://www.iana.org/domains/root/db/bosch.html +bosch + +// bostik : Bostik SA +// https://www.iana.org/domains/root/db/bostik.html +bostik + +// boston : Registry Services, LLC +// https://www.iana.org/domains/root/db/boston.html +boston + +// bot : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/bot.html +bot + +// boutique : Binky Moon, LLC +// https://www.iana.org/domains/root/db/boutique.html +boutique + +// box : Intercap Registry Inc. +// https://www.iana.org/domains/root/db/box.html +box + +// bradesco : Banco Bradesco S.A. +// https://www.iana.org/domains/root/db/bradesco.html +bradesco + +// bridgestone : Bridgestone Corporation +// https://www.iana.org/domains/root/db/bridgestone.html +bridgestone + +// broadway : Celebrate Broadway, Inc. +// https://www.iana.org/domains/root/db/broadway.html +broadway + +// broker : Dog Beach, LLC +// https://www.iana.org/domains/root/db/broker.html +broker + +// brother : Brother Industries, Ltd. +// https://www.iana.org/domains/root/db/brother.html +brother + +// brussels : DNS.be vzw +// https://www.iana.org/domains/root/db/brussels.html +brussels + +// build : Plan Bee LLC +// https://www.iana.org/domains/root/db/build.html +build + +// builders : Binky Moon, LLC +// https://www.iana.org/domains/root/db/builders.html +builders + +// business : Binky Moon, LLC +// https://www.iana.org/domains/root/db/business.html +business + +// buy : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/buy.html +buy + +// buzz : DOTSTRATEGY CO. +// https://www.iana.org/domains/root/db/buzz.html +buzz + +// bzh : Association www.bzh +// https://www.iana.org/domains/root/db/bzh.html +bzh + +// cab : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cab.html +cab + +// cafe : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cafe.html +cafe + +// cal : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/cal.html +cal + +// call : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/call.html +call + +// calvinklein : PVH gTLD Holdings LLC +// https://www.iana.org/domains/root/db/calvinklein.html +calvinklein + +// cam : Cam Connecting SARL +// https://www.iana.org/domains/root/db/cam.html +cam + +// camera : Binky Moon, LLC +// https://www.iana.org/domains/root/db/camera.html +camera + +// camp : Binky Moon, LLC +// https://www.iana.org/domains/root/db/camp.html +camp + +// canon : Canon Inc. +// https://www.iana.org/domains/root/db/canon.html +canon + +// capetown : ZA Central Registry NPC trading as ZA Central Registry +// https://www.iana.org/domains/root/db/capetown.html +capetown + +// capital : Binky Moon, LLC +// https://www.iana.org/domains/root/db/capital.html +capital + +// capitalone : Capital One Financial Corporation +// https://www.iana.org/domains/root/db/capitalone.html +capitalone + +// car : XYZ.COM LLC +// https://www.iana.org/domains/root/db/car.html +car + +// caravan : Caravan International, Inc. +// https://www.iana.org/domains/root/db/caravan.html +caravan + +// cards : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cards.html +cards + +// care : Binky Moon, LLC +// https://www.iana.org/domains/root/db/care.html +care + +// career : dotCareer LLC +// https://www.iana.org/domains/root/db/career.html +career + +// careers : Binky Moon, LLC +// https://www.iana.org/domains/root/db/careers.html +careers + +// cars : XYZ.COM LLC +// https://www.iana.org/domains/root/db/cars.html +cars + +// casa : Registry Services, LLC +// https://www.iana.org/domains/root/db/casa.html +casa + +// case : Digity, LLC +// https://www.iana.org/domains/root/db/case.html +case + +// cash : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cash.html +cash + +// casino : Binky Moon, LLC +// https://www.iana.org/domains/root/db/casino.html +casino + +// catering : Binky Moon, LLC +// https://www.iana.org/domains/root/db/catering.html +catering + +// catholic : Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +// https://www.iana.org/domains/root/db/catholic.html +catholic + +// cba : COMMONWEALTH BANK OF AUSTRALIA +// https://www.iana.org/domains/root/db/cba.html +cba + +// cbn : The Christian Broadcasting Network, Inc. +// https://www.iana.org/domains/root/db/cbn.html +cbn + +// cbre : CBRE, Inc. +// https://www.iana.org/domains/root/db/cbre.html +cbre + +// center : Binky Moon, LLC +// https://www.iana.org/domains/root/db/center.html +center + +// ceo : XYZ.COM LLC +// https://www.iana.org/domains/root/db/ceo.html +ceo + +// cern : European Organization for Nuclear Research ("CERN") +// https://www.iana.org/domains/root/db/cern.html +cern + +// cfa : CFA Institute +// https://www.iana.org/domains/root/db/cfa.html +cfa + +// cfd : ShortDot SA +// https://www.iana.org/domains/root/db/cfd.html +cfd + +// chanel : Chanel International B.V. +// https://www.iana.org/domains/root/db/chanel.html +chanel + +// channel : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/channel.html +channel + +// charity : Public Interest Registry +// https://www.iana.org/domains/root/db/charity.html +charity + +// chase : JPMorgan Chase Bank, National Association +// https://www.iana.org/domains/root/db/chase.html +chase + +// chat : Binky Moon, LLC +// https://www.iana.org/domains/root/db/chat.html +chat + +// cheap : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cheap.html +cheap + +// chintai : CHINTAI Corporation +// https://www.iana.org/domains/root/db/chintai.html +chintai + +// christmas : XYZ.COM LLC +// https://www.iana.org/domains/root/db/christmas.html +christmas + +// chrome : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/chrome.html +chrome + +// church : Binky Moon, LLC +// https://www.iana.org/domains/root/db/church.html +church + +// cipriani : Hotel Cipriani Srl +// https://www.iana.org/domains/root/db/cipriani.html +cipriani + +// circle : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/circle.html +circle + +// cisco : Cisco Technology, Inc. +// https://www.iana.org/domains/root/db/cisco.html +cisco + +// citadel : Citadel Domain LLC +// https://www.iana.org/domains/root/db/citadel.html +citadel + +// citi : Citigroup Inc. +// https://www.iana.org/domains/root/db/citi.html +citi + +// citic : CITIC Group Corporation +// https://www.iana.org/domains/root/db/citic.html +citic + +// city : Binky Moon, LLC +// https://www.iana.org/domains/root/db/city.html +city + +// claims : Binky Moon, LLC +// https://www.iana.org/domains/root/db/claims.html +claims + +// cleaning : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cleaning.html +cleaning + +// click : Waterford Limited +// https://www.iana.org/domains/root/db/click.html +click + +// clinic : Binky Moon, LLC +// https://www.iana.org/domains/root/db/clinic.html +clinic + +// clinique : The Estée Lauder Companies Inc. +// https://www.iana.org/domains/root/db/clinique.html +clinique + +// clothing : Binky Moon, LLC +// https://www.iana.org/domains/root/db/clothing.html +clothing + +// cloud : Aruba PEC S.p.A. +// https://www.iana.org/domains/root/db/cloud.html +cloud + +// club : Registry Services, LLC +// https://www.iana.org/domains/root/db/club.html +club + +// clubmed : Club Méditerranée S.A. +// https://www.iana.org/domains/root/db/clubmed.html +clubmed + +// coach : Binky Moon, LLC +// https://www.iana.org/domains/root/db/coach.html +coach + +// codes : Binky Moon, LLC +// https://www.iana.org/domains/root/db/codes.html +codes + +// coffee : Binky Moon, LLC +// https://www.iana.org/domains/root/db/coffee.html +coffee + +// college : XYZ.COM LLC +// https://www.iana.org/domains/root/db/college.html +college + +// cologne : dotKoeln GmbH +// https://www.iana.org/domains/root/db/cologne.html +cologne + +// commbank : COMMONWEALTH BANK OF AUSTRALIA +// https://www.iana.org/domains/root/db/commbank.html +commbank + +// community : Binky Moon, LLC +// https://www.iana.org/domains/root/db/community.html +community + +// company : Binky Moon, LLC +// https://www.iana.org/domains/root/db/company.html +company + +// compare : Registry Services, LLC +// https://www.iana.org/domains/root/db/compare.html +compare + +// computer : Binky Moon, LLC +// https://www.iana.org/domains/root/db/computer.html +computer + +// comsec : VeriSign, Inc. +// https://www.iana.org/domains/root/db/comsec.html +comsec + +// condos : Binky Moon, LLC +// https://www.iana.org/domains/root/db/condos.html +condos + +// construction : Binky Moon, LLC +// https://www.iana.org/domains/root/db/construction.html +construction + +// consulting : Dog Beach, LLC +// https://www.iana.org/domains/root/db/consulting.html +consulting + +// contact : Dog Beach, LLC +// https://www.iana.org/domains/root/db/contact.html +contact + +// contractors : Binky Moon, LLC +// https://www.iana.org/domains/root/db/contractors.html +contractors + +// cooking : Registry Services, LLC +// https://www.iana.org/domains/root/db/cooking.html +cooking + +// cool : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cool.html +cool + +// corsica : Collectivité de Corse +// https://www.iana.org/domains/root/db/corsica.html +corsica + +// country : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/country.html +country + +// coupon : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/coupon.html +coupon + +// coupons : Binky Moon, LLC +// https://www.iana.org/domains/root/db/coupons.html +coupons + +// courses : Registry Services, LLC +// https://www.iana.org/domains/root/db/courses.html +courses + +// cpa : American Institute of Certified Public Accountants +// https://www.iana.org/domains/root/db/cpa.html +cpa + +// credit : Binky Moon, LLC +// https://www.iana.org/domains/root/db/credit.html +credit + +// creditcard : Binky Moon, LLC +// https://www.iana.org/domains/root/db/creditcard.html +creditcard + +// creditunion : DotCooperation LLC +// https://www.iana.org/domains/root/db/creditunion.html +creditunion + +// cricket : dot Cricket Limited +// https://www.iana.org/domains/root/db/cricket.html +cricket + +// crown : Crown Equipment Corporation +// https://www.iana.org/domains/root/db/crown.html +crown + +// crs : Federated Co-operatives Limited +// https://www.iana.org/domains/root/db/crs.html +crs + +// cruise : Viking River Cruises (Bermuda) Ltd. +// https://www.iana.org/domains/root/db/cruise.html +cruise + +// cruises : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cruises.html +cruises + +// cuisinella : SCHMIDT GROUPE S.A.S. +// https://www.iana.org/domains/root/db/cuisinella.html +cuisinella + +// cymru : Nominet UK +// https://www.iana.org/domains/root/db/cymru.html +cymru + +// cyou : ShortDot SA +// https://www.iana.org/domains/root/db/cyou.html +cyou + +// dad : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/dad.html +dad + +// dance : Dog Beach, LLC +// https://www.iana.org/domains/root/db/dance.html +dance + +// data : Dish DBS Corporation +// https://www.iana.org/domains/root/db/data.html +data + +// date : dot Date Limited +// https://www.iana.org/domains/root/db/date.html +date + +// dating : Binky Moon, LLC +// https://www.iana.org/domains/root/db/dating.html +dating + +// datsun : NISSAN MOTOR CO., LTD. +// https://www.iana.org/domains/root/db/datsun.html +datsun + +// day : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/day.html +day + +// dclk : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/dclk.html +dclk + +// dds : Registry Services, LLC +// https://www.iana.org/domains/root/db/dds.html +dds + +// deal : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/deal.html +deal + +// dealer : Intercap Registry Inc. +// https://www.iana.org/domains/root/db/dealer.html +dealer + +// deals : Binky Moon, LLC +// https://www.iana.org/domains/root/db/deals.html +deals + +// degree : Dog Beach, LLC +// https://www.iana.org/domains/root/db/degree.html +degree + +// delivery : Binky Moon, LLC +// https://www.iana.org/domains/root/db/delivery.html +delivery + +// dell : Dell Inc. +// https://www.iana.org/domains/root/db/dell.html +dell + +// deloitte : Deloitte Touche Tohmatsu +// https://www.iana.org/domains/root/db/deloitte.html +deloitte + +// delta : Delta Air Lines, Inc. +// https://www.iana.org/domains/root/db/delta.html +delta + +// democrat : Dog Beach, LLC +// https://www.iana.org/domains/root/db/democrat.html +democrat + +// dental : Binky Moon, LLC +// https://www.iana.org/domains/root/db/dental.html +dental + +// dentist : Dog Beach, LLC +// https://www.iana.org/domains/root/db/dentist.html +dentist + +// desi +// https://www.iana.org/domains/root/db/desi.html +desi + +// design : Registry Services, LLC +// https://www.iana.org/domains/root/db/design.html +design + +// dev : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/dev.html +dev + +// dhl : Deutsche Post AG +// https://www.iana.org/domains/root/db/dhl.html +dhl + +// diamonds : Binky Moon, LLC +// https://www.iana.org/domains/root/db/diamonds.html +diamonds + +// diet : XYZ.COM LLC +// https://www.iana.org/domains/root/db/diet.html +diet + +// digital : Binky Moon, LLC +// https://www.iana.org/domains/root/db/digital.html +digital + +// direct : Binky Moon, LLC +// https://www.iana.org/domains/root/db/direct.html +direct + +// directory : Binky Moon, LLC +// https://www.iana.org/domains/root/db/directory.html +directory + +// discount : Binky Moon, LLC +// https://www.iana.org/domains/root/db/discount.html +discount + +// discover : Discover Financial Services +// https://www.iana.org/domains/root/db/discover.html +discover + +// dish : Dish DBS Corporation +// https://www.iana.org/domains/root/db/dish.html +dish + +// diy : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/diy.html +diy + +// dnp : Dai Nippon Printing Co., Ltd. +// https://www.iana.org/domains/root/db/dnp.html +dnp + +// docs : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/docs.html +docs + +// doctor : Binky Moon, LLC +// https://www.iana.org/domains/root/db/doctor.html +doctor + +// dog : Binky Moon, LLC +// https://www.iana.org/domains/root/db/dog.html +dog + +// domains : Binky Moon, LLC +// https://www.iana.org/domains/root/db/domains.html +domains + +// dot : Dish DBS Corporation +// https://www.iana.org/domains/root/db/dot.html +dot + +// download : dot Support Limited +// https://www.iana.org/domains/root/db/download.html +download + +// drive : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/drive.html +drive + +// dtv : Dish DBS Corporation +// https://www.iana.org/domains/root/db/dtv.html +dtv + +// dubai : Dubai Smart Government Department +// https://www.iana.org/domains/root/db/dubai.html +dubai + +// dupont : DuPont Specialty Products USA, LLC +// https://www.iana.org/domains/root/db/dupont.html +dupont + +// durban : ZA Central Registry NPC trading as ZA Central Registry +// https://www.iana.org/domains/root/db/durban.html +durban + +// dvag : Deutsche Vermögensberatung Aktiengesellschaft DVAG +// https://www.iana.org/domains/root/db/dvag.html +dvag + +// dvr : DISH Technologies L.L.C. +// https://www.iana.org/domains/root/db/dvr.html +dvr + +// earth : Interlink Systems Innovation Institute K.K. +// https://www.iana.org/domains/root/db/earth.html +earth + +// eat : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/eat.html +eat + +// eco : Big Room Inc. +// https://www.iana.org/domains/root/db/eco.html +eco + +// edeka : EDEKA Verband kaufmännischer Genossenschaften e.V. +// https://www.iana.org/domains/root/db/edeka.html +edeka + +// education : Binky Moon, LLC +// https://www.iana.org/domains/root/db/education.html +education + +// email : Binky Moon, LLC +// https://www.iana.org/domains/root/db/email.html +email + +// emerck : Merck KGaA +// https://www.iana.org/domains/root/db/emerck.html +emerck + +// energy : Binky Moon, LLC +// https://www.iana.org/domains/root/db/energy.html +energy + +// engineer : Dog Beach, LLC +// https://www.iana.org/domains/root/db/engineer.html +engineer + +// engineering : Binky Moon, LLC +// https://www.iana.org/domains/root/db/engineering.html +engineering + +// enterprises : Binky Moon, LLC +// https://www.iana.org/domains/root/db/enterprises.html +enterprises + +// epson : Seiko Epson Corporation +// https://www.iana.org/domains/root/db/epson.html +epson + +// equipment : Binky Moon, LLC +// https://www.iana.org/domains/root/db/equipment.html +equipment + +// ericsson : Telefonaktiebolaget L M Ericsson +// https://www.iana.org/domains/root/db/ericsson.html +ericsson + +// erni : ERNI Group Holding AG +// https://www.iana.org/domains/root/db/erni.html +erni + +// esq : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/esq.html +esq + +// estate : Binky Moon, LLC +// https://www.iana.org/domains/root/db/estate.html +estate + +// eurovision : European Broadcasting Union (EBU) +// https://www.iana.org/domains/root/db/eurovision.html +eurovision + +// eus : Puntueus Fundazioa +// https://www.iana.org/domains/root/db/eus.html +eus + +// events : Binky Moon, LLC +// https://www.iana.org/domains/root/db/events.html +events + +// exchange : Binky Moon, LLC +// https://www.iana.org/domains/root/db/exchange.html +exchange + +// expert : Binky Moon, LLC +// https://www.iana.org/domains/root/db/expert.html +expert + +// exposed : Binky Moon, LLC +// https://www.iana.org/domains/root/db/exposed.html +exposed + +// express : Binky Moon, LLC +// https://www.iana.org/domains/root/db/express.html +express + +// extraspace : Extra Space Storage LLC +// https://www.iana.org/domains/root/db/extraspace.html +extraspace + +// fage : Fage International S.A. +// https://www.iana.org/domains/root/db/fage.html +fage + +// fail : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fail.html +fail + +// fairwinds : FairWinds Partners, LLC +// https://www.iana.org/domains/root/db/fairwinds.html +fairwinds + +// faith : dot Faith Limited +// https://www.iana.org/domains/root/db/faith.html +faith + +// family : Dog Beach, LLC +// https://www.iana.org/domains/root/db/family.html +family + +// fan : Dog Beach, LLC +// https://www.iana.org/domains/root/db/fan.html +fan + +// fans : ZDNS International Limited +// https://www.iana.org/domains/root/db/fans.html +fans + +// farm : Binky Moon, LLC +// https://www.iana.org/domains/root/db/farm.html +farm + +// farmers : Farmers Insurance Exchange +// https://www.iana.org/domains/root/db/farmers.html +farmers + +// fashion : Registry Services, LLC +// https://www.iana.org/domains/root/db/fashion.html +fashion + +// fast : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/fast.html +fast + +// fedex : Federal Express Corporation +// https://www.iana.org/domains/root/db/fedex.html +fedex + +// feedback : Top Level Spectrum, Inc. +// https://www.iana.org/domains/root/db/feedback.html +feedback + +// ferrari : Fiat Chrysler Automobiles N.V. +// https://www.iana.org/domains/root/db/ferrari.html +ferrari + +// ferrero : Ferrero Trading Lux S.A. +// https://www.iana.org/domains/root/db/ferrero.html +ferrero + +// fidelity : Fidelity Brokerage Services LLC +// https://www.iana.org/domains/root/db/fidelity.html +fidelity + +// fido : Rogers Communications Canada Inc. +// https://www.iana.org/domains/root/db/fido.html +fido + +// film : Motion Picture Domain Registry Pty Ltd +// https://www.iana.org/domains/root/db/film.html +film + +// final : Núcleo de Informação e Coordenação do Ponto BR - NIC.br +// https://www.iana.org/domains/root/db/final.html +final + +// finance : Binky Moon, LLC +// https://www.iana.org/domains/root/db/finance.html +finance + +// financial : Binky Moon, LLC +// https://www.iana.org/domains/root/db/financial.html +financial + +// fire : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/fire.html +fire + +// firestone : Bridgestone Licensing Services, Inc +// https://www.iana.org/domains/root/db/firestone.html +firestone + +// firmdale : Firmdale Holdings Limited +// https://www.iana.org/domains/root/db/firmdale.html +firmdale + +// fish : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fish.html +fish + +// fishing : Registry Services, LLC +// https://www.iana.org/domains/root/db/fishing.html +fishing + +// fit : Registry Services, LLC +// https://www.iana.org/domains/root/db/fit.html +fit + +// fitness : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fitness.html +fitness + +// flickr : Flickr, Inc. +// https://www.iana.org/domains/root/db/flickr.html +flickr + +// flights : Binky Moon, LLC +// https://www.iana.org/domains/root/db/flights.html +flights + +// flir : FLIR Systems, Inc. +// https://www.iana.org/domains/root/db/flir.html +flir + +// florist : Binky Moon, LLC +// https://www.iana.org/domains/root/db/florist.html +florist + +// flowers : XYZ.COM LLC +// https://www.iana.org/domains/root/db/flowers.html +flowers + +// fly : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/fly.html +fly + +// foo : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/foo.html +foo + +// food : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/food.html +food + +// football : Binky Moon, LLC +// https://www.iana.org/domains/root/db/football.html +football + +// ford : Ford Motor Company +// https://www.iana.org/domains/root/db/ford.html +ford + +// forex : Dog Beach, LLC +// https://www.iana.org/domains/root/db/forex.html +forex + +// forsale : Dog Beach, LLC +// https://www.iana.org/domains/root/db/forsale.html +forsale + +// forum : Waterford Limited +// https://www.iana.org/domains/root/db/forum.html +forum + +// foundation : Public Interest Registry +// https://www.iana.org/domains/root/db/foundation.html +foundation + +// fox : FOX Registry, LLC +// https://www.iana.org/domains/root/db/fox.html +fox + +// free : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/free.html +free + +// fresenius : Fresenius Immobilien-Verwaltungs-GmbH +// https://www.iana.org/domains/root/db/fresenius.html +fresenius + +// frl : FRLregistry B.V. +// https://www.iana.org/domains/root/db/frl.html +frl + +// frogans : OP3FT +// https://www.iana.org/domains/root/db/frogans.html +frogans + +// frontier : Frontier Communications Corporation +// https://www.iana.org/domains/root/db/frontier.html +frontier + +// ftr : Frontier Communications Corporation +// https://www.iana.org/domains/root/db/ftr.html +ftr + +// fujitsu : Fujitsu Limited +// https://www.iana.org/domains/root/db/fujitsu.html +fujitsu + +// fun : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/fun.html +fun + +// fund : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fund.html +fund + +// furniture : Binky Moon, LLC +// https://www.iana.org/domains/root/db/furniture.html +furniture + +// futbol : Dog Beach, LLC +// https://www.iana.org/domains/root/db/futbol.html +futbol + +// fyi : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fyi.html +fyi + +// gal : Asociación puntoGAL +// https://www.iana.org/domains/root/db/gal.html +gal + +// gallery : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gallery.html +gallery + +// gallo : Gallo Vineyards, Inc. +// https://www.iana.org/domains/root/db/gallo.html +gallo + +// gallup : Gallup, Inc. +// https://www.iana.org/domains/root/db/gallup.html +gallup + +// game : XYZ.COM LLC +// https://www.iana.org/domains/root/db/game.html +game + +// games : Dog Beach, LLC +// https://www.iana.org/domains/root/db/games.html +games + +// gap : The Gap, Inc. +// https://www.iana.org/domains/root/db/gap.html +gap + +// garden : Registry Services, LLC +// https://www.iana.org/domains/root/db/garden.html +garden + +// gay : Registry Services, LLC +// https://www.iana.org/domains/root/db/gay.html +gay + +// gbiz : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/gbiz.html +gbiz + +// gdn : Joint Stock Company "Navigation-information systems" +// https://www.iana.org/domains/root/db/gdn.html +gdn + +// gea : GEA Group Aktiengesellschaft +// https://www.iana.org/domains/root/db/gea.html +gea + +// gent : Easyhost BV +// https://www.iana.org/domains/root/db/gent.html +gent + +// genting : Resorts World Inc Pte. Ltd. +// https://www.iana.org/domains/root/db/genting.html +genting + +// george : Wal-Mart Stores, Inc. +// https://www.iana.org/domains/root/db/george.html +george + +// ggee : GMO Internet, Inc. +// https://www.iana.org/domains/root/db/ggee.html +ggee + +// gift : DotGift, LLC +// https://www.iana.org/domains/root/db/gift.html +gift + +// gifts : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gifts.html +gifts + +// gives : Public Interest Registry +// https://www.iana.org/domains/root/db/gives.html +gives + +// giving : Public Interest Registry +// https://www.iana.org/domains/root/db/giving.html +giving + +// glass : Binky Moon, LLC +// https://www.iana.org/domains/root/db/glass.html +glass + +// gle : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/gle.html +gle + +// global : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/global.html +global + +// globo : Globo Comunicação e Participações S.A +// https://www.iana.org/domains/root/db/globo.html +globo + +// gmail : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/gmail.html +gmail + +// gmbh : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gmbh.html +gmbh + +// gmo : GMO Internet, Inc. +// https://www.iana.org/domains/root/db/gmo.html +gmo + +// gmx : 1&1 Mail & Media GmbH +// https://www.iana.org/domains/root/db/gmx.html +gmx + +// godaddy : Go Daddy East, LLC +// https://www.iana.org/domains/root/db/godaddy.html +godaddy + +// gold : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gold.html +gold + +// goldpoint : YODOBASHI CAMERA CO.,LTD. +// https://www.iana.org/domains/root/db/goldpoint.html +goldpoint + +// golf : Binky Moon, LLC +// https://www.iana.org/domains/root/db/golf.html +golf + +// goo : NTT DOCOMO, INC. +// https://www.iana.org/domains/root/db/goo.html +goo + +// goodyear : The Goodyear Tire & Rubber Company +// https://www.iana.org/domains/root/db/goodyear.html +goodyear + +// goog : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/goog.html +goog + +// google : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/google.html +google + +// gop : Republican State Leadership Committee, Inc. +// https://www.iana.org/domains/root/db/gop.html +gop + +// got : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/got.html +got + +// grainger : Grainger Registry Services, LLC +// https://www.iana.org/domains/root/db/grainger.html +grainger + +// graphics : Binky Moon, LLC +// https://www.iana.org/domains/root/db/graphics.html +graphics + +// gratis : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gratis.html +gratis + +// green : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/green.html +green + +// gripe : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gripe.html +gripe + +// grocery : Wal-Mart Stores, Inc. +// https://www.iana.org/domains/root/db/grocery.html +grocery + +// group : Binky Moon, LLC +// https://www.iana.org/domains/root/db/group.html +group + +// gucci : Guccio Gucci S.p.a. +// https://www.iana.org/domains/root/db/gucci.html +gucci + +// guge : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/guge.html +guge + +// guide : Binky Moon, LLC +// https://www.iana.org/domains/root/db/guide.html +guide + +// guitars : XYZ.COM LLC +// https://www.iana.org/domains/root/db/guitars.html +guitars + +// guru : Binky Moon, LLC +// https://www.iana.org/domains/root/db/guru.html +guru + +// hair : XYZ.COM LLC +// https://www.iana.org/domains/root/db/hair.html +hair + +// hamburg : Hamburg Top-Level-Domain GmbH +// https://www.iana.org/domains/root/db/hamburg.html +hamburg + +// hangout : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/hangout.html +hangout + +// haus : Dog Beach, LLC +// https://www.iana.org/domains/root/db/haus.html +haus + +// hbo : HBO Registry Services, Inc. +// https://www.iana.org/domains/root/db/hbo.html +hbo + +// hdfc : HDFC BANK LIMITED +// https://www.iana.org/domains/root/db/hdfc.html +hdfc + +// hdfcbank : HDFC BANK LIMITED +// https://www.iana.org/domains/root/db/hdfcbank.html +hdfcbank + +// health : Registry Services, LLC +// https://www.iana.org/domains/root/db/health.html +health + +// healthcare : Binky Moon, LLC +// https://www.iana.org/domains/root/db/healthcare.html +healthcare + +// help : Innovation service Limited +// https://www.iana.org/domains/root/db/help.html +help + +// helsinki : City of Helsinki +// https://www.iana.org/domains/root/db/helsinki.html +helsinki + +// here : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/here.html +here + +// hermes : HERMES INTERNATIONAL +// https://www.iana.org/domains/root/db/hermes.html +hermes + +// hiphop : Dot Hip Hop, LLC +// https://www.iana.org/domains/root/db/hiphop.html +hiphop + +// hisamitsu : Hisamitsu Pharmaceutical Co.,Inc. +// https://www.iana.org/domains/root/db/hisamitsu.html +hisamitsu + +// hitachi : Hitachi, Ltd. +// https://www.iana.org/domains/root/db/hitachi.html +hitachi + +// hiv : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/hiv.html +hiv + +// hkt : PCCW-HKT DataCom Services Limited +// https://www.iana.org/domains/root/db/hkt.html +hkt + +// hockey : Binky Moon, LLC +// https://www.iana.org/domains/root/db/hockey.html +hockey + +// holdings : Binky Moon, LLC +// https://www.iana.org/domains/root/db/holdings.html +holdings + +// holiday : Binky Moon, LLC +// https://www.iana.org/domains/root/db/holiday.html +holiday + +// homedepot : Home Depot Product Authority, LLC +// https://www.iana.org/domains/root/db/homedepot.html +homedepot + +// homegoods : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/homegoods.html +homegoods + +// homes : XYZ.COM LLC +// https://www.iana.org/domains/root/db/homes.html +homes + +// homesense : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/homesense.html +homesense + +// honda : Honda Motor Co., Ltd. +// https://www.iana.org/domains/root/db/honda.html +honda + +// horse : Registry Services, LLC +// https://www.iana.org/domains/root/db/horse.html +horse + +// hospital : Binky Moon, LLC +// https://www.iana.org/domains/root/db/hospital.html +hospital + +// host : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/host.html +host + +// hosting : XYZ.COM LLC +// https://www.iana.org/domains/root/db/hosting.html +hosting + +// hot : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/hot.html +hot + +// hotel : HOTEL Top-Level-Domain S.a.r.l +// https://www.iana.org/domains/root/db/hotel.html +hotel + +// hotels : Booking.com B.V. +// https://www.iana.org/domains/root/db/hotels.html +hotels + +// hotmail : Microsoft Corporation +// https://www.iana.org/domains/root/db/hotmail.html +hotmail + +// house : Binky Moon, LLC +// https://www.iana.org/domains/root/db/house.html +house + +// how : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/how.html +how + +// hsbc : HSBC Global Services (UK) Limited +// https://www.iana.org/domains/root/db/hsbc.html +hsbc + +// hughes : Hughes Satellite Systems Corporation +// https://www.iana.org/domains/root/db/hughes.html +hughes + +// hyatt : Hyatt GTLD, L.L.C. +// https://www.iana.org/domains/root/db/hyatt.html +hyatt + +// hyundai : Hyundai Motor Company +// https://www.iana.org/domains/root/db/hyundai.html +hyundai + +// ibm : International Business Machines Corporation +// https://www.iana.org/domains/root/db/ibm.html +ibm + +// icbc : Industrial and Commercial Bank of China Limited +// https://www.iana.org/domains/root/db/icbc.html +icbc + +// ice : IntercontinentalExchange, Inc. +// https://www.iana.org/domains/root/db/ice.html +ice + +// icu : ShortDot SA +// https://www.iana.org/domains/root/db/icu.html +icu + +// ieee : IEEE Global LLC +// https://www.iana.org/domains/root/db/ieee.html +ieee + +// ifm : ifm electronic gmbh +// https://www.iana.org/domains/root/db/ifm.html +ifm + +// ikano : Ikano S.A. +// https://www.iana.org/domains/root/db/ikano.html +ikano + +// imamat : Fondation Aga Khan (Aga Khan Foundation) +// https://www.iana.org/domains/root/db/imamat.html +imamat + +// imdb : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/imdb.html +imdb + +// immo : Binky Moon, LLC +// https://www.iana.org/domains/root/db/immo.html +immo + +// immobilien : Dog Beach, LLC +// https://www.iana.org/domains/root/db/immobilien.html +immobilien + +// inc : Intercap Registry Inc. +// https://www.iana.org/domains/root/db/inc.html +inc + +// industries : Binky Moon, LLC +// https://www.iana.org/domains/root/db/industries.html +industries + +// infiniti : NISSAN MOTOR CO., LTD. +// https://www.iana.org/domains/root/db/infiniti.html +infiniti + +// ing : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/ing.html +ing + +// ink : Registry Services, LLC +// https://www.iana.org/domains/root/db/ink.html +ink + +// institute : Binky Moon, LLC +// https://www.iana.org/domains/root/db/institute.html +institute + +// insurance : fTLD Registry Services LLC +// https://www.iana.org/domains/root/db/insurance.html +insurance + +// insure : Binky Moon, LLC +// https://www.iana.org/domains/root/db/insure.html +insure + +// international : Binky Moon, LLC +// https://www.iana.org/domains/root/db/international.html +international + +// intuit : Intuit Administrative Services, Inc. +// https://www.iana.org/domains/root/db/intuit.html +intuit + +// investments : Binky Moon, LLC +// https://www.iana.org/domains/root/db/investments.html +investments + +// ipiranga : Ipiranga Produtos de Petroleo S.A. +// https://www.iana.org/domains/root/db/ipiranga.html +ipiranga + +// irish : Binky Moon, LLC +// https://www.iana.org/domains/root/db/irish.html +irish + +// ismaili : Fondation Aga Khan (Aga Khan Foundation) +// https://www.iana.org/domains/root/db/ismaili.html +ismaili + +// ist : Istanbul Metropolitan Municipality +// https://www.iana.org/domains/root/db/ist.html +ist + +// istanbul : Istanbul Metropolitan Municipality +// https://www.iana.org/domains/root/db/istanbul.html +istanbul + +// itau : Itau Unibanco Holding S.A. +// https://www.iana.org/domains/root/db/itau.html +itau + +// itv : ITV Services Limited +// https://www.iana.org/domains/root/db/itv.html +itv + +// jaguar : Jaguar Land Rover Ltd +// https://www.iana.org/domains/root/db/jaguar.html +jaguar + +// java : Oracle Corporation +// https://www.iana.org/domains/root/db/java.html +java + +// jcb : JCB Co., Ltd. +// https://www.iana.org/domains/root/db/jcb.html +jcb + +// jeep : FCA US LLC. +// https://www.iana.org/domains/root/db/jeep.html +jeep + +// jetzt : Binky Moon, LLC +// https://www.iana.org/domains/root/db/jetzt.html +jetzt + +// jewelry : Binky Moon, LLC +// https://www.iana.org/domains/root/db/jewelry.html +jewelry + +// jio : Reliance Industries Limited +// https://www.iana.org/domains/root/db/jio.html +jio + +// jll : Jones Lang LaSalle Incorporated +// https://www.iana.org/domains/root/db/jll.html +jll + +// jmp : Matrix IP LLC +// https://www.iana.org/domains/root/db/jmp.html +jmp + +// jnj : Johnson & Johnson Services, Inc. +// https://www.iana.org/domains/root/db/jnj.html +jnj + +// joburg : ZA Central Registry NPC trading as ZA Central Registry +// https://www.iana.org/domains/root/db/joburg.html +joburg + +// jot : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/jot.html +jot + +// joy : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/joy.html +joy + +// jpmorgan : JPMorgan Chase Bank, National Association +// https://www.iana.org/domains/root/db/jpmorgan.html +jpmorgan + +// jprs : Japan Registry Services Co., Ltd. +// https://www.iana.org/domains/root/db/jprs.html +jprs + +// juegos : Dog Beach, LLC +// https://www.iana.org/domains/root/db/juegos.html +juegos + +// juniper : JUNIPER NETWORKS, INC. +// https://www.iana.org/domains/root/db/juniper.html +juniper + +// kaufen : Dog Beach, LLC +// https://www.iana.org/domains/root/db/kaufen.html +kaufen + +// kddi : KDDI CORPORATION +// https://www.iana.org/domains/root/db/kddi.html +kddi + +// kerryhotels : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/kerryhotels.html +kerryhotels + +// kerryproperties : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/kerryproperties.html +kerryproperties + +// kfh : Kuwait Finance House +// https://www.iana.org/domains/root/db/kfh.html +kfh + +// kia : KIA MOTORS CORPORATION +// https://www.iana.org/domains/root/db/kia.html +kia + +// kids : DotKids Foundation Limited +// https://www.iana.org/domains/root/db/kids.html +kids + +// kim : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/kim.html +kim + +// kindle : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/kindle.html +kindle + +// kitchen : Binky Moon, LLC +// https://www.iana.org/domains/root/db/kitchen.html +kitchen + +// kiwi : DOT KIWI LIMITED +// https://www.iana.org/domains/root/db/kiwi.html +kiwi + +// koeln : dotKoeln GmbH +// https://www.iana.org/domains/root/db/koeln.html +koeln + +// komatsu : Komatsu Ltd. +// https://www.iana.org/domains/root/db/komatsu.html +komatsu + +// kosher : Kosher Marketing Assets LLC +// https://www.iana.org/domains/root/db/kosher.html +kosher + +// kpmg : KPMG International Cooperative (KPMG International Genossenschaft) +// https://www.iana.org/domains/root/db/kpmg.html +kpmg + +// kpn : Koninklijke KPN N.V. +// https://www.iana.org/domains/root/db/kpn.html +kpn + +// krd : KRG Department of Information Technology +// https://www.iana.org/domains/root/db/krd.html +krd + +// kred : KredTLD Pty Ltd +// https://www.iana.org/domains/root/db/kred.html +kred + +// kuokgroup : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/kuokgroup.html +kuokgroup + +// kyoto : Academic Institution: Kyoto Jyoho Gakuen +// https://www.iana.org/domains/root/db/kyoto.html +kyoto + +// lacaixa : Fundación Bancaria Caixa d’Estalvis i Pensions de Barcelona, “la Caixa” +// https://www.iana.org/domains/root/db/lacaixa.html +lacaixa + +// lamborghini : Automobili Lamborghini S.p.A. +// https://www.iana.org/domains/root/db/lamborghini.html +lamborghini + +// lamer : The Estée Lauder Companies Inc. +// https://www.iana.org/domains/root/db/lamer.html +lamer + +// land : Binky Moon, LLC +// https://www.iana.org/domains/root/db/land.html +land + +// landrover : Jaguar Land Rover Ltd +// https://www.iana.org/domains/root/db/landrover.html +landrover + +// lanxess : LANXESS Corporation +// https://www.iana.org/domains/root/db/lanxess.html +lanxess + +// lasalle : Jones Lang LaSalle Incorporated +// https://www.iana.org/domains/root/db/lasalle.html +lasalle + +// lat : XYZ.COM LLC +// https://www.iana.org/domains/root/db/lat.html +lat + +// latino : Dish DBS Corporation +// https://www.iana.org/domains/root/db/latino.html +latino + +// latrobe : La Trobe University +// https://www.iana.org/domains/root/db/latrobe.html +latrobe + +// law : Registry Services, LLC +// https://www.iana.org/domains/root/db/law.html +law + +// lawyer : Dog Beach, LLC +// https://www.iana.org/domains/root/db/lawyer.html +lawyer + +// lds : IRI Domain Management, LLC +// https://www.iana.org/domains/root/db/lds.html +lds + +// lease : Binky Moon, LLC +// https://www.iana.org/domains/root/db/lease.html +lease + +// leclerc : A.C.D. LEC Association des Centres Distributeurs Edouard Leclerc +// https://www.iana.org/domains/root/db/leclerc.html +leclerc + +// lefrak : LeFrak Organization, Inc. +// https://www.iana.org/domains/root/db/lefrak.html +lefrak + +// legal : Binky Moon, LLC +// https://www.iana.org/domains/root/db/legal.html +legal + +// lego : LEGO Juris A/S +// https://www.iana.org/domains/root/db/lego.html +lego + +// lexus : TOYOTA MOTOR CORPORATION +// https://www.iana.org/domains/root/db/lexus.html +lexus + +// lgbt : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/lgbt.html +lgbt + +// lidl : Schwarz Domains und Services GmbH & Co. KG +// https://www.iana.org/domains/root/db/lidl.html +lidl + +// life : Binky Moon, LLC +// https://www.iana.org/domains/root/db/life.html +life + +// lifeinsurance : American Council of Life Insurers +// https://www.iana.org/domains/root/db/lifeinsurance.html +lifeinsurance + +// lifestyle : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/lifestyle.html +lifestyle + +// lighting : Binky Moon, LLC +// https://www.iana.org/domains/root/db/lighting.html +lighting + +// like : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/like.html +like + +// lilly : Eli Lilly and Company +// https://www.iana.org/domains/root/db/lilly.html +lilly + +// limited : Binky Moon, LLC +// https://www.iana.org/domains/root/db/limited.html +limited + +// limo : Binky Moon, LLC +// https://www.iana.org/domains/root/db/limo.html +limo + +// lincoln : Ford Motor Company +// https://www.iana.org/domains/root/db/lincoln.html +lincoln + +// link : Nova Registry Ltd +// https://www.iana.org/domains/root/db/link.html +link + +// live : Dog Beach, LLC +// https://www.iana.org/domains/root/db/live.html +live + +// living : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/living.html +living + +// llc : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/llc.html +llc + +// llp : Intercap Registry Inc. +// https://www.iana.org/domains/root/db/llp.html +llp + +// loan : dot Loan Limited +// https://www.iana.org/domains/root/db/loan.html +loan + +// loans : Binky Moon, LLC +// https://www.iana.org/domains/root/db/loans.html +loans + +// locker : Orange Domains LLC +// https://www.iana.org/domains/root/db/locker.html +locker + +// locus : Locus Analytics LLC +// https://www.iana.org/domains/root/db/locus.html +locus + +// lol : XYZ.COM LLC +// https://www.iana.org/domains/root/db/lol.html +lol + +// london : Dot London Domains Limited +// https://www.iana.org/domains/root/db/london.html +london + +// lotte : Lotte Holdings Co., Ltd. +// https://www.iana.org/domains/root/db/lotte.html +lotte + +// lotto : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/lotto.html +lotto + +// love : Waterford Limited +// https://www.iana.org/domains/root/db/love.html +love + +// lpl : LPL Holdings, Inc. +// https://www.iana.org/domains/root/db/lpl.html +lpl + +// lplfinancial : LPL Holdings, Inc. +// https://www.iana.org/domains/root/db/lplfinancial.html +lplfinancial + +// ltd : Binky Moon, LLC +// https://www.iana.org/domains/root/db/ltd.html +ltd + +// ltda : InterNetX, Corp +// https://www.iana.org/domains/root/db/ltda.html +ltda + +// lundbeck : H. Lundbeck A/S +// https://www.iana.org/domains/root/db/lundbeck.html +lundbeck + +// luxe : Registry Services, LLC +// https://www.iana.org/domains/root/db/luxe.html +luxe + +// luxury : Luxury Partners, LLC +// https://www.iana.org/domains/root/db/luxury.html +luxury + +// madrid : Comunidad de Madrid +// https://www.iana.org/domains/root/db/madrid.html +madrid + +// maif : Mutuelle Assurance Instituteur France (MAIF) +// https://www.iana.org/domains/root/db/maif.html +maif + +// maison : Binky Moon, LLC +// https://www.iana.org/domains/root/db/maison.html +maison + +// makeup : XYZ.COM LLC +// https://www.iana.org/domains/root/db/makeup.html +makeup + +// man : MAN Truck & Bus SE +// https://www.iana.org/domains/root/db/man.html +man + +// management : Binky Moon, LLC +// https://www.iana.org/domains/root/db/management.html +management + +// mango : PUNTO FA S.L. +// https://www.iana.org/domains/root/db/mango.html +mango + +// map : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/map.html +map + +// market : Dog Beach, LLC +// https://www.iana.org/domains/root/db/market.html +market + +// marketing : Binky Moon, LLC +// https://www.iana.org/domains/root/db/marketing.html +marketing + +// markets : Dog Beach, LLC +// https://www.iana.org/domains/root/db/markets.html +markets + +// marriott : Marriott Worldwide Corporation +// https://www.iana.org/domains/root/db/marriott.html +marriott + +// marshalls : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/marshalls.html +marshalls + +// mattel : Mattel IT Services, Inc. +// https://www.iana.org/domains/root/db/mattel.html +mattel + +// mba : Binky Moon, LLC +// https://www.iana.org/domains/root/db/mba.html +mba + +// mckinsey : McKinsey Holdings, Inc. +// https://www.iana.org/domains/root/db/mckinsey.html +mckinsey + +// med : Medistry LLC +// https://www.iana.org/domains/root/db/med.html +med + +// media : Binky Moon, LLC +// https://www.iana.org/domains/root/db/media.html +media + +// meet : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/meet.html +meet + +// melbourne : The Crown in right of the State of Victoria, represented by its Department of State Development, Business and Innovation +// https://www.iana.org/domains/root/db/melbourne.html +melbourne + +// meme : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/meme.html +meme + +// memorial : Dog Beach, LLC +// https://www.iana.org/domains/root/db/memorial.html +memorial + +// men : Exclusive Registry Limited +// https://www.iana.org/domains/root/db/men.html +men + +// menu : Dot Menu Registry, LLC +// https://www.iana.org/domains/root/db/menu.html +menu + +// merck : Merck Registry Holdings, Inc. +// https://www.iana.org/domains/root/db/merck.html +merck + +// merckmsd : MSD Registry Holdings, Inc. +// https://www.iana.org/domains/root/db/merckmsd.html +merckmsd + +// miami : Registry Services, LLC +// https://www.iana.org/domains/root/db/miami.html +miami + +// microsoft : Microsoft Corporation +// https://www.iana.org/domains/root/db/microsoft.html +microsoft + +// mini : Bayerische Motoren Werke Aktiengesellschaft +// https://www.iana.org/domains/root/db/mini.html +mini + +// mint : Intuit Administrative Services, Inc. +// https://www.iana.org/domains/root/db/mint.html +mint + +// mit : Massachusetts Institute of Technology +// https://www.iana.org/domains/root/db/mit.html +mit + +// mitsubishi : Mitsubishi Corporation +// https://www.iana.org/domains/root/db/mitsubishi.html +mitsubishi + +// mlb : MLB Advanced Media DH, LLC +// https://www.iana.org/domains/root/db/mlb.html +mlb + +// mls : The Canadian Real Estate Association +// https://www.iana.org/domains/root/db/mls.html +mls + +// mma : MMA IARD +// https://www.iana.org/domains/root/db/mma.html +mma + +// mobile : Dish DBS Corporation +// https://www.iana.org/domains/root/db/mobile.html +mobile + +// moda : Dog Beach, LLC +// https://www.iana.org/domains/root/db/moda.html +moda + +// moe : Interlink Systems Innovation Institute K.K. +// https://www.iana.org/domains/root/db/moe.html +moe + +// moi : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/moi.html +moi + +// mom : XYZ.COM LLC +// https://www.iana.org/domains/root/db/mom.html +mom + +// monash : Monash University +// https://www.iana.org/domains/root/db/monash.html +monash + +// money : Binky Moon, LLC +// https://www.iana.org/domains/root/db/money.html +money + +// monster : XYZ.COM LLC +// https://www.iana.org/domains/root/db/monster.html +monster + +// mormon : IRI Domain Management, LLC +// https://www.iana.org/domains/root/db/mormon.html +mormon + +// mortgage : Dog Beach, LLC +// https://www.iana.org/domains/root/db/mortgage.html +mortgage + +// moscow : Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID) +// https://www.iana.org/domains/root/db/moscow.html +moscow + +// moto : Motorola Trademark Holdings, LLC +// https://www.iana.org/domains/root/db/moto.html +moto + +// motorcycles : XYZ.COM LLC +// https://www.iana.org/domains/root/db/motorcycles.html +motorcycles + +// mov : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/mov.html +mov + +// movie : Binky Moon, LLC +// https://www.iana.org/domains/root/db/movie.html +movie + +// msd : MSD Registry Holdings, Inc. +// https://www.iana.org/domains/root/db/msd.html +msd + +// mtn : MTN Dubai Limited +// https://www.iana.org/domains/root/db/mtn.html +mtn + +// mtr : MTR Corporation Limited +// https://www.iana.org/domains/root/db/mtr.html +mtr + +// music : DotMusic Limited +// https://www.iana.org/domains/root/db/music.html +music + +// nab : National Australia Bank Limited +// https://www.iana.org/domains/root/db/nab.html +nab + +// nagoya : GMO Registry, Inc. +// https://www.iana.org/domains/root/db/nagoya.html +nagoya + +// navy : Dog Beach, LLC +// https://www.iana.org/domains/root/db/navy.html +navy + +// nba : NBA REGISTRY, LLC +// https://www.iana.org/domains/root/db/nba.html +nba + +// nec : NEC Corporation +// https://www.iana.org/domains/root/db/nec.html +nec + +// netbank : COMMONWEALTH BANK OF AUSTRALIA +// https://www.iana.org/domains/root/db/netbank.html +netbank + +// netflix : Netflix, Inc. +// https://www.iana.org/domains/root/db/netflix.html +netflix + +// network : Binky Moon, LLC +// https://www.iana.org/domains/root/db/network.html +network + +// neustar : NeuStar, Inc. +// https://www.iana.org/domains/root/db/neustar.html +neustar + +// new : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/new.html +new + +// news : Dog Beach, LLC +// https://www.iana.org/domains/root/db/news.html +news + +// next : Next plc +// https://www.iana.org/domains/root/db/next.html +next + +// nextdirect : Next plc +// https://www.iana.org/domains/root/db/nextdirect.html +nextdirect + +// nexus : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/nexus.html +nexus + +// nfl : NFL Reg Ops LLC +// https://www.iana.org/domains/root/db/nfl.html +nfl + +// ngo : Public Interest Registry +// https://www.iana.org/domains/root/db/ngo.html +ngo + +// nhk : Japan Broadcasting Corporation (NHK) +// https://www.iana.org/domains/root/db/nhk.html +nhk + +// nico : DWANGO Co., Ltd. +// https://www.iana.org/domains/root/db/nico.html +nico + +// nike : NIKE, Inc. +// https://www.iana.org/domains/root/db/nike.html +nike + +// nikon : NIKON CORPORATION +// https://www.iana.org/domains/root/db/nikon.html +nikon + +// ninja : Dog Beach, LLC +// https://www.iana.org/domains/root/db/ninja.html +ninja + +// nissan : NISSAN MOTOR CO., LTD. +// https://www.iana.org/domains/root/db/nissan.html +nissan + +// nissay : Nippon Life Insurance Company +// https://www.iana.org/domains/root/db/nissay.html +nissay + +// nokia : Nokia Corporation +// https://www.iana.org/domains/root/db/nokia.html +nokia + +// norton : Gen Digital Inc. +// https://www.iana.org/domains/root/db/norton.html +norton + +// now : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/now.html +now + +// nowruz +// https://www.iana.org/domains/root/db/nowruz.html +nowruz + +// nowtv : Starbucks (HK) Limited +// https://www.iana.org/domains/root/db/nowtv.html +nowtv + +// nra : National Rifle Association of America +// https://www.iana.org/domains/root/db/nra.html +nra + +// nrw : Minds + Machines GmbH +// https://www.iana.org/domains/root/db/nrw.html +nrw + +// ntt : NIPPON TELEGRAPH AND TELEPHONE CORPORATION +// https://www.iana.org/domains/root/db/ntt.html +ntt + +// nyc : The City of New York by and through the New York City Department of Information Technology & Telecommunications +// https://www.iana.org/domains/root/db/nyc.html +nyc + +// obi : OBI Group Holding SE & Co. KGaA +// https://www.iana.org/domains/root/db/obi.html +obi + +// observer : Fegistry, LLC +// https://www.iana.org/domains/root/db/observer.html +observer + +// office : Microsoft Corporation +// https://www.iana.org/domains/root/db/office.html +office + +// okinawa : BRregistry, Inc. +// https://www.iana.org/domains/root/db/okinawa.html +okinawa + +// olayan : Competrol (Luxembourg) Sarl +// https://www.iana.org/domains/root/db/olayan.html +olayan + +// olayangroup : Competrol (Luxembourg) Sarl +// https://www.iana.org/domains/root/db/olayangroup.html +olayangroup + +// ollo : Dish DBS Corporation +// https://www.iana.org/domains/root/db/ollo.html +ollo + +// omega : The Swatch Group Ltd +// https://www.iana.org/domains/root/db/omega.html +omega + +// one : One.com A/S +// https://www.iana.org/domains/root/db/one.html +one + +// ong : Public Interest Registry +// https://www.iana.org/domains/root/db/ong.html +ong + +// onl : iRegistry GmbH +// https://www.iana.org/domains/root/db/onl.html +onl + +// online : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/online.html +online + +// ooo : INFIBEAM AVENUES LIMITED +// https://www.iana.org/domains/root/db/ooo.html +ooo + +// open : American Express Travel Related Services Company, Inc. +// https://www.iana.org/domains/root/db/open.html +open + +// oracle : Oracle Corporation +// https://www.iana.org/domains/root/db/oracle.html +oracle + +// orange : Orange Brand Services Limited +// https://www.iana.org/domains/root/db/orange.html +orange + +// organic : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/organic.html +organic + +// origins : The Estée Lauder Companies Inc. +// https://www.iana.org/domains/root/db/origins.html +origins + +// osaka : Osaka Registry Co., Ltd. +// https://www.iana.org/domains/root/db/osaka.html +osaka + +// otsuka : Otsuka Holdings Co., Ltd. +// https://www.iana.org/domains/root/db/otsuka.html +otsuka + +// ott : Dish DBS Corporation +// https://www.iana.org/domains/root/db/ott.html +ott + +// ovh : MédiaBC +// https://www.iana.org/domains/root/db/ovh.html +ovh + +// page : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/page.html +page + +// panasonic : Panasonic Holdings Corporation +// https://www.iana.org/domains/root/db/panasonic.html +panasonic + +// paris : City of Paris +// https://www.iana.org/domains/root/db/paris.html +paris + +// pars +// https://www.iana.org/domains/root/db/pars.html +pars + +// partners : Binky Moon, LLC +// https://www.iana.org/domains/root/db/partners.html +partners + +// parts : Binky Moon, LLC +// https://www.iana.org/domains/root/db/parts.html +parts + +// party : Blue Sky Registry Limited +// https://www.iana.org/domains/root/db/party.html +party + +// pay : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/pay.html +pay + +// pccw : PCCW Enterprises Limited +// https://www.iana.org/domains/root/db/pccw.html +pccw + +// pet : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/pet.html +pet + +// pfizer : Pfizer Inc. +// https://www.iana.org/domains/root/db/pfizer.html +pfizer + +// pharmacy : National Association of Boards of Pharmacy +// https://www.iana.org/domains/root/db/pharmacy.html +pharmacy + +// phd : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/phd.html +phd + +// philips : Koninklijke Philips N.V. +// https://www.iana.org/domains/root/db/philips.html +philips + +// phone : Dish DBS Corporation +// https://www.iana.org/domains/root/db/phone.html +phone + +// photo : Registry Services, LLC +// https://www.iana.org/domains/root/db/photo.html +photo + +// photography : Binky Moon, LLC +// https://www.iana.org/domains/root/db/photography.html +photography + +// photos : Binky Moon, LLC +// https://www.iana.org/domains/root/db/photos.html +photos + +// physio : PhysBiz Pty Ltd +// https://www.iana.org/domains/root/db/physio.html +physio + +// pics : XYZ.COM LLC +// https://www.iana.org/domains/root/db/pics.html +pics + +// pictet : Banque Pictet & Cie SA +// https://www.iana.org/domains/root/db/pictet.html +pictet + +// pictures : Binky Moon, LLC +// https://www.iana.org/domains/root/db/pictures.html +pictures + +// pid : Top Level Spectrum, Inc. +// https://www.iana.org/domains/root/db/pid.html +pid + +// pin : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/pin.html +pin + +// ping : Ping Registry Provider, Inc. +// https://www.iana.org/domains/root/db/ping.html +ping + +// pink : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/pink.html +pink + +// pioneer : Pioneer Corporation +// https://www.iana.org/domains/root/db/pioneer.html +pioneer + +// pizza : Binky Moon, LLC +// https://www.iana.org/domains/root/db/pizza.html +pizza + +// place : Binky Moon, LLC +// https://www.iana.org/domains/root/db/place.html +place + +// play : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/play.html +play + +// playstation : Sony Interactive Entertainment Inc. +// https://www.iana.org/domains/root/db/playstation.html +playstation + +// plumbing : Binky Moon, LLC +// https://www.iana.org/domains/root/db/plumbing.html +plumbing + +// plus : Binky Moon, LLC +// https://www.iana.org/domains/root/db/plus.html +plus + +// pnc : PNC Domain Co., LLC +// https://www.iana.org/domains/root/db/pnc.html +pnc + +// pohl : Deutsche Vermögensberatung Aktiengesellschaft DVAG +// https://www.iana.org/domains/root/db/pohl.html +pohl + +// poker : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/poker.html +poker + +// politie : Politie Nederland +// https://www.iana.org/domains/root/db/politie.html +politie + +// porn : ICM Registry PN LLC +// https://www.iana.org/domains/root/db/porn.html +porn + +// praxi : Praxi S.p.A. +// https://www.iana.org/domains/root/db/praxi.html +praxi + +// press : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/press.html +press + +// prime : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/prime.html +prime + +// prod : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/prod.html +prod + +// productions : Binky Moon, LLC +// https://www.iana.org/domains/root/db/productions.html +productions + +// prof : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/prof.html +prof + +// progressive : Progressive Casualty Insurance Company +// https://www.iana.org/domains/root/db/progressive.html +progressive + +// promo : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/promo.html +promo + +// properties : Binky Moon, LLC +// https://www.iana.org/domains/root/db/properties.html +properties + +// property : Digital Property Infrastructure Limited +// https://www.iana.org/domains/root/db/property.html +property + +// protection : XYZ.COM LLC +// https://www.iana.org/domains/root/db/protection.html +protection + +// pru : Prudential Financial, Inc. +// https://www.iana.org/domains/root/db/pru.html +pru + +// prudential : Prudential Financial, Inc. +// https://www.iana.org/domains/root/db/prudential.html +prudential + +// pub : Dog Beach, LLC +// https://www.iana.org/domains/root/db/pub.html +pub + +// pwc : PricewaterhouseCoopers LLP +// https://www.iana.org/domains/root/db/pwc.html +pwc + +// qpon : dotQPON LLC +// https://www.iana.org/domains/root/db/qpon.html +qpon + +// quebec : PointQuébec Inc +// https://www.iana.org/domains/root/db/quebec.html +quebec + +// quest : XYZ.COM LLC +// https://www.iana.org/domains/root/db/quest.html +quest + +// racing : Premier Registry Limited +// https://www.iana.org/domains/root/db/racing.html +racing + +// radio : Digity, LLC +// https://www.iana.org/domains/root/db/radio.html +radio + +// read : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/read.html +read + +// realestate : dotRealEstate LLC +// https://www.iana.org/domains/root/db/realestate.html +realestate + +// realtor : Real Estate Domains LLC +// https://www.iana.org/domains/root/db/realtor.html +realtor + +// realty : Waterford Limited +// https://www.iana.org/domains/root/db/realty.html +realty + +// recipes : Binky Moon, LLC +// https://www.iana.org/domains/root/db/recipes.html +recipes + +// red : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/red.html +red + +// redumbrella : Travelers TLD, LLC +// https://www.iana.org/domains/root/db/redumbrella.html +redumbrella + +// rehab : Dog Beach, LLC +// https://www.iana.org/domains/root/db/rehab.html +rehab + +// reise : Binky Moon, LLC +// https://www.iana.org/domains/root/db/reise.html +reise + +// reisen : Binky Moon, LLC +// https://www.iana.org/domains/root/db/reisen.html +reisen + +// reit : National Association of Real Estate Investment Trusts, Inc. +// https://www.iana.org/domains/root/db/reit.html +reit + +// reliance : Reliance Industries Limited +// https://www.iana.org/domains/root/db/reliance.html +reliance + +// ren : ZDNS International Limited +// https://www.iana.org/domains/root/db/ren.html +ren + +// rent : XYZ.COM LLC +// https://www.iana.org/domains/root/db/rent.html +rent + +// rentals : Binky Moon, LLC +// https://www.iana.org/domains/root/db/rentals.html +rentals + +// repair : Binky Moon, LLC +// https://www.iana.org/domains/root/db/repair.html +repair + +// report : Binky Moon, LLC +// https://www.iana.org/domains/root/db/report.html +report + +// republican : Dog Beach, LLC +// https://www.iana.org/domains/root/db/republican.html +republican + +// rest : Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable +// https://www.iana.org/domains/root/db/rest.html +rest + +// restaurant : Binky Moon, LLC +// https://www.iana.org/domains/root/db/restaurant.html +restaurant + +// review : dot Review Limited +// https://www.iana.org/domains/root/db/review.html +review + +// reviews : Dog Beach, LLC +// https://www.iana.org/domains/root/db/reviews.html +reviews + +// rexroth : Robert Bosch GMBH +// https://www.iana.org/domains/root/db/rexroth.html +rexroth + +// rich : iRegistry GmbH +// https://www.iana.org/domains/root/db/rich.html +rich + +// richardli : Pacific Century Asset Management (HK) Limited +// https://www.iana.org/domains/root/db/richardli.html +richardli + +// ricoh : Ricoh Company, Ltd. +// https://www.iana.org/domains/root/db/ricoh.html +ricoh + +// ril : Reliance Industries Limited +// https://www.iana.org/domains/root/db/ril.html +ril + +// rio : Empresa Municipal de Informática SA - IPLANRIO +// https://www.iana.org/domains/root/db/rio.html +rio + +// rip : Dog Beach, LLC +// https://www.iana.org/domains/root/db/rip.html +rip + +// rocks : Dog Beach, LLC +// https://www.iana.org/domains/root/db/rocks.html +rocks + +// rodeo : Registry Services, LLC +// https://www.iana.org/domains/root/db/rodeo.html +rodeo + +// rogers : Rogers Communications Canada Inc. +// https://www.iana.org/domains/root/db/rogers.html +rogers + +// room : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/room.html +room + +// rsvp : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/rsvp.html +rsvp + +// rugby : World Rugby Strategic Developments Limited +// https://www.iana.org/domains/root/db/rugby.html +rugby + +// ruhr : dotSaarland GmbH +// https://www.iana.org/domains/root/db/ruhr.html +ruhr + +// run : Binky Moon, LLC +// https://www.iana.org/domains/root/db/run.html +run + +// rwe : RWE AG +// https://www.iana.org/domains/root/db/rwe.html +rwe + +// ryukyu : BRregistry, Inc. +// https://www.iana.org/domains/root/db/ryukyu.html +ryukyu + +// saarland : dotSaarland GmbH +// https://www.iana.org/domains/root/db/saarland.html +saarland + +// safe : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/safe.html +safe + +// safety : Safety Registry Services, LLC. +// https://www.iana.org/domains/root/db/safety.html +safety + +// sakura : SAKURA Internet Inc. +// https://www.iana.org/domains/root/db/sakura.html +sakura + +// sale : Dog Beach, LLC +// https://www.iana.org/domains/root/db/sale.html +sale + +// salon : Binky Moon, LLC +// https://www.iana.org/domains/root/db/salon.html +salon + +// samsclub : Wal-Mart Stores, Inc. +// https://www.iana.org/domains/root/db/samsclub.html +samsclub + +// samsung : SAMSUNG SDS CO., LTD +// https://www.iana.org/domains/root/db/samsung.html +samsung + +// sandvik : Sandvik AB +// https://www.iana.org/domains/root/db/sandvik.html +sandvik + +// sandvikcoromant : Sandvik AB +// https://www.iana.org/domains/root/db/sandvikcoromant.html +sandvikcoromant + +// sanofi : Sanofi +// https://www.iana.org/domains/root/db/sanofi.html +sanofi + +// sap : SAP AG +// https://www.iana.org/domains/root/db/sap.html +sap + +// sarl : Binky Moon, LLC +// https://www.iana.org/domains/root/db/sarl.html +sarl + +// sas : Research IP LLC +// https://www.iana.org/domains/root/db/sas.html +sas + +// save : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/save.html +save + +// saxo : Saxo Bank A/S +// https://www.iana.org/domains/root/db/saxo.html +saxo + +// sbi : STATE BANK OF INDIA +// https://www.iana.org/domains/root/db/sbi.html +sbi + +// sbs : ShortDot SA +// https://www.iana.org/domains/root/db/sbs.html +sbs + +// scb : The Siam Commercial Bank Public Company Limited ("SCB") +// https://www.iana.org/domains/root/db/scb.html +scb + +// schaeffler : Schaeffler Technologies AG & Co. KG +// https://www.iana.org/domains/root/db/schaeffler.html +schaeffler + +// schmidt : SCHMIDT GROUPE S.A.S. +// https://www.iana.org/domains/root/db/schmidt.html +schmidt + +// scholarships : Scholarships.com, LLC +// https://www.iana.org/domains/root/db/scholarships.html +scholarships + +// school : Binky Moon, LLC +// https://www.iana.org/domains/root/db/school.html +school + +// schule : Binky Moon, LLC +// https://www.iana.org/domains/root/db/schule.html +schule + +// schwarz : Schwarz Domains und Services GmbH & Co. KG +// https://www.iana.org/domains/root/db/schwarz.html +schwarz + +// science : dot Science Limited +// https://www.iana.org/domains/root/db/science.html +science + +// scot : Dot Scot Registry Limited +// https://www.iana.org/domains/root/db/scot.html +scot + +// search : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/search.html +search + +// seat : SEAT, S.A. (Sociedad Unipersonal) +// https://www.iana.org/domains/root/db/seat.html +seat + +// secure : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/secure.html +secure + +// security : XYZ.COM LLC +// https://www.iana.org/domains/root/db/security.html +security + +// seek : Seek Limited +// https://www.iana.org/domains/root/db/seek.html +seek + +// select : Registry Services, LLC +// https://www.iana.org/domains/root/db/select.html +select + +// sener : Sener Ingeniería y Sistemas, S.A. +// https://www.iana.org/domains/root/db/sener.html +sener + +// services : Binky Moon, LLC +// https://www.iana.org/domains/root/db/services.html +services + +// seven : Seven West Media Ltd +// https://www.iana.org/domains/root/db/seven.html +seven + +// sew : SEW-EURODRIVE GmbH & Co KG +// https://www.iana.org/domains/root/db/sew.html +sew + +// sex : ICM Registry SX LLC +// https://www.iana.org/domains/root/db/sex.html +sex + +// sexy : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/sexy.html +sexy + +// sfr : Societe Francaise du Radiotelephone - SFR +// https://www.iana.org/domains/root/db/sfr.html +sfr + +// shangrila : Shangri‐La International Hotel Management Limited +// https://www.iana.org/domains/root/db/shangrila.html +shangrila + +// sharp : Sharp Corporation +// https://www.iana.org/domains/root/db/sharp.html +sharp + +// shell : Shell Information Technology International Inc +// https://www.iana.org/domains/root/db/shell.html +shell + +// shia +// https://www.iana.org/domains/root/db/shia.html +shia + +// shiksha : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/shiksha.html +shiksha + +// shoes : Binky Moon, LLC +// https://www.iana.org/domains/root/db/shoes.html +shoes + +// shop : GMO Registry, Inc. +// https://www.iana.org/domains/root/db/shop.html +shop + +// shopping : Binky Moon, LLC +// https://www.iana.org/domains/root/db/shopping.html +shopping + +// shouji : Beijing Qihu Keji Co., Ltd. +// https://www.iana.org/domains/root/db/shouji.html +shouji + +// show : Binky Moon, LLC +// https://www.iana.org/domains/root/db/show.html +show + +// silk : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/silk.html +silk + +// sina : Sina Corporation +// https://www.iana.org/domains/root/db/sina.html +sina + +// singles : Binky Moon, LLC +// https://www.iana.org/domains/root/db/singles.html +singles + +// site : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/site.html +site + +// ski : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/ski.html +ski + +// skin : XYZ.COM LLC +// https://www.iana.org/domains/root/db/skin.html +skin + +// sky : Sky UK Limited +// https://www.iana.org/domains/root/db/sky.html +sky + +// skype : Microsoft Corporation +// https://www.iana.org/domains/root/db/skype.html +skype + +// sling : DISH Technologies L.L.C. +// https://www.iana.org/domains/root/db/sling.html +sling + +// smart : Smart Communications, Inc. (SMART) +// https://www.iana.org/domains/root/db/smart.html +smart + +// smile : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/smile.html +smile + +// sncf : Société Nationale SNCF +// https://www.iana.org/domains/root/db/sncf.html +sncf + +// soccer : Binky Moon, LLC +// https://www.iana.org/domains/root/db/soccer.html +soccer + +// social : Dog Beach, LLC +// https://www.iana.org/domains/root/db/social.html +social + +// softbank : SoftBank Group Corp. +// https://www.iana.org/domains/root/db/softbank.html +softbank + +// software : Dog Beach, LLC +// https://www.iana.org/domains/root/db/software.html +software + +// sohu : Sohu.com Limited +// https://www.iana.org/domains/root/db/sohu.html +sohu + +// solar : Binky Moon, LLC +// https://www.iana.org/domains/root/db/solar.html +solar + +// solutions : Binky Moon, LLC +// https://www.iana.org/domains/root/db/solutions.html +solutions + +// song : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/song.html +song + +// sony : Sony Group Corporation +// https://www.iana.org/domains/root/db/sony.html +sony + +// soy : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/soy.html +soy + +// spa : Asia Spa and Wellness Promotion Council Limited +// https://www.iana.org/domains/root/db/spa.html +spa + +// space : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/space.html +space + +// sport : SportAccord +// https://www.iana.org/domains/root/db/sport.html +sport + +// spot : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/spot.html +spot + +// srl : InterNetX, Corp +// https://www.iana.org/domains/root/db/srl.html +srl + +// stada : STADA Arzneimittel AG +// https://www.iana.org/domains/root/db/stada.html +stada + +// staples : Staples, Inc. +// https://www.iana.org/domains/root/db/staples.html +staples + +// star : Star India Private Limited +// https://www.iana.org/domains/root/db/star.html +star + +// statebank : STATE BANK OF INDIA +// https://www.iana.org/domains/root/db/statebank.html +statebank + +// statefarm : State Farm Mutual Automobile Insurance Company +// https://www.iana.org/domains/root/db/statefarm.html +statefarm + +// stc : Saudi Telecom Company +// https://www.iana.org/domains/root/db/stc.html +stc + +// stcgroup : Saudi Telecom Company +// https://www.iana.org/domains/root/db/stcgroup.html +stcgroup + +// stockholm : Stockholms kommun +// https://www.iana.org/domains/root/db/stockholm.html +stockholm + +// storage : XYZ.COM LLC +// https://www.iana.org/domains/root/db/storage.html +storage + +// store : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/store.html +store + +// stream : dot Stream Limited +// https://www.iana.org/domains/root/db/stream.html +stream + +// studio : Dog Beach, LLC +// https://www.iana.org/domains/root/db/studio.html +studio + +// study : Registry Services, LLC +// https://www.iana.org/domains/root/db/study.html +study + +// style : Binky Moon, LLC +// https://www.iana.org/domains/root/db/style.html +style + +// sucks : Vox Populi Registry Ltd. +// https://www.iana.org/domains/root/db/sucks.html +sucks + +// supplies : Binky Moon, LLC +// https://www.iana.org/domains/root/db/supplies.html +supplies + +// supply : Binky Moon, LLC +// https://www.iana.org/domains/root/db/supply.html +supply + +// support : Binky Moon, LLC +// https://www.iana.org/domains/root/db/support.html +support + +// surf : Registry Services, LLC +// https://www.iana.org/domains/root/db/surf.html +surf + +// surgery : Binky Moon, LLC +// https://www.iana.org/domains/root/db/surgery.html +surgery + +// suzuki : SUZUKI MOTOR CORPORATION +// https://www.iana.org/domains/root/db/suzuki.html +suzuki + +// swatch : The Swatch Group Ltd +// https://www.iana.org/domains/root/db/swatch.html +swatch + +// swiss : Swiss Confederation +// https://www.iana.org/domains/root/db/swiss.html +swiss + +// sydney : State of New South Wales, Department of Premier and Cabinet +// https://www.iana.org/domains/root/db/sydney.html +sydney + +// systems : Binky Moon, LLC +// https://www.iana.org/domains/root/db/systems.html +systems + +// tab : Tabcorp Holdings Limited +// https://www.iana.org/domains/root/db/tab.html +tab + +// taipei : Taipei City Government +// https://www.iana.org/domains/root/db/taipei.html +taipei + +// talk : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/talk.html +talk + +// taobao : Alibaba Group Holding Limited +// https://www.iana.org/domains/root/db/taobao.html +taobao + +// target : Target Domain Holdings, LLC +// https://www.iana.org/domains/root/db/target.html +target + +// tatamotors : Tata Motors Ltd +// https://www.iana.org/domains/root/db/tatamotors.html +tatamotors + +// tatar : Limited Liability Company "Coordination Center of Regional Domain of Tatarstan Republic" +// https://www.iana.org/domains/root/db/tatar.html +tatar + +// tattoo : Registry Services, LLC +// https://www.iana.org/domains/root/db/tattoo.html +tattoo + +// tax : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tax.html +tax + +// taxi : Binky Moon, LLC +// https://www.iana.org/domains/root/db/taxi.html +taxi + +// tci +// https://www.iana.org/domains/root/db/tci.html +tci + +// tdk : TDK Corporation +// https://www.iana.org/domains/root/db/tdk.html +tdk + +// team : Binky Moon, LLC +// https://www.iana.org/domains/root/db/team.html +team + +// tech : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/tech.html +tech + +// technology : Binky Moon, LLC +// https://www.iana.org/domains/root/db/technology.html +technology + +// temasek : Temasek Holdings (Private) Limited +// https://www.iana.org/domains/root/db/temasek.html +temasek + +// tennis : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tennis.html +tennis + +// teva : Teva Pharmaceutical Industries Limited +// https://www.iana.org/domains/root/db/teva.html +teva + +// thd : Home Depot Product Authority, LLC +// https://www.iana.org/domains/root/db/thd.html +thd + +// theater : Binky Moon, LLC +// https://www.iana.org/domains/root/db/theater.html +theater + +// theatre : XYZ.COM LLC +// https://www.iana.org/domains/root/db/theatre.html +theatre + +// tiaa : Teachers Insurance and Annuity Association of America +// https://www.iana.org/domains/root/db/tiaa.html +tiaa + +// tickets : XYZ.COM LLC +// https://www.iana.org/domains/root/db/tickets.html +tickets + +// tienda : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tienda.html +tienda + +// tips : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tips.html +tips + +// tires : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tires.html +tires + +// tirol : punkt Tirol GmbH +// https://www.iana.org/domains/root/db/tirol.html +tirol + +// tjmaxx : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/tjmaxx.html +tjmaxx + +// tjx : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/tjx.html +tjx + +// tkmaxx : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/tkmaxx.html +tkmaxx + +// tmall : Alibaba Group Holding Limited +// https://www.iana.org/domains/root/db/tmall.html +tmall + +// today : Binky Moon, LLC +// https://www.iana.org/domains/root/db/today.html +today + +// tokyo : GMO Registry, Inc. +// https://www.iana.org/domains/root/db/tokyo.html +tokyo + +// tools : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tools.html +tools + +// top : Hong Kong Zhongze International Limited +// https://www.iana.org/domains/root/db/top.html +top + +// toray : Toray Industries, Inc. +// https://www.iana.org/domains/root/db/toray.html +toray + +// toshiba : TOSHIBA Corporation +// https://www.iana.org/domains/root/db/toshiba.html +toshiba + +// total : TotalEnergies SE +// https://www.iana.org/domains/root/db/total.html +total + +// tours : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tours.html +tours + +// town : Binky Moon, LLC +// https://www.iana.org/domains/root/db/town.html +town + +// toyota : TOYOTA MOTOR CORPORATION +// https://www.iana.org/domains/root/db/toyota.html +toyota + +// toys : Binky Moon, LLC +// https://www.iana.org/domains/root/db/toys.html +toys + +// trade : Elite Registry Limited +// https://www.iana.org/domains/root/db/trade.html +trade + +// trading : Dog Beach, LLC +// https://www.iana.org/domains/root/db/trading.html +trading + +// training : Binky Moon, LLC +// https://www.iana.org/domains/root/db/training.html +training + +// travel : Dog Beach, LLC +// https://www.iana.org/domains/root/db/travel.html +travel + +// travelers : Travelers TLD, LLC +// https://www.iana.org/domains/root/db/travelers.html +travelers + +// travelersinsurance : Travelers TLD, LLC +// https://www.iana.org/domains/root/db/travelersinsurance.html +travelersinsurance + +// trust : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/trust.html +trust + +// trv : Travelers TLD, LLC +// https://www.iana.org/domains/root/db/trv.html +trv + +// tube : Latin American Telecom LLC +// https://www.iana.org/domains/root/db/tube.html +tube + +// tui : TUI AG +// https://www.iana.org/domains/root/db/tui.html +tui + +// tunes : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/tunes.html +tunes + +// tushu : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/tushu.html +tushu + +// tvs : T V SUNDRAM IYENGAR & SONS LIMITED +// https://www.iana.org/domains/root/db/tvs.html +tvs + +// ubank : National Australia Bank Limited +// https://www.iana.org/domains/root/db/ubank.html +ubank + +// ubs : UBS AG +// https://www.iana.org/domains/root/db/ubs.html +ubs + +// unicom : China United Network Communications Corporation Limited +// https://www.iana.org/domains/root/db/unicom.html +unicom + +// university : Binky Moon, LLC +// https://www.iana.org/domains/root/db/university.html +university + +// uno : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/uno.html +uno + +// uol : UBN INTERNET LTDA. +// https://www.iana.org/domains/root/db/uol.html +uol + +// ups : UPS Market Driver, Inc. +// https://www.iana.org/domains/root/db/ups.html +ups + +// vacations : Binky Moon, LLC +// https://www.iana.org/domains/root/db/vacations.html +vacations + +// vana : D3 Registry LLC +// https://www.iana.org/domains/root/db/vana.html +vana + +// vanguard : The Vanguard Group, Inc. +// https://www.iana.org/domains/root/db/vanguard.html +vanguard + +// vegas : Dot Vegas, Inc. +// https://www.iana.org/domains/root/db/vegas.html +vegas + +// ventures : Binky Moon, LLC +// https://www.iana.org/domains/root/db/ventures.html +ventures + +// verisign : VeriSign, Inc. +// https://www.iana.org/domains/root/db/verisign.html +verisign + +// versicherung : tldbox GmbH +// https://www.iana.org/domains/root/db/versicherung.html +versicherung + +// vet : Dog Beach, LLC +// https://www.iana.org/domains/root/db/vet.html +vet + +// viajes : Binky Moon, LLC +// https://www.iana.org/domains/root/db/viajes.html +viajes + +// video : Dog Beach, LLC +// https://www.iana.org/domains/root/db/video.html +video + +// vig : VIENNA INSURANCE GROUP AG Wiener Versicherung Gruppe +// https://www.iana.org/domains/root/db/vig.html +vig + +// viking : Viking River Cruises (Bermuda) Ltd. +// https://www.iana.org/domains/root/db/viking.html +viking + +// villas : Binky Moon, LLC +// https://www.iana.org/domains/root/db/villas.html +villas + +// vin : Binky Moon, LLC +// https://www.iana.org/domains/root/db/vin.html +vin + +// vip : Registry Services, LLC +// https://www.iana.org/domains/root/db/vip.html +vip + +// virgin : Virgin Enterprises Limited +// https://www.iana.org/domains/root/db/virgin.html +virgin + +// visa : Visa Worldwide Pte. Limited +// https://www.iana.org/domains/root/db/visa.html +visa + +// vision : Binky Moon, LLC +// https://www.iana.org/domains/root/db/vision.html +vision + +// viva : Saudi Telecom Company +// https://www.iana.org/domains/root/db/viva.html +viva + +// vivo : Telefonica Brasil S.A. +// https://www.iana.org/domains/root/db/vivo.html +vivo + +// vlaanderen : DNS.be vzw +// https://www.iana.org/domains/root/db/vlaanderen.html +vlaanderen + +// vodka : Registry Services, LLC +// https://www.iana.org/domains/root/db/vodka.html +vodka + +// volvo : Volvo Holding Sverige Aktiebolag +// https://www.iana.org/domains/root/db/volvo.html +volvo + +// vote : Monolith Registry LLC +// https://www.iana.org/domains/root/db/vote.html +vote + +// voting : Valuetainment Corp. +// https://www.iana.org/domains/root/db/voting.html +voting + +// voto : Monolith Registry LLC +// https://www.iana.org/domains/root/db/voto.html +voto + +// voyage : Binky Moon, LLC +// https://www.iana.org/domains/root/db/voyage.html +voyage + +// wales : Nominet UK +// https://www.iana.org/domains/root/db/wales.html +wales + +// walmart : Wal-Mart Stores, Inc. +// https://www.iana.org/domains/root/db/walmart.html +walmart + +// walter : Sandvik AB +// https://www.iana.org/domains/root/db/walter.html +walter + +// wang : Zodiac Wang Limited +// https://www.iana.org/domains/root/db/wang.html +wang + +// wanggou : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/wanggou.html +wanggou + +// watch : Binky Moon, LLC +// https://www.iana.org/domains/root/db/watch.html +watch + +// watches : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/watches.html +watches + +// weather : The Weather Company, LLC +// https://www.iana.org/domains/root/db/weather.html +weather + +// weatherchannel : The Weather Company, LLC +// https://www.iana.org/domains/root/db/weatherchannel.html +weatherchannel + +// webcam : dot Webcam Limited +// https://www.iana.org/domains/root/db/webcam.html +webcam + +// weber : Saint-Gobain Weber SA +// https://www.iana.org/domains/root/db/weber.html +weber + +// website : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/website.html +website + +// wed +// https://www.iana.org/domains/root/db/wed.html +wed + +// wedding : Registry Services, LLC +// https://www.iana.org/domains/root/db/wedding.html +wedding + +// weibo : Sina Corporation +// https://www.iana.org/domains/root/db/weibo.html +weibo + +// weir : Weir Group IP Limited +// https://www.iana.org/domains/root/db/weir.html +weir + +// whoswho : Who's Who Registry +// https://www.iana.org/domains/root/db/whoswho.html +whoswho + +// wien : punkt.wien GmbH +// https://www.iana.org/domains/root/db/wien.html +wien + +// wiki : Registry Services, LLC +// https://www.iana.org/domains/root/db/wiki.html +wiki + +// williamhill : William Hill Organization Limited +// https://www.iana.org/domains/root/db/williamhill.html +williamhill + +// win : First Registry Limited +// https://www.iana.org/domains/root/db/win.html +win + +// windows : Microsoft Corporation +// https://www.iana.org/domains/root/db/windows.html +windows + +// wine : Binky Moon, LLC +// https://www.iana.org/domains/root/db/wine.html +wine + +// winners : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/winners.html +winners + +// wme : William Morris Endeavor Entertainment, LLC +// https://www.iana.org/domains/root/db/wme.html +wme + +// wolterskluwer : Wolters Kluwer N.V. +// https://www.iana.org/domains/root/db/wolterskluwer.html +wolterskluwer + +// woodside : Woodside Petroleum Limited +// https://www.iana.org/domains/root/db/woodside.html +woodside + +// work : Registry Services, LLC +// https://www.iana.org/domains/root/db/work.html +work + +// works : Binky Moon, LLC +// https://www.iana.org/domains/root/db/works.html +works + +// world : Binky Moon, LLC +// https://www.iana.org/domains/root/db/world.html +world + +// wow : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/wow.html +wow + +// wtc : World Trade Centers Association, Inc. +// https://www.iana.org/domains/root/db/wtc.html +wtc + +// wtf : Binky Moon, LLC +// https://www.iana.org/domains/root/db/wtf.html +wtf + +// xbox : Microsoft Corporation +// https://www.iana.org/domains/root/db/xbox.html +xbox + +// xerox : Xerox DNHC LLC +// https://www.iana.org/domains/root/db/xerox.html +xerox + +// xihuan : Beijing Qihu Keji Co., Ltd. +// https://www.iana.org/domains/root/db/xihuan.html +xihuan + +// xin : Elegant Leader Limited +// https://www.iana.org/domains/root/db/xin.html +xin + +// xn--11b4c3d : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--11b4c3d.html +कॉम + +// xn--1ck2e1b : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--1ck2e1b.html +セール + +// xn--1qqw23a : Guangzhou YU Wei Information Technology Co., Ltd. +// https://www.iana.org/domains/root/db/xn--1qqw23a.html +佛山 + +// xn--30rr7y : Excellent First Limited +// https://www.iana.org/domains/root/db/xn--30rr7y.html +慈善 + +// xn--3bst00m : Eagle Horizon Limited +// https://www.iana.org/domains/root/db/xn--3bst00m.html +集团 + +// xn--3ds443g : Beijing TLD Registry Technology Limited +// https://www.iana.org/domains/root/db/xn--3ds443g.html +在线 + +// xn--3pxu8k : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--3pxu8k.html +点看 + +// xn--42c2d9a : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--42c2d9a.html +คอม + +// xn--45q11c : Zodiac Gemini Ltd +// https://www.iana.org/domains/root/db/xn--45q11c.html +八卦 + +// xn--4gbrim : Helium TLDs Ltd +// https://www.iana.org/domains/root/db/xn--4gbrim.html +موقع + +// xn--55qw42g : China Organizational Name Administration Center +// https://www.iana.org/domains/root/db/xn--55qw42g.html +公益 + +// xn--55qx5d : China Internet Network Information Center (CNNIC) +// https://www.iana.org/domains/root/db/xn--55qx5d.html +公司 + +// xn--5su34j936bgsg : Shangri‐La International Hotel Management Limited +// https://www.iana.org/domains/root/db/xn--5su34j936bgsg.html +香格里拉 + +// xn--5tzm5g : Global Website TLD Asia Limited +// https://www.iana.org/domains/root/db/xn--5tzm5g.html +网站 + +// xn--6frz82g : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/xn--6frz82g.html +移动 + +// xn--6qq986b3xl : Tycoon Treasure Limited +// https://www.iana.org/domains/root/db/xn--6qq986b3xl.html +我爱你 + +// xn--80adxhks : Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID) +// https://www.iana.org/domains/root/db/xn--80adxhks.html +москва + +// xn--80aqecdr1a : Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +// https://www.iana.org/domains/root/db/xn--80aqecdr1a.html +католик + +// xn--80asehdb : CORE Association +// https://www.iana.org/domains/root/db/xn--80asehdb.html +онлайн + +// xn--80aswg : CORE Association +// https://www.iana.org/domains/root/db/xn--80aswg.html +сайт + +// xn--8y0a063a : China United Network Communications Corporation Limited +// https://www.iana.org/domains/root/db/xn--8y0a063a.html +联通 + +// xn--9dbq2a : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--9dbq2a.html +קום + +// xn--9et52u : RISE VICTORY LIMITED +// https://www.iana.org/domains/root/db/xn--9et52u.html +时尚 + +// xn--9krt00a : Sina Corporation +// https://www.iana.org/domains/root/db/xn--9krt00a.html +微博 + +// xn--b4w605ferd : Temasek Holdings (Private) Limited +// https://www.iana.org/domains/root/db/xn--b4w605ferd.html +淡马锡 + +// xn--bck1b9a5dre4c : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--bck1b9a5dre4c.html +ファッション + +// xn--c1avg : Public Interest Registry +// https://www.iana.org/domains/root/db/xn--c1avg.html +орг + +// xn--c2br7g : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--c2br7g.html +नेट + +// xn--cck2b3b : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--cck2b3b.html +ストア + +// xn--cckwcxetd : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--cckwcxetd.html +アマゾン + +// xn--cg4bki : SAMSUNG SDS CO., LTD +// https://www.iana.org/domains/root/db/xn--cg4bki.html +삼성 + +// xn--czr694b : Internet DotTrademark Organisation Limited +// https://www.iana.org/domains/root/db/xn--czr694b.html +商标 + +// xn--czrs0t : Binky Moon, LLC +// https://www.iana.org/domains/root/db/xn--czrs0t.html +商店 + +// xn--czru2d : Zodiac Aquarius Limited +// https://www.iana.org/domains/root/db/xn--czru2d.html +商城 + +// xn--d1acj3b : The Foundation for Network Initiatives “The Smart Internet” +// https://www.iana.org/domains/root/db/xn--d1acj3b.html +дети + +// xn--eckvdtc9d : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--eckvdtc9d.html +ポイント + +// xn--efvy88h : Guangzhou YU Wei Information Technology Co., Ltd. +// https://www.iana.org/domains/root/db/xn--efvy88h.html +新闻 + +// xn--fct429k : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--fct429k.html +家電 + +// xn--fhbei : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--fhbei.html +كوم + +// xn--fiq228c5hs : Beijing TLD Registry Technology Limited +// https://www.iana.org/domains/root/db/xn--fiq228c5hs.html +中文网 + +// xn--fiq64b : CITIC Group Corporation +// https://www.iana.org/domains/root/db/xn--fiq64b.html +中信 + +// xn--fjq720a : Binky Moon, LLC +// https://www.iana.org/domains/root/db/xn--fjq720a.html +娱乐 + +// xn--flw351e : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/xn--flw351e.html +谷歌 + +// xn--fzys8d69uvgm : PCCW Enterprises Limited +// https://www.iana.org/domains/root/db/xn--fzys8d69uvgm.html +電訊盈科 + +// xn--g2xx48c : Nawang Heli(Xiamen) Network Service Co., LTD. +// https://www.iana.org/domains/root/db/xn--g2xx48c.html +购物 + +// xn--gckr3f0f : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--gckr3f0f.html +クラウド + +// xn--gk3at1e : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--gk3at1e.html +通販 + +// xn--hxt814e : Zodiac Taurus Limited +// https://www.iana.org/domains/root/db/xn--hxt814e.html +网店 + +// xn--i1b6b1a6a2e : Public Interest Registry +// https://www.iana.org/domains/root/db/xn--i1b6b1a6a2e.html +संगठन + +// xn--imr513n : Internet DotTrademark Organisation Limited +// https://www.iana.org/domains/root/db/xn--imr513n.html +餐厅 + +// xn--io0a7i : China Internet Network Information Center (CNNIC) +// https://www.iana.org/domains/root/db/xn--io0a7i.html +网络 + +// xn--j1aef : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--j1aef.html +ком + +// xn--jlq480n2rg : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--jlq480n2rg.html +亚马逊 + +// xn--jvr189m : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--jvr189m.html +食品 + +// xn--kcrx77d1x4a : Koninklijke Philips N.V. +// https://www.iana.org/domains/root/db/xn--kcrx77d1x4a.html +飞利浦 + +// xn--kput3i : Beijing RITT-Net Technology Development Co., Ltd +// https://www.iana.org/domains/root/db/xn--kput3i.html +手机 + +// xn--mgba3a3ejt : Aramco Services Company +// https://www.iana.org/domains/root/db/xn--mgba3a3ejt.html +ارامكو + +// xn--mgba7c0bbn0a : Competrol (Luxembourg) Sarl +// https://www.iana.org/domains/root/db/xn--mgba7c0bbn0a.html +العليان + +// xn--mgbab2bd : CORE Association +// https://www.iana.org/domains/root/db/xn--mgbab2bd.html +بازار + +// xn--mgbca7dzdo : Abu Dhabi Systems and Information Centre +// https://www.iana.org/domains/root/db/xn--mgbca7dzdo.html +ابوظبي + +// xn--mgbi4ecexp : Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +// https://www.iana.org/domains/root/db/xn--mgbi4ecexp.html +كاثوليك + +// xn--mgbt3dhd +// https://www.iana.org/domains/root/db/xn--mgbt3dhd.html +همراه + +// xn--mk1bu44c : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--mk1bu44c.html +닷컴 + +// xn--mxtq1m : Net-Chinese Co., Ltd. +// https://www.iana.org/domains/root/db/xn--mxtq1m.html +政府 + +// xn--ngbc5azd : International Domain Registry Pty. Ltd. +// https://www.iana.org/domains/root/db/xn--ngbc5azd.html +شبكة + +// xn--ngbe9e0a : Kuwait Finance House +// https://www.iana.org/domains/root/db/xn--ngbe9e0a.html +بيتك + +// xn--ngbrx : League of Arab States +// https://www.iana.org/domains/root/db/xn--ngbrx.html +عرب + +// xn--nqv7f : Public Interest Registry +// https://www.iana.org/domains/root/db/xn--nqv7f.html +机构 + +// xn--nqv7fs00ema : Public Interest Registry +// https://www.iana.org/domains/root/db/xn--nqv7fs00ema.html +组织机构 + +// xn--nyqy26a : Stable Tone Limited +// https://www.iana.org/domains/root/db/xn--nyqy26a.html +健康 + +// xn--otu796d : Jiang Yu Liang Cai Technology Company Limited +// https://www.iana.org/domains/root/db/xn--otu796d.html +招聘 + +// xn--p1acf : Rusnames Limited +// https://www.iana.org/domains/root/db/xn--p1acf.html +рус + +// xn--pssy2u : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--pssy2u.html +大拿 + +// xn--q9jyb4c : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/xn--q9jyb4c.html +みんな + +// xn--qcka1pmc : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/xn--qcka1pmc.html +グーグル + +// xn--rhqv96g : Stable Tone Limited +// https://www.iana.org/domains/root/db/xn--rhqv96g.html +世界 + +// xn--rovu88b : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--rovu88b.html +書籍 + +// xn--ses554g : KNET Co., Ltd. +// https://www.iana.org/domains/root/db/xn--ses554g.html +网址 + +// xn--t60b56a : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--t60b56a.html +닷넷 + +// xn--tckwe : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--tckwe.html +コム + +// xn--tiq49xqyj : Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +// https://www.iana.org/domains/root/db/xn--tiq49xqyj.html +天主教 + +// xn--unup4y : Binky Moon, LLC +// https://www.iana.org/domains/root/db/xn--unup4y.html +游戏 + +// xn--vermgensberater-ctb : Deutsche Vermögensberatung Aktiengesellschaft DVAG +// https://www.iana.org/domains/root/db/xn--vermgensberater-ctb.html +vermögensberater + +// xn--vermgensberatung-pwb : Deutsche Vermögensberatung Aktiengesellschaft DVAG +// https://www.iana.org/domains/root/db/xn--vermgensberatung-pwb.html +vermögensberatung + +// xn--vhquv : Binky Moon, LLC +// https://www.iana.org/domains/root/db/xn--vhquv.html +企业 + +// xn--vuq861b : Beijing Tele-info Technology Co., Ltd. +// https://www.iana.org/domains/root/db/xn--vuq861b.html +信息 + +// xn--w4r85el8fhu5dnra : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/xn--w4r85el8fhu5dnra.html +嘉里大酒店 + +// xn--w4rs40l : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/xn--w4rs40l.html +嘉里 + +// xn--xhq521b : Guangzhou YU Wei Information Technology Co., Ltd. +// https://www.iana.org/domains/root/db/xn--xhq521b.html +广东 + +// xn--zfr164b : China Organizational Name Administration Center +// https://www.iana.org/domains/root/db/xn--zfr164b.html +政务 + +// xyz : XYZ.COM LLC +// https://www.iana.org/domains/root/db/xyz.html +xyz + +// yachts : XYZ.COM LLC +// https://www.iana.org/domains/root/db/yachts.html +yachts + +// yahoo : Yahoo Inc. +// https://www.iana.org/domains/root/db/yahoo.html +yahoo + +// yamaxun : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/yamaxun.html +yamaxun + +// yandex : YANDEX, LLC +// https://www.iana.org/domains/root/db/yandex.html +yandex + +// yodobashi : YODOBASHI CAMERA CO.,LTD. +// https://www.iana.org/domains/root/db/yodobashi.html +yodobashi + +// yoga : Registry Services, LLC +// https://www.iana.org/domains/root/db/yoga.html +yoga + +// yokohama : GMO Registry, Inc. +// https://www.iana.org/domains/root/db/yokohama.html +yokohama + +// you : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/you.html +you + +// youtube : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/youtube.html +youtube + +// yun : Beijing Qihu Keji Co., Ltd. +// https://www.iana.org/domains/root/db/yun.html +yun + +// zappos : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/zappos.html +zappos + +// zara : Industria de Diseño Textil, S.A. (INDITEX, S.A.) +// https://www.iana.org/domains/root/db/zara.html +zara + +// zero : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/zero.html +zero + +// zip : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/zip.html +zip + +// zone : Binky Moon, LLC +// https://www.iana.org/domains/root/db/zone.html +zone + +// zuerich : Kanton Zürich (Canton of Zurich) +// https://www.iana.org/domains/root/db/zuerich.html +zuerich + +// ===END ICANN DOMAINS=== diff --git a/client/src/test/java/org/asynchttpclient/AsyncHttpClientDefaultsTest.java b/client/src/test/java/org/asynchttpclient/AsyncHttpClientDefaultsTest.java index 8b7d172a45..6dcd27ad39 100644 --- a/client/src/test/java/org/asynchttpclient/AsyncHttpClientDefaultsTest.java +++ b/client/src/test/java/org/asynchttpclient/AsyncHttpClientDefaultsTest.java @@ -125,6 +125,11 @@ public void testDefaultHashedWheelTimerSize() { testIntegerSystemProperty("hashedWheelTimerSize", "defaultHashedWheelTimerSize", "512"); } + public void testDefaultMaxDecompressedResponseSize() { + Assert.assertEquals(AsyncHttpClientConfigDefaults.defaultMaxDecompressedResponseSize(), 256 * 1024 * 1024); + testIntegerSystemProperty("maxDecompressedResponseSize", "defaultMaxDecompressedResponseSize", "1024"); + } + private void testIntegerSystemProperty(String propertyName, String methodName, String value) { String previous = System.getProperty(ASYNC_CLIENT_CONFIG_ROOT + propertyName); System.setProperty(ASYNC_CLIENT_CONFIG_ROOT + propertyName, value); diff --git a/client/src/test/java/org/asynchttpclient/CookieStoreTest.java b/client/src/test/java/org/asynchttpclient/CookieStoreTest.java index e248e9a0c4..c5ad915487 100644 --- a/client/src/test/java/org/asynchttpclient/CookieStoreTest.java +++ b/client/src/test/java/org/asynchttpclient/CookieStoreTest.java @@ -28,14 +28,14 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.stream.Collectors; import static org.testng.Assert.assertTrue; -import com.google.common.collect.Sets; - public class CookieStoreTest { private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -55,6 +55,7 @@ public void tearDownGlobal() { public void runAllSequentiallyBecauseNotThreadSafe() throws Exception { addCookieWithEmptyPath(); dontReturnCookieForAnotherDomain(); + dontStoreCookieForUnrelatedDomainAttribute(); returnCookieWhenItWasSetOnSamePath(); returnCookieWhenItWasSetOnParentPath(); dontReturnCookieWhenDomainMatchesButPathIsDifferent(); @@ -93,6 +94,14 @@ private void addCookieWithEmptyPath() { assertTrue(store.get(uri).size() > 0); } + // rfc6265#section-5.3 step 6: a host must not be able to set a cookie for an unrelated domain + private void dontStoreCookieForUnrelatedDomainAttribute() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.evil.com/"), ClientCookieDecoder.LAX.decode("SID=attacker; Domain=victim.com")); + assertTrue(store.get(Uri.create("https://victim.com/account")).isEmpty()); + assertTrue(store.getAll().isEmpty()); + } + private void dontReturnCookieForAnotherDomain() { CookieStore store = new ThreadSafeCookieStore(); store.add(Uri.create("http://www.foo.com"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; path=")); @@ -359,7 +368,7 @@ private void shouldCleanExpiredCookieFromUnderlyingDataStructure() throws Except store.evictExpired(); assertTrue(store.getUnderlying().size() == 2); Collection unexpiredCookieNames = store.getAll().stream().map(Cookie::name).collect(Collectors.toList()); - assertTrue(unexpiredCookieNames.containsAll(Sets.newHashSet("UNEXPIRED_BAR", "UNEXPIRED_FOOBAR"))); + assertTrue(unexpiredCookieNames.containsAll(new HashSet<>(Arrays.asList("UNEXPIRED_BAR", "UNEXPIRED_FOOBAR")))); } private static Cookie getCookie(String key, String value, int maxAge) { diff --git a/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientConfigTest.java b/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientConfigTest.java new file mode 100644 index 0000000000..e069b89c6f --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientConfigTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2015-2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient; + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class DefaultAsyncHttpClientConfigTest { + + @Test + public void testStripAuthorizationOnRedirect_DefaultIsFalse() { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder().build(); + assertFalse(config.isStripAuthorizationOnRedirect(), "Default should be false"); + } + + @Test + public void testStripAuthorizationOnRedirect_SetTrue() { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setStripAuthorizationOnRedirect(true) + .build(); + assertTrue(config.isStripAuthorizationOnRedirect(), "Should be true when set"); + } + + @Test + public void testStripAuthorizationOnRedirect_SetFalse() { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setStripAuthorizationOnRedirect(false) + .build(); + assertFalse(config.isStripAuthorizationOnRedirect(), "Should be false when set to false"); + } +} diff --git a/client/src/test/java/org/asynchttpclient/DigestCnonceTest.java b/client/src/test/java/org/asynchttpclient/DigestCnonceTest.java new file mode 100644 index 0000000000..3a7f81bfe0 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/DigestCnonceTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient; + +import org.testng.annotations.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.security.SecureRandom; +import java.util.HashSet; +import java.util.Set; + +import static org.asynchttpclient.Dsl.digestAuthRealm; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +/** + * RFC 7616 section 3.3 requires the client nonce to be unpredictable: it is what stops a hostile or + * compromised server from choosing the whole digest input and precomputing responses. A non-cryptographic + * PRNG such as {@link java.util.concurrent.ThreadLocalRandom} does not provide that, so the cnonce must be + * drawn from a {@link SecureRandom}. + */ +public class DigestCnonceTest { + + /** + * Structural check: whatever generator {@code Realm.Builder} holds for the cnonce, it must be a + * {@link SecureRandom}. Deliberately does not hard-code the field name so a rename does not break it. + */ + @Test + public void cnonceIsDrawnFromASecureRandom() throws Exception { + SecureRandom found = null; + for (Field field : Realm.Builder.class.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers())) { + continue; + } + field.setAccessible(true); + Object value = field.get(null); + if (value instanceof SecureRandom) { + found = (SecureRandom) value; + } else if (value instanceof ThreadLocal) { + Object supplied = ((ThreadLocal) value).get(); + if (supplied instanceof SecureRandom) { + found = (SecureRandom) supplied; + } + } + } + + assertNotNull(found, "Realm.Builder must draw the Digest cnonce from a SecureRandom (RFC 7616 section 3.3)"); + } + + @Test + public void everyDigestRealmGetsItsOwnCnonce() { + Set cnonces = new HashSet<>(); + for (int i = 0; i < 64; i++) { + Realm realm = digestAuthRealm("user", "password") + .setRealmName("realm") + .setNonce("aabbccddeeff") + .setQop("auth") + .build(); + assertNotNull(realm.getCnonce(), "a Digest realm built against a server nonce must carry a cnonce"); + cnonces.add(realm.getCnonce()); + } + + assertTrue(cnonces.size() == 64, "each Digest realm must get a fresh cnonce, got " + cnonces.size() + " distinct out of 64"); + } +} diff --git a/client/src/test/java/org/asynchttpclient/Http1DecompressionLimitTest.java b/client/src/test/java/org/asynchttpclient/Http1DecompressionLimitTest.java new file mode 100644 index 0000000000..894a6713ab --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/Http1DecompressionLimitTest.java @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import io.netty.handler.codec.compression.DecompressionException; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timer; +import org.asynchttpclient.netty.channel.ChannelManager; +import org.asynchttpclient.netty.handler.BoundedHttpContentDecompressor; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPOutputStream; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +/** + * HTTP/1.1 automatic response decompression must honour + * {@link AsyncHttpClientConfig#getMaxDecompressedResponseSize()} cumulatively over the whole + * response, so a small, highly compressible body cannot inflate without bound. + * + *

The distinction matters. Netty's {@code HttpContentDecompressor(int maxAllocation)} caps the output + * of one {@code ZlibDecoder.decode()} call and keeps no counter across calls, and the HTTP codec + * feeds the decompressor at most {@code httpClientCodecMaxChunkSize} (8 KiB) at a time. A body with a + * merely ordinary compression ratio therefore sails straight through such a "limit" no matter how large + * its total becomes - which is what {@link #cumulativeLimitFiresEvenWhenNoSingleChunkExceedsIt()} pins + * down. Passing a non-zero maxAllocation is also unsafe: Netty 4.1 forwards it to + * {@code new BrotliDecoder(int)}, an input buffer size that is eagerly allocated as native + * memory, so a 256 MiB "ceiling" would mean a 256 MiB direct allocation per {@code br} response - see + * {@link #decompressorIsBuiltWithoutANettyMaxAllocation()}. + */ +public class Http1DecompressionLimitTest { + + // 4 MiB of a single repeated byte: gzips to a few KiB but inflates to 4 MiB. + private static final byte[] LARGE_PAYLOAD = new byte[4 * 1024 * 1024]; + + /** + * 8 MiB that gzips at a ratio of roughly 20:1. Deliberately NOT a run of one byte: at 20:1 the largest + * amount a single 8 KiB codec chunk can inflate to is about 160 KiB, which is far below both the 1 MiB + * ceiling this payload is served against and Netty's 256 MiB maxAllocation default. Only a counter that + * spans the whole response can stop it. + */ + private static final byte[] RATIO_20_PAYLOAD = ratio20Payload(8 * 1024 * 1024); + + private static final int LOW_RATIO_LIMIT = 1024 * 1024; + + static { + Arrays.fill(LARGE_PAYLOAD, (byte) 'a'); + } + + private static byte[] ratio20Payload(int size) { + byte[] bytes = new byte[size]; + Arrays.fill(bytes, (byte) 'a'); + Random random = new Random(42); // fixed seed: the compression ratio must not vary between runs + for (int i = 0; i < size; i++) { + if (random.nextInt(48) == 0) { + bytes[i] = (byte) ('b' + random.nextInt(20)); + } + } + return bytes; + } + + private static byte[] gzip(byte[] payload) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(out); + gzip.write(payload); + gzip.finish(); + gzip.close(); + return out.toByteArray(); + } + + private static HttpServer httpServer; + + @BeforeClass + public static void setupServer() throws Exception { + httpServer = HttpServer.create(new InetSocketAddress("localhost", 0), 0); + httpServer.createContext("/gzip-bomb").setHandler(gzipHandler(LARGE_PAYLOAD)); + httpServer.createContext("/gzip-ratio-20").setHandler(gzipHandler(RATIO_20_PAYLOAD)); + httpServer.start(); + } + + private static HttpHandler gzipHandler(final byte[] payload) { + return new HttpHandler() { + @Override + public void handle(HttpExchange exchange) throws IOException { + exchange.getResponseHeaders().set("Content-Encoding", "gzip"); + exchange.sendResponseHeaders(200, 0); + OutputStream out = exchange.getResponseBody(); + try { + GZIPOutputStream gzip = new GZIPOutputStream(out); + gzip.write(payload); + gzip.finish(); + gzip.close(); + } finally { + out.close(); + } + } + }; + } + + @AfterClass + public static void stopServer() { + if (httpServer != null) { + httpServer.stop(0); + } + } + + private static AsyncHttpClient clientWithLimit(int maxDecompressedResponseSize) { + return clientWithLimit(maxDecompressedResponseSize, false); + } + + private static AsyncHttpClient clientWithLimit(int maxDecompressedResponseSize, boolean keepEncodingHeader) { + AsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setCompressionEnforced(true) + .setKeepEncodingHeader(keepEncodingHeader) + .setMaxDecompressedResponseSize(maxDecompressedResponseSize) + .build(); + return new DefaultAsyncHttpClient(config); + } + + private static String url(String path) { + return "http://localhost:" + httpServer.getAddress().getPort() + path; + } + + private static boolean hasDecompressionCause(Throwable t) { + for (Throwable c = t; c != null; c = c.getCause()) { + if (c instanceof DecompressionException) { + return true; + } + } + return false; + } + + /** + * The core of the fix. The body inflates to 8 MiB against a 1 MiB ceiling, but no single decode step + * produces more than about 160 KiB, so nothing that looks only at one chunk - including Netty's + * maxAllocation - can catch it. The arithmetic is asserted, not assumed, so the payload cannot silently + * drift into being a trivially compressible one. + */ + @Test + public void cumulativeLimitFiresEvenWhenNoSingleChunkExceedsIt() throws Exception { + byte[] compressed = gzip(RATIO_20_PAYLOAD); + double ratio = (double) RATIO_20_PAYLOAD.length / compressed.length; + long largestSingleChunkInflation = (long) Math.ceil(ratio * 8192); + + assertTrue(RATIO_20_PAYLOAD.length > LOW_RATIO_LIMIT, + "the payload must exceed the ceiling in total, else the test proves nothing"); + assertTrue(largestSingleChunkInflation < LOW_RATIO_LIMIT, + "no single 8 KiB codec chunk may reach the ceiling on its own, else a per-chunk limit would " + + "also pass this test; ratio=" + ratio + ", per-chunk=" + largestSingleChunkInflation); + + try (AsyncHttpClient client = clientWithLimit(LOW_RATIO_LIMIT)) { + try { + client.prepareGet(url("/gzip-ratio-20")).execute().get(30, TimeUnit.SECONDS); + fail("a body inflating past the configured ceiling must not be delivered, even in small steps"); + } catch (ExecutionException ex) { + assertTrue(hasDecompressionCause(ex), + "expected a DecompressionException in the cause chain but got: " + ex.getCause()); + } + } + } + + @Test + public void decompressionBeyondLimitFails() throws Exception { + // 256 KiB ceiling, but the body inflates to 4 MiB. + try (AsyncHttpClient client = clientWithLimit(256 * 1024)) { + try { + client.prepareGet(url("/gzip-bomb")).execute().get(30, TimeUnit.SECONDS); + fail("a body inflating past the configured ceiling must not be delivered"); + } catch (ExecutionException ex) { + assertTrue(hasDecompressionCause(ex), + "expected a DecompressionException in the cause chain but got: " + ex.getCause()); + } + } + } + + @Test + public void decompressionWithinLimitSucceeds() throws Exception { + // A generous ceiling comfortably above the 4 MiB inflated size lets the same body through unchanged. + try (AsyncHttpClient client = clientWithLimit(64 * 1024 * 1024)) { + Response response = client.prepareGet(url("/gzip-bomb")).execute().get(30, TimeUnit.SECONDS); + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getResponseBodyAsBytes().length, LARGE_PAYLOAD.length); + } + } + + /** + * The counter must restart per response, so a client whose ceiling is above a single body's inflated + * size can serve an unbounded number of them on a pooled connection. + */ + @Test + public void limitIsPerResponseNotPerConnection() throws Exception { + try (AsyncHttpClient client = clientWithLimit(8 * 1024 * 1024)) { + for (int i = 0; i < 3; i++) { + Response response = client.prepareGet(url("/gzip-bomb")).execute().get(30, TimeUnit.SECONDS); + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getResponseBodyAsBytes().length, LARGE_PAYLOAD.length, + "response " + i + " must decompress fully; the counter must not carry over"); + } + } + } + + @Test + public void decompressionBeyondLimitFailsWithKeepEncodingHeader() throws Exception { + // keepEncodingHeader used to select a separate anonymous HttpContentDecompressor subclass; it is now a + // constructor flag, and this covers the half-fix where only one of the two branches was bounded. + try (AsyncHttpClient client = clientWithLimit(256 * 1024, true)) { + try { + client.prepareGet(url("/gzip-bomb")).execute().get(30, TimeUnit.SECONDS); + fail("the keepEncodingHeader decompressor must honour the ceiling too"); + } catch (ExecutionException ex) { + assertTrue(hasDecompressionCause(ex), + "expected a DecompressionException in the cause chain but got: " + ex.getCause()); + } + } + } + + @Test + public void keepEncodingHeaderStillKeepsTheEncodingHeader() throws Exception { + try (AsyncHttpClient client = clientWithLimit(64 * 1024 * 1024, true)) { + Response response = client.prepareGet(url("/gzip-bomb")).execute().get(30, TimeUnit.SECONDS); + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getHeader("Content-Encoding"), "gzip", + "keepEncodingHeader must leave the response advertising the encoding it arrived with"); + assertEquals(response.getResponseBodyAsBytes().length, LARGE_PAYLOAD.length); + } + } + + @Test + public void zeroDisablesTheLimit() throws Exception { + // 0 keeps the unbounded behaviour for callers that deliberately opt out. + try (AsyncHttpClient client = clientWithLimit(0)) { + Response response = client.prepareGet(url("/gzip-bomb")).execute().get(30, TimeUnit.SECONDS); + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getResponseBodyAsBytes().length, LARGE_PAYLOAD.length); + } + } + + /** + * brotli4j is not on the 2.x classpath, so a {@code br} response cannot be exercised end to end here. + * What can be pinned - and is the part that matters - is that AHC never hands Netty a non-zero + * maxAllocation. In Netty 4.1, {@code HttpContentDecompressor.newContentDecoder} passes maxAllocation to + * {@code new BrotliDecoder(int)}, whose one-argument form is {@code inputBufferSize}, and + * {@code BrotliDecoder.handlerAdded} eagerly allocates {@code new DecoderJNI.Wrapper(inputBufferSize)} in + * direct memory. With maxAllocation left at 256 MiB, any peer that answers + * {@code Content-Encoding: br} - even with an empty body - would cost the client 256 MiB of native + * memory per concurrent response: cheaper for an attacker than the bomb this limit exists to stop. + */ + @Test + public void decompressorIsBuiltWithoutANettyMaxAllocation() throws Exception { + AsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setMaxDecompressedResponseSize(256 * 1024 * 1024) + .build(); + Timer timer = new HashedWheelTimer(); + ChannelManager channelManager = new ChannelManager(config, timer); + try { + Method factory = ChannelManager.class.getDeclaredMethod("newHttpContentDecompressor"); + factory.setAccessible(true); + HttpContentDecompressor decompressor = (HttpContentDecompressor) factory.invoke(channelManager); + + assertTrue(decompressor instanceof BoundedHttpContentDecompressor, + "the pipeline must use the counting decompressor, not a bare Netty one: " + decompressor.getClass()); + + Field maxAllocation = HttpContentDecompressor.class.getDeclaredField("maxAllocation"); + maxAllocation.setAccessible(true); + assertEquals(maxAllocation.getInt(decompressor), 0, + "Netty's maxAllocation must stay 0: it does not bound a response, and a non-zero value is " + + "used as the eagerly allocated BrotliDecoder input buffer size"); + } finally { + channelManager.close(); + timer.stop(); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/HttpsDowngradeRedirectTest.java b/client/src/test/java/org/asynchttpclient/HttpsDowngradeRedirectTest.java new file mode 100644 index 0000000000..a7d25b32bc --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/HttpsDowngradeRedirectTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2015-2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.asynchttpclient.test.TestUtils.addHttpConnector; +import static org.asynchttpclient.test.TestUtils.addHttpsConnector; +import static org.testng.Assert.assertNull; + +/** + * Verifies that credentials are stripped when a redirect downgrades the scheme + * from HTTPS to HTTP, even if host and port would otherwise match. + */ +public class HttpsDowngradeRedirectTest { + + private static Server server; + private static int httpPort; + private static int httpsPort; + private static final AtomicReference authOnHttpTarget = new AtomicReference<>(); + private static final AtomicReference cookieOnHttpTarget = new AtomicReference<>(); + + @BeforeClass(alwaysRun = true) + public static void setUp() throws Exception { + server = new Server(); + ServerConnector httpConnector = addHttpConnector(server); + ServerConnector httpsConnector = addHttpsConnector(server); + + server.setHandler(new AbstractHandler() { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { + if ("/redirect".equals(target)) { + // Redirect from HTTPS to plain HTTP on the same host — a scheme downgrade. + response.setStatus(302); + response.setHeader("Location", "http://localhost:" + httpPort + "/target"); + } else if ("/target".equals(target)) { + authOnHttpTarget.set(request.getHeader("Authorization")); + cookieOnHttpTarget.set(request.getHeader("Cookie")); + response.setStatus(200); + } + baseRequest.setHandled(true); + } + }); + + server.start(); + httpPort = httpConnector.getLocalPort(); + httpsPort = httpsConnector.getLocalPort(); + } + + @AfterClass(alwaysRun = true) + public static void tearDown() throws Exception { + if (server != null) { + server.stop(); + } + } + + @Test + public void httpsToHttpDowngradeStripsAuthorization() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .setUseInsecureTrustManager(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + authOnHttpTarget.set(null); + + client.prepareGet("https://localhost:" + httpsPort + "/redirect") + .setHeader("Authorization", "Bearer secret-token") + .execute() + .get(10, TimeUnit.SECONDS); + + // HTTPS -> HTTP is a scheme downgrade: credentials must not leak to the plain-HTTP target. + assertNull(authOnHttpTarget.get(), + "Authorization header must be stripped when redirect downgrades HTTPS to HTTP"); + } + } + + /** + * HTTPS-to-HTTP downgrade also strips the Cookie header. Regression test for GHSA-fmxf-pm6p-7xgm. + */ + @Test + public void httpsToHttpDowngradeStripsCookie() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .setUseInsecureTrustManager(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + cookieOnHttpTarget.set(null); + + client.prepareGet("https://localhost:" + httpsPort + "/redirect") + .setHeader("Cookie", "session=secret-session") + .execute() + .get(10, TimeUnit.SECONDS); + + assertNull(cookieOnHttpTarget.get(), + "Cookie header must be stripped when redirect downgrades HTTPS to HTTP"); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/RealmTest.java b/client/src/test/java/org/asynchttpclient/RealmTest.java index 5f17a9b6fd..7f278cbb9e 100644 --- a/client/src/test/java/org/asynchttpclient/RealmTest.java +++ b/client/src/test/java/org/asynchttpclient/RealmTest.java @@ -24,6 +24,54 @@ import static org.testng.Assert.assertEquals; public class RealmTest { + + /** + * A Digest challenge that yields no nonce must not become a Basic one. Answering it as Basic puts the + * password on the wire in the clear, and a server cannot obtain that by offering Basic outright, + * because a Digest realm refuses a challenge that is not Digest. Omitting the nonce, or sending it + * empty, was enough to get the password out of the client. + */ + @Test + public void aDigestChallengeWithNoNonceMustNotDowngradeToBasic() { + Realm realm = new Realm.Builder("user", "pass") + .parseWWWAuthenticateHeader("Digest realm=\"protected\"") + .build(); + + assertEquals(realm.getScheme(), Realm.AuthScheme.DIGEST, + "an unreadable Digest challenge must fail, not answer in cleartext"); + } + + @Test + public void aDigestChallengeWithAnEmptyNonceMustNotDowngradeToBasic() { + Realm realm = new Realm.Builder("user", "pass") + .parseWWWAuthenticateHeader("Digest realm=\"protected\", nonce=\"\"") + .build(); + + assertEquals(realm.getScheme(), Realm.AuthScheme.DIGEST); + } + + @Test + public void aProxyDigestChallengeWithNoNonceMustNotDowngradeToBasic() { + Realm realm = new Realm.Builder("user", "pass") + .parseProxyAuthenticateHeader("Digest realm=\"protected\"") + .build(); + + assertEquals(realm.getScheme(), Realm.AuthScheme.DIGEST, + "an unreadable proxy Digest challenge must fail, not answer in cleartext"); + } + + /** + * A genuine Basic challenge is still Basic: the fix must not turn every challenge into Digest. + */ + @Test + public void aBasicChallengeIsStillBasic() { + Realm realm = new Realm.Builder("user", "pass") + .parseWWWAuthenticateHeader("Basic realm=\"protected\"") + .build(); + + assertEquals(realm.getScheme(), Realm.AuthScheme.BASIC); + } + @Test public void testClone() { Realm orig = basicAuthRealm("user", "pass").setCharset(UTF_16) diff --git a/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java b/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java new file mode 100644 index 0000000000..6b52fbf94c --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java @@ -0,0 +1,724 @@ +/* + * Copyright (c) 2015-2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient; + +import com.sun.net.httpserver.HttpServer; +import io.netty.handler.codec.http.cookie.DefaultCookie; +import org.asynchttpclient.cookie.ThreadSafeCookieStore; +import org.asynchttpclient.uri.Uri; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.asynchttpclient.Dsl.basicAuthRealm; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +/** + * Tests for credential stripping on cross-domain redirects and HTTPS-to-HTTP downgrades. + * Verifies that Authorization headers, Cookie headers, and Realm credentials are not leaked + * to different origins. + */ +public class RedirectCredentialSecurityTest { + + private static HttpServer serverA; + private static HttpServer serverB; + private static HttpServer serverC; + private static int portA; + private static int portB; + private static int portC; + private static final AtomicReference lastAuthHeaderOnA = new AtomicReference<>(); + private static final AtomicReference lastAuthHeaderOnB = new AtomicReference<>(); + private static final AtomicReference authAtChainStep2 = new AtomicReference<>(); + private static final AtomicReference authOnBounceBack = new AtomicReference<>(); + private static final AtomicReference authOn307Target = new AtomicReference<>(); + private static final AtomicReference bodyOn307Target = new AtomicReference<>(); + private static final AtomicReference authOn308Target = new AtomicReference<>(); + private static final AtomicReference bodyOn308Target = new AtomicReference<>(); + private static final AtomicReference proxyAuthOnB = new AtomicReference<>(); + private static final AtomicReference authOnHttpsDowngradeTarget = new AtomicReference<>(); + private static final AtomicReference lastCookieHeaderOnA = new AtomicReference<>(); + private static final AtomicReference lastCookieHeaderOnB = new AtomicReference<>(); + private static final AtomicReference cookieAtChainStep2 = new AtomicReference<>(); + private static final AtomicReference cookieOnBounceBack = new AtomicReference<>(); + private static final AtomicReference authOn401Target = new AtomicReference<>(); + + @BeforeClass + public static void startServers() throws Exception { + // Server A: the "origin" server that issues redirects + serverA = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + portA = serverA.getAddress().getPort(); + + // Server B: the "target" server that receives redirected requests (different port = different origin) + serverB = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + portB = serverB.getAddress().getPort(); + + // Server C: a third server for multi-hop redirect chains + serverC = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + portC = serverC.getAddress().getPort(); + + // Server A endpoints + serverA.createContext("/redirect-to-b", exchange -> { + lastAuthHeaderOnA.set(exchange.getRequestHeaders().getFirst("Authorization")); + lastCookieHeaderOnA.set(exchange.getRequestHeaders().getFirst("Cookie")); + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + serverA.createContext("/redirect-same-origin", exchange -> { + lastAuthHeaderOnA.set(exchange.getRequestHeaders().getFirst("Authorization")); + lastCookieHeaderOnA.set(exchange.getRequestHeaders().getFirst("Cookie")); + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portA + "/final"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + serverA.createContext("/final", exchange -> { + lastAuthHeaderOnA.set(exchange.getRequestHeaders().getFirst("Authorization")); + lastCookieHeaderOnA.set(exchange.getRequestHeaders().getFirst("Cookie")); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + exchange.close(); + }); + + // Server B endpoints + serverB.createContext("/target", exchange -> { + lastAuthHeaderOnB.set(exchange.getRequestHeaders().getFirst("Authorization")); + lastCookieHeaderOnB.set(exchange.getRequestHeaders().getFirst("Cookie")); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + exchange.close(); + }); + + // Multi-hop: A -> A (same-origin) -> B (cross-domain) + serverA.createContext("/chain-same-then-cross", exchange -> { + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portA + "/chain-step2"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + serverA.createContext("/chain-step2", exchange -> { + authAtChainStep2.set(exchange.getRequestHeaders().getFirst("Authorization")); + cookieAtChainStep2.set(exchange.getRequestHeaders().getFirst("Cookie")); + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + // Multi-hop: A -> B (cross-domain, credentials stripped) -> C (credentials stay stripped) + serverA.createContext("/chain-cross-and-back", exchange -> { + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/bounce-to-c"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + serverB.createContext("/bounce-to-c", exchange -> { + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portC + "/chain-final"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + serverC.createContext("/chain-final", exchange -> { + authOnBounceBack.set(exchange.getRequestHeaders().getFirst("Authorization")); + cookieOnBounceBack.set(exchange.getRequestHeaders().getFirst("Cookie")); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + exchange.close(); + }); + + // 307 Temporary Redirect: A -> B (body preserved, auth stripped) + serverA.createContext("/redirect-307-to-b", exchange -> { + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target-307"); + exchange.sendResponseHeaders(307, -1); + exchange.close(); + }); + + serverB.createContext("/target-307", exchange -> { + authOn307Target.set(exchange.getRequestHeaders().getFirst("Authorization")); + bodyOn307Target.set(readAll(exchange.getRequestBody())); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + exchange.close(); + }); + + // 308 Permanent Redirect: A -> B (body preserved, auth stripped) + serverA.createContext("/redirect-308-to-b", exchange -> { + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target-308"); + exchange.sendResponseHeaders(308, -1); + exchange.close(); + }); + + serverB.createContext("/target-308", exchange -> { + authOn308Target.set(exchange.getRequestHeaders().getFirst("Authorization")); + bodyOn308Target.set(readAll(exchange.getRequestBody())); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + exchange.close(); + }); + + // Proxy-Authorization cross-domain redirect: A -> B + serverA.createContext("/redirect-to-b-proxy", exchange -> { + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target-proxy"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + serverB.createContext("/target-proxy", exchange -> { + proxyAuthOnB.set(exchange.getRequestHeaders().getFirst("Proxy-Authorization")); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + exchange.close(); + }); + + // Cross-domain redirect to a target that answers 401: the target must never receive + // credentials, even those configured client-wide via config.setRealm(...). + serverA.createContext("/redirect-to-b-401", exchange -> { + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target-401"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + serverB.createContext("/target-401", exchange -> { + String auth = exchange.getRequestHeaders().getFirst("Authorization"); + if (auth != null) { + authOn401Target.set(auth); + } + exchange.getResponseHeaders().add("WWW-Authenticate", "Basic realm=\"target\""); + // Streamed framing (length 0 plus a closed body), which this server emits as Transfer-Encoding: + // chunked with a terminating chunk, so the connection survives the exchange. + // + // What decides that is the getResponseBody().close() below, not the 0. Measured against + // com.sun.net.httpserver on JDK 11.0.31 and JDK 25: with this handler otherwise unchanged, + // sendResponseHeaders(401, -1) reuses the connection too, the authenticated retry this test is + // watching for still reaches this handler, and the leak is still observed. Only dropping the body + // close strands it, and even then nothing is reset - -1 sends Content-Length: 0 and at worst a + // clean FIN. On the client side Unauthorized401Interceptor opens a fresh connection whenever the + // channel is not reusable, so a closed idle socket would not strand the retry either. + exchange.sendResponseHeaders(401, 0); + exchange.getResponseBody().close(); + exchange.close(); + }); + + serverA.start(); + serverB.start(); + serverC.start(); + } + + @AfterClass + public static void stopServers() { + serverA.stop(0); + serverB.stop(0); + serverC.stop(0); + } + + private static String readAll(InputStream in) throws java.io.IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + + /** + * Cross-domain redirect (different port) must strip Authorization header. + */ + @Test + public void crossDomainRedirectStripsAuthHeader() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeaderOnA.set(null); + lastAuthHeaderOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b") + .setHeader("Authorization", "Bearer secret-token") + .execute() + .get(5, TimeUnit.SECONDS); + + // Auth header should be present on the original request to server A + assertEquals(lastAuthHeaderOnA.get(), "Bearer secret-token", + "Authorization header should be present on original request"); + // Auth header must NOT be forwarded to the cross-domain target (server B) + assertNull(lastAuthHeaderOnB.get(), + "Authorization header must be stripped on cross-domain redirect"); + } + } + + /** + * Same-origin redirect (same host and port) should preserve Authorization header. + */ + @Test + public void sameOriginRedirectPreservesAuthHeader() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeaderOnA.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-same-origin") + .setHeader("Authorization", "Bearer secret-token") + .execute() + .get(5, TimeUnit.SECONDS); + + // Auth header should still be present after same-origin redirect + assertEquals(lastAuthHeaderOnA.get(), "Bearer secret-token", + "Authorization header should be preserved on same-origin redirect"); + } + } + + /** + * Cross-domain redirect must strip Proxy-Authorization header too. + */ + @Test + public void crossDomainRedirectStripsProxyAuthHeader() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + proxyAuthOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b-proxy") + .setHeader("Proxy-Authorization", "Basic cHJveHk6cGFzcw==") + .execute() + .get(5, TimeUnit.SECONDS); + + assertNull(proxyAuthOnB.get(), + "Proxy-Authorization header must be stripped on cross-domain redirect"); + } + } + + /** + * Realm-based BASIC auth credentials must NOT be propagated on cross-domain redirects. + * This tests that the Realm object is cleared, preventing credential regeneration + * via NettyRequestFactory. + */ + @Test + public void crossDomainRedirectDoesNotPropagateRealm() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeaderOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b") + .setRealm(basicAuthRealm("user", "password").setUsePreemptiveAuth(true).build()) + .execute() + .get(5, TimeUnit.SECONDS); + + // The Realm-generated Authorization header must NOT appear on the cross-domain target + assertNull(lastAuthHeaderOnB.get(), + "Realm-based credentials must not be propagated on cross-domain redirect"); + } + } + + /** + * Realm bypass is closed: even with stripAuthorizationOnRedirect=true on a same-origin redirect, + * the Realm credential regeneration must be prevented (no Authorization header on target). + */ + @Test + public void stripAuthorizationOnRedirectAlsoStripsRealm() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .setStripAuthorizationOnRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeaderOnA.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-same-origin") + .setRealm(basicAuthRealm("user", "password").setUsePreemptiveAuth(true).build()) + .execute() + .get(5, TimeUnit.SECONDS); + + // Even on same-origin, with stripAuthorizationOnRedirect=true, the Realm-regenerated + // Authorization header must NOT appear (this was the bypass bug) + assertNull(lastAuthHeaderOnA.get(), + "stripAuthorizationOnRedirect=true must also prevent Realm-based credential regeneration"); + } + } + + /** + * Same-origin redirect should preserve Realm-based credentials when stripping is not enabled. + */ + @Test + public void sameOriginRedirectPreservesRealmCredentials() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeaderOnA.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-same-origin") + .setRealm(basicAuthRealm("user", "password").setUsePreemptiveAuth(true).build()) + .execute() + .get(5, TimeUnit.SECONDS); + + // On same-origin, Realm credentials should be preserved + assertEquals(lastAuthHeaderOnA.get(), "Basic dXNlcjpwYXNzd29yZA==", + "Realm-based credentials should be preserved on same-origin redirect"); + } + } + + /** + * Multi-hop redirect: A -> A (same-origin) -> B (cross-domain). + * Credentials should survive the same-origin hop but be stripped on the cross-domain hop. + */ + @Test + public void multiHopSameOriginThenCrossDomainStripsCredentials() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + authAtChainStep2.set(null); + lastAuthHeaderOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/chain-same-then-cross") + .setHeader("Authorization", "Bearer secret-token") + .execute() + .get(5, TimeUnit.SECONDS); + + // Credentials should survive the same-origin intermediate hop (A -> A) + assertEquals(authAtChainStep2.get(), "Bearer secret-token", + "Authorization header should be preserved on same-origin intermediate redirect"); + // Credentials must be stripped on the final cross-domain hop (A -> B) + assertNull(lastAuthHeaderOnB.get(), + "Authorization header must be stripped on cross-domain hop in redirect chain"); + } + } + + /** + * Multi-hop redirect: A -> B (cross-domain) -> C (another domain). + * Once credentials are stripped at the first cross-domain hop, they must not reappear + * on subsequent hops even if the Realm was originally set. + */ + @Test + public void multiHopCredentialsStayStrippedAfterCrossDomain() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + authOnBounceBack.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/chain-cross-and-back") + .setRealm(basicAuthRealm("user", "password").setUsePreemptiveAuth(true).build()) + .execute() + .get(5, TimeUnit.SECONDS); + + // Credentials were stripped at A -> B; they must not reappear at B -> C + assertNull(authOnBounceBack.get(), + "Credentials must not reappear after being stripped at a cross-domain hop"); + } + } + + /** + * 307 Temporary Redirect cross-domain: body is preserved but Authorization is stripped. + */ + @Test + public void redirect307CrossDomainStripsAuthButPreservesBody() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + authOn307Target.set(null); + bodyOn307Target.set(null); + + client.preparePost("http://127.0.0.1:" + portA + "/redirect-307-to-b") + .setHeader("Authorization", "Bearer secret-token") + .setBody("request-body-content") + .execute() + .get(5, TimeUnit.SECONDS); + + assertNull(authOn307Target.get(), + "Authorization header must be stripped on cross-domain 307 redirect"); + assertEquals(bodyOn307Target.get(), "request-body-content", + "Request body must be preserved on 307 redirect"); + } + } + + /** + * 308 Permanent Redirect cross-domain: body is preserved but Authorization is stripped. + */ + @Test + public void redirect308CrossDomainStripsAuthButPreservesBody() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + authOn308Target.set(null); + bodyOn308Target.set(null); + + client.preparePost("http://127.0.0.1:" + portA + "/redirect-308-to-b") + .setHeader("Authorization", "Bearer secret-token") + .setBody("request-body-content") + .execute() + .get(5, TimeUnit.SECONDS); + + assertNull(authOn308Target.get(), + "Authorization header must be stripped on cross-domain 308 redirect"); + assertEquals(bodyOn308Target.get(), "request-body-content", + "Request body must be preserved on 308 redirect"); + } + } + + /** + * Cross-domain redirect (different port) must strip a user-supplied Cookie header. + * Regression test for GHSA-fmxf-pm6p-7xgm. + */ + @Test + public void crossDomainRedirectStripsCookieHeader() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastCookieHeaderOnA.set(null); + lastCookieHeaderOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b") + .setHeader("Cookie", "session=abc123; csrf=xyz789") + .execute() + .get(5, TimeUnit.SECONDS); + + // Cookie should be present on the original request to server A + assertEquals(lastCookieHeaderOnA.get(), "session=abc123; csrf=xyz789", + "Cookie header should be present on original request"); + // Cookie must NOT be forwarded to the cross-domain target (server B) + assertNull(lastCookieHeaderOnB.get(), + "Cookie header must be stripped on cross-domain redirect"); + } + } + + /** + * Same-origin redirect (same host and port) should preserve the Cookie header. + */ + @Test + public void sameOriginRedirectPreservesCookieHeader() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastCookieHeaderOnA.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-same-origin") + .setHeader("Cookie", "session=abc123") + .execute() + .get(5, TimeUnit.SECONDS); + + assertEquals(lastCookieHeaderOnA.get(), "session=abc123", + "Cookie header should be preserved on same-origin redirect"); + } + } + + /** + * Cross-domain redirect must strip both Authorization and Cookie when both are set. + * Combined regression that mirrors the original PoC. + */ + @Test + public void crossDomainRedirectStripsBothCookieAndAuthorization() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeaderOnA.set(null); + lastAuthHeaderOnB.set(null); + lastCookieHeaderOnA.set(null); + lastCookieHeaderOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b") + .setHeader("Authorization", "Bearer token123") + .setHeader("Cookie", "session=abc123; api_key=secret") + .execute() + .get(5, TimeUnit.SECONDS); + + assertEquals(lastAuthHeaderOnA.get(), "Bearer token123", + "Authorization header should be present on original request"); + assertEquals(lastCookieHeaderOnA.get(), "session=abc123; api_key=secret", + "Cookie header should be present on original request"); + assertNull(lastAuthHeaderOnB.get(), + "Authorization header must be stripped on cross-domain redirect"); + assertNull(lastCookieHeaderOnB.get(), + "Cookie header must be stripped on cross-domain redirect"); + } + } + + /** + * Multi-hop: A -> A (same-origin, Cookie preserved) -> B (cross-domain, Cookie stripped). + */ + @Test + public void multiHopChainStripsCookieAtFirstCrossOriginHop() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + cookieAtChainStep2.set(null); + lastCookieHeaderOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/chain-same-then-cross") + .setHeader("Cookie", "session=abc123") + .execute() + .get(5, TimeUnit.SECONDS); + + // Cookie should survive the same-origin intermediate hop (A -> A) + assertEquals(cookieAtChainStep2.get(), "session=abc123", + "Cookie header should be preserved on same-origin intermediate redirect"); + // Cookie must be stripped on the cross-domain hop (A -> B) + assertNull(lastCookieHeaderOnB.get(), + "Cookie header must be stripped on cross-domain hop in redirect chain"); + } + } + + /** + * Once Cookie is stripped at a cross-domain hop, it must not reappear on subsequent hops. + */ + @Test + public void multiHopCookieStaysStrippedAfterCrossDomain() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + cookieOnBounceBack.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/chain-cross-and-back") + .setHeader("Cookie", "session=abc123") + .execute() + .get(5, TimeUnit.SECONDS); + + assertNull(cookieOnBounceBack.get(), + "Cookie must not reappear after being stripped at a cross-domain hop"); + } + } + + /** + * setStripAuthorizationOnRedirect(true) also strips Cookie on same-origin redirects: + * the conditions are coupled, so users opting into strict credential stripping get cookie + * stripping on the same-origin path too. + */ + @Test + public void stripAuthorizationOnRedirectFlagAlsoStripsCookie() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .setStripAuthorizationOnRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastCookieHeaderOnA.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-same-origin") + .setHeader("Cookie", "session=abc123") + .execute() + .get(5, TimeUnit.SECONDS); + + // With stripAuthorizationOnRedirect=true, even same-origin redirects strip the Cookie + assertNull(lastCookieHeaderOnA.get(), + "stripAuthorizationOnRedirect=true must also strip Cookie on same-origin redirect"); + } + } + + /** + * Regression: a cookie added to the URI-scoped CookieStore for server B is still delivered + * to server B after a cross-origin redirect from A -> B. Cookie stripping must not break the + * legitimate cookie-store flow (store cookies are added after the strip step in + * Redirect30xInterceptor and are URI-matched). + */ + @Test + public void cookieStoreManagedCookiesUnaffectedByStrip() throws Exception { + ThreadSafeCookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://127.0.0.1:" + portB + "/target"), + new DefaultCookie("store_cookie", "store_value")); + + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .setCookieStore(store) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastCookieHeaderOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b") + .execute() + .get(5, TimeUnit.SECONDS); + + // The store cookie scoped to server B's URI should reach server B + assertNotNull(lastCookieHeaderOnB.get(), + "URI-scoped CookieStore cookies should be delivered after cross-domain redirect"); + assertTrue(lastCookieHeaderOnB.get().contains("store_cookie=store_value"), + "Expected store cookie in Cookie header, got: " + lastCookieHeaderOnB.get()); + } + } + + /** + * Same host (127.0.0.1) on a different port is treated as cross-origin: both Cookie and + * Authorization are stripped. Locks in the port-as-part-of-origin behavior so that a future + * change to {@link Uri#isSameBase} cannot accidentally narrow the origin to host-only. + */ + @Test + public void portChangeOnSameHostIsTreatedAsCrossOrigin() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeaderOnB.set(null); + lastCookieHeaderOnB.set(null); + + // /redirect-to-b: 127.0.0.1:portA -> 127.0.0.1:portB. Same host, different port. + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b") + .setHeader("Authorization", "Bearer same-host-token") + .setHeader("Cookie", "session=same-host-cookie") + .execute() + .get(5, TimeUnit.SECONDS); + + assertNull(lastAuthHeaderOnB.get(), + "Authorization must be stripped when only the port differs (origin includes port)"); + assertNull(lastCookieHeaderOnB.get(), + "Cookie must be stripped when only the port differs (origin includes port)"); + } + } + + /** + * Client-wide credentials set via {@code config.setRealm(...)} must not be sent to a cross-domain + * redirect target, even when that target answers 401 to solicit them. The redirect clears the + * request/future realm, but the config realm must not be re-applied. + */ + @Test + public void crossDomainRedirectTo401TargetDoesNotLeakConfigRealm() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .setRealm(basicAuthRealm("user", "password").build()) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + authOn401Target.set(null); + + Response response = client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b-401") + .execute() + .get(5, TimeUnit.SECONDS); + + // The redirect is followed and the 401 is returned to the caller: the exchange completes either + // way, so a leak cannot hide behind a failed request. + assertEquals(response.getStatusCode(), 401); + assertNull(authOn401Target.get(), + "client-wide config Realm must not be sent to a cross-domain 401 target after redirect"); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/StripAuthorizationOnRedirectHttpTest.java b/client/src/test/java/org/asynchttpclient/StripAuthorizationOnRedirectHttpTest.java new file mode 100644 index 0000000000..722612e1b6 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/StripAuthorizationOnRedirectHttpTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2015-2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +public class StripAuthorizationOnRedirectHttpTest { + private static HttpServer server; + private static int port; + private static volatile String lastAuthHeader; + + @BeforeClass + public static void startServer() throws Exception { + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + port = server.getAddress().getPort(); + server.createContext("/redirect", new RedirectHandler()); + server.createContext("/final", new FinalHandler()); + server.start(); + } + + @AfterClass + public static void stopServer() { + server.stop(0); + } + + static class RedirectHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) { + String auth = exchange.getRequestHeaders().getFirst("Authorization"); + lastAuthHeader = auth; + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + port + "/final"); + try { + exchange.sendResponseHeaders(302, -1); + } catch (Exception ignored) { + } + exchange.close(); + } + } + + static class FinalHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) { + String auth = exchange.getRequestHeaders().getFirst("Authorization"); + lastAuthHeader = auth; + try { + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + } catch (Exception ignored) { + } + exchange.close(); + } + } + + @Test + public void testAuthHeaderPropagatedByDefault() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeader = null; + client.prepareGet("http://127.0.0.1:" + port + "/redirect") + .setHeader("Authorization", "Bearer testtoken") + .execute() + .get(5, TimeUnit.SECONDS); + // Same-origin (same host:port): default should preserve Authorization on /final + assertEquals(lastAuthHeader, "Bearer testtoken", + "Authorization header should be present on same-origin redirect by default"); + } + } + + @Test + public void testAuthHeaderStrippedWhenEnabled() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .setStripAuthorizationOnRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeader = null; + client.prepareGet("http://127.0.0.1:" + port + "/redirect") + .setHeader("Authorization", "Bearer testtoken") + .execute() + .get(5, TimeUnit.SECONDS); + // When enabled, Authorization header must be stripped even on same-origin redirects + assertNull(lastAuthHeader, "Authorization header should be stripped on redirect when enabled"); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/cookie/PublicSuffixCookieTest.java b/client/src/test/java/org/asynchttpclient/cookie/PublicSuffixCookieTest.java new file mode 100644 index 0000000000..7abe39f67e --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/cookie/PublicSuffixCookieTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient.cookie; + +import io.netty.handler.codec.http.cookie.DefaultCookie; +import org.asynchttpclient.uri.Uri; +import org.testng.annotations.Test; + +import java.util.List; +import java.util.Locale; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * RFC 6265 Section 5.3 step 5: a cookie whose {@code Domain} names a public suffix must be ignored. + * Without it, one site under a registry can plant a cookie that every other site under that registry + * receives. Step 6, which asks only whether the request host sits under the Domain, does not catch it. + */ +public class PublicSuffixCookieTest { + + private static boolean reaches(String setterHost, String domainAttribute, String victimHost) { + ThreadSafeCookieStore store = new ThreadSafeCookieStore(); + DefaultCookie cookie = new DefaultCookie("SID", "planted"); + if (domainAttribute != null) { + cookie.setDomain(domainAttribute); + } + cookie.setPath("/"); + store.add(Uri.create("http://" + setterHost + "/"), cookie); + + List got = store.get(Uri.create("http://" + victimHost + "/")); + return got.stream().anyMatch(c -> "SID".equals(c.name())); + } + + @Test + public void aCookieForAPublicSuffixIsNotPlanted() { + assertFalse(reaches("evil.co.uk", "co.uk", "bank.co.uk"), + "a host under co.uk must not set a cookie for co.uk itself"); + assertFalse(reaches("evil.co.uk", "uk", "bank.co.uk"), + "a host must not set a cookie for a bare TLD"); + assertFalse(reaches("evil.com", "com", "bank.com"), + "a host must not set a cookie for com"); + } + + /** + * The existing cookie-tossing guard must keep working: this is the case #2196 fixed, and it is what + * shows the new check is not the only thing standing between these two hosts. + */ + @Test + public void theExistingCrossSiteGuardStillHolds() { + assertFalse(reaches("evil.co.uk", "bank.co.uk", "bank.co.uk"), + "one host must not set a cookie naming an unrelated host"); + } + + /** + * Ordinary cookies must be unaffected, or every user loses their session handling. + */ + @Test + public void ordinaryCookiesAreUnaffected() { + assertTrue(reaches("bank.co.uk", "bank.co.uk", "www.bank.co.uk"), + "a site must still set a cookie for its own registrable domain"); + assertTrue(reaches("bank.co.uk", null, "bank.co.uk"), + "a host-only cookie must still be returned to that host"); + assertTrue(reaches("www.example.com", "example.com", "api.example.com"), + "a site must still share a cookie across its own subdomains"); + } + + /** + * RFC 6265 section 5.3 step 5 keeps a cookie whose Domain equals the request host, as a host-only + * cookie. Dropping it would break ordinary single-label hosts, because dev, app, box, cloud and a + * dozen more are ICANN suffixes as well as the short names Docker Compose and Kubernetes hand out. + */ + @Test + public void aSingleLabelHostCanStillSetItsOwnCookie() { + for (String host : new String[]{"dev", "app", "box", "cloud", "build", "run"}) { + assertTrue(PublicSuffixList.isPublicSuffix(host), host + " is expected to be an ICANN suffix"); + assertTrue(reaches(host, host, host), + "a host whose own name is a public suffix must still set a cookie for itself: " + host); + } + } + + /** + * The locale must not decide whether the check engages. Under Turkish the default lowercasing turns I + * into a dotless i, so every I-initial suffix would stop matching and the guard would fail open. + */ + @Test + public void matchingDoesNotDependOnTheDefaultLocale() { + Locale original = Locale.getDefault(); + try { + Locale.setDefault(new Locale("tr", "TR")); + assertTrue(PublicSuffixList.isPublicSuffix("INFO"), "INFO must match under a Turkish locale"); + assertTrue(PublicSuffixList.isPublicSuffix("CO.IN"), "CO.IN must match under a Turkish locale"); + assertFalse(reaches("evil.co.in", "co.in", "bank.co.in"), + "the guard must hold under a Turkish locale"); + } finally { + Locale.setDefault(original); + } + } + + @Test + public void theListRecognisesSuffixesAndRegistrableDomains() { + assertTrue(PublicSuffixList.isPublicSuffix("co.uk")); + assertTrue(PublicSuffixList.isPublicSuffix("com")); + assertTrue(PublicSuffixList.isPublicSuffix("com.au")); + assertTrue(PublicSuffixList.isPublicSuffix("CO.UK"), "matching must be case-insensitive"); + + assertFalse(PublicSuffixList.isPublicSuffix("bank.co.uk")); + assertFalse(PublicSuffixList.isPublicSuffix("example.com")); + assertFalse(PublicSuffixList.isPublicSuffix("")); + assertFalse(PublicSuffixList.isPublicSuffix(null)); + } +} diff --git a/client/src/test/java/org/asynchttpclient/filter/CrossHostReplayTest.java b/client/src/test/java/org/asynchttpclient/filter/CrossHostReplayTest.java new file mode 100644 index 0000000000..50e67d70d3 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/filter/CrossHostReplayTest.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.filter; + +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.Dsl; +import org.asynchttpclient.Realm; +import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.proxy.ProxyServer; +import org.testng.annotations.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; + +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * A {@link ResponseFilter} that replays onto a different host is the documented way to do + * failover. The request the future was built for and the request it ends up sending then disagree, and + * anything derived from the target has to move with it. + *

+ * The existing replay tests all replay to the same host, so nothing here was covered: the future kept + * describing the first origin, which sent that origin's credentials to the second one and filed the second + * one's socket in the connection pool under the first one's key. + */ +public class CrossHostReplayTest { + + private static final String SECRET = "s3cr3t-for-A"; + + /** + * A tiny origin that records the request line and Authorization header of everything it receives. + */ + private static final class Recorder implements AutoCloseable { + final ServerSocket socket = new ServerSocket(0); + final List seen = new CopyOnWriteArrayList<>(); + private final int status; + + Recorder(int status) throws IOException { + this.status = status; + Thread t = new Thread(this::serve); + t.setDaemon(true); + t.start(); + } + + int port() { + return socket.getLocalPort(); + } + + private void serve() { + while (!socket.isClosed()) { + final Socket s; + try { + s = socket.accept(); + } catch (IOException e) { + return; + } + // One thread per connection, and the connection is kept open across requests. Without + // keep-alive nothing is ever offered to the connection pool, and a test about pool keys + // would pass no matter what the product code did. + Thread worker = new Thread(() -> handle(s)); + worker.setDaemon(true); + worker.start(); + } + } + + private void handle(Socket s) { + try (Socket conn = s) { + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), ISO_8859_1)); + while (true) { + String requestLine = in.readLine(); + if (requestLine == null) { + return; + } + String auth = null; + String line; + while ((line = in.readLine()) != null && !line.isEmpty()) { + if (line.toLowerCase().startsWith("authorization:")) { + auth = line.substring("authorization:".length()).trim(); + } + } + seen.add(requestLine.split(" ")[1] + " auth=" + (auth == null ? "" : auth)); + conn.getOutputStream().write( + ("HTTP/1.1 " + status + " X\r\nContent-Length: 0\r\n\r\n").getBytes(ISO_8859_1)); + conn.getOutputStream().flush(); + } + } catch (IOException ignored) { + // client hung up + } + } + + @Override + public void close() throws IOException { + socket.close(); + } + } + + /** + * Replays onto {@code url} when the origin answers 503. ResponseFilter.filter is generic, so this + * cannot be a lambda. + */ + private static ResponseFilter failoverTo(String url) { + return new ResponseFilter() { + @Override + public FilterContext filter(FilterContext ctx) { + if (ctx.getResponseStatus() != null && ctx.getResponseStatus().getStatusCode() == 503) { + return new FilterContext.FilterContextBuilder<>(ctx) + .request(new RequestBuilder("GET").setUrl(url).build()) + .replayRequest(true) + .build(); + } + return ctx; + } + }; + } + + /** + * Replays onto {@code url} through the proxy on {@code proxyPort} when the origin answers 503. + */ + private static ResponseFilter failoverVia(String url, int proxyPort) { + return new ResponseFilter() { + @Override + public FilterContext filter(FilterContext ctx) { + if (ctx.getResponseStatus() != null && ctx.getResponseStatus().getStatusCode() == 503) { + return new FilterContext.FilterContextBuilder<>(ctx) + .request(new RequestBuilder("GET").setUrl(url) + .setProxyServer(new ProxyServer.Builder("127.0.0.1", proxyPort).build()) + .build()) + .replayRequest(true) + .build(); + } + return ctx; + } + }; + } + + private static Realm realmForA() { + return Dsl.basicAuthRealm("alice", SECRET).setUsePreemptiveAuth(true).build(); + } + + /** + * The credentials configured for the first origin must not be sent to the host the request is replayed + * onto. The replay carries no realm of its own, and the previous origin's must not be reused for it. + */ + @Test + public void replayToADifferentHostDoesNotCarryTheFirstHostsCredentials() throws Exception { + try (Recorder a = new Recorder(503); Recorder b = new Recorder(200)) { + String urlB = "http://127.0.0.1:" + b.port() + "/failover"; + ResponseFilter failover = failoverTo(urlB); + + try (AsyncHttpClient client = Dsl.asyncHttpClient( + Dsl.config().setMaxRequestRetry(0).addResponseFilter(failover))) { + client.prepareGet("http://127.0.0.1:" + a.port() + "/probe") + .setRealm(realmForA()) + .execute().get(30, TimeUnit.SECONDS); + } + + assertEquals(b.seen.size(), 1, "the failover host should have been called exactly once: " + b.seen); + assertTrue(b.seen.get(0).endsWith("auth="), + "the first host's credentials must not follow the replay: " + b.seen.get(0)); + } + } + + /** + * The socket opened for the replay target must be filed in the connection pool under that target, not + * under the origin the future was created for. Filed under the wrong key, a later request the + * application addresses to the first host is served over the connection to the second one, and the + * first host's credentials go there with it. + */ + @Test + public void replayToADifferentHostDoesNotFileTheSocketUnderTheFirstHostsKey() throws Exception { + try (Recorder a = new Recorder(503); Recorder b = new Recorder(200)) { + String urlB = "http://127.0.0.1:" + b.port() + "/failover"; + ResponseFilter failover = failoverTo(urlB); + + try (AsyncHttpClient client = Dsl.asyncHttpClient( + Dsl.config().setMaxRequestRetry(0).addResponseFilter(failover))) { + client.prepareGet("http://127.0.0.1:" + a.port() + "/probe") + .setRealm(realmForA()) + .execute().get(30, TimeUnit.SECONDS); + + // Addressed to A. If the replay left B's socket pooled under A's key, this is served by B. + client.prepareGet("http://127.0.0.1:" + a.port() + "/admin") + .setRealm(realmForA()) + .execute().get(30, TimeUnit.SECONDS); + } + + assertTrue(b.seen.stream().noneMatch(r -> r.startsWith("/admin")), + "a request addressed to the first host was served by the failover host: " + b.seen); + assertTrue(b.seen.stream().noneMatch(r -> r.contains(SECRET) || r.contains("YWxpY2U6")), + "the first host's credentials reached the failover host: " + b.seen); + } + } + + /** + * A replay that routes through a proxy must file its socket under a key naming that proxy. The proxy is + * half of the connection pool key and is memoized alongside the target, so moving the target while the + * proxy still names the previous route files a proxied connection under the direct key for the new + * host. The next request the application sends directly to that host then draws a socket that runs + * through the proxy, and its credentials go to the proxy. + */ + @Test + public void replayThroughAProxyDoesNotFileTheProxiedSocketAsDirect() throws Exception { + try (Recorder a = new Recorder(503); Recorder proxy = new Recorder(200); Recorder b = new Recorder(200)) { + String urlB = "http://127.0.0.1:" + b.port() + "/failover"; + + try (AsyncHttpClient client = Dsl.asyncHttpClient( + Dsl.config().setMaxRequestRetry(0).addResponseFilter(failoverVia(urlB, proxy.port())))) { + // Direct to A, which fails over onto B through the proxy. + client.prepareGet("http://127.0.0.1:" + a.port() + "/probe") + .setRealm(realmForA()) + .execute().get(30, TimeUnit.SECONDS); + + // Addressed directly to B, no proxy. Must not reuse the connection that runs through one. + client.prepareGet("http://127.0.0.1:" + b.port() + "/admin") + .setRealm(realmForA()) + .execute().get(30, TimeUnit.SECONDS); + } + + assertTrue(proxy.seen.stream().noneMatch(r -> r.contains("/admin")), + "a request addressed directly to the origin was served over the pooled proxy connection: " + + proxy.seen); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessorStoreTest.java b/client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessorStoreTest.java new file mode 100644 index 0000000000..4b907fcddf --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessorStoreTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.handler.resumable; + +import org.testng.SkipException; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Map; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * Covers how the resumable index store is created in the shared temp directory. The store sits at a fixed, + * predictable path that every local user can write to, so it must be created owner-only and must never be + * written or read through something another user planted there. + */ +public class PropertiesBasedResumableProcessorStoreTest { + + private static final Path STORE = Paths.get(System.getProperty("java.io.tmpdir"), "ahc", "ResumableAsyncHandler.properties"); + + private static void requirePosix() { + if (!FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { + throw new SkipException("POSIX file permissions are not supported on this file system"); + } + } + + private static void deleteStore() throws IOException { + Files.deleteIfExists(STORE); + } + + @Test + public void storeIsCreatedOwnerOnly() throws IOException { + requirePosix(); + deleteStore(); + + PropertiesBasedResumableProcessor processor = new PropertiesBasedResumableProcessor(); + processor.put("http://localhost/owner-only.url", 15L); + processor.save(null); + + assertEquals(PosixFilePermissions.toString(Files.getPosixFilePermissions(STORE)), "rw-------", + "the store must be created owner-readable/writable only"); + } + + @Test + public void saveDoesNotWriteThroughASymlink() throws IOException { + requirePosix(); + deleteStore(); + Files.createDirectories(STORE.getParent()); + + Path target = Files.createTempFile("ahc-symlink-target", ".txt"); + try { + Files.write(target, "untouched".getBytes(StandardCharsets.UTF_8)); + Files.createSymbolicLink(STORE, target); + + PropertiesBasedResumableProcessor processor = new PropertiesBasedResumableProcessor(); + processor.put("http://localhost/symlink.url", 15L); + processor.save(null); + + assertEquals(new String(Files.readAllBytes(target), StandardCharsets.UTF_8), "untouched", + "a symlink planted at the store path must not be followed and its target must not be truncated"); + } finally { + Files.deleteIfExists(target); + deleteStore(); + } + } + + @Test + public void loadDoesNotReadThroughASymlink() throws IOException { + requirePosix(); + deleteStore(); + Files.createDirectories(STORE.getParent()); + + Path target = Files.createTempFile("ahc-symlink-source", ".txt"); + try { + Files.write(target, "http://localhost/planted.url=42\n".getBytes(StandardCharsets.UTF_8)); + Files.createSymbolicLink(STORE, target); + + Map loaded = new PropertiesBasedResumableProcessor().load(); + + assertTrue(loaded.isEmpty(), "state must not be read back through a planted symlink, got " + loaded); + } finally { + Files.deleteIfExists(target); + deleteStore(); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/netty/channel/PooledConnectionIdentityTest.java b/client/src/test/java/org/asynchttpclient/netty/channel/PooledConnectionIdentityTest.java new file mode 100644 index 0000000000..bbdd6c33ca --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/channel/PooledConnectionIdentityTest.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient.netty.channel; + +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.Dsl; +import org.asynchttpclient.Realm; +import org.testng.annotations.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static org.testng.Assert.assertEquals; + +/** + * A connection authenticated by NTLM or Negotiate belongs to the identity that authenticated it, because + * those schemes authenticate the socket rather than the request. Handing it to another principal makes the + * server serve that request as the first principal, with nothing on the wire to show it. + *

+ * Counting distinct accepted sockets is what makes this observable: reuse and isolation are invisible in + * the responses, and the offer and poll sides deriving their key differently shows up only here. + */ +public class PooledConnectionIdentityTest { + + /** Keep-alive origin that records how many distinct connections it accepted. */ + private static final class CountingServer implements AutoCloseable { + final ServerSocket socket = new ServerSocket(0); + final Set connections = ConcurrentHashMap.newKeySet(); + + CountingServer() throws IOException { + Thread t = new Thread(this::accept); + t.setDaemon(true); + t.start(); + } + + int port() { + return socket.getLocalPort(); + } + + private void accept() { + while (!socket.isClosed()) { + final Socket s; + try { + s = socket.accept(); + } catch (IOException e) { + return; + } + connections.add(s.getPort()); + Thread w = new Thread(() -> serve(s)); + w.setDaemon(true); + w.start(); + } + } + + private void serve(Socket s) { + try (Socket conn = s) { + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), ISO_8859_1)); + String line; + while ((line = in.readLine()) != null) { + if (line.isEmpty()) { + conn.getOutputStream().write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n".getBytes(ISO_8859_1)); + conn.getOutputStream().flush(); + } + } + } catch (IOException ignored) { + // client hung up + } + } + + @Override + public void close() throws IOException { + socket.close(); + } + } + + private static Realm ntlm(String principal) { + return new Realm.Builder(principal, "secret") + .setScheme(Realm.AuthScheme.NTLM) + .setUsePreemptiveAuth(true) + .setNtlmDomain("DOMAIN") + .setNtlmHost("host") + .build(); + } + + @Test + public void twoPrincipalsDoNotShareAPooledConnection() throws Exception { + try (CountingServer server = new CountingServer(); + AsyncHttpClient client = Dsl.asyncHttpClient(Dsl.config().setMaxRequestRetry(0))) { + String url = "http://127.0.0.1:" + server.port() + "/"; + + client.prepareGet(url).setRealm(ntlm("alice")).execute().get(30, TimeUnit.SECONDS); + client.prepareGet(url).setRealm(ntlm("bob")).execute().get(30, TimeUnit.SECONDS); + + assertEquals(server.connections.size(), 2, + "bob must not be served over the socket alice authenticated"); + } + } + + /** + * The isolation must not cost every other request its connection reuse, and one identity must keep + * reusing its own. + */ + @Test + public void oneIdentityStillReusesItsOwnConnection() throws Exception { + try (CountingServer server = new CountingServer(); + AsyncHttpClient client = Dsl.asyncHttpClient(Dsl.config().setMaxRequestRetry(0))) { + String url = "http://127.0.0.1:" + server.port() + "/"; + + client.prepareGet(url).setRealm(ntlm("alice")).execute().get(30, TimeUnit.SECONDS); + client.prepareGet(url).setRealm(ntlm("alice")).execute().get(30, TimeUnit.SECONDS); + + assertEquals(server.connections.size(), 1, + "alice's second request must reuse her own pooled connection"); + } + } + + @Test + public void requestsWithoutARealmStillReuseAConnection() throws Exception { + try (CountingServer server = new CountingServer(); + AsyncHttpClient client = Dsl.asyncHttpClient(Dsl.config().setMaxRequestRetry(0))) { + String url = "http://127.0.0.1:" + server.port() + "/"; + + client.prepareGet(url).execute().get(30, TimeUnit.SECONDS); + client.prepareGet(url).execute().get(30, TimeUnit.SECONDS); + + assertEquals(server.connections.size(), 1, "ordinary pooling must be unaffected"); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/netty/channel/PrincipalScopedPartitionKeyTest.java b/client/src/test/java/org/asynchttpclient/netty/channel/PrincipalScopedPartitionKeyTest.java new file mode 100644 index 0000000000..296c51d27f --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/channel/PrincipalScopedPartitionKeyTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient.netty.channel; + +import org.asynchttpclient.Dsl; +import org.asynchttpclient.Realm; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertSame; + +/** + * NTLM and Negotiate authenticate the connection rather than the request, so two principals must never + * share a pooled socket. Schemes that authenticate each request must keep sharing them, or every one of + * them loses connection reuse. + */ +public class PrincipalScopedPartitionKeyTest { + + private static final Object BASE = "host:443"; + + private static Realm realm(Realm.AuthScheme scheme, String principal) { + return new Realm.Builder(principal, "secret").setScheme(scheme).build(); + } + + @Test + public void twoPrincipalsOnAConnectionScopedSchemeDoNotShareAKey() { + for (Realm.AuthScheme scheme : new Realm.AuthScheme[]{ + Realm.AuthScheme.NTLM, Realm.AuthScheme.KERBEROS, Realm.AuthScheme.SPNEGO}) { + Object alice = PrincipalScopedPartitionKey.scope(BASE, realm(scheme, "alice")); + Object bob = PrincipalScopedPartitionKey.scope(BASE, realm(scheme, "bob")); + + assertNotEquals(alice, bob, scheme + ": one principal's connection must not be reused by another"); + assertNotEquals(BASE, alice, scheme + ": the scoped key must differ from the unscoped one"); + } + } + + @Test + public void theSamePrincipalKeepsTheSameKey() { + Object first = PrincipalScopedPartitionKey.scope(BASE, realm(Realm.AuthScheme.NTLM, "alice")); + Object second = PrincipalScopedPartitionKey.scope(BASE, realm(Realm.AuthScheme.NTLM, "alice")); + + assertEquals(first, second, "the same principal must keep reusing its own connections"); + assertEquals(second.hashCode(), first.hashCode()); + } + + /** + * Basic and Digest send credentials with every request, so their connections are not tied to an + * identity. Scoping them would cost reuse for no benefit. + */ + @Test + public void requestScopedSchemesAreLeftAlone() { + assertSame(BASE, PrincipalScopedPartitionKey.scope(BASE, realm(Realm.AuthScheme.BASIC, "alice"))); + assertSame(BASE, PrincipalScopedPartitionKey.scope(BASE, realm(Realm.AuthScheme.DIGEST, "alice"))); + assertSame(BASE, PrincipalScopedPartitionKey.scope(BASE, Dsl.basicAuthRealm("alice", "s").build())); + } + + @Test + public void noRealmAndNoPrincipalAreLeftAlone() { + assertSame(BASE, PrincipalScopedPartitionKey.scope(BASE, null)); + assertSame(BASE, PrincipalScopedPartitionKey.scope(BASE, + new Realm.Builder(null, null).setScheme(Realm.AuthScheme.NTLM).build())); + } + + /** + * Different hosts must stay separate even for one principal, or the scoping would collapse the + * distinction the base key exists to make. + */ + @Test + public void theBaseKeyStillSeparatesHosts() { + Realm alice = realm(Realm.AuthScheme.NTLM, "alice"); + + assertNotEquals(PrincipalScopedPartitionKey.scope("host-a:443", alice), + PrincipalScopedPartitionKey.scope("host-b:443", alice)); + } +} diff --git a/client/src/test/java/org/asynchttpclient/netty/handler/WebSocketHandlerDoneFutureTest.java b/client/src/test/java/org/asynchttpclient/netty/handler/WebSocketHandlerDoneFutureTest.java new file mode 100644 index 0000000000..b7f4a3fcbe --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/handler/WebSocketHandlerDoneFutureTest.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.handler; + +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timer; +import org.asynchttpclient.AsyncHttpClientConfig; +import org.asynchttpclient.AsyncHttpClientState; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; +import org.asynchttpclient.Request; +import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.channel.ChannelPoolPartitioning; +import org.asynchttpclient.netty.NettyResponseFuture; +import org.asynchttpclient.netty.channel.ChannelManager; +import org.asynchttpclient.netty.channel.Channels; +import org.asynchttpclient.netty.request.NettyRequest; +import org.asynchttpclient.netty.request.NettyRequestSender; +import org.asynchttpclient.netty.ws.NettyWebSocket; +import org.asynchttpclient.ws.WebSocket; +import org.asynchttpclient.ws.WebSocketListener; +import org.asynchttpclient.ws.WebSocketUpgradeHandler; +import org.asynchttpclient.ws.WebSocketUtils; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.lang.reflect.Constructor; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; +import static io.netty.handler.codec.http.HttpHeaderNames.SEC_WEBSOCKET_ACCEPT; +import static io.netty.handler.codec.http.HttpHeaderNames.SEC_WEBSOCKET_KEY; +import static io.netty.handler.codec.http.HttpHeaderNames.UPGRADE; +import static io.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS; +import static org.asynchttpclient.ws.WebSocketUtils.getAcceptKey; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * A WebSocket upgrade response that lands on a future which is already finished must be dropped. + * {@link NettyResponseFuture#abort} and {@link NettyResponseFuture#cancel} both mark the future done before + * (cancel) or without (a direct abort) marking the channel discarded, so the event loop can still find the + * future as the channel attribute and deliver the 101 into {@code handleRead}. Without a guard the handler + * upgrades the pipeline and fires {@code onOpen} on a listener that has already had {@code onThrowable}: + * the caller is told the request failed and then handed a live WebSocket. + * + *

The window is a race in production, so it is reproduced here at the handler seam, where the state can + * be set up exactly. The two upgrade-still-works cases are not decoration: the guard MUST stay scoped to + * the {@code HttpResponse} branch, because {@code upgrade()} ends with {@code future.done()} and a + * whole-method guard would therefore drop every frame of every healthy WebSocket. + */ +public class WebSocketHandlerDoneFutureTest { + + private Timer timer; + private ChannelManager channelManager; + private WebSocketHandler handler; + private EmbeddedChannel channel; + private String webSocketKey; + + private final List events = new CopyOnWriteArrayList<>(); + + private final WebSocketListener listener = new WebSocketListener() { + @Override + public void onOpen(WebSocket websocket) { + events.add("onOpen"); + } + + @Override + public void onClose(WebSocket websocket, int code, String reason) { + events.add("onClose"); + } + + @Override + public void onError(Throwable t) { + events.add("onError"); + } + + @Override + public void onTextFrame(String payload, boolean finalFragment, int rsv) { + events.add("onTextFrame:" + payload); + } + }; + + // NettyRequest and AsyncHttpClientState are package-private to their own packages; a WebSocketHandler + // cannot be exercised in isolation without them. + private static NettyRequest newNettyRequest(HttpRequest httpRequest) throws Exception { + Constructor constructor = NettyRequest.class.getDeclaredConstructor(HttpRequest.class, + Class.forName("org.asynchttpclient.netty.request.body.NettyBody")); + constructor.setAccessible(true); + return constructor.newInstance(httpRequest, null); + } + + private static AsyncHttpClientState newClientState() throws Exception { + Constructor constructor = AsyncHttpClientState.class.getDeclaredConstructor(AtomicBoolean.class); + constructor.setAccessible(true); + return constructor.newInstance(new AtomicBoolean(false)); + } + + @BeforeMethod + public void setUp() throws Exception { + AsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder().build(); + timer = new HashedWheelTimer(); + channelManager = new ChannelManager(config, timer); + NettyRequestSender requestSender = new NettyRequestSender(config, channelManager, timer, newClientState()); + handler = new WebSocketHandler(config, channelManager, requestSender); + + channel = new EmbeddedChannel(); + // upgradePipelineForWebSockets() inserts the WebSocket codecs relative to the HTTP codec. + channel.pipeline().addLast(ChannelManager.HTTP_CLIENT_CODEC, new HttpClientCodec()); + + webSocketKey = WebSocketUtils.getWebSocketKey(); + events.clear(); + } + + @AfterMethod + public void tearDown() { + if (channel != null) { + channel.finishAndReleaseAll(); + } + if (channelManager != null) { + channelManager.close(); + } + if (timer != null) { + timer.stop(); + } + } + + private NettyResponseFuture newFuture() throws Exception { + HttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); + httpRequest.headers().set(SEC_WEBSOCKET_KEY, webSocketKey); + + Request request = new RequestBuilder("GET").setUrl("ws://localhost:8080/").build(); + WebSocketUpgradeHandler upgradeHandler = new WebSocketUpgradeHandler.Builder() + .addWebSocketListener(listener) + .build(); + + NettyResponseFuture future = new NettyResponseFuture<>(request, + upgradeHandler, + newNettyRequest(httpRequest), + 0, + ChannelPoolPartitioning.PerHostChannelPoolPartitioning.INSTANCE, + null, + null); + Channels.setAttribute(channel, future); + return future; + } + + /** + * A textbook-valid 101: nothing but the state of the future may stop the upgrade. + */ + private HttpResponse newUpgradeResponse() { + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, SWITCHING_PROTOCOLS); + response.headers() + .set(UPGRADE, "websocket") + .set(CONNECTION, "Upgrade") + .set(SEC_WEBSOCKET_ACCEPT, getAcceptKey(webSocketKey)); + return response; + } + + @Test + public void doesNotUpgradeWhenTheFutureIsAlreadyDone() throws Exception { + NettyResponseFuture future = newFuture(); + + // What a request timeout leaves behind: the exchange is over and the listener has been told. + future.abort(new TimeoutException("Request timeout")); + assertTrue(future.isDone()); + assertEquals(events, java.util.Collections.singletonList("onError")); + + handler.handleRead(channel, future, newUpgradeResponse()); + + assertFalse(events.contains("onOpen"), + "onOpen must not fire after the future was aborted, but got: " + events); + assertFalse(channel.isOpen(), "the orphaned channel must be closed rather than left upgraded"); + } + + @Test + public void stillUpgradesWhenTheFutureIsLive() throws Exception { + NettyResponseFuture future = newFuture(); + + handler.handleRead(channel, future, newUpgradeResponse()); + + assertTrue(events.contains("onOpen"), "a valid 101 on a live future must still upgrade, got: " + events); + assertFalse(events.contains("onError"), "the successful upgrade must not report an error: " + events); + } + + /** + * upgrade() completes the future, so isDone() is true for the entire life of a healthy WebSocket. The + * guard must not reach the frame branch. + */ + @Test + public void stillDeliversFramesAfterTheUpgradeCompletedTheFuture() throws Exception { + NettyResponseFuture future = newFuture(); + handler.handleRead(channel, future, newUpgradeResponse()); + assertTrue(future.isDone(), "upgrade() is expected to complete the future"); + + handler.handleRead(channel, future, new TextWebSocketFrame("hello")); + + assertTrue(events.contains("onTextFrame:hello"), + "frames on an established WebSocket must still be delivered, got: " + events); + } +} diff --git a/client/src/test/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407InterceptorTest.java b/client/src/test/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407InterceptorTest.java new file mode 100644 index 0000000000..5715f7b135 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407InterceptorTest.java @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.handler.intercept; + +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timer; +import org.asynchttpclient.AsyncHandler; +import org.asynchttpclient.AsyncHttpClientConfig; +import org.asynchttpclient.AsyncHttpClientState; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; +import org.asynchttpclient.Realm; +import org.asynchttpclient.Request; +import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.Response; +import org.asynchttpclient.channel.ChannelPoolPartitioning; +import org.asynchttpclient.AsyncCompletionHandlerBase; +import org.asynchttpclient.netty.NettyResponseFuture; +import org.asynchttpclient.netty.channel.ChannelManager; +import org.asynchttpclient.netty.request.NettyRequest; +import org.asynchttpclient.netty.request.NettyRequestSender; +import org.asynchttpclient.proxy.ProxyServer; +import org.asynchttpclient.proxy.ProxyType; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.lang.reflect.Constructor; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.asynchttpclient.Dsl.basicAuthRealm; +import static org.asynchttpclient.Dsl.proxyServer; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * A 407 is an HTTP-proxy mechanism. A SOCKS proxy negotiates at the transport layer and never writes an + * HTTP status line, so a 407 arriving over one was written by the ORIGIN - and answering it hands the + * proxy's credentials to that origin. + * + *

The preemptive path is already gated by proxy type in NettyRequestFactory and NettyRequestSender (see + * {@code SocksProxyCredentialLeakTest}), but this interceptor bypassed both: its NTLM and Kerberos/SPNEGO + * branches write {@code Proxy-Authorization} straight onto the request headers, and {@code newNettyRequest} + * copies request headers verbatim afterwards. + */ +public class ProxyUnauthorized407InterceptorTest { + + private Timer timer; + private ChannelManager channelManager; + private ProxyUnauthorized407Interceptor interceptor; + private EmbeddedChannel channel; + + private static AsyncHttpClientState newClientState() throws Exception { + Constructor constructor = AsyncHttpClientState.class.getDeclaredConstructor(AtomicBoolean.class); + constructor.setAccessible(true); + return constructor.newInstance(new AtomicBoolean(false)); + } + + private static NettyRequest newNettyRequest(HttpRequest httpRequest) throws Exception { + Constructor constructor = NettyRequest.class.getDeclaredConstructor(HttpRequest.class, + Class.forName("org.asynchttpclient.netty.request.body.NettyBody")); + constructor.setAccessible(true); + return constructor.newInstance(httpRequest, null); + } + + @BeforeMethod + public void setUp() throws Exception { + AsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder().build(); + timer = new HashedWheelTimer(); + channelManager = new ChannelManager(config, timer); + NettyRequestSender requestSender = new NettyRequestSender(config, channelManager, timer, newClientState()); + interceptor = new ProxyUnauthorized407Interceptor(channelManager, requestSender); + channel = new EmbeddedChannel(); + } + + @AfterMethod + public void tearDown() { + if (channel != null) { + channel.finishAndReleaseAll(); + } + if (channelManager != null) { + channelManager.close(); + } + if (timer != null) { + timer.stop(); + } + } + + private static Realm nonPreemptiveProxyRealm() { + return basicAuthRealm("proxy-user", "proxy-secret").build(); + } + + private NettyResponseFuture newFuture(ProxyServer proxyServer, Realm proxyRealm) throws Exception { + return newFuture(proxyServer, proxyRealm, "http://origin.example.com/resource", HttpMethod.GET); + } + + private NettyResponseFuture newFuture(ProxyServer proxyServer, + Realm proxyRealm, + String url, + HttpMethod method) throws Exception { + Request request = new RequestBuilder(method.name()).setUrl(url).build(); + HttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, + method == HttpMethod.CONNECT ? "origin.example.com:443" : "/resource"); + AsyncHandler handler = new AsyncCompletionHandlerBase(); + + NettyResponseFuture future = new NettyResponseFuture<>(request, + handler, + newNettyRequest(httpRequest), + 0, + ChannelPoolPartitioning.PerHostChannelPoolPartitioning.INSTANCE, + null, + proxyServer); + future.setProxyRealm(proxyRealm); + return future; + } + + private static HttpResponse new407() { + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED); + response.headers().set(HttpHeaderNames.PROXY_AUTHENTICATE, "Basic realm=\"proxy\""); + return response; + } + + private boolean handle407(ProxyServer proxyServer, Realm proxyRealm) throws Exception { + NettyResponseFuture future = newFuture(proxyServer, proxyRealm); + return interceptor.exitAfterHandling407(channel, + future, + new407(), + future.getCurrentRequest(), + proxyServer, + future.getNettyRequest().getHttpRequest()); + } + + @Test + public void socks5Proxy407IsNotAnswered() throws Exception { + Realm proxyRealm = nonPreemptiveProxyRealm(); + ProxyServer socks = proxyServer("proxy.example.com", 1080).setProxyType(ProxyType.SOCKS_V5).setRealm(proxyRealm).build(); + + assertFalse(handle407(socks, proxyRealm), + "a 407 over a SOCKS proxy came from the origin and must not be answered with proxy credentials"); + } + + @Test + public void socks4Proxy407IsNotAnswered() throws Exception { + Realm proxyRealm = nonPreemptiveProxyRealm(); + ProxyServer socks = proxyServer("proxy.example.com", 1080).setProxyType(ProxyType.SOCKS_V4).setRealm(proxyRealm).build(); + + assertFalse(handle407(socks, proxyRealm), + "a 407 over a SOCKS proxy came from the origin and must not be answered with proxy credentials"); + } + + @Test + public void noProxyAtAllMeans407CameFromTheOrigin() throws Exception { + // A realm can be carried on the future without a ProxyServer being in play at all. + assertFalse(handle407(null, nonPreemptiveProxyRealm()), + "with no proxy configured a 407 can only have come from the origin"); + } + + /** + * The regression guard: an HTTP proxy's 407 is still answered, and the proxy realm is switched to + * preemptive so the retried request carries Proxy-Authorization. + */ + @Test + public void httpProxy407IsStillAnswered() throws Exception { + Realm proxyRealm = nonPreemptiveProxyRealm(); + ProxyServer http = proxyServer("proxy.example.com", 8080).setRealm(proxyRealm).build(); + NettyResponseFuture future = newFuture(http, proxyRealm); + + boolean handled = interceptor.exitAfterHandling407(channel, + future, + new407(), + future.getCurrentRequest(), + http, + future.getNettyRequest().getHttpRequest()); + + assertTrue(handled, "an HTTP proxy's 407 must still be answered"); + assertTrue(future.getProxyRealm().isUsePreemptiveAuth(), + "the retried request must carry the proxy credentials"); + } + + /** + * Once a CONNECT has succeeded the peer on the far end of the socket is the ORIGIN, not the proxy, so a + * 407 arriving there was written by the origin. The proxy type is still HTTP, which is why asking what + * kind of proxy is configured does not answer the question that matters - who wrote this response. + */ + @Test + public void origin407InsideAnEstablishedTunnelIsNotAnswered() throws Exception { + Realm proxyRealm = nonPreemptiveProxyRealm(); + ProxyServer http = proxyServer("proxy.example.com", 8080).setRealm(proxyRealm).build(); + NettyResponseFuture future = newFuture(http, proxyRealm, "https://origin.example.com/resource", HttpMethod.GET); + future.setTunnelEstablished(true); + + boolean handled = interceptor.exitAfterHandling407(channel, + future, + new407(), + future.getCurrentRequest(), + http, + future.getNettyRequest().getHttpRequest()); + + assertFalse(handled, + "a 407 seen inside an established tunnel was written by the origin and must not be answered " + + "with the proxy's credentials"); + assertFalse(future.getProxyRealm().isUsePreemptiveAuth(), + "the proxy realm must not be armed to send Proxy-Authorization to the origin"); + } + + /** + * The same request one exchange later. When the tunnelled channel comes from the pool the tunnel was + * established by an EARLIER future, so tunnelEstablished is false on this one - but the target still + * says what the socket must be: a secured or WebSocket target behind an HTTP proxy is only ever reached + * through a CONNECT, so anything that is not the CONNECT itself is talking to the origin. + */ + @Test + public void origin407OnATunnelInheritedFromThePoolIsNotAnswered() throws Exception { + Realm proxyRealm = nonPreemptiveProxyRealm(); + ProxyServer http = proxyServer("proxy.example.com", 8080).setRealm(proxyRealm).build(); + NettyResponseFuture future = newFuture(http, proxyRealm, "https://origin.example.com/resource", HttpMethod.GET); + + assertFalse(future.isTunnelEstablished(), "this exchange did not build the tunnel itself"); + assertFalse(interceptor.exitAfterHandling407(channel, + future, + new407(), + future.getCurrentRequest(), + http, + future.getNettyRequest().getHttpRequest()), + "a secured target behind an HTTP proxy is reached through a tunnel, so a 407 on anything but " + + "the CONNECT came from the origin"); + } + + /** + * The regression guard for the flow the 407 mechanism exists for: the proxy challenges the CONNECT + * itself, which is addressed to it and travels in the clear. + */ + @Test + public void proxy407OnTheConnectIsStillAnswered() throws Exception { + Realm proxyRealm = nonPreemptiveProxyRealm(); + ProxyServer http = proxyServer("proxy.example.com", 8080).setRealm(proxyRealm).build(); + NettyResponseFuture future = newFuture(http, proxyRealm, "https://origin.example.com/resource", HttpMethod.CONNECT); + + assertTrue(interceptor.exitAfterHandling407(channel, + future, + new407(), + future.getCurrentRequest(), + http, + future.getNettyRequest().getHttpRequest()), + "the proxy's challenge to the CONNECT must still be answered"); + assertTrue(future.getProxyRealm().isUsePreemptiveAuth(), + "the retried CONNECT must carry the proxy credentials"); + } + + /** + * A 407 this interceptor refuses to answer must not consume the exchange's one-shot proxy-auth latch + * either. Burning it means a later, legitimate proxy challenge on the same exchange is declined with + * "auth was already performed" when no proxy auth was ever performed at all. + */ + @Test + public void a407ThisInterceptorRefusesDoesNotBurnTheProxyAuthLatch() throws Exception { + Realm proxyRealm = nonPreemptiveProxyRealm(); + ProxyServer socks = proxyServer("proxy.example.com", 1080).setProxyType(ProxyType.SOCKS_V5).setRealm(proxyRealm).build(); + NettyResponseFuture future = newFuture(socks, proxyRealm); + + assertFalse(interceptor.exitAfterHandling407(channel, + future, + new407(), + future.getCurrentRequest(), + socks, + future.getNettyRequest().getHttpRequest())); + + assertFalse(future.isInProxyAuth(), + "declining a 407 must leave the proxy-auth latch untouched, since no proxy auth was performed"); + } +} diff --git a/client/src/test/java/org/asynchttpclient/netty/request/ConnectRequestAuthorizationTest.java b/client/src/test/java/org/asynchttpclient/netty/request/ConnectRequestAuthorizationTest.java new file mode 100644 index 0000000000..4743da73d9 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/request/ConnectRequestAuthorizationTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.request; + +import io.netty.handler.codec.http.HttpHeaderNames; +import org.asynchttpclient.Realm; +import org.asynchttpclient.Request; +import org.asynchttpclient.proxy.ProxyServer; +import org.testng.annotations.Test; + +import static org.asynchttpclient.Dsl.basicAuthRealm; +import static org.asynchttpclient.Dsl.config; +import static org.asynchttpclient.Dsl.get; +import static org.asynchttpclient.Dsl.proxyServer; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * A CONNECT request opens the proxy tunnel and is sent to the proxy in the clear, so it must not carry the + * origin {@code Authorization} header (which would expose the origin credentials to the proxy). The header + * belongs only on the request sent through the established tunnel. + */ +public class ConnectRequestAuthorizationTest { + + private static NettyRequestFactory factory() { + return new NettyRequestFactory(config().build()); + } + + @Test + public void connectRequestDoesNotCarryOriginAuthorization() { + Request request = get("https://origin.example.com/resource").build(); + Realm realm = basicAuthRealm("user", "secret").setUsePreemptiveAuth(true).build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest connect = factory().newNettyRequest(request, true, proxy, realm, null); + + assertFalse(connect.getHttpRequest().headers().contains(HttpHeaderNames.AUTHORIZATION), + "CONNECT request must not expose the origin Authorization to the proxy"); + } + + @Test + public void tunneledRequestKeepsOriginAuthorization() { + Request request = get("https://origin.example.com/resource").build(); + Realm realm = basicAuthRealm("user", "secret").setUsePreemptiveAuth(true).build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest tunneled = factory().newNettyRequest(request, false, proxy, realm, null); + + assertTrue(tunneled.getHttpRequest().headers().contains(HttpHeaderNames.AUTHORIZATION), + "the request sent through the tunnel must still carry the origin Authorization"); + } + + @Test + public void connectRequestDoesNotCarryOriginAuthorizationSetAsAPlainHeader() { + Request request = get("https://origin.example.com/resource") + .setHeader(HttpHeaderNames.AUTHORIZATION, "Bearer origin-token") + .build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest connect = factory().newNettyRequest(request, true, proxy, null, null); + + assertFalse(connect.getHttpRequest().headers().contains(HttpHeaderNames.AUTHORIZATION), + "an explicitly set origin Authorization must not ride the plaintext CONNECT either"); + } +} diff --git a/client/src/test/java/org/asynchttpclient/netty/request/ProxyRequestUriUserInfoTest.java b/client/src/test/java/org/asynchttpclient/netty/request/ProxyRequestUriUserInfoTest.java new file mode 100644 index 0000000000..c115516165 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/request/ProxyRequestUriUserInfoTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.request; + +import org.asynchttpclient.Request; +import org.asynchttpclient.proxy.ProxyServer; +import org.testng.annotations.Test; + +import static org.asynchttpclient.Dsl.config; +import static org.asynchttpclient.Dsl.get; +import static org.asynchttpclient.Dsl.proxyServer; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +/** + * A plaintext request routed through an HTTP proxy carries an absolute-form request target, which the proxy + * sees and logs in the clear. RFC 9110 section 4.2.4 forbids the userinfo subcomponent in a generated + * request target, so the URL credentials must not appear on that request line. + */ +public class ProxyRequestUriUserInfoTest { + + private static NettyRequestFactory factory() { + return new NettyRequestFactory(config().build()); + } + + @Test + public void proxiedRequestTargetDropsUserInfo() { + Request request = get("http://user:secret@origin.example.com/resource?a=b").build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest proxied = factory().newNettyRequest(request, false, proxy, null, null); + String requestTarget = proxied.getHttpRequest().uri(); + + assertFalse(requestTarget.contains("secret"), + "the absolute-form request target must not expose the URL credentials to the proxy"); + assertEquals(requestTarget, "http://origin.example.com/resource?a=b"); + } + + @Test + public void proxiedRequestTargetDropsUserInfoOnAnExplicitPort() { + Request request = get("http://user:secret@origin.example.com:8081/resource").build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest proxied = factory().newNettyRequest(request, false, proxy, null, null); + + assertEquals(proxied.getHttpRequest().uri(), "http://origin.example.com:8081/resource"); + } + + @Test + public void proxiedRequestTargetWithoutUserInfoIsUnchanged() { + Request request = get("http://origin.example.com/resource?a=b").build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest proxied = factory().newNettyRequest(request, false, proxy, null, null); + + assertEquals(proxied.getHttpRequest().uri(), "http://origin.example.com/resource?a=b"); + } + + @Test + public void directRequestTargetStaysRelative() { + Request request = get("http://user:secret@origin.example.com/resource?a=b").build(); + + NettyRequest direct = factory().newNettyRequest(request, false, null, null, null); + + assertEquals(direct.getHttpRequest().uri(), "/resource?a=b"); + } +} diff --git a/client/src/test/java/org/asynchttpclient/netty/request/SocksProxyCredentialLeakTest.java b/client/src/test/java/org/asynchttpclient/netty/request/SocksProxyCredentialLeakTest.java new file mode 100644 index 0000000000..f33f9dc362 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/request/SocksProxyCredentialLeakTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.request; + +import io.netty.handler.codec.http.HttpHeaderNames; +import org.asynchttpclient.Realm; +import org.asynchttpclient.Request; +import org.asynchttpclient.proxy.ProxyServer; +import org.asynchttpclient.proxy.ProxyType; +import org.testng.annotations.Test; + +import static org.asynchttpclient.Dsl.basicAuthRealm; +import static org.asynchttpclient.Dsl.config; +import static org.asynchttpclient.Dsl.get; +import static org.asynchttpclient.Dsl.proxyServer; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * A SOCKS proxy tunnels at the transport layer, so the HTTP request it carries reaches the ORIGIN, not the + * proxy. Attaching a {@code Proxy-Authorization} header for a SOCKS proxy therefore leaks the proxy + * credentials to the origin server. The origin request must carry the header only for an HTTP proxy. + */ +public class SocksProxyCredentialLeakTest { + + private static NettyRequestFactory factory() { + return new NettyRequestFactory(config().build()); + } + + private static Realm preemptiveBasicProxyRealm() { + return basicAuthRealm("proxy-user", "proxy-secret").setUsePreemptiveAuth(true).build(); + } + + private static ProxyServer proxy(ProxyType type, Realm proxyRealm) { + return proxyServer("proxy.example.com", 1080).setProxyType(type).setRealm(proxyRealm).build(); + } + + private static boolean hasProxyAuthorization(NettyRequest nettyRequest) { + return nettyRequest.getHttpRequest().headers().contains(HttpHeaderNames.PROXY_AUTHORIZATION); + } + + @Test + public void socks5ProxyDoesNotAttachProxyAuthorizationToPlaintextOrigin() { + Request request = get("http://origin.example.com/resource").build(); + Realm proxyRealm = preemptiveBasicProxyRealm(); + ProxyServer socksProxy = proxy(ProxyType.SOCKS_V5, proxyRealm); + + NettyRequest nettyRequest = factory().newNettyRequest(request, false, socksProxy, null, proxyRealm); + + assertFalse(hasProxyAuthorization(nettyRequest), + "SOCKS proxy tunnels to the origin; Proxy-Authorization must not be sent to the origin"); + } + + @Test + public void socks4ProxyDoesNotAttachProxyAuthorizationToPlaintextOrigin() { + Request request = get("http://origin.example.com/resource").build(); + Realm proxyRealm = preemptiveBasicProxyRealm(); + ProxyServer socksProxy = proxy(ProxyType.SOCKS_V4, proxyRealm); + + NettyRequest nettyRequest = factory().newNettyRequest(request, false, socksProxy, null, proxyRealm); + + assertFalse(hasProxyAuthorization(nettyRequest), + "SOCKS proxy tunnels to the origin; Proxy-Authorization must not be sent to the origin"); + } + + @Test + public void socks5ProxyDoesNotAttachProxyAuthorizationToSecuredOrigin() { + // A https:// origin behind a SOCKS proxy never gets a CONNECT (needConnect requires an HTTP proxy), + // so the request itself is tunnelled straight to the origin. + Request request = get("https://origin.example.com/resource").build(); + Realm proxyRealm = preemptiveBasicProxyRealm(); + ProxyServer socksProxy = proxy(ProxyType.SOCKS_V5, proxyRealm); + + NettyRequest nettyRequest = factory().newNettyRequest(request, false, socksProxy, null, proxyRealm); + + assertFalse(hasProxyAuthorization(nettyRequest), + "SOCKS proxy tunnels to the origin; Proxy-Authorization must not be sent to the origin"); + } + + @Test + public void httpProxyStillAttachesProxyAuthorizationToPlaintextOrigin() { + Request request = get("http://origin.example.com/resource").build(); + Realm proxyRealm = preemptiveBasicProxyRealm(); + // Default proxy type is HTTP. + ProxyServer httpProxy = proxyServer("proxy.example.com", 8080).setRealm(proxyRealm).build(); + + NettyRequest nettyRequest = factory().newNettyRequest(request, false, httpProxy, null, proxyRealm); + + assertTrue(hasProxyAuthorization(nettyRequest), + "HTTP proxy receives the origin request directly; Proxy-Authorization must be sent"); + assertTrue(nettyRequest.getHttpRequest().headers().get(HttpHeaderNames.PROXY_AUTHORIZATION).startsWith("Basic "), + "expected a preemptive Basic Proxy-Authorization header"); + } + + @Test + public void connectRequestThroughHttpProxyStillCarriesProxyAuthorization() { + // A CONNECT to open a tunnel for an HTTPS origin is sent to the proxy in the clear, so it legitimately + // carries the proxy credentials. + Request request = get("https://origin.example.com/resource").build(); + Realm proxyRealm = preemptiveBasicProxyRealm(); + ProxyServer httpProxy = proxyServer("proxy.example.com", 8080).setRealm(proxyRealm).build(); + + NettyRequest connect = factory().newNettyRequest(request, true, httpProxy, null, proxyRealm); + + assertTrue(hasProxyAuthorization(connect), + "CONNECT is sent to the proxy to open the tunnel; Proxy-Authorization must be sent"); + } + + @Test + public void directRequestWithoutAProxyCarriesNoProxyAuthorization() { + Request request = get("http://origin.example.com/resource").build(); + Realm proxyRealm = preemptiveBasicProxyRealm(); + + NettyRequest nettyRequest = factory().newNettyRequest(request, false, null, null, proxyRealm); + + assertFalse(hasProxyAuthorization(nettyRequest), + "there is no proxy to authenticate to"); + } +} diff --git a/client/src/test/java/org/asynchttpclient/netty/request/WebSocketTunnelProxyAuthTest.java b/client/src/test/java/org/asynchttpclient/netty/request/WebSocketTunnelProxyAuthTest.java new file mode 100644 index 0000000000..a27db86ae6 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/request/WebSocketTunnelProxyAuthTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.request; + +import io.netty.handler.codec.http.HttpHeaderNames; +import org.asynchttpclient.Realm; +import org.asynchttpclient.Request; +import org.asynchttpclient.proxy.ProxyServer; +import org.testng.annotations.Test; + +import static org.asynchttpclient.Dsl.basicAuthRealm; +import static org.asynchttpclient.Dsl.config; +import static org.asynchttpclient.Dsl.get; +import static org.asynchttpclient.Dsl.proxyServer; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * A ws:// request through an HTTP proxy is tunnelled with CONNECT (NettyRequestSender's needConnect check + * treats ws:// like https://), so the upgrade request that follows travels through the tunnel to the origin, + * not to the proxy. It must therefore be treated like wss://: the proxy credentials belong only on the + * CONNECT (which the proxy actually receives), and the request target must be origin-form. + */ +public class WebSocketTunnelProxyAuthTest { + + private static NettyRequestFactory factory() { + return new NettyRequestFactory(config().build()); + } + + private static Realm preemptiveBasicProxyRealm() { + return basicAuthRealm("proxy-user", "proxy-secret").setUsePreemptiveAuth(true).build(); + } + + @Test + public void connectRequestForWebSocketCarriesProxyAuthorization() { + Request request = get("ws://origin.example.com/socket").build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest connect = factory().newNettyRequest(request, true, proxy, null, preemptiveBasicProxyRealm()); + + assertTrue(connect.getHttpRequest().headers().contains(HttpHeaderNames.PROXY_AUTHORIZATION), + "the CONNECT request must still authenticate to the proxy"); + } + + @Test + public void tunneledWebSocketUpgradeDoesNotCarryProxyAuthorization() { + Request request = get("ws://origin.example.com/socket").build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest tunneled = factory().newNettyRequest(request, false, proxy, null, preemptiveBasicProxyRealm()); + + assertFalse(tunneled.getHttpRequest().headers().contains(HttpHeaderNames.PROXY_AUTHORIZATION), + "the tunnelled ws:// upgrade reaches the origin, not the proxy, and must not expose the proxy credentials"); + assertEquals(tunneled.getHttpRequest().uri(), "/socket", + "the tunnelled ws:// upgrade must use an origin-form request target, not the proxy's absolute-form"); + } + + @Test + public void tunneledSecureWebSocketUpgradeDoesNotCarryProxyAuthorization() { + Request request = get("wss://origin.example.com/socket").build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest tunneled = factory().newNettyRequest(request, false, proxy, null, preemptiveBasicProxyRealm()); + + assertFalse(tunneled.getHttpRequest().headers().contains(HttpHeaderNames.PROXY_AUTHORIZATION), + "wss:// was already tunnel-safe; this guards against a regression"); + } + + @Test + public void plainHttpRequestToProxyKeepsProxyAuthorization() { + Request request = get("http://origin.example.com/resource").build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest direct = factory().newNettyRequest(request, false, proxy, null, preemptiveBasicProxyRealm()); + + assertTrue(direct.getHttpRequest().headers().contains(HttpHeaderNames.PROXY_AUTHORIZATION), + "a plaintext http:// request sent directly to the proxy still authenticates to it"); + assertEquals(direct.getHttpRequest().uri(), "http://origin.example.com/resource", + "a plaintext http:// request to the proxy still uses an absolute-form request target"); + } +} diff --git a/client/src/test/java/org/asynchttpclient/proxy/ConnectTunnelStateTest.java b/client/src/test/java/org/asynchttpclient/proxy/ConnectTunnelStateTest.java new file mode 100644 index 0000000000..fe24db6440 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/proxy/ConnectTunnelStateTest.java @@ -0,0 +1,430 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.proxy; + +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.DefaultAsyncHttpClient; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; +import org.asynchttpclient.Response; +import org.asynchttpclient.ws.WebSocketUpgradeHandler; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.Test; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.asynchttpclient.Dsl.basicAuthRealm; +import static org.asynchttpclient.Dsl.proxyServer; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertTrue; + +/** + * A CONNECT is sent to the proxy over a socket that is still plaintext; the tunnel exists only once the + * proxy has answered 2xx. AHC used to infer "the tunnel is up" from nothing more than "the last request was + * a CONNECT", which is equally true when the proxy REJECTED it. On a 401 or a redirect the origin-request + * interceptors then rebuilt the exchange as the ORIGIN request with {@code setReuseChannel(true)}, and + * because that request is not a CONNECT the request factory attached the origin's {@code Authorization} to + * it - handing the origin's credentials to the proxy, in the clear, on demand. A proxy needed only to + * answer 401 (a non-preemptive realm, which is the default) or 302 to collect them. + * + *

The proxy here is a raw socket that records every request head it is sent, so the assertion is made on + * the actual bytes on the wire rather than on any client-side state. + */ +public class ConnectTunnelStateTest { + + private static final String ORIGIN_USER = "origin-user"; + private static final String ORIGIN_PASSWORD = "origin-secret"; + // Base64("origin-user:origin-secret"), i.e. what a leak looks like on the wire. + private static final String ORIGIN_BASIC = "b3JpZ2luLXVzZXI6b3JpZ2luLXNlY3JldA=="; + + private RecordingProxy proxy; + + @AfterMethod(alwaysRun = true) + public void tearDown() throws IOException { + if (proxy != null) { + proxy.close(); + proxy = null; + } + } + + /** + * One request head as the proxy saw it, together with the identity of the TCP connection it arrived on. + * The connection is the point: "the socket was closed" and "the socket was poisoned and left in the + * pool" are indistinguishable from a single request, and only the second one leaks. + */ + private static final class RecordedHead { + + final int connectionId; + final String head; + + RecordedHead(int connectionId, String head) { + this.connectionId = connectionId; + this.head = head; + } + + @Override + public String toString() { + return "[conn#" + connectionId + "] " + head; + } + } + + /** + * A minimal HTTP proxy that answers each request head it receives with the next scripted response and + * keeps the connection open, recording everything the client sends on it afterwards. Each accepted + * connection is served on its own thread and numbered, so a client that opens a second connection is + * told apart from one that reuses the first. + */ + private static final class RecordingProxy implements Closeable { + + private final ServerSocket serverSocket; + private final String[] responses; + private final List requestHeads = new CopyOnWriteArrayList<>(); + private final List accepted = new CopyOnWriteArrayList<>(); + private final AtomicInteger connectionIds = new AtomicInteger(); + /** + * Counts down to the number of request heads the UNFIXED client would send, so a run that reproduces + * the leak does not have to wait out a request timeout to record it. + */ + private final CountDownLatch expectedHeads; + private final Thread thread; + + RecordingProxy(int expectedHeads, String... responses) throws IOException { + this.responses = responses; + this.expectedHeads = new CountDownLatch(expectedHeads); + serverSocket = new ServerSocket(0, 4, InetAddress.getByName("localhost")); + thread = new Thread(this::serve); + thread.setDaemon(true); + thread.start(); + } + + int port() { + return serverSocket.getLocalPort(); + } + + private void serve() { + while (!serverSocket.isClosed()) { + try { + Socket socket = serverSocket.accept(); + accepted.add(socket); + int connectionId = connectionIds.incrementAndGet(); + Thread connectionThread = new Thread(() -> serveConnection(socket, connectionId)); + connectionThread.setDaemon(true); + connectionThread.start(); + } catch (Exception ignored) { + // accept() throws once the server socket is closed in tearDown. + return; + } + } + } + + private void serveConnection(Socket socket, int connectionId) { + try (Socket toClose = socket) { + toClose.setSoTimeout(5000); + InputStream in = toClose.getInputStream(); + OutputStream out = toClose.getOutputStream(); + int answered = 0; + String head; + while ((head = readHead(in)) != null) { + requestHeads.add(new RecordedHead(connectionId, head)); + expectedHeads.countDown(); + if (answered < responses.length) { + out.write(responses[answered++].getBytes(StandardCharsets.US_ASCII)); + out.flush(); + } + } + } catch (Exception ignored) { + // A read times out or fails once the client has nothing more to say on this connection. Either + // way there is nothing left to record on it. + } + } + + /** + * Reads one request head, i.e. up to and including the CRLFCRLF. Returns null at end of stream. Note + * that a TLS ClientHello (which is what a client sends once a tunnel really is established) contains no + * CRLFCRLF, so it is never mistaken for a request. + */ + private static String readHead(InputStream in) throws IOException { + StringBuilder head = new StringBuilder(); + int b3 = -1, b2 = -1, b1 = -1, b; + while ((b = in.read()) != -1) { + head.append((char) b); + if (b3 == '\r' && b2 == '\n' && b1 == '\r' && b == '\n') { + return head.toString(); + } + b3 = b2; + b2 = b1; + b1 = b; + } + return null; + } + + void awaitExpectedHeads() throws InterruptedException { + expectedHeads.await(6, TimeUnit.SECONDS); + } + + @Override + public void close() throws IOException { + serverSocket.close(); + for (Socket socket : accepted) { + try { + socket.close(); + } catch (IOException ignored) { + // Already closed by the client or by the connection thread. + } + } + thread.interrupt(); + } + } + + private static boolean isConnect(String head) { + return head.startsWith("CONNECT "); + } + + /** + * The origin's Authorization, as opposed to Proxy-Authorization which legitimately goes to the proxy. + */ + private static boolean carriesOriginAuthorization(String head) { + for (String line : head.split("\r\n")) { + String lower = line.toLowerCase(); + if (lower.startsWith("authorization:") && line.contains(ORIGIN_BASIC)) { + return true; + } + } + return false; + } + + /** + * Any Authorization at all, whatever it carries. Nothing addressed to the origin has any business on a + * socket the proxy refused to turn into a tunnel. + */ + private static boolean carriesAnyAuthorization(String head) { + for (String line : head.split("\r\n")) { + if (line.toLowerCase().startsWith("authorization:")) { + return true; + } + } + return false; + } + + /** + * The contract for a proxy that never established a tunnel: the only thing it may ever be sent is a + * CONNECT, and certainly not the origin's credentials. + */ + private void assertOnlyConnectsReachedTheProxy() { + assertFalse(proxy.requestHeads.isEmpty(), "the proxy should have received the CONNECT"); + // The credential leak is checked across every head before the weaker "it was not even a CONNECT" + // check, so that a failure names the secret that escaped rather than the first symptom on the wire. + for (RecordedHead recorded : proxy.requestHeads) { + assertFalse(carriesAnyAuthorization(recorded.head), + "the origin's credentials must never be written to a socket on which no tunnel was " + + "established; the proxy received:\n" + recorded); + } + for (RecordedHead recorded : proxy.requestHeads) { + assertTrue(isConnect(recorded.head), + "a proxy that has not established a tunnel must only ever be sent CONNECTs, but got:\n" + recorded); + } + } + + private AsyncHttpClient clientWithOriginRealm(boolean followRedirect) { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(followRedirect) + .setRequestTimeout(4000) + // Non-preemptive, which is the default: the credentials are sent only in answer to a + // challenge. That is precisely what makes a 401 from the proxy enough to solicit them. + .setRealm(basicAuthRealm(ORIGIN_USER, ORIGIN_PASSWORD).build()) + .setProxyServer(proxyServer("localhost", proxy.port()).build()) + .build(); + return new DefaultAsyncHttpClient(config); + } + + /** + * The proxy rejects the CONNECT with a 401. The tunnel does not exist, so nothing that follows may carry + * the origin's credentials down that socket. + */ + @Test(timeOut = 30000) + public void proxyRejectingConnectWith401DoesNotHarvestOriginCredentials() throws Exception { + // Two heads are scripted because an UNFIXED client sends a second request - the origin GET, bearing + // the credentials the 401 solicited - and answering it keeps the reproduction fast. + proxy = new RecordingProxy(2, + "HTTP/1.1 401 Unauthorized\r\n" + + "WWW-Authenticate: Basic realm=\"origin\"\r\n" + + "Content-Length: 0\r\n" + + "\r\n", + "HTTP/1.1 200 OK\r\n" + + "Content-Length: 0\r\n" + + "\r\n"); + + Response response = null; + try (AsyncHttpClient client = clientWithOriginRealm(false)) { + try { + response = client.prepareGet("https://origin.example.com/secret").execute().get(10, TimeUnit.SECONDS); + } catch (Exception ignored) { + // What was written is the contract; whether the exchange completed is asserted separately below. + } + } finally { + proxy.awaitExpectedHeads(); + } + + assertOnlyConnectsReachedTheProxy(); + // The 401 is the proxy's answer to the CONNECT. It is surfaced to the caller as-is, and must never be + // mistaken for an origin challenge to answer. + assertEquals(response == null ? null : response.getStatusCode(), (Integer) 401, + "the proxy's rejection of the CONNECT must be surfaced to the caller"); + } + + /** + * Same, with a redirect instead of a challenge. The Location deliberately stays on the same origin, so + * the redirect is not cross-origin and the credential-stripping path does not mask the bug. + */ + @Test(timeOut = 30000) + public void proxyRejectingConnectWith302DoesNotHarvestOriginCredentials() throws Exception { + // An unfixed client follows the redirect on the same plaintext socket, is challenged there, and sends + // the credentials on a third request. + proxy = new RecordingProxy(3, + "HTTP/1.1 302 Found\r\n" + + "Location: https://origin.example.com/secret2\r\n" + + "Content-Length: 0\r\n" + + "\r\n", + "HTTP/1.1 401 Unauthorized\r\n" + + "WWW-Authenticate: Basic realm=\"origin\"\r\n" + + "Content-Length: 0\r\n" + + "\r\n", + "HTTP/1.1 200 OK\r\n" + + "Content-Length: 0\r\n" + + "\r\n"); + + try (AsyncHttpClient client = clientWithOriginRealm(true)) { + try { + client.prepareGet("https://origin.example.com/secret").execute().get(10, TimeUnit.SECONDS); + } catch (Exception ignored) { + // Whether the exchange completes is not the contract under test; what was written is. + } + } finally { + proxy.awaitExpectedHeads(); + } + + assertOnlyConnectsReachedTheProxy(); + } + + /** + * The regression guard for the other direction: once the proxy DOES establish the tunnel, the exchange + * must proceed exactly as before - the CONNECT carries Proxy-Authorization and the tunnelled request + * carries the origin's Authorization. A {@code ws://} target is used so the tunnelled hop is cleartext + * and can be inspected; the {@code https://} equivalent is covered end to end by HttpsProxyTest. + */ + @Test(timeOut = 30000) + public void establishedTunnelStillCarriesOriginAuthOnTheTunnelledRequest() throws Exception { + proxy = new RecordingProxy(2, "HTTP/1.1 200 Connection Established\r\n\r\n"); + + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setRequestTimeout(4000) + .setRealm(basicAuthRealm(ORIGIN_USER, ORIGIN_PASSWORD).setUsePreemptiveAuth(true).build()) + .setProxyServer(proxyServer("localhost", proxy.port()) + .setRealm(basicAuthRealm("proxy-user", "proxy-secret").setUsePreemptiveAuth(true)) + .build()) + .build(); + + try (AsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + try { + client.prepareGet("ws://origin.example.com/socket") + .execute(new WebSocketUpgradeHandler.Builder().build()) + .get(10, TimeUnit.SECONDS); + } catch (Exception ignored) { + // The fake proxy never completes the WebSocket handshake; only what was written matters here. + } + } finally { + proxy.awaitExpectedHeads(); + } + + assertTrue(proxy.requestHeads.size() >= 2, + "the tunnelled request must be sent on the same socket after the 200, but got: " + proxy.requestHeads); + + RecordedHead connect = proxy.requestHeads.get(0); + assertTrue(isConnect(connect.head), "the first request must be a CONNECT: " + connect); + assertTrue(connect.head.toLowerCase().contains("proxy-authorization: basic "), + "the CONNECT is addressed to the proxy and must carry its credentials:\n" + connect); + assertFalse(carriesOriginAuthorization(connect.head), + "the CONNECT travels in the clear and must NOT carry the origin's credentials:\n" + connect); + + RecordedHead tunnelled = proxy.requestHeads.get(1); + assertFalse(isConnect(tunnelled.head), "the second request must be the tunnelled one: " + tunnelled); + assertEquals(tunnelled.connectionId, connect.connectionId, + "the tunnelled request must go down the very socket the CONNECT established: " + proxy.requestHeads); + assertTrue(carriesOriginAuthorization(tunnelled.head), + "once the tunnel is established the origin's credentials must be sent through it:\n" + tunnelled); + } + + /** + * Two requests, one client. Asserting on a single request is not enough: "the socket was closed" and + * "the socket was left in the pool, still plaintext, under the https-origin partition key" produce the + * same one request head, and only the second one leaks. It leaks on the request AFTER the rejected + * CONNECT, because {@code NettyRequestSender.sendRequestThroughProxy} takes a channel polled under that + * key to be a tunnel that is already up and therefore sends the ORIGIN request on it rather than a new + * CONNECT - at which point the proxy's next 401 collects the origin's credentials. + * + *

So the second request must arrive on a NEW connection, identified server-side, and no Authorization + * may appear anywhere on the wire. + */ + @Test(timeOut = 30000) + public void proxyRejectingConnectDoesNotLeavePoisonedConnectionInThePool() throws Exception { + String unauthorized = "HTTP/1.1 401 Unauthorized\r\n" + + "WWW-Authenticate: Basic realm=\"origin\"\r\n" + + "Content-Length: 0\r\n" + + "\r\n"; + // Three heads are scripted because that is what the UNFIXED client sends: the CONNECT, then the origin + // GET down the pooled plaintext socket, then the same GET carrying the credentials the second 401 + // solicited. Answering all three keeps the reproduction fast and lets the leak be recorded in full. + proxy = new RecordingProxy(3, unauthorized, unauthorized, + "HTTP/1.1 200 OK\r\n" + + "Content-Length: 0\r\n" + + "\r\n"); + + try (AsyncHttpClient client = clientWithOriginRealm(false)) { + for (int i = 0; i < 2; i++) { + try { + client.prepareGet("https://origin.example.com/secret").execute().get(10, TimeUnit.SECONDS); + } catch (Exception ignored) { + // What was written is the contract; the per-request status is asserted by the tests above. + } + } + } finally { + // Give a client that would still leak every chance to send the third head before asserting. + proxy.awaitExpectedHeads(); + } + + assertOnlyConnectsReachedTheProxy(); + + assertTrue(proxy.requestHeads.size() >= 2, + "the second request must have reached the proxy, but it only saw: " + proxy.requestHeads); + RecordedHead first = proxy.requestHeads.get(0); + for (int i = 1; i < proxy.requestHeads.size(); i++) { + assertNotEquals(proxy.requestHeads.get(i).connectionId, first.connectionId, + "the socket on which the proxy REFUSED the CONNECT is not a tunnel and must never be pooled; " + + "the client kept using it: " + proxy.requestHeads); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/request/body/InputStreamTest.java b/client/src/test/java/org/asynchttpclient/request/body/InputStreamTest.java index d7c21b618b..eb17cea627 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/InputStreamTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/InputStreamTest.java @@ -57,7 +57,7 @@ public void testInvalidInputStream() throws IOException, ExecutionException, Int @Override public int available() { - return 1; // Fake + return readAllowed < 3 ? 1 : 0; } @Override diff --git a/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java b/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java index 92ff4a4d78..2370e6d855 100644 --- a/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java +++ b/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java @@ -3,6 +3,11 @@ import org.apache.commons.io.FileUtils; import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; import org.asynchttpclient.AbstractBasicTest; +import org.asynchttpclient.Realm; +import org.asynchttpclient.Request; +import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.proxy.ProxyServer; +import org.asynchttpclient.util.AuthenticatorUtils; import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; @@ -51,6 +56,10 @@ public static void startServers() throws Exception { kerbyServer.createPrincipal(alice, "alice"); kerbyServer.createPrincipal(bob, "bob"); + // Host based service principal for the origin, so a token can be obtained for HTTP@localhost and + // only for that host. negotiateTokenTargetsTheOriginEvenBehindAProxy relies on there being no such + // principal for the proxy host. + kerbyServer.createPrincipal("HTTP/localhost@service.ws.apache.org", "httppwd"); aliceKeytab = new File(basedir + File.separator + "target" + File.separator + "alice.keytab"); bobKeytab = new File(basedir + File.separator + "target" + File.separator + "bob.keytab"); @@ -77,6 +86,40 @@ public void testSpnegoGenerateTokenWithUsernamePassword() throws Exception { Assert.assertTrue(token.startsWith("YII")); } + /** + * The origin realm's Negotiate token must be minted for the origin service even when a proxy is + * configured. Building it against the proxy host produced a service ticket for the proxy's SPN: a + * confused deputy where the origin's credential is delivered to, and only usable by, the proxy, while + * origin authentication fails. + *

+ * This drives the real {@code perConnectionAuthorizationHeader} rather than the host-selection helper, + * so that restoring the proxy host in the product code makes it fail. The proxy host below has no + * service principal in the KDC, so a token can only be produced if the origin was chosen. It lives in + * this class because obtaining a token initialises the JVM's Kerberos configuration process-wide, and + * this is the class that already stands up a KDC and installs a krb5.conf for exactly that. + */ + @Test + public void negotiateTokenTargetsTheOriginEvenBehindAProxy() throws Exception { + Request request = new RequestBuilder("GET").setUrl("http://localhost:8080/resource").build(); + ProxyServer proxyServer = new ProxyServer.Builder("no-such-proxy.invalid", 3128).build(); + Realm realm = new Realm.Builder("alice", "alice") + .setScheme(Realm.AuthScheme.KERBEROS) + .setUsePreemptiveAuth(true) + // No explicit service principal name on purpose: setting one makes the SPN a constant and the + // host irrelevant, which is what made an earlier version of this test pass against both arms. + // Left unset, the name is HTTP@, so only the origin has a principal the KDC will issue for. + .setRealmName("service.ws.apache.org") + .setUseCanonicalHostname(false) + .setLoginContextName("alice") + .build(); + + String header = AuthenticatorUtils.perConnectionAuthorizationHeader(request, proxyServer, realm); + + Assert.assertNotNull(header, "no Negotiate header was produced for the origin"); + Assert.assertTrue(header.startsWith("Negotiate YII"), + "the origin credential must be minted for the origin service, not the proxy's SPN: " + header); + } + @Test(expectedExceptions = SpnegoEngineException.class) public void testSpnegoGenerateTokenWithUsernamePasswordFail() throws Exception { SpnegoEngine spnegoEngine = new SpnegoEngine("alice", diff --git a/client/src/test/java/org/asynchttpclient/test/TestUtils.java b/client/src/test/java/org/asynchttpclient/test/TestUtils.java index 7d90b2c9b1..83e0c46e46 100644 --- a/client/src/test/java/org/asynchttpclient/test/TestUtils.java +++ b/client/src/test/java/org/asynchttpclient/test/TestUtils.java @@ -154,7 +154,8 @@ public static ServerConnector addHttpConnector(Server server) { public static ServerConnector addHttpsConnector(Server server) throws IOException, URISyntaxException { String keyStoreFile = resourceAsFile("ssltest-keystore.jks").getAbsolutePath(); - SslContextFactory sslContextFactory = new SslContextFactory(keyStoreFile); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath(keyStoreFile); sslContextFactory.setKeyStorePassword("changeit"); String trustStoreFile = resourceAsFile("ssltest-cacerts.jks").getAbsolutePath(); diff --git a/client/src/test/java/org/asynchttpclient/uri/UriTest.java b/client/src/test/java/org/asynchttpclient/uri/UriTest.java index 7ead2a6528..1d4bf41789 100644 --- a/client/src/test/java/org/asynchttpclient/uri/UriTest.java +++ b/client/src/test/java/org/asynchttpclient/uri/UriTest.java @@ -137,6 +137,22 @@ public void testToUrlWithUserInfoPortPathAndQuery() { assertEquals(uri.toUrl(), "http://user@example.com:44/path/path2?query=4", "toUrl returned incorrect url"); } + @Test + public void testToUrlWithoutUserInfoDropsUserInfo() { + Uri uri = new Uri("http", "user:secret", "example.com", 44, "/path/path2", "query=4", null); + assertEquals(uri.toUrlWithoutUserInfo(), "http://example.com:44/path/path2?query=4", + "toUrlWithoutUserInfo must not emit the userinfo subcomponent"); + assertEquals(uri.toUrl(), "http://user:secret@example.com:44/path/path2?query=4", + "toUrl must keep the userinfo for the caller-visible URL"); + } + + @Test + public void testToUrlWithoutUserInfoWithoutUserInfoMatchesToUrl() { + Uri uri = new Uri("http", null, "example.com", -1, "/path", "query=4", null); + assertEquals(uri.toUrlWithoutUserInfo(), uri.toUrl(), + "toUrlWithoutUserInfo must be identical to toUrl when there is no userinfo"); + } + @Test public void testQueryWithNonRootPath() { Uri uri = Uri.create("http://hello.com/foo?query=value"); diff --git a/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java b/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java new file mode 100644 index 0000000000..7d67ac2348 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.util; + +import org.asynchttpclient.Realm; +import org.asynchttpclient.Request; +import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.uri.Uri; +import org.testng.annotations.Test; + +import static org.asynchttpclient.util.AuthenticatorUtils.computeRealmURI; +import static org.asynchttpclient.util.AuthenticatorUtils.perRequestAuthorizationHeader; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class AuthenticatorUtilsTest { + + private static final Uri URI_WITH_USERINFO = Uri.create("http://user:secret@example.com:8080/path?q=1"); + + /** + * The Digest {@code uri=} parameter goes on the wire and is covered by the digest itself. RFC 9110 + * section 4.2.4 forbids generating the userinfo subcomponent in a request target, and rendering it here + * would put the password in the clear on the very hop Digest exists to protect. + */ + @Test + public void computeRealmURIAbsoluteDropsUserInfo() { + assertEquals(computeRealmURI(URI_WITH_USERINFO, true, false), "http://example.com:8080/path?q=1"); + } + + @Test + public void computeRealmURIAbsoluteOmitQueryDropsUserInfo() { + // omitQuery rebuilds the Uri via withNewQuery(null), which carries the userinfo over: a second, + // independent path to the same leak. + assertEquals(computeRealmURI(URI_WITH_USERINFO, true, true), "http://example.com:8080/path"); + } + + @Test + public void computeRealmURIAbsoluteWithoutUserInfoIsUnchanged() { + Uri uri = Uri.create("http://example.com:8080/path?q=1"); + assertEquals(computeRealmURI(uri, true, false), "http://example.com:8080/path?q=1"); + assertEquals(computeRealmURI(uri, true, true), "http://example.com:8080/path"); + } + + @Test + public void computeRealmURIRelativeIsUnchanged() { + assertEquals(computeRealmURI(URI_WITH_USERINFO, false, false), "/path?q=1"); + assertEquals(computeRealmURI(URI_WITH_USERINFO, false, true), "/path"); + } + + /** + * End-to-end over the header the client actually emits: no fragment of the userinfo may appear anywhere + * in the Authorization value. + */ + @Test + public void digestAuthorizationHeaderDoesNotCarryUserInfo() { + Realm realm = new Realm.Builder("user", "secret") + .setScheme(Realm.AuthScheme.DIGEST) + .setUri(URI_WITH_USERINFO) + .setMethodName("GET") + .setUsePreemptiveAuth(true) + .setUseAbsoluteURI(true) + .setRealmName("realm") + .setNonce("nonce") + .build(); + + Request request = new RequestBuilder("GET").setUri(URI_WITH_USERINFO).build(); + String header = perRequestAuthorizationHeader(request, realm); + + assertTrue(header.startsWith("Digest "), "expected a Digest header but got: " + header); + assertTrue(header.contains("uri=\"http://example.com:8080/path?q=1\""), + "expected a userinfo-free uri parameter but got: " + header); + assertFalse(header.contains("secret@"), "password must not appear in the Digest header: " + header); + assertFalse(header.contains("user:secret"), "credentials must not appear in the Digest header: " + header); + } +} diff --git a/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java b/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java index aa9235101c..9bc71d0e15 100644 --- a/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java +++ b/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java @@ -23,11 +23,16 @@ import org.asynchttpclient.uri.Uri; import org.testng.annotations.Test; +import java.lang.reflect.Field; import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.security.SecureRandom; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Random; +import java.util.Set; import static io.netty.handler.codec.http.HttpHeaderValues.APPLICATION_JSON; import static java.nio.charset.StandardCharsets.*; @@ -113,6 +118,37 @@ public void testGetFollowRedirectPriorityGivenToRequest() { assertFalse(followRedirect, "Follow redirect value set in request should be given priority"); } + /** + * The multipart boundary is the only thing separating parts whose content is never escaped, so a caller + * that can predict it can close a part early and append a forged Content-Disposition. It is also the + * widest window a peer gets onto the generator - 30 to 40 consecutive draws echoed in the clear - so + * anything else drawn from the same generator (the Digest cnonce) inherits the weakness. It must come + * from a cryptographically strong source, not from ThreadLocalRandom. + */ + @Test + public void multipartBoundaryComesFromASecureGenerator() throws Exception { + Field field = HttpUtils.class.getDeclaredField("BOUNDARY_RANDOM"); + field.setAccessible(true); + @SuppressWarnings("unchecked") + ThreadLocal holder = (ThreadLocal) field.get(null); + assertTrue(holder.get() instanceof SecureRandom, + "the multipart boundary generator must be a SecureRandom but was: " + holder.get().getClass()); + } + + @Test + public void computeMultipartBoundaryRespectsItsContract() { + String allowed = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + Set boundaries = new HashSet<>(); + for (int i = 0; i < 1000; i++) { + String boundary = new String(HttpUtils.computeMultipartBoundary(), US_ASCII); + assertTrue(boundary.length() >= 30 && boundary.length() <= 40, "Unexpected boundary length: " + boundary.length()); + for (int j = 0; j < boundary.length(); j++) { + assertTrue(allowed.indexOf(boundary.charAt(j)) != -1, "Illegal boundary char: " + boundary.charAt(j)); + } + assertTrue(boundaries.add(boundary), "Boundary was generated twice: " + boundary); + } + } + private void formUrlEncoding(Charset charset) throws Exception { String key = "key"; String value = "中文"; diff --git a/client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java b/client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java new file mode 100644 index 0000000000..7c6e0c61e6 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.util; + +import org.asynchttpclient.Request; +import org.asynchttpclient.RequestBuilder; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +/** + * The contract of the host that names the service a Negotiate token is minted for. + *

+ * That the origin is chosen rather than the proxy is asserted by + * {@code SpnegoEngineTest.negotiateTokenTargetsTheOriginEvenBehindAProxy}, which drives the real + * {@code perConnectionAuthorizationHeader} against a KDC and so fails if the product code regresses. These + * are the cheap unit-level companions to it, covering the two inputs that test does not vary. They mint no + * token and touch no Kerberos configuration. + */ +public class PerConnectionAuthorizationHeaderTest { + + @Test + public void negotiateTokenPrefersTheVirtualHost() { + Request request = new RequestBuilder("GET") + .setUrl("http://origin.example.com/resource") + .setVirtualHost("virtual.example.com") + .build(); + + assertEquals(AuthenticatorUtils.negotiateHost(request), "virtual.example.com", + "a configured virtual host names the service the request is really for"); + } + + @Test + public void negotiateHostIgnoresThePort() { + Request request = new RequestBuilder("GET").setUrl("http://origin.example.com:8443/resource").build(); + + assertEquals(AuthenticatorUtils.negotiateHost(request), "origin.example.com", + "the service principal name is built from the host alone"); + } +} diff --git a/client/src/test/java/org/asynchttpclient/ws/TextMessageTest.java b/client/src/test/java/org/asynchttpclient/ws/TextMessageTest.java index 72c3e1d244..feaa3748e4 100644 --- a/client/src/test/java/org/asynchttpclient/ws/TextMessageTest.java +++ b/client/src/test/java/org/asynchttpclient/ws/TextMessageTest.java @@ -76,9 +76,6 @@ public void onFailureTest() throws Throwable { try (AsyncHttpClient c = asyncHttpClient()) { c.prepareGet("ws://abcdefg").execute(new WebSocketUpgradeHandler.Builder().build()).get(); } catch (ExecutionException e) { - - String expectedMessage = "DNS name not found"; - assertTrue(e.getCause().toString().contains(expectedMessage)); throw e.getCause(); } } diff --git a/client/src/test/java/org/asynchttpclient/ws/WebSocketHandshakeValidationTest.java b/client/src/test/java/org/asynchttpclient/ws/WebSocketHandshakeValidationTest.java new file mode 100644 index 0000000000..9525d90141 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/ws/WebSocketHandshakeValidationTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.ws; + +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.AsyncHttpClientConfig; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static org.asynchttpclient.Dsl.asyncHttpClient; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +/** + * RFC 6455 section 4.1: if the {@code Sec-WebSocket-Accept} value in the server's opening handshake does + * not match the expected base64 of SHA-1 of the nonce, the client MUST fail the WebSocket connection. It + * must not go on to upgrade the pipeline or notify the listener of an open socket. + */ +public class WebSocketHandshakeValidationTest { + + private ServerSocket serverSocket; + private Thread serverThread; + + @AfterMethod + public void tearDown() throws Exception { + if (serverSocket != null) { + serverSocket.close(); + } + if (serverThread != null) { + serverThread.interrupt(); + } + } + + // Completes the HTTP upgrade with a 101 but a Sec-WebSocket-Accept that does not match the key. + private int startServerWithBadAccept() throws IOException { + serverSocket = new ServerSocket(0, 1, InetAddress.getByName("localhost")); + int port = serverSocket.getLocalPort(); + serverThread = new Thread(() -> { + try (Socket socket = serverSocket.accept()) { + InputStream in = socket.getInputStream(); + int b3 = -1, b2 = -1, b1 = -1, b; + while ((b = in.read()) != -1) { + if (b3 == '\r' && b2 == '\n' && b1 == '\r' && b == '\n') { + break; + } + b3 = b2; + b2 = b1; + b1 = b; + } + OutputStream out = socket.getOutputStream(); + out.write(("HTTP/1.1 101 Switching Protocols\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Accept: this-is-not-the-expected-accept\r\n" + + "\r\n").getBytes(StandardCharsets.US_ASCII)); + out.flush(); + Thread.sleep(5000); + } catch (Exception ignored) { + } + }); + serverThread.setDaemon(true); + serverThread.start(); + return port; + } + + @Test(timeOut = 30000) + public void doesNotUpgradeOnInvalidAcceptKey() throws Exception { + int port = startServerWithBadAccept(); + CountDownLatch openLatch = new CountDownLatch(1); + + try (AsyncHttpClient c = asyncHttpClient()) { + try { + c.prepareGet("ws://localhost:" + port + "/") + .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketListener() { + @Override + public void onOpen(WebSocket websocket) { + openLatch.countDown(); + } + + @Override + public void onClose(WebSocket websocket, int code, String reason) { + } + + @Override + public void onError(Throwable t) { + } + }).build()).get(); + fail("the handshake must fail when Sec-WebSocket-Accept is invalid"); + } catch (ExecutionException expected) { + // the request is aborted with "Invalid challenge" + } + + assertFalse(openLatch.await(2, TimeUnit.SECONDS), + "onOpen must not fire when the server Sec-WebSocket-Accept is invalid"); + } + } + + // Reads the request, then stalls well past the client's request timeout before sending a perfectly + // valid 101, so the upgrade response lands on a future the timeout has already aborted. + private int startServerWithLateHandshake(long delayMillis) throws IOException { + serverSocket = new ServerSocket(0, 1, InetAddress.getByName("localhost")); + int port = serverSocket.getLocalPort(); + serverThread = new Thread(() -> { + try (Socket socket = serverSocket.accept()) { + InputStream in = socket.getInputStream(); + StringBuilder request = new StringBuilder(); + int b3 = -1, b2 = -1, b1 = -1, b; + while ((b = in.read()) != -1) { + request.append((char) b); + if (b3 == '\r' && b2 == '\n' && b1 == '\r' && b == '\n') { + break; + } + b3 = b2; + b2 = b1; + b1 = b; + } + + // Echo back the accept value the client expects, so the only reason not to upgrade is that the + // exchange is already over. + String key = null; + for (String line : request.toString().split("\r\n")) { + if (line.regionMatches(true, 0, "Sec-WebSocket-Key:", 0, "Sec-WebSocket-Key:".length())) { + key = line.substring("Sec-WebSocket-Key:".length()).trim(); + } + } + + Thread.sleep(delayMillis); + + OutputStream out = socket.getOutputStream(); + out.write(("HTTP/1.1 101 Switching Protocols\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Accept: " + WebSocketUtils.getAcceptKey(key) + "\r\n" + + "\r\n").getBytes(StandardCharsets.US_ASCII)); + out.flush(); + Thread.sleep(5000); + } catch (Exception ignored) { + } + }); + serverThread.setDaemon(true); + serverThread.start(); + return port; + } + + /** + * End-to-end contract: a 101 that arrives after the request timeout must never surface as onOpen. Two + * independent things uphold it - NettyRequestSender.abort marks the channel discarded before it completes + * the future, and WebSocketHandler drops an upgrade response landing on a finished future. The second is + * what covers the paths where the first does not run in that order (see + * {@code WebSocketHandlerDoneFutureTest}, which pins that guard directly). + */ + @Test(timeOut = 30000) + public void doesNotUpgradeAfterTheFutureWasAborted() throws Exception { + int port = startServerWithLateHandshake(2000); + + CountDownLatch openLatch = new CountDownLatch(1); + CountDownLatch errorLatch = new CountDownLatch(1); + + AsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setRequestTimeout(500) + .build(); + try (AsyncHttpClient c = asyncHttpClient(config)) { + try { + c.prepareGet("ws://localhost:" + port + "/") + .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketListener() { + @Override + public void onOpen(WebSocket websocket) { + openLatch.countDown(); + } + + @Override + public void onClose(WebSocket websocket, int code, String reason) { + } + + @Override + public void onError(Throwable t) { + errorLatch.countDown(); + } + }).build()).get(); + fail("the request must fail on the request timeout"); + } catch (ExecutionException expected) { + // TimeoutException + } + + assertTrue(errorLatch.await(2, TimeUnit.SECONDS), "onError must fire when the request times out"); + assertFalse(openLatch.await(4, TimeUnit.SECONDS), + "onOpen must not fire on a 101 that arrives after the future was aborted"); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/ws/WebSocketUtilsTest.java b/client/src/test/java/org/asynchttpclient/ws/WebSocketUtilsTest.java new file mode 100644 index 0000000000..5dae180d7d --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/ws/WebSocketUtilsTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.ws; + +import org.testng.annotations.Test; + +import java.lang.reflect.Field; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +public class WebSocketUtilsTest { + + /** + * RFC 6455 section 10.3: "the nonce MUST be selected randomly for each connection" from a source that + * "cannot be guessed", because Sec-WebSocket-Accept is a pure function of the key. An off-path party who + * can predict the key can precompute a valid accept value, so the handshake check stops proving the 101 + * came from a peer that saw the request. Netty's ThreadLocalRandom is a 48-bit LCG. + */ + @Test + public void webSocketKeyComesFromASecureGenerator() throws Exception { + Field field = WebSocketUtils.class.getDeclaredField("KEY_RANDOM"); + field.setAccessible(true); + @SuppressWarnings("unchecked") + ThreadLocal holder = (ThreadLocal) field.get(null); + assertTrue(holder.get() instanceof SecureRandom, + "the Sec-WebSocket-Key generator must be a SecureRandom but was: " + holder.get().getClass()); + } + + @Test + public void webSocketKeyIsSixteenFreshBytes() { + Set keys = new HashSet<>(); + for (int i = 0; i < 1000; i++) { + String key = WebSocketUtils.getWebSocketKey(); + assertEquals(Base64.getDecoder().decode(key).length, 16, "RFC 6455 requires a 16-byte nonce: " + key); + assertTrue(keys.add(key), "Sec-WebSocket-Key was generated twice: " + key); + } + } + + @Test + public void acceptKeyMatchesTheRfcExample() { + // RFC 6455 section 1.3. + assertEquals(WebSocketUtils.getAcceptKey("dGhlIHNhbXBsZSBub25jZQ=="), "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="); + } +} diff --git a/example/pom.xml b/example/pom.xml index 5643feaab9..dd73d7e765 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.12.3 + 2.16.1 4.0.0 async-http-client-example diff --git a/extras/guava/pom.xml b/extras/guava/pom.xml index 39fd913a5f..1f30fa4d4d 100644 --- a/extras/guava/pom.xml +++ b/extras/guava/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.12.3 + 2.16.1 4.0.0 async-http-client-extras-guava diff --git a/extras/jdeferred/pom.xml b/extras/jdeferred/pom.xml index d3c7d6a9e4..8df187d565 100644 --- a/extras/jdeferred/pom.xml +++ b/extras/jdeferred/pom.xml @@ -18,7 +18,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.3 + 2.16.1 async-http-client-extras-jdeferred Asynchronous Http Client JDeferred Extras diff --git a/extras/pom.xml b/extras/pom.xml index 5fccc3bce6..7fda2624da 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.12.3 + 2.16.1 4.0.0 async-http-client-extras-parent diff --git a/extras/registry/pom.xml b/extras/registry/pom.xml index 492ef41f65..835e1e4874 100644 --- a/extras/registry/pom.xml +++ b/extras/registry/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.12.3 + 2.16.1 4.0.0 async-http-client-extras-registry diff --git a/extras/retrofit2/pom.xml b/extras/retrofit2/pom.xml index f95bd3a092..86a4e9e672 100644 --- a/extras/retrofit2/pom.xml +++ b/extras/retrofit2/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.3 + 2.16.1 async-http-client-extras-retrofit2 diff --git a/extras/rxjava/pom.xml b/extras/rxjava/pom.xml index 06680338a4..236a22c6c2 100644 --- a/extras/rxjava/pom.xml +++ b/extras/rxjava/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.3 + 2.16.1 async-http-client-extras-rxjava Asynchronous Http Client RxJava Extras diff --git a/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/AsyncHttpObservableTest.java b/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/AsyncHttpObservableTest.java index 8adbecd3af..0b7784c534 100644 --- a/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/AsyncHttpObservableTest.java +++ b/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/AsyncHttpObservableTest.java @@ -26,7 +26,7 @@ public class AsyncHttpObservableTest { - @Test + @Test(groups = "online") public void testToObservableNoError() { final TestSubscriber tester = new TestSubscriber<>(); @@ -46,7 +46,7 @@ public void testToObservableNoError() { } } - @Test + @Test(groups = "online") public void testToObservableError() { final TestSubscriber tester = new TestSubscriber<>(); @@ -66,7 +66,7 @@ public void testToObservableError() { } } - @Test + @Test(groups = "online") public void testObserveNoError() { final TestSubscriber tester = new TestSubscriber<>(); @@ -86,7 +86,7 @@ public void testObserveNoError() { } } - @Test + @Test(groups = "online") public void testObserveError() { final TestSubscriber tester = new TestSubscriber<>(); @@ -106,7 +106,7 @@ public void testObserveError() { } } - @Test + @Test(groups = "online") public void testObserveMultiple() { final TestSubscriber tester = new TestSubscriber<>(); diff --git a/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/single/AsyncHttpSingleTest.java b/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/single/AsyncHttpSingleTest.java index 018da8044c..e25c5ad18c 100644 --- a/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/single/AsyncHttpSingleTest.java +++ b/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/single/AsyncHttpSingleTest.java @@ -261,7 +261,7 @@ public void testErrorInOnThrowablePropagation() { assertEquals(error.getExceptions(), Arrays.asList(processingException, thrownException)); } - @Test + @Test(groups = "online") public void testAbort() throws Exception { final TestSubscriber subscriber = new TestSubscriber<>(); diff --git a/extras/rxjava2/pom.xml b/extras/rxjava2/pom.xml index e1c7af8f3d..fbb1b94a30 100644 --- a/extras/rxjava2/pom.xml +++ b/extras/rxjava2/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.3 + 2.16.1 async-http-client-extras-rxjava2 Asynchronous Http Client RxJava2 Extras diff --git a/extras/simple/pom.xml b/extras/simple/pom.xml index 92ee8730e3..99c61c77bc 100644 --- a/extras/simple/pom.xml +++ b/extras/simple/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.3 + 2.16.1 async-http-client-extras-simple Asynchronous Http Simple Client diff --git a/extras/typesafeconfig/pom.xml b/extras/typesafeconfig/pom.xml index 437b657438..8c144715cf 100644 --- a/extras/typesafeconfig/pom.xml +++ b/extras/typesafeconfig/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.3 + 2.16.1 async-http-client-extras-typesafe-config diff --git a/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java b/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java index fa5d87bcf3..b0dbbddddd 100644 --- a/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java +++ b/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java @@ -289,6 +289,11 @@ public boolean isKeepEncodingHeader() { return getBooleanOpt(KEEP_ENCODING_HEADER_CONFIG).orElse(defaultKeepEncodingHeader()); } + @Override + public int getMaxDecompressedResponseSize() { + return getIntegerOpt(MAX_DECOMPRESSED_RESPONSE_SIZE_CONFIG).orElse(defaultMaxDecompressedResponseSize()); + } + @Override public int getShutdownQuietPeriod() { return getIntegerOpt(SHUTDOWN_QUIET_PERIOD_CONFIG).orElse(defaultShutdownQuietPeriod()); @@ -364,6 +369,11 @@ public boolean isValidateResponseHeaders() { return getBooleanOpt(VALIDATE_RESPONSE_HEADERS_CONFIG).orElse(defaultValidateResponseHeaders()); } + @Override + public boolean isStripAuthorizationOnRedirect() { + return false; + } + @Override public boolean isAggregateWebSocketFrameFragments() { return getBooleanOpt(AGGREGATE_WEBSOCKET_FRAME_FRAGMENTS_CONFIG).orElse(defaultAggregateWebSocketFrameFragments()); diff --git a/netty-utils/pom.xml b/netty-utils/pom.xml index d2be381f14..f2479fd9e4 100644 --- a/netty-utils/pom.xml +++ b/netty-utils/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.12.3 + 2.16.1 4.0.0 async-http-client-netty-utils diff --git a/pom.xml b/pom.xml index 0ab1e952ec..95b1ca6be6 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.asynchttpclient async-http-client-project - 2.12.3 + 2.16.1 pom Asynchronous Http Client Project @@ -24,9 +24,9 @@ - slandelle - Stephane Landelle - slandelle@gatling.io + hyperxpro + Aayush Atharva + aayush@shieldblaze.com @@ -34,17 +34,17 @@ scm:git:git@github.com:AsyncHttpClient/async-http-client.git scm:git:git@github.com:AsyncHttpClient/async-http-client.git https://github.com/AsyncHttpClient/async-http-client/tree/master - async-http-client-project-2.12.3 + async-http-client-project-2.16.1 - sonatype-nexus-staging - https://oss.sonatype.org/content/repositories/snapshots + central + https://central.sonatype.com/repository/maven-snapshots/ - sonatype-nexus-staging - https://oss.sonatype.org/service/local/staging/deploy/maven2/ + central + https://central.sonatype.com @@ -220,42 +220,38 @@ + + org.sonatype.central + central-publishing-maven-plugin + 0.10.0 + true + + central + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.7 + + + sign-artifacts + verify + + sign + + + + + --pinentry-mode + loopback + + + + + - - - release-sign-artifacts - - - performRelease - true - - - - - - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - - - - - - - test-output - - false - - - bom @@ -466,22 +462,22 @@ true 1.8 1.8 - 4.1.60.Final - 1.7.30 - 1.0.3 + 4.1.136.Final + 1.7.36 + 1.0.4 1.2.2 - 2.0.4 + 2.0.17 1.3.8 - 2.2.19 - 1.2.3 - 7.1.0 + 2.2.21 + 1.2.13 + 7.5.1 9.4.18.v20190429 9.0.31 - 2.6 - 1.3.3 + 2.21.0 + 1.6.0 1.2.2 - 3.4.6 + 3.12.4 2.2 - 2.0.0 + 2.1.1