// // Copyright (c) 2016 Couchbase, Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software distributed under the // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, // either express or implied. See the License for the specific language governing permissions // and limitations under the License. // package com.couchbase.lite.replicator; import com.couchbase.lite.LiteTestCaseWithDB; import com.couchbase.lite.mockserver.MockCheckpointPut; import com.couchbase.lite.mockserver.MockDispatcher; import com.couchbase.lite.mockserver.MockHelper; import com.couchbase.lite.mockserver.WrappedSmartMockResponse; import com.couchbase.lite.support.CouchbaseLiteHttpClientFactory; import com.couchbase.lite.support.PersistentCookieJar; import com.couchbase.lite.util.Utils; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import okhttp3.Response; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; public class RemoteRequestTest extends LiteTestCaseWithDB { /** * Make RemoteRequests will retry correctly. * <p/> * Return MAX_RETRIES - 1 503 transient errors, followed by a 404 non-transient error. * It should retry for the 503 errors and return after it gets the 404. */ public void testRetryLastRequestSuccess() throws Exception { // lower retry to speed up test com.couchbase.lite.replicator.RemoteRequestRetry.RETRY_DELAY_MS = 5; PersistentCookieJar cookieStore = database.getPersistentCookieStore(); CouchbaseLiteHttpClientFactory factory = new CouchbaseLiteHttpClientFactory(cookieStore); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); // respond with 503 error for the first MAX_RETRIES - 1 requests int num503Responses = com.couchbase.lite.replicator.RemoteRequestRetry.MAX_RETRIES - 1; for (int i = 0; i < num503Responses; i++) { dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, new MockResponse().setResponseCode(503)); } // on last request, respond with 404 error MockCheckpointPut mockCheckpointPut = new MockCheckpointPut(); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, mockCheckpointPut); try { server.start(); String urlString = String.format(Locale.ENGLISH, "%s/%s", server.url("/db").url(), "_local"); URL url = new URL(urlString); Map<String, Object> requestBody = new HashMap<String, Object>(); requestBody.put("foo", "bar"); Map<String, Object> requestHeaders = new HashMap<String, Object>(); final CountDownLatch received404Error = new CountDownLatch(1); RemoteRequestCompletion completionBlock = new RemoteRequestCompletion() { @Override public void onCompletion(Response httpResponse, Object result, Throwable e) { if (e instanceof RemoteRequestResponseException) { RemoteRequestResponseException htre = (RemoteRequestResponseException) e; if (htre.getCode() == 404) { received404Error.countDown(); } } } }; ScheduledExecutorService requestExecutorService = Executors.newScheduledThreadPool(5); ScheduledExecutorService workExecutorService = Executors.newSingleThreadScheduledExecutor(); RemoteRequestRetry request = new RemoteRequestRetry( RemoteRequestRetry.RemoteRequestType.REMOTE_REQUEST, requestExecutorService, workExecutorService, factory, "GET", url, true, true, requestBody, null, database, requestHeaders, completionBlock ); // wait for the future to return Future future = request.submit(); future.get(300, TimeUnit.SECONDS); // at this point, the completionBlock should have already been called back // with a 404 error, which will decrement countdown latch. boolean success = received404Error.await(1, TimeUnit.SECONDS); assertTrue(success); // make sure that we saw MAX_RETRIES requests sent to server for (int i = 0; i < com.couchbase.lite.replicator.RemoteRequestRetry.MAX_RETRIES; i++) { RecordedRequest recordedRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_CHECKPOINT); assertNotNull(recordedRequest); } // Note: ExecutorService should be called shutdown() Utils.shutdownAndAwaitTermination(requestExecutorService); Utils.shutdownAndAwaitTermination(workExecutorService); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } public void testRetryAllRequestsFail() throws Exception { // lower retry to speed up test com.couchbase.lite.replicator.RemoteRequestRetry.RETRY_DELAY_MS = 5; PersistentCookieJar cookieStore = database.getPersistentCookieStore(); CouchbaseLiteHttpClientFactory factory = new CouchbaseLiteHttpClientFactory(cookieStore); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); // respond with 503 error for all requests int num503Responses = com.couchbase.lite.replicator.RemoteRequestRetry.MAX_RETRIES + 1; for (int i = 0; i < num503Responses; i++) { dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, new MockResponse().setResponseCode(503)); } try { server.start(); String urlString = String.format(Locale.ENGLISH, "%s/%s", server.url("/db").url(), "_local"); URL url = new URL(urlString); Map<String, Object> requestBody = new HashMap<String, Object>(); requestBody.put("foo", "bar"); Map<String, Object> requestHeaders = new HashMap<String, Object>(); final CountDownLatch received503Error = new CountDownLatch(1); RemoteRequestCompletion completionBlock = new RemoteRequestCompletion() { @Override public void onCompletion(Response httpResponse, Object result, Throwable e) { if (e instanceof RemoteRequestResponseException) { RemoteRequestResponseException htre = (RemoteRequestResponseException) e; if (htre.getCode() == 503) { received503Error.countDown(); } } } }; ScheduledExecutorService requestExecutorService = Executors.newScheduledThreadPool(5); ScheduledExecutorService workExecutorService = Executors.newSingleThreadScheduledExecutor(); RemoteRequestRetry request = new RemoteRequestRetry( RemoteRequestRetry.RemoteRequestType.REMOTE_REQUEST, requestExecutorService, workExecutorService, factory, "GET", url, true, true, requestBody, null, database, requestHeaders, completionBlock ); // wait for the future to return Future future = request.submit(); future.get(300, TimeUnit.SECONDS); // at this point, the completionBlock should have already been called back // with a 404 error, which will decrement countdown latch. boolean success = received503Error.await(1, TimeUnit.SECONDS); assertTrue(success); // make sure that we saw MAX_RETRIES requests sent to server for (int i = 0; i < com.couchbase.lite.replicator.RemoteRequestRetry.MAX_RETRIES; i++) { RecordedRequest recordedRequest = dispatcher.takeRequest(MockHelper.PATH_REGEX_CHECKPOINT); assertNotNull(recordedRequest); } // Note: ExecutorService should be called shutdown() Utils.shutdownAndAwaitTermination(requestExecutorService); Utils.shutdownAndAwaitTermination(workExecutorService); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } /** * Reproduce a severe issue where the pusher stops working because it's remoteRequestExecutor * is full of tasks which are all blocked trying to add more tasks to the queue. */ public void testRetryQueueDeadlock() throws Exception { // lower retry to speed up test int ORG_RETRY_DELAY_MS = com.couchbase.lite.replicator.RemoteRequestRetry.RETRY_DELAY_MS; try { com.couchbase.lite.replicator.RemoteRequestRetry.RETRY_DELAY_MS = 5; PersistentCookieJar cookieStore = database.getPersistentCookieStore(); CouchbaseLiteHttpClientFactory factory = new CouchbaseLiteHttpClientFactory(cookieStore); // create mockwebserver and custom dispatcher MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getMockWebServer(dispatcher); dispatcher.setServerType(MockDispatcher.ServerType.SYNC_GW); try { int numRequests = 10; // NOTE: single MockResponse with multiple threading cause threading issue. // instead of using setSticky(true), create multiple MockResponse instance for request. // respond with 503 error for all requests for (int i = 0; i < numRequests * 5; i++) { MockResponse response = new MockResponse().setResponseCode(503); WrappedSmartMockResponse wrapped = new WrappedSmartMockResponse(response); wrapped.setDelayMs(5); dispatcher.enqueueResponse(MockHelper.PATH_REGEX_CHECKPOINT, wrapped); } server.start(); URL url = new URL(String.format(Locale.ENGLISH, "%s/%s", server.url("/db").url(), "_local")); Map<String, Object> requestBody = new HashMap<String, Object>(); requestBody.put("foo", "bar"); Map<String, Object> requestHeaders = new HashMap<String, Object>(); final CountDownLatch received503Error = new CountDownLatch(numRequests); RemoteRequestCompletion completionBlock = new RemoteRequestCompletion() { @Override public void onCompletion(Response httpResponse, Object result, Throwable e) { if (e instanceof RemoteRequestResponseException) { RemoteRequestResponseException htre = (RemoteRequestResponseException) e; if (htre.getCode() == 503) { received503Error.countDown(); } } } }; ScheduledExecutorService requestExecutorService = Executors.newScheduledThreadPool(5); ScheduledExecutorService workExecutorService = Executors.newSingleThreadScheduledExecutor(); List<Future> requestFutures = new ArrayList<Future>(); for (int i = 0; i < numRequests; i++) { RemoteRequestRetry request = new RemoteRequestRetry( RemoteRequestRetry.RemoteRequestType.REMOTE_REQUEST, requestExecutorService, workExecutorService, factory, "GET", url, true, true, requestBody, null, database, requestHeaders, completionBlock); Future future = request.submit(); requestFutures.add(future); } for (Future future : requestFutures) future.get(); boolean success = received503Error.await(120, TimeUnit.SECONDS); assertTrue(success); // Note: ExecutorService should be called shutdown() Utils.shutdownAndAwaitTermination(requestExecutorService); Utils.shutdownAndAwaitTermination(workExecutorService); } finally { assertTrue(MockHelper.shutdown(server, dispatcher)); } } finally { com.couchbase.lite.replicator.RemoteRequestRetry.RETRY_DELAY_MS = ORG_RETRY_DELAY_MS; } } /** * ReplicatorInternal_Tests.m * - (void) test22_ParseAuthChallenge */ public void testParseAuthHeader() { String authHeader = "OIDC login=\"http://127.0.0.1:4984/openid_db/_oidc_testing/authorize?client_id=sync_gateway&redirect_uri=http%3A%2F%2F127.0.0.1%3A4984%2Fopenid_db%2F_oidc_callback&response_type=code&scope=openid+email&state=\""; Map challenge = RemoteRequest.parseAuthHeader(authHeader); assertNotNull(challenge); assertEquals(3, challenge.size()); assertEquals(authHeader, challenge.get("WWW-Authenticate")); assertEquals("OIDC", challenge.get("Scheme")); assertTrue(challenge.containsKey("login")); assertEquals("http://127.0.0.1:4984/openid_db/_oidc_testing/authorize?client_id=sync_gateway&redirect_uri=http%3A%2F%2F127.0.0.1%3A4984%2Fopenid_db%2F_oidc_callback&response_type=code&scope=openid+email&state=", challenge.get("login")); authHeader = null; challenge = RemoteRequest.parseAuthHeader(authHeader); assertNull(challenge); authHeader = ""; challenge = RemoteRequest.parseAuthHeader(authHeader); assertNull(challenge); authHeader = "Basic realm=Couchbase"; challenge = RemoteRequest.parseAuthHeader(authHeader); assertNotNull(challenge); Map expect = new HashMap(); expect.put("WWW-Authenticate", authHeader); expect.put("Scheme", "Basic"); expect.put("realm", "Couchbase"); assertEquals(expect, challenge); authHeader = "OIDC login=\"http://example.com/login?foo=bar\""; challenge = RemoteRequest.parseAuthHeader(authHeader); assertNotNull(challenge); expect = new HashMap(); expect.put("WWW-Authenticate", authHeader); expect.put("Scheme", "OIDC"); expect.put("login", "http://example.com/login?foo=bar"); assertEquals(expect, challenge); authHeader = "OIDC login=\"http://example.com/login?foo=bar\",something=other"; challenge = RemoteRequest.parseAuthHeader(authHeader); assertNotNull(challenge); expect = new HashMap(); expect.put("WWW-Authenticate", authHeader); expect.put("Scheme", "OIDC"); expect.put("login", "http://example.com/login?foo=bar"); assertEquals(expect, challenge); } }