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