001// Copyright 2011, 2012, 2026 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007//     http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.internal.services.security;
016
017import org.apache.tapestry5.http.services.Request;
018import org.apache.tapestry5.services.security.WhitelistAnalyzer;
019
020import java.net.Inet6Address;
021import java.net.InetAddress;
022import java.net.UnknownHostException;
023import java.util.Set;
024
025/**
026 * Standard analyzer that places requests from loopback addresses onto the whitelist.
027 * Recognized forms include IPv4 (127.x.x.x), IPv6 (::1, 0:0:0:0:0:0:0:1), compressed
028 * or with arbitrary scope IDs (e.g. ::1%lo, 0:0:0:0:0:0:0:1%eth0), and IPv4-mapped
029 * IPv6 loopback (::ffff:127.0.0.1).
030 *
031 * @since 5.3
032 */
033public class LocalhostOnly implements WhitelistAnalyzer
034{
035    // Common loopback address literals that need no parsing.
036    // Adding "localhost" avoids a potential hostname lookup
037    // via InetAddress.getByName().
038    private static final Set<String> LOOPBACK_LITERALS = Set.of(
039        "localhost",
040        "127.0.0.1",
041        "::1",
042        "0:0:0:0:0:0:0:1"
043    );
044
045    public boolean isRequestOnWhitelist(Request request)
046    {
047        String remoteAddr = request.getRemoteAddr();
048
049        // Fastpath
050        if (LOOPBACK_LITERALS.contains(remoteAddr))
051        {
052            return true;
053        }
054
055        // Strip IPv6 scope ID (e.g. ::1%lo, 0:0:0:0:0:0:0:1%eth0) before parsing
056        int percentIdx = remoteAddr.indexOf('%');
057        String addr = (percentIdx >= 0) ? remoteAddr.substring(0, percentIdx) : remoteAddr;
058
059        try
060        {
061            InetAddress inetAddress = InetAddress.getByName(addr);
062
063            if (inetAddress.isLoopbackAddress())
064                return true;
065
066            // InetAddress.isLoopbackAddress() returns false for IPv4-mapped IPv6 loopback
067            // (e.g. ::ffff:127.0.0.1). Extract the embedded IPv4 address and check that instead.
068            if (inetAddress instanceof Inet6Address)
069            {
070                byte[] bytes = inetAddress.getAddress();
071                if (isIPv4MappedAddress(bytes))
072                {
073                    InetAddress embedded = InetAddress.getByAddress(new byte[]{ bytes[12], bytes[13], bytes[14], bytes[15] });
074                    return embedded.isLoopbackAddress();
075                }
076            }
077
078            return false;
079        }
080        catch (UnknownHostException e)
081        {
082            return false;
083        }
084    }
085
086    // IPv4-mapped IPv6 addresses have the form ::ffff:a.b.c.d, encoded as 16 bytes:
087    //  - bytes  0–9:  all zero
088    //  - bytes 10–11: 0xff 0xff  (the "mapped" marker)
089    //  - bytes 12–15: the IPv4 address
090    private static boolean isIPv4MappedAddress(byte[] bytes)
091    {
092        if (bytes.length != 16)
093        {
094            return false;
095        }
096
097        // Bytes 0–9 must all be zero
098        for (int i = 0; i < 10; i++)
099        {
100            if (bytes[i] != 0) {
101                return false;
102            }
103        }
104
105        // Bytes 10–11 must be 0xff 0xff
106        return bytes[10] == (byte) 0xff && bytes[11] == (byte) 0xff;
107    }
108}