1package com.android.hotspot2.osu;
2
3import android.content.Context;
4import android.content.Intent;
5import android.net.Network;
6import android.net.wifi.WifiConfiguration;
7import android.net.wifi.WifiInfo;
8import android.os.SystemClock;
9import android.util.Log;
10
11import com.android.hotspot2.Utils;
12import com.android.hotspot2.flow.FlowService;
13import com.android.hotspot2.flow.OSUInfo;
14import com.android.hotspot2.flow.PlatformAdapter;
15import com.android.hotspot2.pps.HomeSP;
16
17import java.io.IOException;
18import java.net.MalformedURLException;
19import java.util.Iterator;
20import java.util.LinkedList;
21
22import javax.net.ssl.KeyManager;
23
24public class OSUFlowManager {
25    private static final boolean MATCH_BSSID = false;
26    private static final long WAIT_QUANTA = 10000L;
27    private static final long WAIT_TIMEOUT = 1800000L;
28
29    private final Context mContext;
30    private final LinkedList<OSUFlow> mQueue;
31    private FlowWorker mWorker;
32    private OSUFlow mCurrent;
33
34    public OSUFlowManager(Context context) {
35        mContext = context;
36        mQueue = new LinkedList<>();
37    }
38
39    public enum FlowType {Provisioning, Remediation, Policy}
40
41    public static class OSUFlow implements Runnable {
42        private final OSUClient mOSUClient;
43        private final PlatformAdapter mPlatformAdapter;
44        private final HomeSP mHomeSP;
45        private final String mSpName;
46        private final FlowType mFlowType;
47        private final KeyManager mKeyManager;
48        private final Object mNetworkLock = new Object();
49        private final Network mNetwork;
50        private Network mResultNetwork;
51        private boolean mNetworkCreated;
52        private int mWifiNetworkId;
53        private volatile long mLaunchTime;
54        private volatile boolean mAborted;
55
56        /**
57         * A policy flow.
58         * @param osuInfo The OSU information for the flow (SSID, BSSID, URL)
59         * @param platformAdapter the platform adapter
60         * @param km A key manager for TLS
61         * @throws MalformedURLException
62         */
63        public OSUFlow(OSUInfo osuInfo, PlatformAdapter platformAdapter, KeyManager km)
64                throws MalformedURLException {
65
66            mWifiNetworkId = -1;
67            mNetwork = null;
68            mOSUClient = new OSUClient(osuInfo,
69                    platformAdapter.getKeyStore(), platformAdapter.getContext());
70            mPlatformAdapter = platformAdapter;
71            mHomeSP = null;
72            mSpName = osuInfo.getName(OSUManager.LOCALE);
73            mFlowType = FlowType.Provisioning;
74            mKeyManager = km;
75        }
76
77        /**
78         * A Remediation flow for credential or policy provisioning.
79         * @param network The network to use, only set for timed provisioning
80         * @param osuURL The URL to connect to.
81         * @param platformAdapter the platform adapter
82         * @param km A key manager for TLS
83         * @param homeSP The Home SP to which this remediation flow pertains.
84         * @param flowType Remediation or Policy
85         * @throws MalformedURLException
86         */
87        public OSUFlow(Network network, String osuURL,
88                       PlatformAdapter platformAdapter, KeyManager km, HomeSP homeSP,
89                       FlowType flowType) throws MalformedURLException {
90
91            mNetwork = network;
92            mWifiNetworkId = network.netId;
93            mOSUClient = new OSUClient(osuURL,
94                    platformAdapter.getKeyStore(), platformAdapter.getContext());
95            mPlatformAdapter = platformAdapter;
96            mHomeSP = homeSP;
97            mSpName = homeSP.getFriendlyName();
98            mFlowType = flowType;
99            mKeyManager = km;
100        }
101
102        private boolean deleteNetwork(OSUFlow next) {
103            synchronized (mNetworkLock) {
104                if (!mNetworkCreated) {
105                    return false;
106                } else if (next.getFlowType() != FlowType.Provisioning) {
107                    return true;
108                }
109                OSUInfo thisInfo = mOSUClient.getOSUInfo();
110                OSUInfo thatInfo = next.mOSUClient.getOSUInfo();
111                if (thisInfo.getOsuSsid().equals(thatInfo.getOsuSsid())
112                        && thisInfo.getOSUBssid() == thatInfo.getOSUBssid()) {
113                    // Reuse the OSU network from previous and carry forward the creation fact.
114                    mNetworkCreated = true;
115                    return false;
116                } else {
117                    return true;
118                }
119            }
120        }
121
122        private Network connect() throws IOException {
123            Network network = networkMatch();
124
125            synchronized (mNetworkLock) {
126                mResultNetwork = network;
127                if (mResultNetwork != null) {
128                    return mResultNetwork;
129                }
130            }
131
132            Log.d(OSUManager.TAG, "No network match for " + toString());
133
134            int osuNetworkId = -1;
135            boolean created = false;
136
137            if (mFlowType == FlowType.Provisioning) {
138                osuNetworkId = mPlatformAdapter.connect(mOSUClient.getOSUInfo());
139                created = true;
140            }
141
142            synchronized (mNetworkLock) {
143                mNetworkCreated = created;
144                if (created) {
145                    mWifiNetworkId = osuNetworkId;
146                }
147                Log.d(OSUManager.TAG, String.format("%s waiting for %snet ID %d",
148                        toString(), created ? "created " : "existing ", osuNetworkId));
149
150                while (mResultNetwork == null && !mAborted) {
151                    try {
152                        mNetworkLock.wait();
153                    } catch (InterruptedException ie) {
154                        throw new IOException("Interrupted");
155                    }
156                }
157                if (mAborted) {
158                    throw new IOException("Aborted");
159                }
160                Utils.delay(500L);
161            }
162            return mResultNetwork;
163        }
164
165        private Network networkMatch() {
166            if (mFlowType == FlowType.Provisioning) {
167                OSUInfo match = mOSUClient.getOSUInfo();
168                WifiConfiguration config = mPlatformAdapter.getActiveWifiConfig();
169                if (config != null && bssidMatch(match, mPlatformAdapter)
170                        && Utils.decodeSsid(config.SSID).equals(match.getOsuSsid())) {
171                    synchronized (mNetworkLock) {
172                        mWifiNetworkId = config.networkId;
173                    }
174                    return mPlatformAdapter.getCurrentNetwork();
175                }
176            } else {
177                WifiConfiguration config = mPlatformAdapter.getActiveWifiConfig();
178                synchronized (mNetworkLock) {
179                    mWifiNetworkId = config != null ? config.networkId : -1;
180                }
181                return mNetwork;
182            }
183            return null;
184        }
185
186        private void networkChange() {
187            WifiInfo connectionInfo = mPlatformAdapter.getConnectionInfo();
188            if (connectionInfo == null) {
189                return;
190            }
191            Network network = mPlatformAdapter.getCurrentNetwork();
192            Log.d(OSUManager.TAG, "New network " + network
193                    + ", current OSU " + mOSUClient.getOSUInfo() +
194                    ", addr " + Utils.toIpString(connectionInfo.getIpAddress()));
195
196            synchronized (mNetworkLock) {
197                if (mResultNetwork == null && network != null && connectionInfo.getIpAddress() != 0
198                        && connectionInfo.getNetworkId() == mWifiNetworkId) {
199                    mResultNetwork = network;
200                    mNetworkLock.notifyAll();
201                }
202            }
203        }
204
205        public boolean createdNetwork() {
206            synchronized (mNetworkLock) {
207                return mNetworkCreated;
208            }
209        }
210
211        public FlowType getFlowType() {
212            return mFlowType;
213        }
214
215        public PlatformAdapter getPlatformAdapter() {
216            return mPlatformAdapter;
217        }
218
219        private void setLaunchTime() {
220            mLaunchTime = SystemClock.currentThreadTimeMillis();
221        }
222
223        public long getLaunchTime() {
224            return mLaunchTime;
225        }
226
227        private int getWifiNetworkId() {
228            synchronized (mNetworkLock) {
229                return mWifiNetworkId;
230            }
231        }
232
233        @Override
234        public void run() {
235            try {
236                Network network = connect();
237                Log.d(OSUManager.TAG, "OSU SSID Associated at " + network);
238
239                if (mFlowType == FlowType.Provisioning) {
240                    mOSUClient.provision(mPlatformAdapter, network, mKeyManager);
241                } else {
242                    mOSUClient.remediate(mPlatformAdapter, network,
243                            mKeyManager, mHomeSP, mFlowType);
244                }
245            } catch (Throwable t) {
246                if (mAborted) {
247                    Log.d(OSUManager.TAG, "OSU flow aborted: " + t, t);
248                } else {
249                    Log.w(OSUManager.TAG, "OSU flow failed: " + t, t);
250                    mPlatformAdapter.provisioningFailed(mSpName, t.getMessage());
251                }
252            } finally {
253                if (!mAborted) {
254                    mOSUClient.close(false);
255                }
256            }
257        }
258
259        public void abort() {
260            synchronized (mNetworkLock) {
261                mAborted = true;
262                mNetworkLock.notifyAll();
263            }
264            // Sockets cannot be closed on the main thread...
265            // TODO: Might want to change this to a handler.
266            new Thread() {
267                @Override
268                public void run() {
269                    try {
270                        mOSUClient.close(true);
271                    } catch (Throwable t) {
272                        Log.d(OSUManager.TAG, "Exception aborting " + toString());
273                    }
274                }
275            }.start();
276        }
277
278        @Override
279        public String toString() {
280            return mFlowType + " for " + mSpName;
281        }
282    }
283
284    private class FlowWorker extends Thread {
285        private final PlatformAdapter mPlatformAdapter;
286
287        private FlowWorker(PlatformAdapter platformAdapter) {
288            mPlatformAdapter = platformAdapter;
289        }
290
291        @Override
292        public void run() {
293            for (; ; ) {
294                synchronized (mQueue) {
295                    if (mCurrent != null && mCurrent.createdNetwork()
296                            && (mQueue.isEmpty() || mCurrent.deleteNetwork(mQueue.getLast()))) {
297                        mPlatformAdapter.deleteNetwork(mCurrent.getWifiNetworkId());
298                    }
299
300                    mCurrent = null;
301                    while (mQueue.isEmpty()) {
302                        try {
303                            mQueue.wait(WAIT_QUANTA);
304                        } catch (InterruptedException ie) {
305                            return;
306                        }
307                        if (mQueue.isEmpty()) {
308                            // Bail out on time out
309                            Log.d(OSUManager.TAG, "Flow worker terminating.");
310                            mWorker = null;
311                            mContext.stopService(new Intent(mContext, FlowService.class));
312                            return;
313                        }
314                    }
315                    mCurrent = mQueue.removeLast();
316                    mCurrent.setLaunchTime();
317                }
318                Log.d(OSUManager.TAG, "Starting " + mCurrent);
319                mCurrent.run();
320                Log.d(OSUManager.TAG, "Exiting " + mCurrent);
321            }
322        }
323    }
324
325    /*
326     * Provisioning:    Wait until there is an active WiFi info and the active WiFi config
327     *                  matches SSID and optionally BSSID.
328     * WNM Remediation: Wait until the active WiFi info matches BSSID.
329     * Timed remediation: The network is given (may be cellular).
330     */
331
332    public void appendFlow(OSUFlow flow) {
333        synchronized (mQueue) {
334            if (mCurrent != null &&
335                    SystemClock.currentThreadTimeMillis()
336                            - mCurrent.getLaunchTime() >= WAIT_TIMEOUT) {
337                Log.d(OSUManager.TAG, "Aborting stale OSU flow " + mCurrent);
338                mCurrent.abort();
339                mCurrent = null;
340            }
341
342            if (flow.getFlowType() == FlowType.Provisioning) {
343                // Kill any outstanding provisioning flows.
344                Iterator<OSUFlow> flows = mQueue.iterator();
345                while (flows.hasNext()) {
346                    if (flows.next().getFlowType() == FlowType.Provisioning) {
347                        flows.remove();
348                    }
349                }
350
351                if (mCurrent != null
352                        && mCurrent.getFlowType() == FlowType.Provisioning) {
353                    Log.d(OSUManager.TAG, "Aborting current provisioning flow " + mCurrent);
354                    mCurrent.abort();
355                    mCurrent = null;
356                }
357
358                mQueue.addLast(flow);
359            } else {
360                mQueue.addFirst(flow);
361            }
362
363            if (mWorker == null) {
364                // TODO: Might want to change this to a handler.
365                mWorker = new FlowWorker(flow.getPlatformAdapter());
366                mWorker.start();
367            }
368
369            mQueue.notifyAll();
370        }
371    }
372
373    public void networkChange() {
374        OSUFlow pending;
375        synchronized (mQueue) {
376            pending = mCurrent;
377        }
378        Log.d(OSUManager.TAG, "Network change, current flow: " + pending);
379        if (pending != null) {
380            pending.networkChange();
381        }
382    }
383
384    private static boolean bssidMatch(OSUInfo osuInfo, PlatformAdapter platformAdapter) {
385        if (MATCH_BSSID) {
386            WifiInfo wifiInfo = platformAdapter.getConnectionInfo();
387            return wifiInfo != null && Utils.parseMac(wifiInfo.getBSSID()) == osuInfo.getOSUBssid();
388        } else {
389            return true;
390        }
391    }
392}
393