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.util.Log;
14 import java.io.IOException;
15 import java.io.InputStream;
16 import java.net.HttpURLConnection;
17 import java.net.URL;
18 import java.util.ArrayList;
19 import java.util.Scanner;
20 import java.util.List;
21 import org.appspot.apprtc.AppRTCClient.SignalingParameters;
22 import org.appspot.apprtc.util.AsyncHttpURLConnection;
23 import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents;
24 import org.json.JSONArray;
25 import org.json.JSONException;
26 import org.json.JSONObject;
27 import org.webrtc.IceCandidate;
28 import org.webrtc.PeerConnection;
29 import org.webrtc.SessionDescription;
30 
31 /**
32  * AsyncTask that converts an AppRTC room URL into the set of signaling
33  * parameters to use with that room.
34  */
35 public class RoomParametersFetcher {
36   private static final String TAG = "RoomRTCClient";
37   private static final int TURN_HTTP_TIMEOUT_MS = 5000;
38   private final RoomParametersFetcherEvents events;
39   private final String roomUrl;
40   private final String roomMessage;
41 
42   /**
43    * Room parameters fetcher callbacks.
44    */
45   public interface RoomParametersFetcherEvents {
46     /**
47      * Callback fired once the room's signaling parameters
48      * SignalingParameters are extracted.
49      */
onSignalingParametersReady(final SignalingParameters params)50     void onSignalingParametersReady(final SignalingParameters params);
51 
52     /**
53      * Callback for room parameters extraction error.
54      */
onSignalingParametersError(final String description)55     void onSignalingParametersError(final String description);
56   }
57 
RoomParametersFetcher( String roomUrl, String roomMessage, final RoomParametersFetcherEvents events)58   public RoomParametersFetcher(
59       String roomUrl, String roomMessage, final RoomParametersFetcherEvents events) {
60     this.roomUrl = roomUrl;
61     this.roomMessage = roomMessage;
62     this.events = events;
63   }
64 
makeRequest()65   public void makeRequest() {
66     Log.d(TAG, "Connecting to room: " + roomUrl);
67     AsyncHttpURLConnection httpConnection =
68         new AsyncHttpURLConnection("POST", roomUrl, roomMessage, new AsyncHttpEvents() {
69           @Override
70           public void onHttpError(String errorMessage) {
71             Log.e(TAG, "Room connection error: " + errorMessage);
72             events.onSignalingParametersError(errorMessage);
73           }
74 
75           @Override
76           public void onHttpComplete(String response) {
77             roomHttpResponseParse(response);
78           }
79         });
80     httpConnection.send();
81   }
82 
roomHttpResponseParse(String response)83   private void roomHttpResponseParse(String response) {
84     Log.d(TAG, "Room response: " + response);
85     try {
86       List<IceCandidate> iceCandidates = null;
87       SessionDescription offerSdp = null;
88       JSONObject roomJson = new JSONObject(response);
89 
90       String result = roomJson.getString("result");
91       if (!result.equals("SUCCESS")) {
92         events.onSignalingParametersError("Room response error: " + result);
93         return;
94       }
95       response = roomJson.getString("params");
96       roomJson = new JSONObject(response);
97       String roomId = roomJson.getString("room_id");
98       String clientId = roomJson.getString("client_id");
99       String wssUrl = roomJson.getString("wss_url");
100       String wssPostUrl = roomJson.getString("wss_post_url");
101       boolean initiator = (roomJson.getBoolean("is_initiator"));
102       if (!initiator) {
103         iceCandidates = new ArrayList<>();
104         String messagesString = roomJson.getString("messages");
105         JSONArray messages = new JSONArray(messagesString);
106         for (int i = 0; i < messages.length(); ++i) {
107           String messageString = messages.getString(i);
108           JSONObject message = new JSONObject(messageString);
109           String messageType = message.getString("type");
110           Log.d(TAG, "GAE->C #" + i + " : " + messageString);
111           if (messageType.equals("offer")) {
112             offerSdp = new SessionDescription(
113                 SessionDescription.Type.fromCanonicalForm(messageType), message.getString("sdp"));
114           } else if (messageType.equals("candidate")) {
115             IceCandidate candidate = new IceCandidate(
116                 message.getString("id"), message.getInt("label"), message.getString("candidate"));
117             iceCandidates.add(candidate);
118           } else {
119             Log.e(TAG, "Unknown message: " + messageString);
120           }
121         }
122       }
123       Log.d(TAG, "RoomId: " + roomId + ". ClientId: " + clientId);
124       Log.d(TAG, "Initiator: " + initiator);
125       Log.d(TAG, "WSS url: " + wssUrl);
126       Log.d(TAG, "WSS POST url: " + wssPostUrl);
127 
128       List<PeerConnection.IceServer> iceServers =
129           iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
130       boolean isTurnPresent = false;
131       for (PeerConnection.IceServer server : iceServers) {
132         Log.d(TAG, "IceServer: " + server);
133         for (String uri : server.urls) {
134           if (uri.startsWith("turn:")) {
135             isTurnPresent = true;
136             break;
137           }
138         }
139       }
140       // Request TURN servers.
141       if (!isTurnPresent && !roomJson.optString("ice_server_url").isEmpty()) {
142         List<PeerConnection.IceServer> turnServers =
143             requestTurnServers(roomJson.getString("ice_server_url"));
144         for (PeerConnection.IceServer turnServer : turnServers) {
145           Log.d(TAG, "TurnServer: " + turnServer);
146           iceServers.add(turnServer);
147         }
148       }
149 
150       SignalingParameters params = new SignalingParameters(
151           iceServers, initiator, clientId, wssUrl, wssPostUrl, offerSdp, iceCandidates);
152       events.onSignalingParametersReady(params);
153     } catch (JSONException e) {
154       events.onSignalingParametersError("Room JSON parsing error: " + e.toString());
155     } catch (IOException e) {
156       events.onSignalingParametersError("Room IO error: " + e.toString());
157     }
158   }
159 
160   // Requests & returns a TURN ICE Server based on a request URL.  Must be run
161   // off the main thread!
requestTurnServers(String url)162   private List<PeerConnection.IceServer> requestTurnServers(String url)
163       throws IOException, JSONException {
164     List<PeerConnection.IceServer> turnServers = new ArrayList<>();
165     Log.d(TAG, "Request TURN from: " + url);
166     HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
167     connection.setDoOutput(true);
168     connection.setRequestProperty("REFERER", "https://appr.tc");
169     connection.setConnectTimeout(TURN_HTTP_TIMEOUT_MS);
170     connection.setReadTimeout(TURN_HTTP_TIMEOUT_MS);
171     int responseCode = connection.getResponseCode();
172     if (responseCode != 200) {
173       throw new IOException("Non-200 response when requesting TURN server from " + url + " : "
174           + connection.getHeaderField(null));
175     }
176     InputStream responseStream = connection.getInputStream();
177     String response = drainStream(responseStream);
178     connection.disconnect();
179     Log.d(TAG, "TURN response: " + response);
180     JSONObject responseJSON = new JSONObject(response);
181     JSONArray iceServers = responseJSON.getJSONArray("iceServers");
182     for (int i = 0; i < iceServers.length(); ++i) {
183       JSONObject server = iceServers.getJSONObject(i);
184       JSONArray turnUrls = server.getJSONArray("urls");
185       String username = server.has("username") ? server.getString("username") : "";
186       String credential = server.has("credential") ? server.getString("credential") : "";
187       for (int j = 0; j < turnUrls.length(); j++) {
188         String turnUrl = turnUrls.getString(j);
189         PeerConnection.IceServer turnServer =
190             PeerConnection.IceServer.builder(turnUrl)
191               .setUsername(username)
192               .setPassword(credential)
193               .createIceServer();
194         turnServers.add(turnServer);
195       }
196     }
197     return turnServers;
198   }
199 
200   // Return the list of ICE servers described by a WebRTCPeerConnection
201   // configuration string.
iceServersFromPCConfigJSON(String pcConfig)202   private List<PeerConnection.IceServer> iceServersFromPCConfigJSON(String pcConfig)
203       throws JSONException {
204     JSONObject json = new JSONObject(pcConfig);
205     JSONArray servers = json.getJSONArray("iceServers");
206     List<PeerConnection.IceServer> ret = new ArrayList<>();
207     for (int i = 0; i < servers.length(); ++i) {
208       JSONObject server = servers.getJSONObject(i);
209       String url = server.getString("urls");
210       String credential = server.has("credential") ? server.getString("credential") : "";
211         PeerConnection.IceServer turnServer =
212             PeerConnection.IceServer.builder(url)
213               .setPassword(credential)
214               .createIceServer();
215       ret.add(turnServer);
216     }
217     return ret;
218   }
219 
220   // Return the contents of an InputStream as a String.
drainStream(InputStream in)221   private static String drainStream(InputStream in) {
222     Scanner s = new Scanner(in, "UTF-8").useDelimiter("\\A");
223     return s.hasNext() ? s.next() : "";
224   }
225 }
226