tor-browser

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

WebSocketRTCClient.java (15857B)


      1 /*
      2 *  Copyright 2014 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.os.Handler;
     14 import android.os.HandlerThread;
     15 import android.util.Log;
     16 import androidx.annotation.Nullable;
     17 import org.appspot.apprtc.RoomParametersFetcher.RoomParametersFetcherEvents;
     18 import org.appspot.apprtc.WebSocketChannelClient.WebSocketChannelEvents;
     19 import org.appspot.apprtc.WebSocketChannelClient.WebSocketConnectionState;
     20 import org.appspot.apprtc.util.AsyncHttpURLConnection;
     21 import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents;
     22 import org.json.JSONArray;
     23 import org.json.JSONException;
     24 import org.json.JSONObject;
     25 import org.webrtc.IceCandidate;
     26 import org.webrtc.SessionDescription;
     27 
     28 /**
     29 * Negotiates signaling for chatting with https://appr.tc "rooms".
     30 * Uses the client<->server specifics of the apprtc AppEngine webapp.
     31 *
     32 * <p>To use: create an instance of this object (registering a message handler) and
     33 * call connectToRoom().  Once room connection is established
     34 * onConnectedToRoom() callback with room parameters is invoked.
     35 * Messages to other party (with local Ice candidates and answer SDP) can
     36 * be sent after WebSocket connection is established.
     37 */
     38 public class WebSocketRTCClient implements AppRTCClient, WebSocketChannelEvents {
     39  private static final String TAG = "WSRTCClient";
     40  private static final String ROOM_JOIN = "join";
     41  private static final String ROOM_MESSAGE = "message";
     42  private static final String ROOM_LEAVE = "leave";
     43 
     44  private enum ConnectionState { NEW, CONNECTED, CLOSED, ERROR }
     45 
     46  private enum MessageType { MESSAGE, LEAVE }
     47 
     48  private final Handler handler;
     49  private boolean initiator;
     50  private SignalingEvents events;
     51  private WebSocketChannelClient wsClient;
     52  private ConnectionState roomState;
     53  private RoomConnectionParameters connectionParameters;
     54  private String messageUrl;
     55  private String leaveUrl;
     56 
     57  public WebSocketRTCClient(SignalingEvents events) {
     58    this.events = events;
     59    roomState = ConnectionState.NEW;
     60    final HandlerThread handlerThread = new HandlerThread(TAG);
     61    handlerThread.start();
     62    handler = new Handler(handlerThread.getLooper());
     63  }
     64 
     65  // --------------------------------------------------------------------
     66  // AppRTCClient interface implementation.
     67  // Asynchronously connect to an AppRTC room URL using supplied connection
     68  // parameters, retrieves room parameters and connect to WebSocket server.
     69  @Override
     70  public void connectToRoom(RoomConnectionParameters connectionParameters) {
     71    this.connectionParameters = connectionParameters;
     72    handler.post(new Runnable() {
     73      @Override
     74      public void run() {
     75        connectToRoomInternal();
     76      }
     77    });
     78  }
     79 
     80  @Override
     81  public void disconnectFromRoom() {
     82    handler.post(new Runnable() {
     83      @Override
     84      public void run() {
     85        disconnectFromRoomInternal();
     86        handler.getLooper().quit();
     87      }
     88    });
     89  }
     90 
     91  // Connects to room - function runs on a local looper thread.
     92  private void connectToRoomInternal() {
     93    String connectionUrl = getConnectionUrl(connectionParameters);
     94    Log.d(TAG, "Connect to room: " + connectionUrl);
     95    roomState = ConnectionState.NEW;
     96    wsClient = new WebSocketChannelClient(handler, this);
     97 
     98    RoomParametersFetcherEvents callbacks = new RoomParametersFetcherEvents() {
     99      @Override
    100      public void onSignalingParametersReady(final SignalingParameters params) {
    101        WebSocketRTCClient.this.handler.post(new Runnable() {
    102          @Override
    103          public void run() {
    104            WebSocketRTCClient.this.signalingParametersReady(params);
    105          }
    106        });
    107      }
    108 
    109      @Override
    110      public void onSignalingParametersError(String description) {
    111        WebSocketRTCClient.this.reportError(description);
    112      }
    113    };
    114 
    115    new RoomParametersFetcher(connectionUrl, null, callbacks).makeRequest();
    116  }
    117 
    118  // Disconnect from room and send bye messages - runs on a local looper thread.
    119  private void disconnectFromRoomInternal() {
    120    Log.d(TAG, "Disconnect. Room state: " + roomState);
    121    if (roomState == ConnectionState.CONNECTED) {
    122      Log.d(TAG, "Closing room.");
    123      sendPostMessage(MessageType.LEAVE, leaveUrl, null);
    124    }
    125    roomState = ConnectionState.CLOSED;
    126    if (wsClient != null) {
    127      wsClient.disconnect(true);
    128    }
    129  }
    130 
    131  // Helper functions to get connection, post message and leave message URLs
    132  private String getConnectionUrl(RoomConnectionParameters connectionParameters) {
    133    return connectionParameters.roomUrl + "/" + ROOM_JOIN + "/" + connectionParameters.roomId
    134        + getQueryString(connectionParameters);
    135  }
    136 
    137  private String getMessageUrl(
    138      RoomConnectionParameters connectionParameters, SignalingParameters signalingParameters) {
    139    return connectionParameters.roomUrl + "/" + ROOM_MESSAGE + "/" + connectionParameters.roomId
    140        + "/" + signalingParameters.clientId + getQueryString(connectionParameters);
    141  }
    142 
    143  private String getLeaveUrl(
    144      RoomConnectionParameters connectionParameters, SignalingParameters signalingParameters) {
    145    return connectionParameters.roomUrl + "/" + ROOM_LEAVE + "/" + connectionParameters.roomId + "/"
    146        + signalingParameters.clientId + getQueryString(connectionParameters);
    147  }
    148 
    149  private String getQueryString(RoomConnectionParameters connectionParameters) {
    150    if (connectionParameters.urlParameters != null) {
    151      return "?" + connectionParameters.urlParameters;
    152    } else {
    153      return "";
    154    }
    155  }
    156 
    157  // Callback issued when room parameters are extracted. Runs on local
    158  // looper thread.
    159  private void signalingParametersReady(final SignalingParameters signalingParameters) {
    160    Log.d(TAG, "Room connection completed.");
    161    if (connectionParameters.loopback
    162        && (!signalingParameters.initiator || signalingParameters.offerSdp != null)) {
    163      reportError("Loopback room is busy.");
    164      return;
    165    }
    166    if (!connectionParameters.loopback && !signalingParameters.initiator
    167        && signalingParameters.offerSdp == null) {
    168      Log.w(TAG, "No offer SDP in room response.");
    169    }
    170    initiator = signalingParameters.initiator;
    171    messageUrl = getMessageUrl(connectionParameters, signalingParameters);
    172    leaveUrl = getLeaveUrl(connectionParameters, signalingParameters);
    173    Log.d(TAG, "Message URL: " + messageUrl);
    174    Log.d(TAG, "Leave URL: " + leaveUrl);
    175    roomState = ConnectionState.CONNECTED;
    176 
    177    // Fire connection and signaling parameters events.
    178    events.onConnectedToRoom(signalingParameters);
    179 
    180    // Connect and register WebSocket client.
    181    wsClient.connect(signalingParameters.wssUrl, signalingParameters.wssPostUrl);
    182    wsClient.register(connectionParameters.roomId, signalingParameters.clientId);
    183  }
    184 
    185  // Send local offer SDP to the other participant.
    186  @Override
    187  public void sendOfferSdp(final SessionDescription sdp) {
    188    handler.post(new Runnable() {
    189      @Override
    190      public void run() {
    191        if (roomState != ConnectionState.CONNECTED) {
    192          reportError("Sending offer SDP in non connected state.");
    193          return;
    194        }
    195        JSONObject json = new JSONObject();
    196        jsonPut(json, "sdp", sdp.description);
    197        jsonPut(json, "type", "offer");
    198        sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString());
    199        if (connectionParameters.loopback) {
    200          // In loopback mode rename this offer to answer and route it back.
    201          SessionDescription sdpAnswer = new SessionDescription(
    202              SessionDescription.Type.fromCanonicalForm("answer"), sdp.description);
    203          events.onRemoteDescription(sdpAnswer);
    204        }
    205      }
    206    });
    207  }
    208 
    209  // Send local answer SDP to the other participant.
    210  @Override
    211  public void sendAnswerSdp(final SessionDescription sdp) {
    212    handler.post(new Runnable() {
    213      @Override
    214      public void run() {
    215        if (connectionParameters.loopback) {
    216          Log.e(TAG, "Sending answer in loopback mode.");
    217          return;
    218        }
    219        JSONObject json = new JSONObject();
    220        jsonPut(json, "sdp", sdp.description);
    221        jsonPut(json, "type", "answer");
    222        wsClient.send(json.toString());
    223      }
    224    });
    225  }
    226 
    227  // Send Ice candidate to the other participant.
    228  @Override
    229  public void sendLocalIceCandidate(final IceCandidate candidate) {
    230    handler.post(new Runnable() {
    231      @Override
    232      public void run() {
    233        JSONObject json = new JSONObject();
    234        jsonPut(json, "type", "candidate");
    235        jsonPut(json, "label", candidate.sdpMLineIndex);
    236        jsonPut(json, "id", candidate.sdpMid);
    237        jsonPut(json, "candidate", candidate.sdp);
    238        if (initiator) {
    239          // Call initiator sends ice candidates to GAE server.
    240          if (roomState != ConnectionState.CONNECTED) {
    241            reportError("Sending ICE candidate in non connected state.");
    242            return;
    243          }
    244          sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString());
    245          if (connectionParameters.loopback) {
    246            events.onRemoteIceCandidate(candidate);
    247          }
    248        } else {
    249          // Call receiver sends ice candidates to websocket server.
    250          wsClient.send(json.toString());
    251        }
    252      }
    253    });
    254  }
    255 
    256  // Send removed Ice candidates to the other participant.
    257  @Override
    258  public void sendLocalIceCandidateRemovals(final IceCandidate[] candidates) {
    259    handler.post(new Runnable() {
    260      @Override
    261      public void run() {
    262        JSONObject json = new JSONObject();
    263        jsonPut(json, "type", "remove-candidates");
    264        JSONArray jsonArray = new JSONArray();
    265        for (final IceCandidate candidate : candidates) {
    266          jsonArray.put(toJsonCandidate(candidate));
    267        }
    268        jsonPut(json, "candidates", jsonArray);
    269        if (initiator) {
    270          // Call initiator sends ice candidates to GAE server.
    271          if (roomState != ConnectionState.CONNECTED) {
    272            reportError("Sending ICE candidate removals in non connected state.");
    273            return;
    274          }
    275          sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString());
    276          if (connectionParameters.loopback) {
    277            events.onRemoteIceCandidatesRemoved(candidates);
    278          }
    279        } else {
    280          // Call receiver sends ice candidates to websocket server.
    281          wsClient.send(json.toString());
    282        }
    283      }
    284    });
    285  }
    286 
    287  // --------------------------------------------------------------------
    288  // WebSocketChannelEvents interface implementation.
    289  // All events are called by WebSocketChannelClient on a local looper thread
    290  // (passed to WebSocket client constructor).
    291  @Override
    292  public void onWebSocketMessage(final String msg) {
    293    if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
    294      Log.e(TAG, "Got WebSocket message in non registered state.");
    295      return;
    296    }
    297    try {
    298      JSONObject json = new JSONObject(msg);
    299      String msgText = json.getString("msg");
    300      String errorText = json.optString("error");
    301      if (msgText.length() > 0) {
    302        json = new JSONObject(msgText);
    303        String type = json.optString("type");
    304        if (type.equals("candidate")) {
    305          events.onRemoteIceCandidate(toJavaCandidate(json));
    306        } else if (type.equals("remove-candidates")) {
    307          JSONArray candidateArray = json.getJSONArray("candidates");
    308          IceCandidate[] candidates = new IceCandidate[candidateArray.length()];
    309          for (int i = 0; i < candidateArray.length(); ++i) {
    310            candidates[i] = toJavaCandidate(candidateArray.getJSONObject(i));
    311          }
    312          events.onRemoteIceCandidatesRemoved(candidates);
    313        } else if (type.equals("answer")) {
    314          if (initiator) {
    315            SessionDescription sdp = new SessionDescription(
    316                SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp"));
    317            events.onRemoteDescription(sdp);
    318          } else {
    319            reportError("Received answer for call initiator: " + msg);
    320          }
    321        } else if (type.equals("offer")) {
    322          if (!initiator) {
    323            SessionDescription sdp = new SessionDescription(
    324                SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp"));
    325            events.onRemoteDescription(sdp);
    326          } else {
    327            reportError("Received offer for call receiver: " + msg);
    328          }
    329        } else if (type.equals("bye")) {
    330          events.onChannelClose();
    331        } else {
    332          reportError("Unexpected WebSocket message: " + msg);
    333        }
    334      } else {
    335        if (errorText != null && errorText.length() > 0) {
    336          reportError("WebSocket error message: " + errorText);
    337        } else {
    338          reportError("Unexpected WebSocket message: " + msg);
    339        }
    340      }
    341    } catch (JSONException e) {
    342      reportError("WebSocket message JSON parsing error: " + e.toString());
    343    }
    344  }
    345 
    346  @Override
    347  public void onWebSocketClose() {
    348    events.onChannelClose();
    349  }
    350 
    351  @Override
    352  public void onWebSocketError(String description) {
    353    reportError("WebSocket error: " + description);
    354  }
    355 
    356  // --------------------------------------------------------------------
    357  // Helper functions.
    358  private void reportError(final String errorMessage) {
    359    Log.e(TAG, errorMessage);
    360    handler.post(new Runnable() {
    361      @Override
    362      public void run() {
    363        if (roomState != ConnectionState.ERROR) {
    364          roomState = ConnectionState.ERROR;
    365          events.onChannelError(errorMessage);
    366        }
    367      }
    368    });
    369  }
    370 
    371  // Put a `key`->`value` mapping in `json`.
    372  private static void jsonPut(JSONObject json, String key, Object value) {
    373    try {
    374      json.put(key, value);
    375    } catch (JSONException e) {
    376      throw new RuntimeException(e);
    377    }
    378  }
    379 
    380  // Send SDP or ICE candidate to a room server.
    381  private void sendPostMessage(
    382      final MessageType messageType, final String url, @Nullable final String message) {
    383    String logInfo = url;
    384    if (message != null) {
    385      logInfo += ". Message: " + message;
    386    }
    387    Log.d(TAG, "C->GAE: " + logInfo);
    388    AsyncHttpURLConnection httpConnection =
    389        new AsyncHttpURLConnection("POST", url, message, new AsyncHttpEvents() {
    390          @Override
    391          public void onHttpError(String errorMessage) {
    392            reportError("GAE POST error: " + errorMessage);
    393          }
    394 
    395          @Override
    396          public void onHttpComplete(String response) {
    397            if (messageType == MessageType.MESSAGE) {
    398              try {
    399                JSONObject roomJson = new JSONObject(response);
    400                String result = roomJson.getString("result");
    401                if (!result.equals("SUCCESS")) {
    402                  reportError("GAE POST error: " + result);
    403                }
    404              } catch (JSONException e) {
    405                reportError("GAE POST JSON error: " + e.toString());
    406              }
    407            }
    408          }
    409        });
    410    httpConnection.send();
    411  }
    412 
    413  // Converts a Java candidate to a JSONObject.
    414  private JSONObject toJsonCandidate(final IceCandidate candidate) {
    415    JSONObject json = new JSONObject();
    416    jsonPut(json, "label", candidate.sdpMLineIndex);
    417    jsonPut(json, "id", candidate.sdpMid);
    418    jsonPut(json, "candidate", candidate.sdp);
    419    return json;
    420  }
    421 
    422  // Converts a JSON candidate to a Java object.
    423  IceCandidate toJavaCandidate(JSONObject json) throws JSONException {
    424    return new IceCandidate(
    425        json.getString("id"), json.getInt("label"), json.getString("candidate"));
    426  }
    427 }