001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.client.classic.remote;
018
019import java.util.*;
020import java.util.concurrent.*;
021import java.util.logging.*;
022
023import org.apache.juneau.commons.svl.*;
024import org.apache.juneau.http.remote.*;
025
026/**
027 * Shared helpers for the classic REST-proxy engine.
028 *
029 * <p>
030 * The parse/apply logic here is ported from the next-generation engine
031 * (<c>org.apache.juneau.rest.client.remote.RemoteClient</c> and <c>RrpcInterfaceMeta</c>) so that the classic
032 * engine can honor the same {@link Remote @Remote}/{@link RemoteOp @RemoteOp}
033 * members without introducing a cross-module dependency on freshly-installed common code.
034 *
035 * <h5 class='section'>See Also:</h5><ul>
036 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestProxies">REST Proxies</a>
037 * </ul>
038 */
039public class RemoteProxyUtils {
040
041   private RemoteProxyUtils() {}
042
043   private static final Logger LOG = Logger.getLogger("org.apache.juneau.rest.client.classic.remote");
044
045   /** Interfaces already warned about an engine-specific member, to keep the build-time warning one-time. */
046   private static final Set<String> WARNED = ConcurrentHashMap.newKeySet();
047
048   /**
049    * Parses an array of constant {@code "name<delim>value"} strings into resolved name/value entries.
050    *
051    * <p>
052    * Ported from the next-generation engine's {@code RrpcInterfaceMeta.parseConstantParts(...)}.  Each entry is
053    * resolved through {@link VarResolver#DEFAULT} (so values such as {@code "$S{sysprop}"} expand), then split on the
054    * first occurrence of {@code delim} ({@code ':'} for the {@code "Name: value"} header form, {@code '='} for the
055    * {@code "name=value"} query/form-data form).  Entries with no delimiter are skipped.  Both the name and the value
056    * are trimmed.
057    *
058    * @param entries The raw annotation strings (may be empty).
059    * @param delim The name/value delimiter character.
060    * @return An unmodifiable list of resolved name/value entries. Never <jk>null</jk>, but may be empty.
061    */
062   public static List<Map.Entry<String,String>> parseConstantParts(String[] entries, char delim) {
063      if (entries == null || entries.length == 0)
064         return List.of();
065      var l = new ArrayList<Map.Entry<String,String>>();
066      for (var e : entries) {
067         var s = VarResolver.DEFAULT.resolve(e);
068         var i = s.indexOf(delim);
069         if (i != -1)
070            l.add(Map.entry(s.substring(0, i).trim(), s.substring(i + 1).trim()));
071      }
072      return Collections.unmodifiableList(l);
073   }
074
075   /**
076    * Merges interface-level and method-level constant parts into a single name-keyed map, with method-level entries
077    * overriding interface-level entries of the same name (declaration order preserved).
078    *
079    * <p>
080    * Ported from the next-generation engine's {@code RemoteInvocationHandler.applyConstants(...)}.
081    *
082    * @param interfaceLevel Interface-level constants (applied first; lower precedence).
083    * @param methodLevel Method-level constants (override interface-level entries of the same name).
084    * @return An ordered, name-keyed map of merged constants. Never <jk>null</jk>, but may be empty.
085    */
086   public static Map<String,String> mergeConstants(List<Map.Entry<String,String>> interfaceLevel, List<Map.Entry<String,String>> methodLevel) {
087      var merged = new LinkedHashMap<String,String>();
088      for (var e : interfaceLevel)
089         merged.put(e.getKey(), e.getValue());
090      for (var e : methodLevel)
091         merged.put(e.getKey(), e.getValue());
092      return merged;
093   }
094
095   /**
096    * Combines a base URL with a method path, avoiding double slashes.
097    *
098    * <p>
099    * Ported verbatim from the next-generation engine's {@code RemoteInvocationHandler.combinePaths(...)}.
100    *
101    * @param base The base URL/path.
102    * @param method The method path.
103    * @return The combined path.
104    */
105   public static String combinePaths(String base, String method) {
106      if (base.isEmpty())
107         return method.isEmpty() ? "" : method;
108      if (method.isEmpty())
109         return base;
110      if (base.endsWith("/") && method.startsWith("/"))
111         return base + method.substring(1);
112      if (! base.endsWith("/") && ! method.startsWith("/"))
113         return base + "/" + method;
114      return base + method;
115   }
116
117   /**
118    * Enforces the SSRF guardrail with the default (deny-private) policy: delegates to
119    * {@link #requireHttpScheme(String, boolean) requireHttpScheme(url, false)}.
120    *
121    * @param url The URL to validate.
122    * @return The unchanged URL.
123    * @throws IllegalArgumentException If {@code url} is absolute and violates the policy.
124    */
125   public static String requireHttpScheme(String url) {
126      return requireHttpScheme(url, false);
127   }
128
129   /**
130    * Enforces the SSRF guardrail: when {@code url} carries a URI scheme it must be {@code http} or {@code https}
131    * with a nonempty host, and &mdash; unless {@code allowPrivateUrls} is set &mdash; must not target a
132    * loopback/private/link-local/metadata host (see {@link RemoteUrlPolicy}).  Scheme-less (relative) values pass
133    * through unchanged.
134    *
135    * <p>
136    * Delegates to the shared {@link RemoteUrlPolicy#requireAllowedUrl(String, boolean)} used by both the classic and
137    * next-generation engines, so the deny-list is defined exactly once.
138    *
139    * @param url The URL to validate.
140    * @param allowPrivateUrls When <jk>true</jk>, the deny-private check is skipped (the http/https scheme
141    *    requirement still applies).
142    * @return The unchanged URL.
143    * @throws IllegalArgumentException If {@code url} is absolute and violates the policy.
144    */
145   public static String requireHttpScheme(String url, boolean allowPrivateUrls) {
146      return RemoteUrlPolicy.requireAllowedUrl(url, allowPrivateUrls);
147   }
148
149   /**
150    * Same as {@link #requireHttpScheme(String, boolean)}, additionally honoring a client's per-client allowed address ranges.
151    *
152    * @param url The URL to validate.
153    * @param allowPrivateUrls When <jk>true</jk>, the deny-private check is skipped.
154    * @param allowedRanges The client's {@link org.apache.juneau.http.remote.RemoteUrlPolicy.AllowedRanges RemoteUrlPolicy.AllowedRanges}, unioned with the {@code RestClient.allowAddressRanges} property.
155    * @return The unchanged URL.
156    * @throws IllegalArgumentException If {@code url} is absolute and violates the policy.
157    */
158   public static String requireHttpScheme(String url, boolean allowPrivateUrls, RemoteUrlPolicy.AllowedRanges allowedRanges) {
159      return RemoteUrlPolicy.requireAllowedUrl(url, allowPrivateUrls, allowedRanges);
160   }
161
162   /**
163    * Emits a one-time build-time warning that an {@link Remote @Remote}/{@link RemoteOp @RemoteOp}
164    * member is set on the classic engine that the classic engine cannot honor.
165    *
166    * <p>
167    * De-duplicated per interface-class + member name so it fires at most once for a given proxy interface.
168    *
169    * @param interfaceClass The proxy interface the member was declared on.
170    * @param member The unsupported member name (e.g. <js>"interceptors"</js>).
171    * @param detail A short explanation of why the member is not honored.
172    */
173   public static void warnUnsupportedMember(Class<?> interfaceClass, String member, String detail) {
174      var key = interfaceClass.getName() + "#" + member;
175      if (WARNED.add(key))
176         LOG.warning(() -> "@Remote/@RemoteOp member '" + member + "' set on " + interfaceClass.getName()
177            + " is not honored by the classic REST-proxy engine: " + detail
178            + "  Use the next-generation engine (RestClient.remote(...)) for this feature.");
179   }
180}