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