001/** 002 * 003 * Copyright © 2014-2019 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 ensurePartDoesNotContain(XmppAddressParttype.localpart, string, LOCALPART_EXCLUDED_CHARACTERS); 097 return string; 098 } 099 100 private static void ensurePartDoesNotContain(XmppAddressParttype parttype, String input, char[] excludedChars) 101 throws XmppStringprepException { 102 assert isSorted(excludedChars); 103 104 for (char c : input.toCharArray()) { 105 int forbiddenCharPos = Arrays.binarySearch(excludedChars, c); 106 if (forbiddenCharPos >= 0) { 107 throw new XmppStringprepException(input, parttype.getCapitalizedName() + " must not contain '" 108 + LOCALPART_FURTHER_EXCLUDED_CHARACTERS[forbiddenCharPos] + "'"); 109 } 110 } 111 } 112 113 /** 114 * Ensure that the input string does not contain any of the further excluded characters of XMPP localparts. 115 * 116 * @param localpart the input string. 117 * @throws XmppStringprepException if one of the further excluded characters is found. 118 * @see <a href="https://tools.ietf.org/html/rfc7622#section-3.3.1">RFC 7622 § 3.3.1</a> 119 */ 120 public static void ensureLocalpartDoesNotIncludeFurtherExcludedCharacters(String localpart) 121 throws XmppStringprepException { 122 ensurePartDoesNotContain(XmppAddressParttype.localpart, localpart, LOCALPART_FURTHER_EXCLUDED_CHARACTERS); 123 } 124 125 @Override 126 public String domainprep(String string) throws XmppStringprepException { 127 return simpleStringprep(string); 128 } 129 130 @Override 131 public String resourceprep(String string) throws XmppStringprepException { 132 // rfc6122-bis specifies that resourceprep uses saslprep-bis OpaqueString Profile which says that 133 // "Uppercase and titlecase characters MUST NOT be mapped to their lowercase equivalents." 134 135 // TODO apply Unicode Normalization Form C (NFC) with help of java.text.Normalize 136 // but unfortunately this is API is only available on Android API 9 or higher and Smack is currently API 8 137 return string; 138 } 139 140 private static String simpleStringprep(String string) { 141 String res = string.toLowerCase(Locale.US); 142 return res; 143 } 144 145 private static boolean isSorted(char[] chars) { 146 for (int i = 1; i < chars.length; i++) { 147 if (chars[i-1] > chars[i]) { 148 return false; 149 } 150 } 151 return true; 152 } 153}