1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.wifi;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.content.res.Resources;
24 import android.net.MacAddress;
25 import android.net.wifi.WifiConfiguration;
26 import android.net.wifi.WifiContext;
27 import android.net.wifi.WifiSsid;
28 import android.os.Handler;
29 import android.text.TextUtils;
30 import android.util.ArrayMap;
31 import android.util.ArraySet;
32 import android.util.Log;
33 import android.util.Pair;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import androidx.annotation.VisibleForTesting;
38 
39 import com.android.wifi.resources.R;
40 
41 import java.io.PrintWriter;
42 import java.nio.ByteBuffer;
43 import java.nio.charset.CharacterCodingException;
44 import java.nio.charset.Charset;
45 import java.nio.charset.CharsetDecoder;
46 import java.nio.charset.CharsetEncoder;
47 import java.nio.charset.CodingErrorAction;
48 import java.nio.charset.StandardCharsets;
49 import java.util.ArrayList;
50 import java.util.Arrays;
51 import java.util.HashMap;
52 import java.util.List;
53 import java.util.Locale;
54 import java.util.Map;
55 import java.util.Set;
56 
57 /**
58  * Utility class to translate between non-UTF-8 SSIDs in the Native layer and UTF-8 SSIDs in the
59  * Framework for SSID Translation.
60  *
61  * SSID Translation is intended to provide backwards compatibility with legacy apps that do not
62  * recognize non-UTF-8 SSIDs. Translating non-UTF-8 SSIDs from Native->Framework into UTF-8
63  * (and back) will effectively switch all non-UTF-8 APs into UTF-8 APs from the perspective of the
64  * Framework and apps.
65  *
66  * The list of alternate non-UTF-8 character sets to translate is defined in
67  * R.string.config_wifiCharsetsForSsidTranslation.
68  *
69  * This class is thread-safe.
70  */
71 public class SsidTranslator {
72     private static final String TAG = "SsidTranslator";
73     private static final String LOCALE_LANGUAGE_ALL = "all";
74     @VisibleForTesting static final long BSSID_CACHE_TIMEOUT_MS = 30_000;
75     private final @NonNull WifiContext mWifiContext;
76     private final @NonNull Handler mWifiHandler;
77 
78     private @Nullable Charset mCurrentLocaleAlternateCharset = null;
79     private @NonNull Map<String, Charset> mCharsetsPerLocaleLanguage = new HashMap<>();
80     private @NonNull Map<String, Charset> mMockCharsetsPerLocaleLanguage = new HashMap<>();
81 
82     // Maps a translated SSID to all of its BSSIDs using the alternate Charset.
83     private @NonNull Map<WifiSsid, Set<MacAddress>> mTranslatedBssids = new ArrayMap<>();
84     // Maps a translated SSID to all of its BSSIDs not using the alternate Charset.
85     private @NonNull Map<WifiSsid, Set<MacAddress>> mUntranslatedBssids = new ArrayMap<>();
86     private final Map<Pair<WifiSsid, MacAddress>, Runnable> mUntranslatedBssidTimeoutRunnables =
87             new ArrayMap<>();
88     private final Map<Pair<WifiSsid, MacAddress>, Runnable> mTranslatedBssidTimeoutRunnables =
89             new ArrayMap<>();
90     private final Map<String, WifiSsid> mTranslatedSsidForStaIface = new ArrayMap<>();
91 
SsidTranslator(@onNull WifiContext wifiContext, @NonNull Handler wifiHandler)92     public SsidTranslator(@NonNull WifiContext wifiContext, @NonNull Handler wifiHandler) {
93         mWifiContext = wifiContext;
94         mWifiHandler = wifiHandler;
95     }
96 
97     /**
98      * Initializes SsidTranslator after boot completes to get boot-dependent resources.
99      */
handleBootCompleted()100     public synchronized void handleBootCompleted() {
101         Resources res = mWifiContext.getResources();
102         if (res == null) {
103             Log.e(TAG, "Boot completed but could not get resources!");
104             return;
105         }
106         String[] charsetCsvs = res.getStringArray(
107                 R.array.config_wifiCharsetsForSsidTranslation);
108         if (charsetCsvs == null) {
109             return;
110         }
111         for (String charsetCsv : charsetCsvs) {
112             String[] charsetNames = charsetCsv.split(",");
113             if (charsetNames.length != 2) {
114                 continue;
115             }
116             String localeLanguage = charsetNames[0];
117             Charset charset;
118             try {
119                 charset = Charset.forName(charsetNames[1]);
120             } catch (IllegalArgumentException e) {
121                 Log.e(TAG, "Could not find Charset with name " + charsetNames[1]);
122                 continue;
123             }
124             mCharsetsPerLocaleLanguage.put(localeLanguage, charset);
125         }
126         mWifiContext.registerReceiver(new BroadcastReceiver() {
127             @Override
128             public void onReceive(Context context, Intent intent) {
129                 if (!Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) {
130                     return;
131                 }
132                 updateCurrentLocaleCharset();
133             }
134         }, new IntentFilter(Intent.ACTION_LOCALE_CHANGED), null, mWifiHandler);
135         updateCurrentLocaleCharset();
136     }
137 
138     /** Updates mCurrentLocaleCharset to the alternate charset of the current Locale language. */
updateCurrentLocaleCharset()139     private synchronized void updateCurrentLocaleCharset() {
140         // Clear existing Charset mappings.
141         for (Runnable runnable : mTranslatedBssidTimeoutRunnables.values()) {
142             mWifiHandler.removeCallbacks(runnable);
143         }
144         mTranslatedBssidTimeoutRunnables.clear();
145         mTranslatedBssids.clear();
146         for (Runnable runnable : mUntranslatedBssidTimeoutRunnables.values()) {
147             mWifiHandler.removeCallbacks(runnable);
148         }
149         mUntranslatedBssidTimeoutRunnables.clear();
150         mUntranslatedBssids.clear();
151         mCurrentLocaleAlternateCharset = null;
152         // Try to find the Charset for the specific language.
153         String language = null;
154         Resources res = mWifiContext.getResources();
155         if (res != null) {
156             Locale locale = res.getConfiguration().getLocales().get(0);
157             if (locale != null) {
158                 language = locale.getLanguage();
159             } else {
160                 Log.e(TAG, "Current Locale is null!");
161             }
162         } else {
163             Log.e(TAG, "Could not get resources to update locale!");
164         }
165         if (language != null) {
166             mCurrentLocaleAlternateCharset = mMockCharsetsPerLocaleLanguage.get(language);
167             if (mCurrentLocaleAlternateCharset == null) {
168                 mCurrentLocaleAlternateCharset = mCharsetsPerLocaleLanguage.get(language);
169             }
170         }
171         // No Charset for the specific language, use the "all" charset if it exists.
172         if (mCurrentLocaleAlternateCharset == null) {
173             mCurrentLocaleAlternateCharset =
174                     mMockCharsetsPerLocaleLanguage.get(LOCALE_LANGUAGE_ALL);
175         }
176         if (mCurrentLocaleAlternateCharset == null) {
177             mCurrentLocaleAlternateCharset = mCharsetsPerLocaleLanguage.get(LOCALE_LANGUAGE_ALL);
178         }
179         Log.i(TAG, "Locale language changed to " + language + ", alternate charset "
180                 + "is now " + mCurrentLocaleAlternateCharset);
181     }
182 
183     /** Translates an SSID from a source Charset to a target Charset */
translateSsid(@onNull WifiSsid ssid, @NonNull Charset sourceCharset, @NonNull Charset targetCharset)184     private WifiSsid translateSsid(@NonNull WifiSsid ssid,
185             @NonNull Charset sourceCharset,
186             @NonNull Charset targetCharset) {
187         CharsetDecoder decoder = sourceCharset.newDecoder()
188                 .onMalformedInput(CodingErrorAction.REPORT)
189                 .onUnmappableCharacter(CodingErrorAction.REPORT);
190         CharsetEncoder encoder = targetCharset.newEncoder()
191                 .onMalformedInput(CodingErrorAction.REPORT)
192                 .onUnmappableCharacter(CodingErrorAction.REPORT);
193         try {
194             ByteBuffer buffer = encoder.encode(decoder.decode(ByteBuffer.wrap(ssid.getBytes())));
195             byte[] bytes = new byte[buffer.limit()];
196             buffer.get(bytes);
197             return WifiSsid.fromBytes(bytes);
198         } catch (CharacterCodingException | IllegalArgumentException e) {
199             // Could not translate to a valid SSID.
200             Log.e(TAG, "Could not translate SSID " + ssid + ": " + e);
201             return null;
202         }
203     }
204 
205     /**
206      * Translate an SSID to UTF-8 if it is not already UTF-8 and is encoded with the alternate
207      * Charset of the current Locale language.
208      *
209      * @param ssid SSID to translate.
210      * @return translated SSID, or the given SSID if it should not be translated.
211      */
getTranslatedSsid(@onNull WifiSsid ssid)212     public synchronized @NonNull WifiSsid getTranslatedSsid(@NonNull WifiSsid ssid) {
213         return getTranslatedSsidAndRecordBssidCharset(ssid, null);
214     }
215 
216     /**
217      * Gets the translated SSID used for a STA iface. This may be different from the default
218      * translation if the untranslated SSID has an ambiguous encoding.
219      */
getTranslatedSsidForStaIface( @onNull WifiSsid untranslated, @NonNull String staIface)220     public synchronized @NonNull WifiSsid getTranslatedSsidForStaIface(
221             @NonNull WifiSsid untranslated, @NonNull String staIface) {
222         WifiSsid translated = mTranslatedSsidForStaIface.get(staIface);
223         if (translated == null || !getAllPossibleOriginalSsids(translated).contains(untranslated)) {
224             // No recorded translation for the iface, use the default translation.
225             return getTranslatedSsid(untranslated);
226         }
227         // Return the recorded translation.
228         return translated;
229     }
230 
231     /**
232      * Record the actual translated SSID used for a STA iface in case the untranslated SSID
233      * has an ambiguous encoding.
234      */
setTranslatedSsidForStaIface( @onNull WifiSsid translated, @NonNull String staIface)235     public synchronized void setTranslatedSsidForStaIface(
236             @NonNull WifiSsid translated, @NonNull String staIface) {
237         mTranslatedSsidForStaIface.put(staIface, translated);
238     }
239 
240     /**
241      * Translate an SSID to UTF-8 if it is not already UTF-8 and is encoded with the alternate
242      * Charset of the current Locale language, and record the BSSID as translated. If the SSID is
243      * already in UTF-8 or is not encoded with the alternate Charset, then the SSID will not be
244      * translated and the BSSID will be recorded as untranslated.
245      *
246      * @param ssid SSID to translate.
247      * @param bssid BSSID to record the Charset of.
248      * @return translated SSID, or the given SSID if it should not be translated.
249      */
getTranslatedSsidAndRecordBssidCharset( @onNull WifiSsid ssid, @Nullable MacAddress bssid)250     public synchronized @NonNull WifiSsid getTranslatedSsidAndRecordBssidCharset(
251             @NonNull WifiSsid ssid, @Nullable MacAddress bssid) {
252         if (mCurrentLocaleAlternateCharset == null) {
253             return ssid;
254         }
255         if (ssid.getUtf8Text() == null) {
256             WifiSsid translatedSsid =
257                     translateSsid(ssid, mCurrentLocaleAlternateCharset, StandardCharsets.UTF_8);
258             if (translatedSsid != null) {
259                 if (bssid != null) {
260                     mTranslatedBssids.computeIfAbsent(translatedSsid, k -> new ArraySet<>())
261                             .add(bssid);
262                     Pair<WifiSsid, MacAddress> ssidBssidPair = new Pair<>(translatedSsid, bssid);
263                     Runnable oldRunnable = mTranslatedBssidTimeoutRunnables.remove(ssidBssidPair);
264                     if (oldRunnable != null) {
265                         mWifiHandler.removeCallbacks(oldRunnable);
266                     }
267                     Runnable timeoutRunnable = new Runnable() {
268                         @Override
269                         public void run() {
270                             handleTranslatedBssidTimeout(translatedSsid, bssid, this);
271                         }
272                     };
273                     mTranslatedBssidTimeoutRunnables.put(ssidBssidPair, timeoutRunnable);
274                     mWifiHandler.postDelayed(timeoutRunnable, BSSID_CACHE_TIMEOUT_MS);
275                 }
276                 return translatedSsid;
277             }
278         }
279         if (bssid != null) {
280             mUntranslatedBssids.computeIfAbsent(ssid, k -> new ArraySet<>()).add(bssid);
281             Pair<WifiSsid, MacAddress> ssidBssidPair = new Pair<>(ssid, bssid);
282             Runnable oldRunnable = mUntranslatedBssidTimeoutRunnables.remove(ssidBssidPair);
283             if (oldRunnable != null) {
284                 mWifiHandler.removeCallbacks(oldRunnable);
285             }
286             Runnable timeoutRunnable = new Runnable() {
287                 @Override
288                 public void run() {
289                     handleUntranslatedBssidTimeout(ssid, bssid, this);
290                 }
291             };
292             mUntranslatedBssidTimeoutRunnables.put(ssidBssidPair, timeoutRunnable);
293             mWifiHandler.postDelayed(timeoutRunnable, BSSID_CACHE_TIMEOUT_MS);
294         }
295 
296         return ssid;
297     }
298 
299     /** Removes a timed out translated ssid/bssid mapping */
handleTranslatedBssidTimeout( WifiSsid ssid, MacAddress bssid, Runnable runnable)300     private synchronized void handleTranslatedBssidTimeout(
301             WifiSsid ssid, MacAddress bssid, Runnable runnable) {
302         Pair<WifiSsid, MacAddress> mapping = new Pair<>(ssid, bssid);
303         if (mTranslatedBssidTimeoutRunnables.get(mapping) != runnable) {
304             // This runnable isn't the active runnable anymore. Ignore.
305             return;
306         }
307         mTranslatedBssidTimeoutRunnables.remove(mapping);
308         Set<MacAddress> bssids = mTranslatedBssids.get(ssid);
309         if (bssids == null) {
310             return;
311         }
312         bssids.remove(bssid);
313         if (bssids.isEmpty()) {
314             mTranslatedBssids.remove(ssid);
315         }
316     }
317 
318     /** Removes a timed out untranslated ssid/bssid mapping */
handleUntranslatedBssidTimeout( WifiSsid ssid, MacAddress bssid, Runnable runnable)319     private synchronized void handleUntranslatedBssidTimeout(
320             WifiSsid ssid, MacAddress bssid, Runnable runnable) {
321         Pair<WifiSsid, MacAddress> mapping = new Pair<>(ssid, bssid);
322         if (mUntranslatedBssidTimeoutRunnables.get(mapping) != runnable) {
323             // This runnable isn't the active runnable anymore. Ignore.
324             return;
325         }
326         mUntranslatedBssidTimeoutRunnables.remove(mapping);
327         Set<MacAddress> bssids = mUntranslatedBssids.get(ssid);
328         if (bssids == null) {
329             return;
330         }
331         bssids.remove(bssid);
332         if (bssids.isEmpty()) {
333             mUntranslatedBssids.remove(ssid);
334         }
335     }
336 
337     /**
338      * Converts the specified translated SSID back to its original Charset if the BSSID is recorded
339      * as translated, or there are translated BSSIDs but no untranslated BSSIDs for this SSID.
340      *
341      * If the BSSID has not been recorded at all, then we will return the SSID as-is.
342      *
343      * @param translatedSsid translated SSID.
344      * @param bssid optional BSSID to look up the Charset.
345      * @return original SSID. May be null if there are no valid translations back to the alternate
346      *         Charset and the translated SSID is not a valid SSID.
347      */
getOriginalSsid( @onNull WifiSsid translatedSsid, @Nullable MacAddress bssid)348     public synchronized @Nullable WifiSsid getOriginalSsid(
349             @NonNull WifiSsid translatedSsid, @Nullable MacAddress bssid) {
350         if (mCurrentLocaleAlternateCharset == null) {
351             return translatedSsid.getBytes().length <= 32 ? translatedSsid : null;
352         }
353         boolean ssidWasTranslatedForSomeBssids = mTranslatedBssids.containsKey(translatedSsid);
354         boolean ssidWasTranslatedForThisBssid = ssidWasTranslatedForSomeBssids
355                 && mTranslatedBssids.get(translatedSsid).contains(bssid);
356         boolean ssidNotTranslatedForSomeBssids = mUntranslatedBssids.containsKey(translatedSsid);
357         if (ssidWasTranslatedForThisBssid
358                 || (ssidWasTranslatedForSomeBssids && !ssidNotTranslatedForSomeBssids)) {
359             // Try to get the SSID in the alternate Charset.
360             WifiSsid altCharsetSsid = translateSsid(
361                     translatedSsid, StandardCharsets.UTF_8, mCurrentLocaleAlternateCharset);
362             if (altCharsetSsid == null || altCharsetSsid.getBytes().length > 32) {
363                 Log.e(TAG, "Could not translate " + translatedSsid + " back to "
364                         + mCurrentLocaleAlternateCharset + " for BSSID " + bssid);
365             } else {
366                 return altCharsetSsid;
367             }
368         }
369         // Use the translated SSID as-is
370         if (translatedSsid.getBytes().length > 32) {
371             return null;
372         }
373         return translatedSsid;
374     }
375 
376     /**
377      * Gets the original SSID of a WifiConfiguration based on its network selection BSSID or
378      * candidate BSSID.
379      */
getOriginalSsid(@onNull WifiConfiguration config)380     public synchronized @Nullable WifiSsid getOriginalSsid(@NonNull WifiConfiguration config) {
381         WifiConfiguration.NetworkSelectionStatus networkSelectionStatus =
382                 config.getNetworkSelectionStatus();
383         String networkSelectionBssid = networkSelectionStatus.getNetworkSelectionBSSID();
384         String candidateBssid = networkSelectionStatus.getCandidate() != null
385                 ? networkSelectionStatus.getCandidate().BSSID : null;
386         MacAddress selectedBssid = null;
387         if (!TextUtils.isEmpty(networkSelectionBssid) && !TextUtils.equals(
388                 networkSelectionBssid, ClientModeImpl.SUPPLICANT_BSSID_ANY)) {
389             selectedBssid = MacAddress.fromString(networkSelectionBssid);
390         } else if (!TextUtils.isEmpty(candidateBssid) && !TextUtils.equals(
391                 candidateBssid, ClientModeImpl.SUPPLICANT_BSSID_ANY)) {
392             selectedBssid = MacAddress.fromString(candidateBssid);
393         }
394         return getOriginalSsid(WifiSsid.fromString(config.SSID), selectedBssid);
395     }
396 
397     /**
398      * Returns a list of all possible original SSIDs for the specified translated SSID. This will
399      * include all charsets declared for the current Locale language, as well as the UTF-8 SSID.
400      *
401      * @param translatedSsid translated SSID.
402      * @return list of untranslated SSIDs. May be empty if there are no valid reverse translations.
403      */
getAllPossibleOriginalSsids( @onNull WifiSsid translatedSsid)404     public synchronized @NonNull List<WifiSsid> getAllPossibleOriginalSsids(
405             @NonNull WifiSsid translatedSsid) {
406         List<WifiSsid> untranslatedSsids = new ArrayList<>();
407         // Add the translated SSID first (UTF-8 or unknown character set)
408         if (translatedSsid.getBytes().length <= 32) {
409             untranslatedSsids.add(translatedSsid);
410         }
411         if (mCurrentLocaleAlternateCharset != null) {
412             WifiSsid altCharsetSsid = translateSsid(translatedSsid,
413                     StandardCharsets.UTF_8, mCurrentLocaleAlternateCharset);
414             if (altCharsetSsid != null && !altCharsetSsid.equals(translatedSsid)
415                     && altCharsetSsid.getBytes().length <= 32) {
416                 untranslatedSsids.add(altCharsetSsid);
417             }
418         }
419         return untranslatedSsids;
420     }
421 
422     /**
423      * Dump of {@link SsidTranslator}.
424      */
dump(PrintWriter pw)425     public synchronized void dump(PrintWriter pw) {
426         pw.println("Dump of SsidTranslator");
427         pw.println("mCurrentLocaleCharset: " + mCurrentLocaleAlternateCharset);
428         pw.println("mCharsetsPerLocaleLanguage Begin ---");
429         for (Map.Entry<String, Charset> entry : mCharsetsPerLocaleLanguage.entrySet()) {
430             pw.println(entry.getKey() + ": " + entry.getValue());
431         }
432         pw.println("mCharsetsPerLocaleLanguage End ---");
433         pw.println("mTranslatedBssids Begin ---");
434         for (Map.Entry<WifiSsid, Set<MacAddress>> translatedBssidsEntry
435                 : mTranslatedBssids.entrySet()) {
436             pw.println("Translated SSID: " + translatedBssidsEntry.getKey() + ", BSSIDS: "
437                     + Arrays.toString(translatedBssidsEntry.getValue().toArray()));
438         }
439         pw.println("mTranslatedBssids End ---");
440         pw.println("mUntranslatedBssids Begin ---");
441         for (Map.Entry<WifiSsid, Set<MacAddress>> untranslatedBssidsEntry
442                 : mUntranslatedBssids.entrySet()) {
443             pw.println("Translated SSID: " + untranslatedBssidsEntry.getKey() + ", BSSIDS: "
444                     + Arrays.toString(untranslatedBssidsEntry.getValue().toArray()));
445         }
446         pw.println("mUntranslatedBssids End ---");
447     }
448 
449     /**
450      * Sets a mock Charset for the specified Locale language.
451      * Use {@link #clearMockLocaleCharsets()} to clear the mock list.
452      */
setMockLocaleCharset( @onNull String localeLanguage, @NonNull Charset charset)453     public synchronized void setMockLocaleCharset(
454             @NonNull String localeLanguage, @NonNull Charset charset) {
455         Log.i(TAG, "Setting mock alternate charset for " + localeLanguage + ": " + charset);
456         mMockCharsetsPerLocaleLanguage.put(localeLanguage, charset);
457         updateCurrentLocaleCharset();
458     }
459 
460     /**
461      * Clears all mocked Charsets set by {@link #setMockLocaleCharset(String, Charset)}.
462      */
clearMockLocaleCharsets()463     public synchronized void clearMockLocaleCharsets() {
464         Log.i(TAG, "Clearing mock charsets");
465         mMockCharsetsPerLocaleLanguage.clear();
466         updateCurrentLocaleCharset();
467     }
468 
469     /**
470      * Indicates whether SSID translation is currently enabled.
471      */
isSsidTranslationEnabled()472     public synchronized boolean isSsidTranslationEnabled() {
473         return mCurrentLocaleAlternateCharset != null;
474     }
475 }
476