content: /*
   * Copyright (C) 2018 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.wifi.hotspot2.soap;
  
  import android.annotation.NonNull;
  import android.os.Handler;
  import android.os.Looper;
  import android.util.Log;
  
  import com.android.internal.annotations.VisibleForTesting;
  
  import java.io.IOException;
  import java.net.InetAddress;
  import java.net.ServerSocket;
  import java.net.URL;
  import java.util.Random;
  
  import fi.iki.elonen.NanoHTTPD;
  
  /**
   * Server for listening for redirect request from the OSU server to indicate the completion
   * of user input.
   *
   * A HTTP server will be started in the {@link RedirectListener#startServer} of {@link
   * RedirectListener}, so the caller will need to invoke {@link RedirectListener#stop} once the
   * redirect server no longer needed.
   */
  public class RedirectListener extends NanoHTTPD {
      // 10 minutes for the maximum wait time.
      @VisibleForTesting
      static final int USER_TIMEOUT_MILLIS = 10 * 60 * 1000;
  
      private static final String TAG = "PasspointRedirectListener";
      private final String mPath;
      private final URL mServerUrl;
      private final Handler mHandler;
      private Runnable mTimeOutTask;
      private RedirectCallback mRedirectCallback;
  
      /**
       * Listener interface for handling redirect events.
       */
      public interface RedirectCallback {
  
          /**
           * Invoked when HTTP redirect response is received.
           */
          void onRedirectReceived();
  
          /**
           * Invoked when timeout occurs on receiving HTTP redirect response.
           */
          void onRedirectTimedOut();
      }
  
      @VisibleForTesting
      /* package */ RedirectListener(Looper looper, int port)
              throws IOException {
          super(InetAddress.getLocalHost().getHostAddress(), port);
  
          Random rnd = new Random(System.currentTimeMillis());
  
          mPath = "rnd" + Integer.toString(Math.abs(rnd.nextInt()), Character.MAX_RADIX);
          mServerUrl = new URL("http", getHostname(), port, mPath);
          mHandler = new Handler(looper);
          mTimeOutTask = () -> mRedirectCallback.onRedirectTimedOut();
      }
  
      /**
       * Create an instance of {@link RedirectListener}
       *
       * @param looper Looper on which the {@link RedirectCallback} will be called.
       * @return Instance of {@link RedirectListener}, {@code null} in any failure.
       */
      public static RedirectListener createInstance(@NonNull Looper looper) {
          RedirectListener redirectListener;
          try {
              ServerSocket serverSocket = new ServerSocket(0, 1, InetAddress.getLocalHost());
              redirectListener = new RedirectListener(looper, serverSocket.getLocalPort());
              redirectListener.setServerSocketFactory(() -> {
                  // Close current server socket so that new server socket is able to bind the port
                  // in the start() of NanoHTTPD.
                  serverSocket.close();
                  return new ServerSocket();
              });
          } catch (IOException e) {
              Log.e(TAG, "fails to create an instance: " + e);
              return null;
          }
          return redirectListener;
      }
  
      /**
       * Start redirect listener
       *
       * @param callback to be notified when the redirect request is received or timed out.
       * @param startHandler handler on which the start code is executed.
       * @return {@code true} in success, {@code false} if the {@code callback} and {@code
       * startHandler} are {@code null} or the server is already running.
       */
      public boolean startServer(@NonNull RedirectCallback callback, @NonNull Handler startHandler) {
          if (callback == null) {
              return false;
          }
  
          if (startHandler == null) {
              return false;
          }
  
          if (isAlive()) {
              Log.e(TAG, "redirect listener is already running");
              return false;
          }
          mRedirectCallback = callback;
  
          startHandler.post(() -> {
              try {
                  start();
              } catch (IOException e) {
                  Log.e(TAG, "unable to start redirect listener: " + e);
              }
          });
          mHandler.postDelayed(mTimeOutTask, USER_TIMEOUT_MILLIS);
          return true;
      }
  
      /**
       * Stop redirect listener
       *
       * @param stopHandler handler on which the stop code is executed.
       */
      public void stopServer(@NonNull Handler stopHandler) {
          if (mHandler.hasCallbacks(mTimeOutTask)) {
              mHandler.removeCallbacks(mTimeOutTask);
          }
          if (stopHandler == null) {
              return;
          }
          if (isServerAlive()) {
              stopHandler.post(() -> stop());
          }
      }
  
      /**
       * Check if the server is alive or not.
       *
       * @return {@code true} if the server is alive.
       */
      public boolean isServerAlive() {
          return isAlive();
      }
  
      /**
       * Get URL to which the local redirect server listens
       *
       * @return The URL for the local redirect server.
       */
      public URL getServerUrl() {
          return mServerUrl;
      }
  
      @Override
      public Response serve(IHTTPSession session) {
  
          // Ignore all other requests except for a HTTP request that has the server url path with
          // GET method.
          if (session.getMethod() != Method.GET || !mServerUrl.getPath().equals(session.getUri())) {
              return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_HTML, "");
          }
  
          mHandler.removeCallbacks(mTimeOutTask);
          mRedirectCallback.onRedirectReceived();
          return newFixedLengthResponse("");
      }
  }
  