/* * * Code derived and adapted from the Jitsi client side STUN framework. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package org.restcomm.media.stun.messages.attributes.general; import org.restcomm.media.stun.StunException; import org.restcomm.media.stun.messages.attributes.StunAttribute; /** * The ERROR-CODE attribute is present in the Binding Error Response and * Shared Secret Error Response. It is a numeric value in the range of * 100 to 699 plus a textual reason phrase encoded in UTF-8, and is * consistent in its code assignments and semantics with SIP [10] and * HTTP [15]. The reason phrase is meant for user consumption, and can * be anything appropriate for the response code. The lengths of the * reason phrases MUST be a multiple of 4 (measured in bytes). This can * be accomplished by added spaces to the end of the text, if necessary. * Recommended reason phrases for the defined response codes are * presented below. * * To facilitate processing, the class of the error code (the hundreds * digit) is encoded separately from the rest of the code. * * 0 1 2 3 * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | 0 |Class| Number | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | Reason Phrase (variable) .. * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * * The class represents the hundreds digit of the response code. The * value MUST be between 1 and 6. The number represents the response * code modulo 100, and its value MUST be between 0 and 99. * * The following response codes, along with their recommended reason * phrases (in brackets) are defined at this time: * * 400 (Bad Request): The request was malformed. The client should not * retry the request without modification from the previous * attempt. * * 401 (Unauthorized): The Binding Request did not contain a MESSAGE- * INTEGRITY attribute. * * 420 (Unknown Attribute): The server did not understand a mandatory * attribute in the request. * * 430 (Stale Credentials): The Binding Request did contain a MESSAGE- * INTEGRITY attribute, but it used a shared secret that has * expired. The client should obtain a new shared secret and try * again. * * 431 (Integrity Check Failure): The Binding Request contained a * MESSAGE-INTEGRITY attribute, but the HMAC failed verification. * This could be a sign of a potential attack, or client * implementation error. * * 432 (Missing Username): The Binding Request contained a MESSAGE- * INTEGRITY attribute, but not a USERNAME attribute. Both must be * present for integrity checks. * * 433 (Use TLS): The Shared Secret request has to be sent over TLS, but * was not received over TLS. * * 500 (Server Error): The server has suffered a temporary error. The * client should try again. * * 600 (Global Failure:) The server is refusing to fulfill the request. * The client should not retry. */ public class ErrorCodeAttribute extends StunAttribute { public static final String NAME = "ERROR-CODE"; // Common error codes public static final char BAD_REQUEST = 400; public static final char UNAUTHORIZED = 401; public static final char UNKNOWN_ATTRIBUTE = 420; public static final char STALE_CREDENTIALS = 430; public static final char INTEGRITY_CHECK_FAILURE = 431; public static final char MISSING_USERNAME = 432; public static final char USE_TLS = 433; public static final char ROLE_CONFLICT = 487; public static final char SERVER_ERROR = 500; public static final char GLOBAL_FAILURE = 600; private static final int ERROR_CODE_DIGITS = 4; private byte errorClass; private byte errorNumber; private byte[] reasonPhrase; public ErrorCodeAttribute() { super(StunAttribute.ERROR_CODE); this.errorClass = 0; this.errorNumber = 0; this.reasonPhrase = null; } public byte getErrorClass() { return errorClass; } /** * Sets the class of the error. * * @param errorClass * The error class. * @throws IllegalArgumentException * Only error classes between 0 and 99 are valid. */ public void setErrorClass(byte errorClass) throws IllegalArgumentException { if (errorClass < 0 || errorClass > 99) { throw new IllegalArgumentException( errorClass + "Only error classes between 0 and 99 are valid. Current class: " + errorClass); } this.errorClass = errorClass; } public byte getErrorNumber() { return errorNumber; } public void setErrorNumber(byte errorNumber) { this.errorNumber = errorNumber; } /** * A convenience method that sets error class and number according to the * specified error code. * <p> * The class represents the hundreds digit of the error code.<br> * The value MUST be between 1 and 6.<br> * The number represents the response code modulo 100, and its value MUST be * between 0 and 99. * </p> * * @param errorCode * the errorCode that this class encapsulates. */ public void setErrorCode(char code) { setErrorClass((byte) (code / 100)); setErrorNumber((byte) (code % 100)); } /** * A convenience method that constructs an error code from this attribute's * class and number. * * @return the code of the error this attribute represents. */ public char getErrorCode() { return (char) (getErrorClass() * 100 + getErrorNumber()); } /** * Returns a default reason phrase corresponding to the specified error * code, as described by rfc 3489. * * @param errorCode * the code of the error that the reason phrase must describe. * @return a default reason phrase corresponding to the specified error * code, as described by rfc 3489. */ public static String getDefaultReasonPhrase(char errorCode) { switch (errorCode) { case BAD_REQUEST: return "(Bad Request): The request was malformed. The client should not " + "retry the request without modification from the previous attempt."; case UNAUTHORIZED: return "(Unauthorized): The Binding Request did not contain a MESSAGE-" + "INTEGRITY attribute."; case UNKNOWN_ATTRIBUTE: return "(Unknown Attribute): The server did not understand a mandatory " + "attribute in the request."; case STALE_CREDENTIALS: return "(Stale Credentials): The Binding Request did contain a MESSAGE-" + "INTEGRITY attribute, but it used a shared secret that has " + "expired. The client should obtain a new shared secret and try" + "again"; case INTEGRITY_CHECK_FAILURE: return "(Integrity Check Failure): The Binding Request contained a " + "MESSAGE-INTEGRITY attribute, but the HMAC failed verification. " + "This could be a sign of a potential attack, or client " + "implementation error."; case MISSING_USERNAME: return "(Missing Username): The Binding Request contained a MESSAGE-" + "INTEGRITY attribute, but not a USERNAME attribute. Both must be" + "present for integrity checks."; case USE_TLS: return "(Use TLS): The Shared Secret request has to be sent over TLS, but" + "was not received over TLS."; case SERVER_ERROR: return "(Server Error): The server has suffered a temporary error. The" + "client should try again."; case GLOBAL_FAILURE: return "(Global Failure:) The server is refusing to fulfill the request." + "The client should not retry."; default: return "Unknown Error"; } } /** * Set's a reason phrase. The reason phrase is meant for user consumption, * and can be anything appropriate for the response code. The lengths of the * reason phrases MUST be a multiple of 4 (measured in bytes). * * @param reasonPhrase * a reason phrase that describes this error. */ public void setReasonPhrase(String reason) { this.reasonPhrase = reason.getBytes(); } /** * Returns the reason phrase. The reason phrase is meant for user * consumption, and can be anything appropriate for the response code. The * lengths of the reason phrases MUST be a multiple of 4 (measured in * bytes). * * @return reasonPhrase a reason phrase that describes this error. */ public byte[] getReasonPhrase() { return reasonPhrase; } @Override public char getDataLength() { char len = (char) (ERROR_CODE_DIGITS + (char) (reasonPhrase == null ? 0 : reasonPhrase.length)); return len; } @Override public String getName() { return NAME; } @Override public boolean equals(Object other) { if (other == null || !(other instanceof ErrorCodeAttribute)) { return false; } if (other == this) { return true; } ErrorCodeAttribute att = (ErrorCodeAttribute) other; if (att.getAttributeType() != this.getAttributeType() || att.getDataLength() != this.getDataLength() || att.getErrorClass() != this.getErrorClass() || att.getErrorNumber() != this.getErrorNumber() || (att.getReasonPhrase() != null && !att.getReasonPhrase() .equals(this.getReasonPhrase()))) { return false; } return true; } @Override public byte[] encode() { // length with padding byte binValue[] = new byte[HEADER_LENGTH + getDataLength() + (4 - getDataLength() % 4) % 4]; // Type binValue[0] = (byte) (getAttributeType() >> 8); binValue[1] = (byte) (getAttributeType() & 0x00FF); // Length binValue[2] = (byte) (getDataLength() >> 8); binValue[3] = (byte) (getDataLength() & 0x00FF); // Not used binValue[4] = 0x00; binValue[5] = 0x00; // Error code binValue[6] = getErrorClass(); binValue[7] = getErrorNumber(); if (reasonPhrase != null) { System.arraycopy(reasonPhrase, 0, binValue, 8, reasonPhrase.length); } return binValue; } @Override protected void decodeAttributeBody(byte[] data, char offset, char length) throws StunException { // skip the zeros offset += 2; // Error code setErrorClass(data[offset++]); setErrorNumber(data[offset++]); // Reason Phrase byte[] reasonBytes = new byte[length - 4]; System.arraycopy(data, offset, reasonBytes, 0, reasonBytes.length); setReasonPhrase(new String(reasonBytes)); } }