1 /*
2  * Copyright (C) 2018 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.hotspot2.soap;
18 
19 import android.annotation.NonNull;
20 import android.os.Handler;
21 import android.os.Looper;
22 import android.util.Log;
23 
24 import com.android.internal.annotations.VisibleForTesting;
25 
26 import java.io.IOException;
27 import java.net.InetAddress;
28 import java.net.ServerSocket;
29 import java.net.URL;
30 import java.util.Random;
31 
32 import fi.iki.elonen.NanoHTTPD;
33 
34 /**
35  * Server for listening for redirect request from the OSU server to indicate the completion
36  * of user input.
37  *
38  * A HTTP server will be started in the {@link RedirectListener#startServer} of {@link
39  * RedirectListener}, so the caller will need to invoke {@link RedirectListener#stop} once the
40  * redirect server no longer needed.
41  */
42 public class RedirectListener extends NanoHTTPD {
43     // 10 minutes for the maximum wait time.
44     @VisibleForTesting
45     static final int USER_TIMEOUT_MILLIS = 10 * 60 * 1000;
46 
47     private static final String TAG = "PasspointRedirectListener";
48     private final String mPath;
49     private final URL mServerUrl;
50     private final Handler mHandler;
51     private Runnable mTimeOutTask;
52     private RedirectCallback mRedirectCallback;
53 
54     /**
55      * Listener interface for handling redirect events.
56      */
57     public interface RedirectCallback {
58 
59         /**
60          * Invoked when HTTP redirect response is received.
61          */
onRedirectReceived()62         void onRedirectReceived();
63 
64         /**
65          * Invoked when timeout occurs on receiving HTTP redirect response.
66          */
onRedirectTimedOut()67         void onRedirectTimedOut();
68     }
69 
70     @VisibleForTesting
RedirectListener(Looper looper, int port)71     /* package */ RedirectListener(Looper looper, int port)
72             throws IOException {
73         super(InetAddress.getLocalHost().getHostAddress(), port);
74 
75         Random rnd = new Random(System.currentTimeMillis());
76 
77         mPath = "rnd" + Integer.toString(Math.abs(rnd.nextInt()), Character.MAX_RADIX);
78         mServerUrl = new URL("http", getHostname(), port, mPath);
79         mHandler = new Handler(looper);
80         mTimeOutTask = () -> mRedirectCallback.onRedirectTimedOut();
81     }
82 
83     /**
84      * Create an instance of {@link RedirectListener}
85      *
86      * @param looper Looper on which the {@link RedirectCallback} will be called.
87      * @return Instance of {@link RedirectListener}, {@code null} in any failure.
88      */
createInstance(@onNull Looper looper)89     public static RedirectListener createInstance(@NonNull Looper looper) {
90         RedirectListener redirectListener;
91         try {
92             ServerSocket serverSocket = new ServerSocket(0, 1, InetAddress.getLocalHost());
93             redirectListener = new RedirectListener(looper, serverSocket.getLocalPort());
94             redirectListener.setServerSocketFactory(() -> {
95                 // Close current server socket so that new server socket is able to bind the port
96                 // in the start() of NanoHTTPD.
97                 serverSocket.close();
98                 return new ServerSocket();
99             });
100         } catch (IOException e) {
101             Log.e(TAG, "fails to create an instance: " + e);
102             return null;
103         }
104         return redirectListener;
105     }
106 
107     /**
108      * Start redirect listener
109      *
110      * @param callback to be notified when the redirect request is received or timed out.
111      * @param startHandler handler on which the start code is executed.
112      * @return {@code true} in success, {@code false} if the {@code callback} and {@code
113      * startHandler} are {@code null} or the server is already running.
114      */
startServer(@onNull RedirectCallback callback, @NonNull Handler startHandler)115     public boolean startServer(@NonNull RedirectCallback callback, @NonNull Handler startHandler) {
116         if (callback == null) {
117             return false;
118         }
119 
120         if (startHandler == null) {
121             return false;
122         }
123 
124         if (isAlive()) {
125             Log.e(TAG, "redirect listener is already running");
126             return false;
127         }
128         mRedirectCallback = callback;
129 
130         startHandler.post(() -> {
131             try {
132                 start();
133             } catch (IOException e) {
134                 Log.e(TAG, "unable to start redirect listener: " + e);
135             }
136         });
137         mHandler.postDelayed(mTimeOutTask, USER_TIMEOUT_MILLIS);
138         return true;
139     }
140 
141     /**
142      * Stop redirect listener
143      *
144      * @param stopHandler handler on which the stop code is executed.
145      */
stopServer(@onNull Handler stopHandler)146     public void stopServer(@NonNull Handler stopHandler) {
147         if (mHandler.hasCallbacks(mTimeOutTask)) {
148             mHandler.removeCallbacks(mTimeOutTask);
149         }
150         if (stopHandler == null) {
151             return;
152         }
153         if (isServerAlive()) {
154             stopHandler.post(() -> stop());
155         }
156     }
157 
158     /**
159      * Check if the server is alive or not.
160      *
161      * @return {@code true} if the server is alive.
162      */
isServerAlive()163     public boolean isServerAlive() {
164         return isAlive();
165     }
166 
167     /**
168      * Get URL to which the local redirect server listens
169      *
170      * @return The URL for the local redirect server.
171      */
getServerUrl()172     public URL getServerUrl() {
173         return mServerUrl;
174     }
175 
176     @Override
serve(IHTTPSession session)177     public Response serve(IHTTPSession session) {
178 
179         // Ignore all other requests except for a HTTP request that has the server url path with
180         // GET method.
181         if (session.getMethod() != Method.GET || !mServerUrl.getPath().equals(session.getUri())) {
182             return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_HTML, "");
183         }
184 
185         mHandler.removeCallbacks(mTimeOutTask);
186         mRedirectCallback.onRedirectReceived();
187         return newFixedLengthResponse("");
188     }
189 }
190