001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.named.ipc;
020
021import java.io.IOException;
022import java.net.Inet6Address;
023import java.net.InetAddress;
024import java.net.InetSocketAddress;
025import java.net.SocketAddress;
026import java.net.StandardProtocolFamily;
027import java.net.UnixDomainSocketAddress;
028import java.net.UnknownHostException;
029import java.nio.channels.ServerSocketChannel;
030import java.nio.file.Files;
031import java.nio.file.Path;
032import java.nio.file.attribute.PosixFilePermission;
033import java.util.EnumSet;
034
035/**
036 * Socket factory.
037 *
038 * @since 2.0.1
039 */
040public enum SocketFamily {
041    inet,
042    unix;
043
044    public ServerSocketChannel openServerSocket() throws IOException {
045        return switch (this) {
046            case inet -> ServerSocketChannel.open().bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
047            case unix -> {
048                ServerSocketChannel channel =
049                        ServerSocketChannel.open(StandardProtocolFamily.UNIX).bind(null, 0);
050                restrictToOwner(channel);
051                yield channel;
052            }
053            default -> throw new IllegalStateException();
054        };
055    }
056
057    /**
058     * Restricts access to the socket file backing the given unix-domain server socket to the owning user: the IPC
059     * lock protocol carries no authentication, so the socket file permissions are what prevents other local users
060     * from connecting to the lock daemon and disrupting or stopping it. Automatically bound sockets are created
061     * in the system temporary directory, which is commonly shared between users. On filesystems without POSIX
062     * permissions (e.g. Windows) this is a no-op.
063     *
064     * @since 2.0.23
065     */
066    private static void restrictToOwner(ServerSocketChannel channel) throws IOException {
067        SocketAddress address = channel.getLocalAddress();
068        if (address instanceof UnixDomainSocketAddress) {
069            Path path = ((UnixDomainSocketAddress) address).getPath();
070            try {
071                Files.setPosixFilePermissions(
072                        path,
073                        EnumSet.of(
074                                PosixFilePermission.OWNER_READ,
075                                PosixFilePermission.OWNER_WRITE,
076                                PosixFilePermission.OWNER_EXECUTE));
077            } catch (UnsupportedOperationException e) {
078                // no POSIX permissions on this filesystem: nothing to tighten here
079            }
080        }
081    }
082
083    public static SocketAddress fromString(String str) {
084        if (str.startsWith("inet:")) {
085            String s = str.substring("inet:".length());
086            int ic = s.lastIndexOf(':');
087            String ia = s.substring(0, ic);
088            int is = ia.indexOf('/');
089            String h = ia.substring(0, is);
090            String a = ia.substring(is + 1);
091            String p = s.substring(ic + 1);
092            InetAddress addr;
093            if ("<unresolved>".equals(a)) {
094                return InetSocketAddress.createUnresolved(h, Integer.parseInt(p));
095            } else {
096                if (a.indexOf('.') > 0) {
097                    String[] as = a.split("\\.");
098                    if (as.length != 4) {
099                        throw new IllegalArgumentException("Unsupported socket address: '" + str + "'");
100                    }
101                    byte[] ab = new byte[4];
102                    for (int i = 0; i < 4; i++) {
103                        ab[i] = (byte) Integer.parseInt(as[i]);
104                    }
105                    try {
106                        addr = InetAddress.getByAddress(h.isEmpty() ? null : h, ab);
107                    } catch (UnknownHostException e) {
108                        throw new IllegalArgumentException("Unsupported address: " + str, e);
109                    }
110                } else {
111                    throw new IllegalArgumentException("Unsupported address: " + str);
112                }
113                return new InetSocketAddress(addr, Integer.parseInt(p));
114            }
115        } else if (str.startsWith("unix:")) {
116            return UnixDomainSocketAddress.of(str.substring("unix:".length()));
117        } else {
118            throw new IllegalArgumentException("Unsupported socket address: '" + str + "'");
119        }
120    }
121
122    public static String toString(SocketAddress address) {
123        switch (familyOf(address)) {
124            case inet:
125                InetSocketAddress isa = (InetSocketAddress) address;
126                String host = isa.getHostString();
127                InetAddress addr = isa.getAddress();
128                int port = isa.getPort();
129                String formatted;
130                if (addr == null) {
131                    formatted = host + "/<unresolved>";
132                } else {
133                    formatted = addr.toString();
134                    if (addr instanceof Inet6Address) {
135                        int i = formatted.lastIndexOf("/");
136                        formatted = formatted.substring(0, i + 1) + "[" + formatted.substring(i + 1) + "]";
137                    }
138                }
139                return "inet:" + formatted + ":" + port;
140            case unix:
141                // to keep address string unchanged across all OSes
142                return "unix:" + address.toString().replace('\\', '/');
143            default:
144                throw new IllegalArgumentException("Unsupported socket address: '" + address + "'");
145        }
146    }
147
148    public static SocketFamily familyOf(SocketAddress address) {
149        if (address instanceof InetSocketAddress) {
150            return SocketFamily.inet;
151        } else if ("java.net.UnixDomainSocketAddress".equals(address.getClass().getName())) {
152            return SocketFamily.unix;
153        } else {
154            throw new IllegalArgumentException("Unsupported socket address '" + address + "'");
155        }
156    }
157}