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.http.header; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.util.*; 022import java.util.function.*; 023 024/** 025 * Represents an HTTP <c>Forwarded</c> header. 026 * 027 * <p> 028 * Disclose original information of a client connecting to a web server through an HTTP proxy. 029 * 030 * <h5 class='section'>See Also:</h5><ul> 031 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/NextGenRestClient">Next-Generation REST Client</a> 032 * </ul> 033 * 034 * @since 9.2.1 035 */ 036public class Forwarded extends HttpStringHeader { 037 038 /** The header name */ 039 public static final String NAME = "Forwarded"; 040 041 /** 042 * Constructor with an eager value. 043 * 044 * @param value The header value. May be <jk>null</jk>. 045 */ 046 public Forwarded(String value) { 047 super(NAME, value); 048 } 049 050 /** 051 * Constructor with a lazy value supplier. 052 * 053 * @param valueSupplier Supplier for the header value. Must not be <jk>null</jk>. 054 */ 055 public Forwarded(Supplier<String> valueSupplier) { 056 super(NAME, valueSupplier); 057 } 058 059 /** 060 * Static factory method with an eager value. 061 * 062 * @param value The header value. May be <jk>null</jk>. 063 * @return A new instance. Never <jk>null</jk>. 064 */ 065 public static Forwarded of(String value) { 066 return new Forwarded(value); 067 } 068 069 /** 070 * Static factory method with a lazy value supplier. 071 * 072 * @param valueSupplier Supplier for the header value. Must not be <jk>null</jk>. 073 * @return A new instance. Never <jk>null</jk>. 074 */ 075 public static Forwarded of(Supplier<String> valueSupplier) { 076 return new Forwarded(valueSupplier); 077 } 078 079 /** 080 * Creates a new empty {@link Builder}. 081 * 082 * @return A new builder. Never <jk>null</jk>. 083 */ 084 public static Builder create() { 085 return new Builder(); 086 } 087 088 //------------------------------------------------------------------------------------------------------------------ 089 // Inner types 090 //------------------------------------------------------------------------------------------------------------------ 091 092 /** 093 * Fluent builder for assembling an HTTP <c>Forwarded</c> header value. 094 * 095 * <p> 096 * Composes the forwarding information defined by 097 * <a class='doclink' href='https://www.rfc-editor.org/rfc/rfc7239'>RFC 7239</a> into the 098 * <c>key=value;key=value, key=value</c> wire format: <c>;</c>-separated parameters within a single forwarded 099 * element (one proxy hop) and <c>,</c>-separated elements across hops. Typed setters cover the four registered 100 * parameters (<c>by</c>, <c>for</c>, <c>host</c>, <c>proto</c>); {@link #param(String, String)} is a generic escape 101 * hatch. Values are emitted as bare tokens when possible and quoted automatically when they are not valid tokens 102 * (e.g. IPv6 node identifiers such as <c>"[2001:db8::1]:8080"</c>). Call {@link #next()} to start a new hop. 103 * 104 * <h5 class='section'>Example:</h5> 105 * <p class='bjava'> 106 * String <jv>value</jv> = Forwarded.<jsm>create</jsm>() 107 * .forValue(<js>"192.0.2.60"</js>) 108 * .proto(<js>"http"</js>) 109 * .by(<js>"203.0.113.43"</js>) 110 * .next() 111 * .forValue(<js>"198.51.100.17"</js>) 112 * .build(); 113 * <jc>// => "for=192.0.2.60;proto=http;by=203.0.113.43, for=198.51.100.17"</jc> 114 * </p> 115 * 116 * <h5 class='section'>See Also:</h5><ul> 117 * <li class='jc'>{@link Forwarded} 118 * <li class='extlink'><a class='doclink' href='https://www.rfc-editor.org/rfc/rfc7239'>RFC 7239 - Forwarded HTTP Extension</a> 119 * </ul> 120 * 121 * @since 10.0.0 122 */ 123 public static class Builder { 124 125 private final List<Map<String,String>> elements = l(); 126 private Map<String,String> current; 127 128 /** 129 * Constructor. 130 */ 131 public Builder() { 132 current = m(); 133 elements.add(current); 134 } 135 136 /** 137 * Sets the <c>by</c> parameter (the interface where the request came in to the proxy) on the current element. 138 * 139 * @param value The node identifier. Must not be <jk>null</jk>. 140 * @return This object. 141 */ 142 public Builder by(String value) { 143 return param("by", value); 144 } 145 146 /** 147 * Sets the <c>for</c> parameter (the client that initiated the request) on the current element. 148 * 149 * @param value The node identifier. Must not be <jk>null</jk>. 150 * @return This object. 151 */ 152 public Builder forValue(String value) { 153 return param("for", value); 154 } 155 156 /** 157 * Sets the <c>host</c> parameter (the original <c>Host</c> request header) on the current element. 158 * 159 * @param value The host. Must not be <jk>null</jk>. 160 * @return This object. 161 */ 162 public Builder host(String value) { 163 return param("host", value); 164 } 165 166 /** 167 * Sets the <c>proto</c> parameter (the protocol used to make the request) on the current element. 168 * 169 * @param value The protocol (e.g. <js>"http"</js>, <js>"https"</js>). Must not be <jk>null</jk>. 170 * @return This object. 171 */ 172 public Builder proto(String value) { 173 return param("proto", value); 174 } 175 176 /** 177 * Generic escape hatch for setting any forwarded parameter by name on the current element. 178 * 179 * <p> 180 * Replaces any previously-registered value for the same parameter while preserving its original position. The 181 * value is emitted as a bare token when valid, otherwise as an escaped quoted-string. 182 * 183 * @param name The parameter name. Must not be <jk>null</jk> or blank. 184 * @param value The parameter value. Must not be <jk>null</jk>. 185 * @return This object. 186 * @throws IllegalArgumentException If {@code name} is <jk>null</jk> or blank. 187 */ 188 public Builder param(String name, String value) { 189 reqnn("name", name); 190 reqnn("value", value); 191 var n = name.trim(); 192 if (n.isEmpty()) 193 throw iaex("forwarded parameter name must not be blank"); 194 current.put(n, value); 195 return this; 196 } 197 198 /** 199 * Commits the current forwarded element and begins a new one (the next proxy hop). 200 * 201 * @return This object. 202 */ 203 public Builder next() { 204 current = m(); 205 elements.add(current); 206 return this; 207 } 208 209 private static boolean isToken(String value) { 210 if (value.isEmpty()) 211 return false; 212 for (var i = 0; i < value.length(); i++) { 213 var c = value.charAt(i); 214 var ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') 215 || "!#$%&'*+-.^_`|~".indexOf(c) >= 0; 216 if (! ok) 217 return false; 218 } 219 return true; 220 } 221 222 private static String render(String value) { 223 if (isToken(value)) 224 return value; 225 var sb = new StringBuilder(value.length() + 2); 226 sb.append('"'); 227 for (var i = 0; i < value.length(); i++) { 228 var c = value.charAt(i); 229 if (c == '\\' || c == '"') 230 sb.append('\\'); 231 sb.append(c); 232 } 233 sb.append('"'); 234 return sb.toString(); 235 } 236 237 /** 238 * Builds the rendered <c>Forwarded</c> header value. 239 * 240 * <p> 241 * Empty elements (hops with no parameters) are omitted. Returns an empty string when nothing has been 242 * registered. 243 * 244 * @return The header value. Never <jk>null</jk>. 245 */ 246 public String build() { 247 var rendered = new ArrayList<String>(elements.size()); 248 for (var e : elements) { 249 if (e.isEmpty()) 250 continue; 251 var parts = new ArrayList<String>(e.size()); 252 e.forEach((k, v) -> parts.add(k + "=" + render(v))); 253 rendered.add(String.join(";", parts)); 254 } 255 return String.join(", ", rendered); 256 } 257 258 /** 259 * Builds a {@link Forwarded} header bean directly from this builder. 260 * 261 * @return A {@link Forwarded} carrying {@link #build()}. Never <jk>null</jk>. 262 */ 263 public Forwarded toHeader() { 264 return Forwarded.of(build()); 265 } 266 267 @Override 268 public String toString() { 269 return build(); 270 } 271 } 272}