001/** 002 * 003 * Copyright © 2014-2024 Florian Schmaus 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * 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.jxmpp.stringprep.simple; 018 019import java.util.Arrays; 020import java.util.Locale; 021 022import org.jxmpp.JxmppContext; 023import org.jxmpp.XmppAddressParttype; 024import org.jxmpp.stringprep.XmppStringprep; 025import org.jxmpp.stringprep.XmppStringprepException; 026import org.jxmpp.util.ArraysUtil; 027 028public final class SimpleXmppStringprep implements XmppStringprep { 029 030 private static SimpleXmppStringprep instance; 031 032 public static final String NAME = "simple"; 033 034 /** 035 * Setup Simple XMPP Stringprep as implementation to use. 036 */ 037 public static void setup() { 038 JxmppContext.setDefaultXmppStringprep(getInstance()); 039 } 040 041 /** 042 * Get the Simple XMPP Stringprep singleton. 043 * 044 * @return the simple XMPP Stringprep singleton. 045 */ 046 public static SimpleXmppStringprep getInstance() { 047 if (instance == null) { 048 instance = new SimpleXmppStringprep(); 049 } 050 return instance; 051 } 052 053 private SimpleXmppStringprep() { 054 } 055 056 /** 057 * From <a href="https://tools.ietf.org/html/rfc7622#section-3.3.1">RFC 7622 § 058 * 3.3.1</a>. 059 */ 060 // @formatter:off 061 private static final char[] LOCALPART_FURTHER_EXCLUDED_CHARACTERS = new char[] { 062 '"', // U+0022 (QUOTATION MARK) , i.e., " 063 '&', // U+0026 (AMPERSAND), i.e., & 064 '\'', // U+0027 (APOSTROPHE), i.e., ' 065 '/', // U+002F (SOLIDUS), i.e., / 066 ':', // U+003A (COLON), i.e., : 067 '<', // U+003C (LESS-THAN SIGN), i.e., < 068 '>', // U+003E (GREATER-THAN SIGN), i.e., > 069 '@', // U+0040 (COMMERCIAL AT), i.e., @ 070 }; 071 // @formatter:on 072 073 // @formatter:off 074 private static final char[] USERNAME_CASE_MAPPED_EXCLUDED_CHARACTERS = new char[] { 075 ' ', // U+0020 (SPACE) - forbidden by PRECIS IdentifierClass. 076 }; 077 // @formatter:on 078 079 private static final char[] LOCALPART_EXCLUDED_CHARACTERS; 080 081 static { 082 // Ensure that the char array is sorted as we use Arrays.binarySearch() on it. 083 Arrays.sort(LOCALPART_FURTHER_EXCLUDED_CHARACTERS); 084 085 // Combine LOCALPART_FURTHER_EXCLUDED_CHARACTERS and USERNAME_CASE_MAPPED_EXCLUDED_CHARACTERS into 086 // LOCALPART_EXCLUDED_CHARACTERS. 087 LOCALPART_EXCLUDED_CHARACTERS = ArraysUtil.concatenate( 088 LOCALPART_FURTHER_EXCLUDED_CHARACTERS, 089 USERNAME_CASE_MAPPED_EXCLUDED_CHARACTERS); 090 Arrays.sort(LOCALPART_EXCLUDED_CHARACTERS); 091 } 092 093 @Override 094 public String localprep(String string) throws XmppStringprepException { 095 string = simpleStringprep(string); 096 097 ensurePartDoesNotContain(XmppAddressParttype.localpart, string, LOCALPART_EXCLUDED_CHARACTERS); 098 ensureValid(XmppAddressParttype.localpart, string); 099 100 return string; 101 } 102 103 private static void ensurePartDoesNotContain(XmppAddressParttype parttype, String input, char[] excludedChars) 104 throws XmppStringprepException { 105 assert isSorted(excludedChars); 106 107 for (int i = 0; i < input.length(); i++) { 108 char c = input.charAt(i); 109 int forbiddenCharPos = Arrays.binarySearch(excludedChars, c); 110 if (forbiddenCharPos >= 0) { 111 throw new XmppStringprepException(input, parttype.getCapitalizedName() + " must not contain '" 112 + excludedChars[forbiddenCharPos] + "'"); 113 } 114 } 115 } 116 117 /** 118 * Ensure that the input string does not contain any of the further excluded characters of XMPP localparts. 119 * 120 * @param localpart the input string. 121 * @throws XmppStringprepException if one of the further excluded characters is found. 122 * @see <a href="https://tools.ietf.org/html/rfc7622#section-3.3.1">RFC 7622 § 3.3.1</a> 123 */ 124 public static void ensureLocalpartDoesNotIncludeFurtherExcludedCharacters(String localpart) 125 throws XmppStringprepException { 126 ensurePartDoesNotContain(XmppAddressParttype.localpart, localpart, LOCALPART_FURTHER_EXCLUDED_CHARACTERS); 127 } 128 129 @Override 130 public String domainprep(String string) throws XmppStringprepException { 131 string = simpleStringprep(string); 132 ensureValid(XmppAddressParttype.localpart, string); 133 return string; 134 } 135 136 @Override 137 public String resourceprep(String string) throws XmppStringprepException { 138 // rfc6122-bis specifies that resourceprep uses saslprep-bis OpaqueString Profile which says that 139 // "Uppercase and titlecase characters MUST NOT be mapped to their lowercase equivalents." 140 141 // TODO apply Unicode Normalization Form C (NFC) with help of java.text.Normalize 142 // but unfortunately this is API is only available on Android API 9 or higher and Smack is currently API 8 143 144 ensureValid(XmppAddressParttype.resourcepart, string); 145 146 return string; 147 } 148 149 private static String simpleStringprep(String string) { 150 String res = string.toLowerCase(Locale.US); 151 return res; 152 } 153 154 private static boolean isSorted(char[] chars) { 155 for (int i = 1; i < chars.length; i++) { 156 if (chars[i-1] > chars[i]) { 157 return false; 158 } 159 } 160 return true; 161 } 162 163 private static void ensureValid(XmppAddressParttype parttype, String input) throws XmppStringprepException { 164 for (int i = 0; i < input.length(); i++) { 165 char c = input.charAt(i); 166 167 boolean disallowedAsciiChar = isDisallowedAsciiChar(c); 168 if (disallowedAsciiChar) 169 throw new XmppStringprepException(input, 170 parttype.getCapitalizedName() + " does contain disallowed ASCII character at pos " + i); 171 } 172 } 173 174 /** 175 * XML 1.0 disallows certain characters. Below 0x20 (SPACE) only 0x09 (\t), 0x0A (\n), and 0x0D (\r) are allowed. See XML 1.0 § 2.2 which has the following production rule: 176 * Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] 177 * 178 * @param c the character is check. 179 * @return true if the character is disallowed by XML. 180 */ 181 private static boolean isDisallowedAsciiChar(char c) { 182 if (c >= 0x20) return false; 183 184 switch (c) { 185 case 0x09: 186 case 0x0A: 187 case 0X0D: 188 return false; 189 default: 190 return true; 191 } 192 } 193}