tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

DirectRTCClient.java (10653B)


      1 /*
      2 *  Copyright 2016 The WebRTC Project Authors. All rights reserved.
      3 *
      4 *  Use of this source code is governed by a BSD-style license
      5 *  that can be found in the LICENSE file in the root of the source
      6 *  tree. An additional intellectual property rights grant can be found
      7 *  in the file PATENTS.  All contributing project authors may
      8 *  be found in the AUTHORS file in the root of the source tree.
      9 */
     10 
     11 package org.appspot.apprtc;
     12 
     13 import android.util.Log;
     14 import androidx.annotation.Nullable;
     15 import java.util.ArrayList;
     16 import java.util.concurrent.ExecutorService;
     17 import java.util.concurrent.Executors;
     18 import java.util.regex.Matcher;
     19 import java.util.regex.Pattern;
     20 import org.json.JSONArray;
     21 import org.json.JSONException;
     22 import org.json.JSONObject;
     23 import org.webrtc.IceCandidate;
     24 import org.webrtc.SessionDescription;
     25 
     26 /**
     27 * Implementation of AppRTCClient that uses direct TCP connection as the signaling channel.
     28 * This eliminates the need for an external server. This class does not support loopback
     29 * connections.
     30 */
     31 public class DirectRTCClient implements AppRTCClient, TCPChannelClient.TCPChannelEvents {
     32  private static final String TAG = "DirectRTCClient";
     33  private static final int DEFAULT_PORT = 8888;
     34 
     35  // Regex pattern used for checking if room id looks like an IP.
     36  static final Pattern IP_PATTERN = Pattern.compile("("
     37      // IPv4
     38      + "((\\d+\\.){3}\\d+)|"
     39      // IPv6
     40      + "\\[((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::"
     41      + "(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)\\]|"
     42      + "\\[(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})\\]|"
     43      // IPv6 without []
     44      + "((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)|"
     45      + "(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})|"
     46      // Literals
     47      + "localhost"
     48      + ")"
     49      // Optional port number
     50      + "(:(\\d+))?");
     51 
     52  private final ExecutorService executor;
     53  private final SignalingEvents events;
     54  @Nullable
     55  private TCPChannelClient tcpClient;
     56  private RoomConnectionParameters connectionParameters;
     57 
     58  private enum ConnectionState { NEW, CONNECTED, CLOSED, ERROR }
     59 
     60  // All alterations of the room state should be done from inside the looper thread.
     61  private ConnectionState roomState;
     62 
     63  public DirectRTCClient(SignalingEvents events) {
     64    this.events = events;
     65 
     66    executor = Executors.newSingleThreadExecutor();
     67    roomState = ConnectionState.NEW;
     68  }
     69 
     70  /**
     71   * Connects to the room, roomId in connectionsParameters is required. roomId must be a valid
     72   * IP address matching IP_PATTERN.
     73   */
     74  @Override
     75  public void connectToRoom(RoomConnectionParameters connectionParameters) {
     76    this.connectionParameters = connectionParameters;
     77 
     78    if (connectionParameters.loopback) {
     79      reportError("Loopback connections aren't supported by DirectRTCClient.");
     80    }
     81 
     82    executor.execute(new Runnable() {
     83      @Override
     84      public void run() {
     85        connectToRoomInternal();
     86      }
     87    });
     88  }
     89 
     90  @Override
     91  public void disconnectFromRoom() {
     92    executor.execute(new Runnable() {
     93      @Override
     94      public void run() {
     95        disconnectFromRoomInternal();
     96      }
     97    });
     98  }
     99 
    100  /**
    101   * Connects to the room.
    102   *
    103   * Runs on the looper thread.
    104   */
    105  private void connectToRoomInternal() {
    106    this.roomState = ConnectionState.NEW;
    107 
    108    String endpoint = connectionParameters.roomId;
    109 
    110    Matcher matcher = IP_PATTERN.matcher(endpoint);
    111    if (!matcher.matches()) {
    112      reportError("roomId must match IP_PATTERN for DirectRTCClient.");
    113      return;
    114    }
    115 
    116    String ip = matcher.group(1);
    117    String portStr = matcher.group(matcher.groupCount());
    118    int port;
    119 
    120    if (portStr != null) {
    121      try {
    122        port = Integer.parseInt(portStr);
    123      } catch (NumberFormatException e) {
    124        reportError("Invalid port number: " + portStr);
    125        return;
    126      }
    127    } else {
    128      port = DEFAULT_PORT;
    129    }
    130 
    131    tcpClient = new TCPChannelClient(executor, this, ip, port);
    132  }
    133 
    134  /**
    135   * Disconnects from the room.
    136   *
    137   * Runs on the looper thread.
    138   */
    139  private void disconnectFromRoomInternal() {
    140    roomState = ConnectionState.CLOSED;
    141 
    142    if (tcpClient != null) {
    143      tcpClient.disconnect();
    144      tcpClient = null;
    145    }
    146    executor.shutdown();
    147  }
    148 
    149  @Override
    150  public void sendOfferSdp(final SessionDescription sdp) {
    151    executor.execute(new Runnable() {
    152      @Override
    153      public void run() {
    154        if (roomState != ConnectionState.CONNECTED) {
    155          reportError("Sending offer SDP in non connected state.");
    156          return;
    157        }
    158        JSONObject json = new JSONObject();
    159        jsonPut(json, "sdp", sdp.description);
    160        jsonPut(json, "type", "offer");
    161        sendMessage(json.toString());
    162      }
    163    });
    164  }
    165 
    166  @Override
    167  public void sendAnswerSdp(final SessionDescription sdp) {
    168    executor.execute(new Runnable() {
    169      @Override
    170      public void run() {
    171        JSONObject json = new JSONObject();
    172        jsonPut(json, "sdp", sdp.description);
    173        jsonPut(json, "type", "answer");
    174        sendMessage(json.toString());
    175      }
    176    });
    177  }
    178 
    179  @Override
    180  public void sendLocalIceCandidate(final IceCandidate candidate) {
    181    executor.execute(new Runnable() {
    182      @Override
    183      public void run() {
    184        JSONObject json = new JSONObject();
    185        jsonPut(json, "type", "candidate");
    186        jsonPut(json, "label", candidate.sdpMLineIndex);
    187        jsonPut(json, "id", candidate.sdpMid);
    188        jsonPut(json, "candidate", candidate.sdp);
    189 
    190        if (roomState != ConnectionState.CONNECTED) {
    191          reportError("Sending ICE candidate in non connected state.");
    192          return;
    193        }
    194        sendMessage(json.toString());
    195      }
    196    });
    197  }
    198 
    199  /** Send removed Ice candidates to the other participant. */
    200  @Override
    201  public void sendLocalIceCandidateRemovals(final IceCandidate[] candidates) {
    202    executor.execute(new Runnable() {
    203      @Override
    204      public void run() {
    205        JSONObject json = new JSONObject();
    206        jsonPut(json, "type", "remove-candidates");
    207        JSONArray jsonArray = new JSONArray();
    208        for (final IceCandidate candidate : candidates) {
    209          jsonArray.put(toJsonCandidate(candidate));
    210        }
    211        jsonPut(json, "candidates", jsonArray);
    212 
    213        if (roomState != ConnectionState.CONNECTED) {
    214          reportError("Sending ICE candidate removals in non connected state.");
    215          return;
    216        }
    217        sendMessage(json.toString());
    218      }
    219    });
    220  }
    221 
    222  // -------------------------------------------------------------------
    223  // TCPChannelClient event handlers
    224 
    225  /**
    226   * If the client is the server side, this will trigger onConnectedToRoom.
    227   */
    228  @Override
    229  public void onTCPConnected(boolean isServer) {
    230    if (isServer) {
    231      roomState = ConnectionState.CONNECTED;
    232 
    233      SignalingParameters parameters = new SignalingParameters(
    234          // Ice servers are not needed for direct connections.
    235          new ArrayList<>(),
    236          isServer, // Server side acts as the initiator on direct connections.
    237          null, // clientId
    238          null, // wssUrl
    239          null, // wwsPostUrl
    240          null, // offerSdp
    241          null // iceCandidates
    242          );
    243      events.onConnectedToRoom(parameters);
    244    }
    245  }
    246 
    247  @Override
    248  public void onTCPMessage(String msg) {
    249    try {
    250      JSONObject json = new JSONObject(msg);
    251      String type = json.optString("type");
    252      if (type.equals("candidate")) {
    253        events.onRemoteIceCandidate(toJavaCandidate(json));
    254      } else if (type.equals("remove-candidates")) {
    255        JSONArray candidateArray = json.getJSONArray("candidates");
    256        IceCandidate[] candidates = new IceCandidate[candidateArray.length()];
    257        for (int i = 0; i < candidateArray.length(); ++i) {
    258          candidates[i] = toJavaCandidate(candidateArray.getJSONObject(i));
    259        }
    260        events.onRemoteIceCandidatesRemoved(candidates);
    261      } else if (type.equals("answer")) {
    262        SessionDescription sdp = new SessionDescription(
    263            SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp"));
    264        events.onRemoteDescription(sdp);
    265      } else if (type.equals("offer")) {
    266        SessionDescription sdp = new SessionDescription(
    267            SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp"));
    268 
    269        SignalingParameters parameters = new SignalingParameters(
    270            // Ice servers are not needed for direct connections.
    271            new ArrayList<>(),
    272            false, // This code will only be run on the client side. So, we are not the initiator.
    273            null, // clientId
    274            null, // wssUrl
    275            null, // wssPostUrl
    276            sdp, // offerSdp
    277            null // iceCandidates
    278            );
    279        roomState = ConnectionState.CONNECTED;
    280        events.onConnectedToRoom(parameters);
    281      } else {
    282        reportError("Unexpected TCP message: " + msg);
    283      }
    284    } catch (JSONException e) {
    285      reportError("TCP message JSON parsing error: " + e.toString());
    286    }
    287  }
    288 
    289  @Override
    290  public void onTCPError(String description) {
    291    reportError("TCP connection error: " + description);
    292  }
    293 
    294  @Override
    295  public void onTCPClose() {
    296    events.onChannelClose();
    297  }
    298 
    299  // --------------------------------------------------------------------
    300  // Helper functions.
    301  private void reportError(final String errorMessage) {
    302    Log.e(TAG, errorMessage);
    303    executor.execute(new Runnable() {
    304      @Override
    305      public void run() {
    306        if (roomState != ConnectionState.ERROR) {
    307          roomState = ConnectionState.ERROR;
    308          events.onChannelError(errorMessage);
    309        }
    310      }
    311    });
    312  }
    313 
    314  private void sendMessage(final String message) {
    315    executor.execute(new Runnable() {
    316      @Override
    317      public void run() {
    318        tcpClient.send(message);
    319      }
    320    });
    321  }
    322 
    323  // Put a `key`->`value` mapping in `json`.
    324  private static void jsonPut(JSONObject json, String key, Object value) {
    325    try {
    326      json.put(key, value);
    327    } catch (JSONException e) {
    328      throw new RuntimeException(e);
    329    }
    330  }
    331 
    332  // Converts a Java candidate to a JSONObject.
    333  private static JSONObject toJsonCandidate(final IceCandidate candidate) {
    334    JSONObject json = new JSONObject();
    335    jsonPut(json, "label", candidate.sdpMLineIndex);
    336    jsonPut(json, "id", candidate.sdpMid);
    337    jsonPut(json, "candidate", candidate.sdp);
    338    return json;
    339  }
    340 
    341  // Converts a JSON candidate to a Java object.
    342  private static IceCandidate toJavaCandidate(JSONObject json) throws JSONException {
    343    return new IceCandidate(
    344        json.getString("id"), json.getInt("label"), json.getString("candidate"));
    345  }
    346 }