/* * Zed Attack Proxy (ZAP) and its related class files. * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * * Copyright 2016 The ZAP Development Team * * 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 org.zaproxy.zap.extension.httppanel.view.util; import javax.swing.JTextArea; import javax.swing.text.BadLocationException; import org.apache.log4j.Logger; import org.parosproxy.paros.network.HttpHeader; /** * Utility methods related to text views of HTTP messages. * * @since 2.6.0 */ public final class HttpTextViewUtils { /** * Position returned when the calculated offsets are greater than the length of the view (e.g. contents of the view do not * match the header). */ public static final int[] INVALID_POSITION = {}; private static final Logger LOGGER = Logger.getLogger(HttpTextViewUtils.class); private HttpTextViewUtils() { } /** * Gets the given {@code start} and {@code end} header positions offset to the given {@code view}. * <p> * The {@code view} is expected to replace the header line endings {@code \r\n} to {@code \n} (e.g. so there's no invisible * newline ({@code \r}) characters when editing), as such the positions of the {@code header} need to be offset to match the * ones in the {@code view}. * * @param view the view that contains the contents of the header * @param header the header shown in the view * @param start the start position * @param end the end position * @return the positions offset for the {@code view}, or {@link #INVALID_POSITION} if the {@code start}, {@code end} or * offset positions are greater than {@code view}'s length. * @throws IllegalArgumentException if any of the conditions is true: * <ul> * <li>the {@code view} is {@code null} or it has no {@link JTextArea#getDocument() Document};</li> * <li>the {@code header} is {@code null};</li> * <li>the {@code start} position is negative or greater than the length of the {@code header};</li> * <li>the {@code end} position is negative or greater than the length of the {@code header};</li> * <li>the {@code start} position is greater than the {@code end} position.</li> * </ul> * @see #getViewToHeaderPosition(JTextArea, int, int) * @see #getBodyToViewPosition(JTextArea, String, int, int) */ public static int[] getHeaderToViewPosition(JTextArea view, String header, int start, int end) { validateView(view); validateHeader(header); validateStartEnd(start, end); if (!isValidStartEndForLength(start, end, header.length())) { return INVALID_POSITION; } int excessChars = 0; int pos = 0; while ((pos = header.indexOf(HttpHeader.CRLF, pos)) != -1 && pos < start) { pos += 2; ++excessChars; } int len = view.getDocument().getLength(); int finalStartPos = start - excessChars; if (finalStartPos > len) { return INVALID_POSITION; } if (pos != -1) { while ((pos = header.indexOf(HttpHeader.CRLF, pos)) != -1 && pos < end) { pos += 2; ++excessChars; } } int finalEndPos = end - excessChars; if (finalEndPos > len) { return INVALID_POSITION; } return new int[] { finalStartPos, finalEndPos }; } /** * Validates that the given {@code view} is not {@code null} and has a {@code Document}. * * @param view the view to be validated * @throws IllegalArgumentException if the {@code view} is {@code null} or it has no {@link JTextArea#getDocument() * Document}. */ private static void validateView(JTextArea view) { if (view == null || view.getDocument() == null) { throw new IllegalArgumentException("Parameter view must not be null and must have a Document."); } } /** * Validates that the given {@code header} is not {@code null}. * * @param header the header to be validated * @throws IllegalArgumentException if the {@code header} is {@code null}. */ private static void validateHeader(String header) { if (header == null) { throw new IllegalArgumentException("Parameter header must not be null."); } } /** * Validates the given {@code start} and {@code end} positions. * * @param start the start position to be validated * @param end the end position to be validated * @throws IllegalArgumentException if any of the conditions is true: * <ul> * <li>the {@code start} position is negative;</li> * <li>the {@code end} position is negative;</li> * <li>the {@code start} position is greater than the {@code end} position.</li> * </ul> */ private static void validateStartEnd(int start, int end) { if (start < 0) { throw new IllegalArgumentException("Parameter start must not be negative."); } if (end < 0) { throw new IllegalArgumentException("Parameter end must not be negative."); } if (start > end) { throw new IllegalArgumentException("Parameter start must not be greater than end."); } } /** * Tells whether or not the given start and end are valid for the given length, that is both start and end are lower that * the length. * * @param start the start position to be validated * @param end the end position to be validated * @param length the length of the contents * @return {@code true} if the start and end positions are lower than the length, {@code false} otherwise. */ private static boolean isValidStartEndForLength(int start, int end, int length) { if (start > length || end > length) { return false; } return true; } /** * Gets the given {@code start} and {@code end} body positions offset to the given {@code view}. * <p> * The {@code view} is expected to replace the header line endings {@code \r\n} to {@code \n} (e.g. so there's no invisible * newline ({@code \r}) characters when editing), as such the positions of the body (shown after the header) need to be * offset to match the ones in the {@code view}. * * @param view the view that contains the contents of the header and the body * @param header the header shown in the view * @param start the start position * @param end the end position * @return the positions offset for the {@code view}, or {@link #INVALID_POSITION} if the {@code start} and {@code end} * positions are greater than the length of the body or the {@code view}. * @throws IllegalArgumentException if any of the conditions is true: * <ul> * <li>the {@code view} is {@code null} or it has no {@link JTextArea#getDocument() Document};</li> * <li>the {@code header} is {@code null};</li> * <li>the {@code start} position is negative;</li> * <li>the {@code end} position is negative;</li> * <li>the {@code start} position is greater than the {@code end} position.</li> * </ul> * @see #getHeaderToViewPosition(JTextArea, String, int, int) * @see #getViewToHeaderBodyPosition(JTextArea, String, int, int) */ public static int[] getBodyToViewPosition(JTextArea view, String header, int start, int end) { validateView(view); validateHeader(header); validateStartEnd(start, end); if (!isValidStartEndForLength(start, end, view.getDocument().getLength())) { return INVALID_POSITION; } int excessChars = 0; int pos = 0; while ((pos = header.indexOf(HttpHeader.CRLF, pos)) != -1) { pos += 2; ++excessChars; } int len = view.getDocument().getLength(); int bodyLen = len - header.length() + excessChars; if (bodyLen < 0 || start > bodyLen || end > bodyLen) { return INVALID_POSITION; } int finalStartPos = start + header.length() - excessChars; int finalEndPos = end + header.length() - excessChars; return new int[] { finalStartPos, finalEndPos }; } /** * Gets the given {@code start} and {@code end} view positions offset to a header. * <p> * The {@code view} is expected to replace the header line endings {@code \r\n} to {@code \n} (e.g. so there's no invisible * newline ({@code \r}) characters when editing), as such the positions of the {@code view} need to be offset to match the * ones in the header. * * @param view the view that contains the contents of a header * @param start the start position * @param end the end position * @return the positions offset for the header, or {@link #INVALID_POSITION} if the {@code start} or {@code end} is greater * than the length of the {@code view} * @throws IllegalArgumentException if any of the conditions is true: * <ul> * <li>the {@code view} is {@code null} or it has no {@link JTextArea#getDocument() Document};</li> * <li>the {@code start} position is negative;</li> * <li>the {@code end} position is negative;</li> * <li>the {@code start} position is greater than the {@code end} position.</li> * </ul> * @see #getHeaderToViewPosition(JTextArea, String, int, int) * @see #getViewToHeaderBodyPosition(JTextArea, String, int, int) */ public static int[] getViewToHeaderPosition(JTextArea view, int start, int end) { validateView(view); validateStartEnd(start, end); if (!isValidStartEndForLength(start, end, view.getDocument().getLength())) { return INVALID_POSITION; } return getViewToHeaderPositionImpl(view, start, end); } /** * Gets the given {@code start} and {@code end} view positions offset to a header. * <p> * The {@code view} is expected to replace the header line endings {@code \r\n} to {@code \n} (e.g. so there's no invisible * newline ({@code \r}) characters when editing), as such the positions of the {@code view} need to be offset to match the * ones in the header. * <p> * <strong>Note:</strong> The {@code view} and {@code start} and {@code end} positions should be validated before calling * this method. * * @param view the view that contains the contents of a header * @param start the start position * @param end the end position * @return the positions offset for the header * @see #validateView(JTextArea) * @see #validateStartEnd(int, int) * @see #isValidStartEndForLength(int, int, int) */ private static int[] getViewToHeaderPositionImpl(JTextArea view, int start, int end) { int finalStartPos = start; try { finalStartPos += view.getLineOfOffset(finalStartPos); } catch (BadLocationException e) { // Shouldn't happen, position was already validated. LOGGER.error(e.getMessage(), e); return INVALID_POSITION; } int finalEndPos = end; try { finalEndPos += view.getLineOfOffset(finalEndPos); } catch (BadLocationException e) { // Shouldn't happen, position was already validated. LOGGER.error(e.getMessage(), e); return INVALID_POSITION; } return new int[] { finalStartPos, finalEndPos }; } /** * Gets the given {@code start} and {@code end} view positions offset to, or after, the given {@code header}. * <p> * The {@code view} is expected to replace the header line endings {@code \r\n} to {@code \n} (e.g. so there's no invisible * newline ({@code \r}) characters when editing), as such the positions of the {@code view} need to be offset to match the * ones in, or after, the {@code header}. * * @param view the view that contains the contents of the header and body * @param header the header shown in the view * @param start the start position * @param end the end position * @return the positions offset for the header or, 3 positions, for after the body (the third position is just to indicate * that it's the body, the value is meaningless), or {@link #INVALID_POSITION} if the {@code start} or {@code end} * is greater than the length of the {@code view} * @throws IllegalArgumentException if any of the conditions is true: * <ul> * <li>the {@code view} is {@code null} or it has no {@link JTextArea#getDocument() Document};</li> * <li>the {@code start} position is negative;</li> * <li>the {@code end} position is negative;</li> * <li>the {@code start} position is greater than the {@code end} position.</li> * </ul> */ public static int[] getViewToHeaderBodyPosition(JTextArea view, String header, int start, int end) { validateView(view); validateHeader(header); validateStartEnd(start, end); if (!isValidStartEndForLength(start, end, view.getDocument().getLength())) { return INVALID_POSITION; } int excessChars = 0; int pos = 0; while ((pos = header.indexOf("\r\n", pos)) != -1) { pos += 2; ++excessChars; } if (start + excessChars < header.length()) { int[] position = getViewToHeaderPositionImpl(view, start, end); if (position[1] > header.length()) { position[1] = header.length(); } return position; } int finalStartPos = start + excessChars - header.length(); int finalEndPos = end + excessChars - header.length(); return new int[] { finalStartPos, finalEndPos, 0 }; } }