All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.fusionauth.http.util.HTTPTools Maven / Gradle / Ivy

Go to download

An HTTP library for Java that provides a lightweight server (currently) and client (eventually) both with a goal of high-performance and simplicity

There is a newer version: 0.4.0-RC.3
Show newest version
/*
 * Copyright (c) 2022, FusionAuth, All Rights Reserved
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific
 * language governing permissions and limitations under the License.
 */
package io.fusionauth.http.util;

import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import io.fusionauth.http.HTTPValues.ControlBytes;
import io.fusionauth.http.HTTPValues.ProtocolBytes;
import io.fusionauth.http.ParseException;
import io.fusionauth.http.io.ByteBufferOutputStream;
import io.fusionauth.http.server.HTTPResponse;

public final class HTTPTools {
  /**
   * Builds the HTTP response head section (status line, headers, etc).
   *
   * @param response  The response.
   * @param maxLength The maximum length of the complete HTTP response head section.
   * @return The bytes of the response head section.
   */
  public static ByteBuffer buildExpectResponsePreamble(HTTPResponse response, int maxLength) {
    ByteBufferOutputStream bbos = new ByteBufferOutputStream(1024, maxLength);
    writeStatusLine(response, bbos);
    if (response.getStatus() != 100) {
      bbos.write("Content-Length: 0".getBytes());
      bbos.write(ControlBytes.CRLF);
      bbos.write("Connection: close".getBytes());
      bbos.write(ControlBytes.CRLF);
    }
    bbos.write(ControlBytes.CRLF);
    return bbos.toByteBuffer();
  }

  /**
   * Builds the HTTP response head section (status line, headers, etc).
   *
   * @param response  The response.
   * @param maxLength The maximum length of the complete HTTP response head section.
   * @return The bytes of the response head section.
   */
  public static ByteBuffer buildResponsePreamble(HTTPResponse response, int maxLength) {
    ByteBufferOutputStream bbos = new ByteBufferOutputStream(1024, maxLength);
    writeStatusLine(response, bbos);
    response.getHeadersMap().forEach((key, values) ->
        values.forEach(value -> {
          bbos.write(key.getBytes());
          bbos.write(':');
          bbos.write(' ');
          bbos.write(value.getBytes());
          bbos.write(ControlBytes.CRLF);
        }));
    bbos.write(ControlBytes.CRLF);
    return bbos.toByteBuffer();
  }

  /**
   * Determines if the given character (byte) is a digit (i.e. 0-9)
   *
   * @param ch The character as a byte since HTTP is ASCII.
   * @return True if the character is a digit.
   */
  public static boolean isDigitCharacter(byte ch) {
    return ch >= '0' && ch <= '9';
  }

  /**
   * Determines if the given character (byte) is an allowed hexadecimal character (i.e. 0-9a-zA-Z)
   *
   * @param ch The character as a byte since HTTP is ASCII.
   * @return True if the character is a hexadecimal character.
   */
  public static boolean isHexadecimalCharacter(byte ch) {
    return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F');
  }

  /**
   * Determines if the given character (byte) is an allowed HTTP token character (header field names, methods, etc).
   * 

* Covered by https://www.rfc-editor.org/rfc/rfc9110.html#name-fields * * @param ch The character as a byte since HTTP is ASCII. * @return True if the character is a token character. */ public static boolean isTokenCharacter(byte ch) { return ch == '!' || ch == '#' || ch == '$' || ch == '%' || ch == '&' || ch == '\'' || ch == '*' || ch == '+' || ch == '-' || ch == '.' || ch == '^' || ch == '_' || ch == '`' || ch == '|' || ch == '~' || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9'); } /** * Naively determines if the given character (byte) is an allowed URI character. * * @param ch The character as a byte since URIs are ASCII. * @return True if the character is a URI character. */ public static boolean isURICharacter(byte ch) { // TODO : Fully implement RFC 3986 to accurate parsing return ch >= '!' && ch <= '~'; } public static boolean isValueCharacter(byte ch) { return isURICharacter(ch) || ch == ' ' || ch == '\t' || ch == '\n'; } /** * Parses URL encoded data either from a URL parameter list in the query string or the form body. * * @param data The data as a character array. * @param start The start index to start parsing from. * @param length The length to parse. * @param result The result Map to put the value into. */ public static void parseEncodedData(byte[] data, int start, int length, Map> result) { boolean inName = true; String name = null; String value; for (int i = start; i < length; i++) { if (data[i] == '=' && inName) { // Names can't start with an equal sign if (i == start) { start++; continue; } inName = false; try { name = URLDecoder.decode(new String(data, start, i - start), StandardCharsets.UTF_8); } catch (Exception e) { name = null; // Malformed } start = i + 1; } else if (data[i] == '&' && !inName) { inName = true; if (name == null || start > i) { continue; // Malformed } try { if (start < i) { value = URLDecoder.decode(new String(data, start, i - start), StandardCharsets.UTF_8); } else { value = ""; } result.computeIfAbsent(name, key -> new LinkedList<>()).add(value); } catch (Exception e) { // Ignore } start = i + 1; name = null; } } if (name != null && !inName) { if (start < length) { value = URLDecoder.decode(new String(data, start, length - start), StandardCharsets.UTF_8); } else { value = ""; } result.computeIfAbsent(name, key -> new LinkedList<>()).add(value); } } /** * Parses an HTTP header value that is a standard semicolon separated list of values. * * @param value The header value. * @return The HeaderValue record. */ public static HeaderValue parseHeaderValue(String value) { String headerValue = null; Map parameters = null; char[] chars = value.toCharArray(); boolean inQuote = false; int start = 0; for (int i = 0; i < chars.length; i++) { char c = chars[i]; if (!inQuote && c == ';') { if (headerValue == null) { headerValue = new String(chars, start, i - start); } else { if (parameters == null) { parameters = new HashMap<>(); } parseHeaderParameter(chars, start, i, parameters); } start = -1; } else if (!inQuote && !Character.isWhitespace(c) && start == -1) { start = i; } else if (!inQuote && c == '"') { inQuote = true; } else if (inQuote && c == '\\' && i < chars.length - 2 && chars[i + 1] == '"') { i++; // Skip the next quote character since it is escaped } else if (inQuote && c == '"') { inQuote = false; } } // Add any final part if (start != -1) { if (headerValue == null) { headerValue = new String(chars, start, chars.length - start); } else { if (parameters == null) { parameters = new HashMap<>(); } parseHeaderParameter(chars, start, chars.length, parameters); } } if (headerValue == null) { throw new ParseException("Unable to parse a parameterized HTTP header [" + value + "]"); } if (parameters == null) { parameters = Map.of(); } return new HeaderValue(headerValue, parameters); } private static void parseHeaderParameter(char[] chars, int start, int end, Map parameters) { boolean encoded = false; Charset charset = null; String name = null; for (int i = start; i < end; i++) { if (name == null && chars[i] == '*') { encoded = true; name = new String(chars, start, i - start).toLowerCase(); start = i + 2; } else if (name == null && chars[i] == '=') { name = new String(chars, start, i - start).toLowerCase(); start = i + 1; } else if (name != null && encoded && charset == null && chars[i] == '\'') { String charsetName = new String(chars, start, i - start); try { charset = Charset.forName(charsetName); } catch (IllegalCharsetNameException e) { charset = StandardCharsets.UTF_8; // Fallback to UTF-8 } start = i + 1; } else if (name != null && encoded && charset != null && chars[i] == '\'') { start = i + 1; } } // This is an invalid parameter, but we won't fail here if (start >= end) { if (name != null) { parameters.put(name, ""); } return; } if (chars[start] == '"') { start++; } if (chars[end - 1] == '"') { end--; } String encodedValue = new String(chars, start, end - start); String value; if (charset != null) { value = URLDecoder.decode(encodedValue, charset); } else { value = URLDecoder.decode(encodedValue, StandardCharsets.UTF_8); } if (name == null) { name = value; value = ""; } // Prefer the encoded version if (!parameters.containsKey(name) || encoded) { parameters.put(name, value); } } /** * Writes out the status line to the given OutputStream. * * @param response The response to pull the status information from. * @param out The OutputStream. */ private static void writeStatusLine(HTTPResponse response, ByteBufferOutputStream out) { out.write(ProtocolBytes.HTTTP1_1); out.write(' '); out.write(Integer.toString(response.getStatus()).getBytes()); out.write(' '); if (response.getStatusMessage() != null) { out.write(response.getStatusMessage().getBytes()); } out.write(ControlBytes.CRLF); } /** * A record that stores a parameterized header value. * * @param value The initial value of the header. * @param parameters The parameters. */ public record HeaderValue(String value, Map parameters) { } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy