| /* |
| * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package jdk.incubator.http.internal.common; |
| |
| import jdk.internal.misc.InnocuousThread; |
| import sun.net.NetProperties; |
| |
| import javax.net.ssl.SSLParameters; |
| import java.io.ByteArrayOutputStream; |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.io.UncheckedIOException; |
| import java.io.PrintStream; |
| import java.io.UnsupportedEncodingException; |
| import java.net.NetPermission; |
| import java.net.URI; |
| import java.net.URLPermission; |
| import java.nio.ByteBuffer; |
| import java.nio.charset.Charset; |
| import java.nio.charset.StandardCharsets; |
| import java.security.AccessController; |
| import java.security.PrivilegedAction; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.concurrent.BlockingQueue; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.function.Predicate; |
| import jdk.incubator.http.HttpHeaders; |
| |
| /** |
| * Miscellaneous utilities |
| */ |
| public final class Utils { |
| |
| /** |
| * Allocated buffer size. Must never be higher than 16K. But can be lower |
| * if smaller allocation units preferred. HTTP/2 mandates that all |
| * implementations support frame payloads of at least 16K. |
| */ |
| public static final int DEFAULT_BUFSIZE = 16 * 1024; |
| |
| public static final int BUFSIZE = getIntegerNetProperty( |
| "jdk.httpclient.bufsize", DEFAULT_BUFSIZE |
| ); |
| |
| private static final Set<String> DISALLOWED_HEADERS_SET = Set.of( |
| "authorization", "connection", "cookie", "content-length", |
| "date", "expect", "from", "host", "origin", "proxy-authorization", |
| "referer", "user-agent", "upgrade", "via", "warning"); |
| |
| public static final Predicate<String> |
| ALLOWED_HEADERS = header -> !Utils.DISALLOWED_HEADERS_SET.contains(header); |
| |
| public static final Predicate<String> |
| ALL_HEADERS = header -> true; |
| |
| public static ByteBuffer getBuffer() { |
| return ByteBuffer.allocate(BUFSIZE); |
| } |
| |
| public static IOException getIOException(Throwable t) { |
| if (t instanceof IOException) { |
| return (IOException) t; |
| } |
| Throwable cause = t.getCause(); |
| if (cause != null) { |
| return getIOException(cause); |
| } |
| return new IOException(t); |
| } |
| |
| /** |
| * We use the same buffer for reading all headers and dummy bodies in an Exchange. |
| */ |
| public static ByteBuffer getExchangeBuffer() { |
| ByteBuffer buf = getBuffer(); |
| // Force a read the first time it is used |
| buf.limit(0); |
| return buf; |
| } |
| |
| /** |
| * Puts position to limit and limit to capacity so we can resume reading |
| * into this buffer, but if required > 0 then limit may be reduced so that |
| * no more than required bytes are read next time. |
| */ |
| static void resumeChannelRead(ByteBuffer buf, int required) { |
| int limit = buf.limit(); |
| buf.position(limit); |
| int capacity = buf.capacity() - limit; |
| if (required > 0 && required < capacity) { |
| buf.limit(limit + required); |
| } else { |
| buf.limit(buf.capacity()); |
| } |
| } |
| |
| private Utils() { } |
| |
| public static ExecutorService innocuousThreadPool() { |
| return Executors.newCachedThreadPool( |
| (r) -> InnocuousThread.newThread("DefaultHttpClient", r)); |
| } |
| |
| // ABNF primitives defined in RFC 7230 |
| private static final boolean[] tchar = new boolean[256]; |
| private static final boolean[] fieldvchar = new boolean[256]; |
| |
| static { |
| char[] allowedTokenChars = |
| ("!#$%&'*+-.^_`|~0123456789" + |
| "abcdefghijklmnopqrstuvwxyz" + |
| "ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray(); |
| for (char c : allowedTokenChars) { |
| tchar[c] = true; |
| } |
| for (char c = 0x21; c < 0xFF; c++) { |
| fieldvchar[c] = true; |
| } |
| fieldvchar[0x7F] = false; // a little hole (DEL) in the range |
| } |
| |
| /* |
| * Validates a RFC 7230 field-name. |
| */ |
| public static boolean isValidName(String token) { |
| for (int i = 0; i < token.length(); i++) { |
| char c = token.charAt(i); |
| if (c > 255 || !tchar[c]) { |
| return false; |
| } |
| } |
| return !token.isEmpty(); |
| } |
| |
| /* |
| * Validates a RFC 7230 field-value. |
| * |
| * "Obsolete line folding" rule |
| * |
| * obs-fold = CRLF 1*( SP / HTAB ) |
| * |
| * is not permitted! |
| */ |
| public static boolean isValidValue(String token) { |
| boolean accepted = true; |
| for (int i = 0; i < token.length(); i++) { |
| char c = token.charAt(i); |
| if (c > 255) { |
| return false; |
| } |
| if (accepted) { |
| if (c == ' ' || c == '\t') { |
| accepted = false; |
| } else if (!fieldvchar[c]) { |
| return false; // forbidden byte |
| } |
| } else { |
| if (c != ' ' && c != '\t') { |
| if (fieldvchar[c]) { |
| accepted = true; |
| } else { |
| return false; // forbidden byte |
| } |
| } |
| } |
| } |
| return accepted; |
| } |
| |
| /** |
| * Returns the security permission required for the given details. |
| * If method is CONNECT, then uri must be of form "scheme://host:port" |
| */ |
| public static URLPermission getPermission(URI uri, |
| String method, |
| Map<String, List<String>> headers) { |
| StringBuilder sb = new StringBuilder(); |
| |
| String urlstring, actionstring; |
| |
| if (method.equals("CONNECT")) { |
| urlstring = uri.toString(); |
| actionstring = "CONNECT"; |
| } else { |
| sb.append(uri.getScheme()) |
| .append("://") |
| .append(uri.getAuthority()) |
| .append(uri.getPath()); |
| urlstring = sb.toString(); |
| |
| sb = new StringBuilder(); |
| sb.append(method); |
| if (headers != null && !headers.isEmpty()) { |
| sb.append(':'); |
| Set<String> keys = headers.keySet(); |
| boolean first = true; |
| for (String key : keys) { |
| if (!first) { |
| sb.append(','); |
| } |
| sb.append(key); |
| first = false; |
| } |
| } |
| actionstring = sb.toString(); |
| } |
| return new URLPermission(urlstring, actionstring); |
| } |
| |
| public static void checkNetPermission(String target) { |
| SecurityManager sm = System.getSecurityManager(); |
| if (sm == null) { |
| return; |
| } |
| NetPermission np = new NetPermission(target); |
| sm.checkPermission(np); |
| } |
| |
| public static int getIntegerNetProperty(String name, int defaultValue) { |
| return AccessController.doPrivileged((PrivilegedAction<Integer>) () -> |
| NetProperties.getInteger(name, defaultValue)); |
| } |
| |
| static String getNetProperty(String name) { |
| return AccessController.doPrivileged((PrivilegedAction<String>) () -> |
| NetProperties.get(name)); |
| } |
| |
| public static SSLParameters copySSLParameters(SSLParameters p) { |
| SSLParameters p1 = new SSLParameters(); |
| p1.setAlgorithmConstraints(p.getAlgorithmConstraints()); |
| p1.setCipherSuites(p.getCipherSuites()); |
| // JDK 8 EXCL START |
| p1.setEnableRetransmissions(p.getEnableRetransmissions()); |
| p1.setMaximumPacketSize(p.getMaximumPacketSize()); |
| // JDK 8 EXCL END |
| p1.setEndpointIdentificationAlgorithm(p.getEndpointIdentificationAlgorithm()); |
| p1.setNeedClientAuth(p.getNeedClientAuth()); |
| String[] protocols = p.getProtocols(); |
| if (protocols != null) { |
| p1.setProtocols(protocols.clone()); |
| } |
| p1.setSNIMatchers(p.getSNIMatchers()); |
| p1.setServerNames(p.getServerNames()); |
| p1.setUseCipherSuitesOrder(p.getUseCipherSuitesOrder()); |
| p1.setWantClientAuth(p.getWantClientAuth()); |
| return p1; |
| } |
| |
| /** |
| * Set limit to position, and position to mark. |
| */ |
| public static void flipToMark(ByteBuffer buffer, int mark) { |
| buffer.limit(buffer.position()); |
| buffer.position(mark); |
| } |
| |
| public static String stackTrace(Throwable t) { |
| ByteArrayOutputStream bos = new ByteArrayOutputStream(); |
| String s = null; |
| try { |
| PrintStream p = new PrintStream(bos, true, "US-ASCII"); |
| t.printStackTrace(p); |
| s = bos.toString("US-ASCII"); |
| } catch (UnsupportedEncodingException ex) { |
| // can't happen |
| } |
| return s; |
| } |
| |
| /** |
| * Copies as much of src to dst as possible. |
| * Return number of bytes copied |
| */ |
| public static int copy(ByteBuffer src, ByteBuffer dst) { |
| int srcLen = src.remaining(); |
| int dstLen = dst.remaining(); |
| if (srcLen > dstLen) { |
| int diff = srcLen - dstLen; |
| int limit = src.limit(); |
| src.limit(limit - diff); |
| dst.put(src); |
| src.limit(limit); |
| } else { |
| dst.put(src); |
| } |
| return srcLen - src.remaining(); |
| } |
| |
| // copy up to amount from src to dst, but no more |
| public static int copyUpTo(ByteBuffer src, ByteBuffer dst, int amount) { |
| int toCopy = Math.min(src.remaining(), Math.min(dst.remaining(), amount)); |
| copy(src, dst, toCopy); |
| return toCopy; |
| } |
| |
| /** |
| * Copy amount bytes from src to dst. at least amount must be |
| * available in both dst and in src |
| */ |
| public static void copy(ByteBuffer src, ByteBuffer dst, int amount) { |
| int excess = src.remaining() - amount; |
| assert excess >= 0; |
| if (excess > 0) { |
| int srclimit = src.limit(); |
| src.limit(srclimit - excess); |
| dst.put(src); |
| src.limit(srclimit); |
| } else { |
| dst.put(src); |
| } |
| } |
| |
| public static ByteBuffer copy(ByteBuffer src) { |
| ByteBuffer dst = ByteBuffer.allocate(src.remaining()); |
| dst.put(src); |
| dst.flip(); |
| return dst; |
| } |
| |
| public static String dump(Object... objects) { |
| return Arrays.toString(objects); |
| } |
| |
| public static String stringOf(Collection<?> source) { |
| // We don't know anything about toString implementation of this |
| // collection, so let's create an array |
| return Arrays.toString(source.toArray()); |
| } |
| |
| public static int remaining(ByteBuffer[] bufs) { |
| int remain = 0; |
| for (ByteBuffer buf : bufs) { |
| remain += buf.remaining(); |
| } |
| return remain; |
| } |
| |
| public static int remaining(List<ByteBuffer> bufs) { |
| int remain = 0; |
| for (ByteBuffer buf : bufs) { |
| remain += buf.remaining(); |
| } |
| return remain; |
| } |
| |
| public static int remaining(ByteBufferReference[] refs) { |
| int remain = 0; |
| for (ByteBufferReference ref : refs) { |
| remain += ref.get().remaining(); |
| } |
| return remain; |
| } |
| |
| // assumes buffer was written into starting at position zero |
| static void unflip(ByteBuffer buf) { |
| buf.position(buf.limit()); |
| buf.limit(buf.capacity()); |
| } |
| |
| public static void close(Closeable... closeables) { |
| for (Closeable c : closeables) { |
| try { |
| c.close(); |
| } catch (IOException ignored) { } |
| } |
| } |
| |
| public static void close(Throwable t, Closeable... closeables) { |
| for (Closeable c : closeables) { |
| try { |
| ExceptionallyCloseable.close(t, c); |
| } catch (IOException ignored) { } |
| } |
| } |
| |
| /** |
| * Returns an array with the same buffers, but starting at position zero |
| * in the array. |
| */ |
| public static ByteBuffer[] reduce(ByteBuffer[] bufs, int start, int number) { |
| if (start == 0 && number == bufs.length) { |
| return bufs; |
| } |
| ByteBuffer[] nbufs = new ByteBuffer[number]; |
| int j = 0; |
| for (int i=start; i<start+number; i++) { |
| nbufs[j++] = bufs[i]; |
| } |
| return nbufs; |
| } |
| |
| static String asString(ByteBuffer buf) { |
| byte[] b = new byte[buf.remaining()]; |
| buf.get(b); |
| return new String(b, StandardCharsets.US_ASCII); |
| } |
| |
| /** |
| * Returns a single threaded executor which uses one invocation |
| * of the parent executor to execute tasks (in sequence). |
| * |
| * Use a null valued Runnable to terminate. |
| */ |
| // TODO: this is a blocking way of doing this; |
| public static Executor singleThreadExecutor(Executor parent) { |
| BlockingQueue<Optional<Runnable>> queue = new LinkedBlockingQueue<>(); |
| parent.execute(() -> { |
| while (true) { |
| try { |
| Optional<Runnable> o = queue.take(); |
| if (!o.isPresent()) { |
| return; |
| } |
| o.get().run(); |
| } catch (InterruptedException ex) { |
| return; |
| } |
| } |
| }); |
| return new Executor() { |
| @Override |
| public void execute(Runnable command) { |
| queue.offer(Optional.ofNullable(command)); |
| } |
| }; |
| } |
| |
| private static void executeInline(Runnable r) { |
| r.run(); |
| } |
| |
| static Executor callingThreadExecutor() { |
| return Utils::executeInline; |
| } |
| |
| // Put all these static 'empty' singletons here |
| @SuppressWarnings("rawtypes") |
| public static final CompletableFuture[] EMPTY_CFARRAY = new CompletableFuture[0]; |
| |
| public static final ByteBuffer EMPTY_BYTEBUFFER = ByteBuffer.allocate(0); |
| public static final ByteBuffer[] EMPTY_BB_ARRAY = new ByteBuffer[0]; |
| |
| public static ByteBuffer slice(ByteBuffer buffer, int amount) { |
| ByteBuffer newb = buffer.slice(); |
| newb.limit(amount); |
| buffer.position(buffer.position() + amount); |
| return newb; |
| } |
| |
| /** |
| * Get the Charset from the Content-encoding header. Defaults to |
| * UTF_8 |
| */ |
| public static Charset charsetFrom(HttpHeaders headers) { |
| String encoding = headers.firstValue("Content-encoding") |
| .orElse("UTF_8"); |
| try { |
| return Charset.forName(encoding); |
| } catch (IllegalArgumentException e) { |
| return StandardCharsets.UTF_8; |
| } |
| } |
| |
| public static UncheckedIOException unchecked(IOException e) { |
| return new UncheckedIOException(e); |
| } |
| } |