package mediabrowser.apiinteraction.connectionmanager; import mediabrowser.apiinteraction.*; import mediabrowser.apiinteraction.connect.ConnectService; import mediabrowser.apiinteraction.device.IDevice; import mediabrowser.apiinteraction.discovery.IServerLocator; import mediabrowser.apiinteraction.http.HttpHeaders; import mediabrowser.apiinteraction.http.HttpRequest; import mediabrowser.apiinteraction.http.IAsyncHttpClient; import mediabrowser.apiinteraction.network.INetworkConnection; import mediabrowser.model.apiclient.*; import mediabrowser.model.connect.*; import mediabrowser.model.dto.IHasServerId; import mediabrowser.model.dto.UserDto; import mediabrowser.model.extensions.StringHelper; import mediabrowser.model.logging.ILogger; import mediabrowser.model.net.HttpException; import mediabrowser.model.registration.RegistrationInfo; import mediabrowser.model.serialization.IJsonSerializer; import mediabrowser.model.session.ClientCapabilities; import mediabrowser.model.system.PublicSystemInfo; import mediabrowser.model.users.AuthenticationResult; import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; import java.util.*; public class ConnectionManager implements IConnectionManager { private ICredentialProvider credentialProvider; private INetworkConnection networkConnection; protected ILogger logger; private IServerLocator serverDiscovery; protected IAsyncHttpClient httpClient; private HashMap<String, ApiClient> apiClients = new HashMap<String, ApiClient>(); protected IJsonSerializer jsonSerializer; protected String applicationName; protected String applicationVersion; protected IDevice device; protected ClientCapabilities clientCapabilities; protected ApiEventListener apiEventListener; private ConnectService connectService; private ConnectUser connectUser; public ConnectionManager(ICredentialProvider credentialProvider, INetworkConnection networkConnectivity, IJsonSerializer jsonSerializer, ILogger logger, IServerLocator serverDiscovery, IAsyncHttpClient httpClient, String applicationName, String applicationVersion, IDevice device, ClientCapabilities clientCapabilities, ApiEventListener apiEventListener) { this.credentialProvider = credentialProvider; networkConnection = networkConnectivity; this.logger = logger; this.serverDiscovery = serverDiscovery; this.httpClient = httpClient; this.applicationName = applicationName; this.applicationVersion = applicationVersion; this.device = device; this.clientCapabilities = clientCapabilities; this.apiEventListener = apiEventListener; this.jsonSerializer = jsonSerializer; connectService = new ConnectService(jsonSerializer, logger, httpClient, applicationName, applicationVersion); } public ClientCapabilities getClientCapabilities() { return clientCapabilities; } @Override public ApiClient GetApiClient(IHasServerId item) { return GetApiClient(item.getServerId()); } @Override public ApiClient GetApiClient(String serverId) { return apiClients.get(serverId); } @Override public ServerInfo getServerInfo(String serverId) { final ServerCredentials credentials = credentialProvider.GetCredentials(); for (ServerInfo server : credentials.getServers()){ if (StringHelper.EqualsIgnoreCase(server.getId(), serverId)){ return server; } } return null; } @Override public IDevice getDevice(){ return this.device; } void OnConnectUserSignIn(ConnectUser user){ connectUser = user; // TODO: Fire event } void OnFailedConnection(Response<ConnectionResult> response){ logger.Debug("No server available"); ConnectionResult result = new ConnectionResult(); result.setState(ConnectionState.Unavailable); result.setConnectUser(connectUser); response.onResponse(result); } void OnFailedConnection(Response<ConnectionResult> response, ArrayList<ServerInfo> servers){ logger.Debug("No saved authentication"); ConnectionResult result = new ConnectionResult(); if (servers.size() == 0 && connectUser == null){ result.setState(ConnectionState.ConnectSignIn); } else{ result.setState(ConnectionState.ServerSelection); } result.setServers(new ArrayList<ServerInfo>()); result.setConnectUser(connectUser); response.onResponse(result); } @Override public void Connect(final Response<ConnectionResult> response) { logger.Debug("Entering initial connection workflow"); GetAvailableServers(new GetAvailableServersResponse(logger, this, response)); } @Override public void GetSavedServers(final Response<ArrayList<ServerInfo>> response){ final ServerCredentials credentials = credentialProvider.GetCredentials(); response.onResponse(credentials.getServers()); } void Connect(final ArrayList<ServerInfo> servers, final Response<ConnectionResult> response){ // Sort by last date accessed, descending Collections.sort(servers, new ServerInfoDateComparator()); Collections.reverse(servers); if (servers.size() == 1) { Connect(servers.get(0), new ConnectionOptions(), new ConnectToSingleServerListResponse(response)); return; } // Check the first server for a saved access token if (servers.size() == 0 || tangible.DotNetToJavaStringHelper.isNullOrEmpty(servers.get(0).getAccessToken())) { OnFailedConnection(response, servers); return; } ServerInfo firstServer = servers.get(0); Connect(firstServer, new ConnectionOptions(), new FirstServerConnectResponse(this, servers, response)); } @Override public void Connect(final ServerInfo server, final Response<ConnectionResult> response) { Connect(server, new ConnectionOptions(), response); } @Override public void Connect(final ServerInfo server, ConnectionOptions options, final Response<ConnectionResult> response) { ArrayList<ConnectionMode> tests = new ArrayList<ConnectionMode>(); tests.add(ConnectionMode.Manual); tests.add(ConnectionMode.Local); tests.add(ConnectionMode.Remote); // If we've connected to the server before, try to optimize by starting with the last used connection mode if (server.getLastConnectionMode() != null) { tests.remove(server.getLastConnectionMode()); tests.add(0, server.getLastConnectionMode()); } boolean isLocalNetworkAvailable = networkConnection.getNetworkStatus().GetIsAnyLocalNetworkAvailable(); // Kick off wake on lan on a separate thread (if applicable) boolean sendWakeOnLan = server.getWakeOnLanInfos().size() > 0 && isLocalNetworkAvailable; if (sendWakeOnLan){ BeginWakeServer(server); } long wakeOnLanSendTime = System.currentTimeMillis(); TestNextConnectionMode(tests, 0, isLocalNetworkAvailable, server, wakeOnLanSendTime, options, response); } void TestNextConnectionMode(final ArrayList<ConnectionMode> tests, final int index, final boolean isLocalNetworkAvailable, final ServerInfo server, final long wakeOnLanSendTime, final ConnectionOptions options, final Response<ConnectionResult> response){ if (index >= tests.size()){ OnFailedConnection(response); return; } final ConnectionMode mode = tests.get(index); final String address = server.GetAddress(mode); boolean enableRetry = false; boolean skipTest = false; int timeout = 15000; if (mode == ConnectionMode.Local){ if (!isLocalNetworkAvailable){ logger.Debug("Skipping local connection test because local network is unavailable"); skipTest = true; } enableRetry = true; timeout = 10000; } else if (mode == ConnectionMode.Manual){ if (StringHelper.EqualsIgnoreCase(address, server.getLocalAddress())){ logger.Debug("Skipping manual connection test because the address is the same as the local address"); skipTest = true; } else if (StringHelper.EqualsIgnoreCase(address, server.getRemoteAddress())){ logger.Debug("Skipping manual connection test because the address is the same as the remote address"); skipTest = true; } } if (skipTest || tangible.DotNetToJavaStringHelper.isNullOrEmpty(address)) { TestNextConnectionMode(tests, index + 1, isLocalNetworkAvailable, server, wakeOnLanSendTime, options, response); return; } TryConnect(address, timeout, new TestNextConnectionModeTryConnectResponse(this, server, tests, mode, address, timeout, options, index, isLocalNetworkAvailable, wakeOnLanSendTime, enableRetry, logger, response)); } void OnSuccessfulConnection(final ServerInfo server, final PublicSystemInfo systemInfo, final ConnectionMode connectionMode, final ConnectionOptions connectionOptions, final Response<ConnectionResult> response) { final ServerCredentials credentials = credentialProvider.GetCredentials(); if (!tangible.DotNetToJavaStringHelper.isNullOrEmpty(credentials.getConnectAccessToken())) { EnsureConnectUser(credentials, new EnsureConnectUserResponse(this, server, credentials, systemInfo, connectionMode, connectionOptions, response)); } else { AfterConnectValidated(server, credentials, systemInfo, connectionMode, true, connectionOptions, response); } } void AddAuthenticationInfoFromConnect(final ServerInfo server, ConnectionMode connectionMode, ServerCredentials credentials, final EmptyResponse response){ if (tangible.DotNetToJavaStringHelper.isNullOrEmpty(server.getExchangeToken())) { throw new IllegalArgumentException("server"); } logger.Debug("Adding authentication info from Connect"); String url = server.GetAddress(connectionMode); url += "/emby/Connect/Exchange?format=json&ConnectUserId=" + credentials.getConnectUserId(); HttpRequest request = new HttpRequest(); request.setUrl(url); request.setMethod("GET"); String auth = "MediaBrowser Client=\"" + applicationName + "\", Device=\"" + getDevice().getDeviceName() + "\", DeviceId=\"" + getDevice().getDeviceId() + "\", Version=\"" + applicationVersion + "\""; request.getRequestHeaders().put("X-Emby-Authorization", auth); request.getRequestHeaders().put("X-MediaBrowser-Token", server.getExchangeToken()); httpClient.Send(request, new ExchangeTokenResponse(jsonSerializer, server, response)); } void AfterConnectValidated(final ServerInfo server, final ServerCredentials credentials, final PublicSystemInfo systemInfo, final ConnectionMode connectionMode, boolean verifyLocalAuthentication, final ConnectionOptions options, final Response<ConnectionResult> response){ if (verifyLocalAuthentication && !tangible.DotNetToJavaStringHelper.isNullOrEmpty(server.getAccessToken())) { ValidateAuthentication(server, connectionMode, new AfterConnectValidatedResponse(this, server, credentials, systemInfo, connectionMode, options, response)); return; } server.ImportInfo(systemInfo); if (options.getUpdateDateLastAccessed()){ server.setDateLastAccessed(new Date()); } server.setLastConnectionMode(connectionMode); credentials.AddOrUpdateServer(server); credentialProvider.SaveCredentials(credentials); ConnectionResult result = new ConnectionResult(); result.setApiClient(GetOrAddApiClient(server, connectionMode)); result.setState(tangible.DotNetToJavaStringHelper.isNullOrEmpty(server.getAccessToken()) ? ConnectionState.ServerSignIn : ConnectionState.SignedIn); result.getServers().add(server); result.getApiClient().EnableAutomaticNetworking(server, connectionMode, networkConnection); if (result.getState() == ConnectionState.SignedIn) { AfterConnected(result.getApiClient(), options); } response.onResponse(result); } @Override public void Connect(final String address, final Response<ConnectionResult> response) { final String normalizedAddress = NormalizeAddress(address); logger.Debug("Attempting to connect to server at %s", address); TryConnect(normalizedAddress, 15000, new ConnectToAddressResponse(this, normalizedAddress, response)); } @Override public void Logout(final EmptyResponse response) { logger.Debug("Logging out of all servers"); LogoutAll(new LogoutAllResponse(credentialProvider, logger, response, this)); } void clearConnectUserAfterLogout() { if (connectUser != null){ connectUser = null; } } private void ValidateAuthentication(final ServerInfo server, ConnectionMode connectionMode, final EmptyResponse response) { final String url = server.GetAddress(connectionMode); HttpHeaders headers = new HttpHeaders(); headers.SetAccessToken(server.getAccessToken()); final HttpRequest request = new HttpRequest(); request.setUrl(url + "/emby/system/info?format=json"); request.setMethod("GET"); request.setRequestHeaders(headers); Response<String> stringResponse = new ValidateAuthenticationResponse(this, jsonSerializer, server, response, request, httpClient, url); httpClient.Send(request, stringResponse); } void TryConnect(String url, int timeout, final Response<PublicSystemInfo> response) { url += "/emby/system/info/public?format=json"; HttpRequest request = new HttpRequest(); request.setUrl(url); request.setMethod("GET"); request.setTimeout(timeout); httpClient.Send(request, new SerializedResponse<PublicSystemInfo>(response, jsonSerializer, PublicSystemInfo.class)); } protected ApiClient InstantiateApiClient(String serverAddress) { return new ApiClient(httpClient, jsonSerializer, logger, serverAddress, applicationName, applicationVersion, device, apiEventListener); } private ApiClient GetOrAddApiClient(ServerInfo server, ConnectionMode connectionMode) { ApiClient apiClient = apiClients.get(server.getId()); if (apiClient == null){ String address = server.GetAddress(connectionMode); apiClient = InstantiateApiClient(address); apiClients.put(server.getId(), apiClient); apiClient.getAuthenticatedObservable().addObserver(new AuthenticatedObserver(this, apiClient)); } if (tangible.DotNetToJavaStringHelper.isNullOrEmpty(server.getAccessToken())) { apiClient.ClearAuthenticationInfo(); } else { apiClient.SetAuthenticationInfo(server.getAccessToken(), server.getUserId()); } return apiClient; } void AfterConnected(ApiClient apiClient, ConnectionOptions options) { if (options.getReportCapabilities()){ apiClient.ReportCapabilities(clientCapabilities, new EmptyResponse()); } if (options.getEnableWebSocket()){ apiClient.ensureWebSocket(); } } void OnAuthenticated(final ApiClient apiClient, final AuthenticationResult result, ConnectionOptions options, final boolean saveCredentials) { logger.Debug("Updating credentials after local authentication"); ServerInfo server = apiClient.getServerInfo(); ServerCredentials credentials = credentialProvider.GetCredentials(); if (options.getUpdateDateLastAccessed()){ server.setDateLastAccessed(new Date()); } if (saveCredentials) { server.setUserId(result.getUser().getId()); server.setAccessToken(result.getAccessToken()); } else { server.setUserId(null); server.setAccessToken(null); } credentials.AddOrUpdateServer(server); SaveUserInfoIntoCredentials(server, result.getUser()); credentialProvider.SaveCredentials(credentials); AfterConnected(apiClient, options); OnLocalUserSignIn(result.getUser()); } private void SaveUserInfoIntoCredentials(ServerInfo server, UserDto user) { ServerUserInfo info = new ServerUserInfo(); info.setIsSignedInOffline(true); info.setId(user.getId()); // Record user info here server.AddOrUpdate(info); } void OnLocalUserSignIn(UserDto user) { // TODO: Fire event } void OnLocalUserSignout(ApiClient apiClient) { // TODO: Fire event } public void GetAvailableServers(final Response<ArrayList<ServerInfo>> response) { logger.Debug("Getting saved servers via credential provider"); ServerCredentials tempCredentials; try { tempCredentials = credentialProvider.GetCredentials(); } catch (Exception ex){ logger.ErrorException("Error getting available servers", ex); response.onResponse(new ArrayList<ServerInfo>()); return; } final ServerCredentials credentials = tempCredentials; final int numTasks = 2; final int[] numTasksCompleted = {0}; final ArrayList<ServerInfo> foundServers = new ArrayList<ServerInfo>(); final ArrayList<ServerInfo> connectServers = new ArrayList<ServerInfo>(); Response<ArrayList<ServerInfo>> findServersResponse = new FindServersResponse(this, credentials, foundServers, connectServers, numTasksCompleted, numTasks, response); logger.Debug("Scanning network for local servers"); FindServers(findServersResponse); EmptyResponse connectServersResponse = new GetConnectServersResponse(logger, connectService, credentials, foundServers, connectServers, numTasks, numTasksCompleted, response, this); if (!tangible.DotNetToJavaStringHelper.isNullOrEmpty(credentials.getConnectAccessToken())) { logger.Debug("Getting server list from Connect"); EnsureConnectUser(credentials, connectServersResponse); } else{ connectServersResponse.onError(null); } } void EnsureConnectUser(ServerCredentials credentials, final EmptyResponse response){ if (connectUser != null && StringHelper.EqualsIgnoreCase(connectUser.getId(), credentials.getConnectUserId())) { response.onResponse(); return; } if (!tangible.DotNetToJavaStringHelper.isNullOrEmpty(credentials.getConnectUserId()) && !tangible.DotNetToJavaStringHelper.isNullOrEmpty(credentials.getConnectAccessToken())) { this.connectUser = null; ConnectUserQuery query = new ConnectUserQuery(); query.setId(credentials.getConnectUserId()); connectService.GetConnectUser(query, credentials.getConnectAccessToken(), new GetConnectUserResponse(this, response)); } } void OnGetServerResponse(ServerCredentials credentials, ArrayList<ServerInfo> foundServers, ArrayList<ServerInfo> connectServers, Response<ArrayList<ServerInfo>> response){ for(ServerInfo newServer : foundServers){ if (tangible.DotNetToJavaStringHelper.isNullOrEmpty(newServer.getManualAddress())) { newServer.setLastConnectionMode(ConnectionMode.Local); } else { newServer.setLastConnectionMode(ConnectionMode.Manual); } credentials.AddOrUpdateServer(newServer); } for(ServerInfo newServer : connectServers){ credentials.AddOrUpdateServer(newServer); } ArrayList<ServerInfo> cleanList = new ArrayList<ServerInfo>(); ArrayList<ServerInfo> existing = credentials.getServers(); for(ServerInfo server : existing){ // It's not a connect server, so assume it's still valid if (tangible.DotNetToJavaStringHelper.isNullOrEmpty(server.getExchangeToken())) { cleanList.add(server); continue; } boolean found = false; for(ServerInfo connectServer : connectServers){ if (StringHelper.EqualsIgnoreCase(server.getId(), connectServer.getId())){ found = true; break; } } if (found) { cleanList.add(server); } else{ logger.Debug("Dropping server "+server.getName()+" - "+server.getId()+" because it's no longer in the user's Connect profile."); } } // Sort by last date accessed, descending Collections.sort(cleanList, new ServerInfoDateComparator()); Collections.reverse(cleanList); credentials.setServers(cleanList); credentialProvider.SaveCredentials(credentials); ArrayList<ServerInfo> clone = new ArrayList<ServerInfo>(); clone.addAll(credentials.getServers()); response.onResponse(clone); } protected void FindServers(final Response<ArrayList<ServerInfo>> response) { FindServersInternal(response); } protected void FindServersInternal(final Response<ArrayList<ServerInfo>> response) { serverDiscovery.FindServers(1000, new FindServersInnerResponse(this, response)); } void WakeAllServers() { logger.Debug("Waking all servers"); for(ServerInfo server : credentialProvider.GetCredentials().getServers()){ WakeServer(server, new EmptyResponse()); } } private void BeginWakeServer(final ServerInfo info) { Thread thread = new Thread(new BeginWakeServerRunnable(this, info)); thread.start(); } void WakeServer(ServerInfo info, final EmptyResponse response) { logger.Debug("Waking server: %s, Id: %s", info.getName(), info.getId()); ArrayList<WakeOnLanInfo> wakeList = info.getWakeOnLanInfos(); final int count = wakeList.size(); if (count == 0){ logger.Debug("Server %s has no saved wake on lan profiles", info.getName()); response.onResponse(); return; } final ArrayList<EmptyResponse> doneList = new ArrayList<EmptyResponse>(); for(WakeOnLanInfo wakeOnLanInfo : wakeList){ WakeServer(wakeOnLanInfo, new WakeServerResponse(doneList, response)); } } private void WakeServer(WakeOnLanInfo info, EmptyResponse response) { networkConnection.SendWakeOnLan(info.getMacAddress(), info.getPort(), response); } String NormalizeAddress(String address) throws IllegalArgumentException { if (tangible.DotNetToJavaStringHelper.isNullOrEmpty(address)) { throw new IllegalArgumentException("address"); } if (StringHelper.IndexOfIgnoreCase(address, "http") == -1) { address = "http://" + address; } return address; } private void LogoutAll(final EmptyResponse response){ Object[] clientList = apiClients.values().toArray(); final int count = clientList.length; if (count == 0){ response.onResponse(); return; } final ArrayList<Integer> doneList = new ArrayList<Integer>(); for(Object clientObj : clientList){ ApiClient client = (ApiClient)clientObj; ApiClientLogoutResponse logoutResponse = new ApiClientLogoutResponse(doneList, count, response, this, client); if (!tangible.DotNetToJavaStringHelper.isNullOrEmpty(client.getAccessToken())) { client.Logout(logoutResponse); } else { logoutResponse.onResponse(false); } } connectUser = null; } public void LoginToConnect(String username, String password, final EmptyResponse response) throws UnsupportedEncodingException, NoSuchAlgorithmException { connectService.Authenticate(username, password, new LoginToConnectResponse(this, credentialProvider, response)); } public void CreatePin(String deviceId, Response<PinCreationResult> response) { connectService.CreatePin(deviceId, response); } public void GetPinStatus(PinCreationResult pin, Response<PinStatusResult> response) { connectService.GetPinStatus(pin, response); } public void ExchangePin(PinCreationResult pin, final Response<PinExchangeResult> response) { connectService.ExchangePin(pin, new ExchangePinResponse(credentialProvider, response)); } public void GetRegistrationInfo(final String featureName, String serverId, String localUsername, final Response<RegistrationInfo> response) { connectService.GetRegistrationInfo(serverId, getDevice().getDeviceId(), localUsername, new EmptyResponse(){ @Override public void onResponse(){ RegistrationInfo reg = new RegistrationInfo(); reg.setName(featureName); reg.setIsTrial(false); reg.setIsRegistered(true); response.onResponse(reg); } @Override public void onError(Exception ex){ logger.ErrorException("Error in GetRegistrationInfo", ex); RegistrationInfo reg = new RegistrationInfo(); reg.setName(featureName); reg.setIsTrial(false); reg.setIsRegistered(false); if (ex instanceof HttpException){ HttpException httpException = (HttpException)ex; if (httpException.getStatusCode() != null){ if (httpException.getStatusCode() == 403){ reg.setIsOverLimit(true); } } } response.onResponse(reg); } }); } public void DeleteServer(final String id, final EmptyResponse response) { ServerCredentials credentials = credentialProvider.GetCredentials(); ArrayList<ServerInfo> existing = credentials.getServers(); ServerInfo server = null; for(ServerInfo current : existing){ if (StringHelper.EqualsIgnoreCase(current.getId(), id)){ server = current; break; } } if (server == null){ response.onResponse(); return; } if (tangible.DotNetToJavaStringHelper.isNullOrEmpty(server.getConnectServerId())) { OnServerDeleteResponse(id, response); return; } String connectUserId = credentials.getConnectUserId(); String connectAccessToken = credentials.getConnectAccessToken(); if (tangible.DotNetToJavaStringHelper.isNullOrEmpty(connectUserId) || tangible.DotNetToJavaStringHelper.isNullOrEmpty(connectAccessToken)) { OnServerDeleteResponse(id, response); return; } connectService.DeleteServer(connectUserId, connectAccessToken, server.getConnectServerId(), new EmptyResponse(response){ @Override public void onResponse(){ OnServerDeleteResponse(id, response); } }); } private void OnServerDeleteResponse(String id, EmptyResponse response){ ServerCredentials credentials = credentialProvider.GetCredentials(); ArrayList<ServerInfo> existing = credentials.getServers(); ArrayList<ServerInfo> newList = new ArrayList<>(); for(ServerInfo current : existing){ if (!StringHelper.EqualsIgnoreCase(current.getId(), id)){ newList.add(current); } } credentials.setServers(newList); credentialProvider.SaveCredentials(credentials); response.onResponse(); } }