/* * matrix-java-sdk - Matrix Client SDK for Java * Copyright (C) 2017 Kamax Sarl * * https://www.kamax.io/ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package io.kamax.matrix.client; import com.google.gson.Gson; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import io.kamax.matrix.MatrixErrorInfo; import io.kamax.matrix._MatrixID; import io.kamax.matrix.hs._MatrixHomeserver; import io.kamax.matrix.json.GsonUtil; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URL; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.Objects; import java.util.Optional; import okhttp3.*; public abstract class AMatrixHttpClient implements _MatrixClientRaw { private Logger log = LoggerFactory.getLogger(AMatrixHttpClient.class); protected MatrixClientContext context; protected Gson gson = GsonUtil.get(); protected JsonParser jsonParser = new JsonParser(); private OkHttpClient client; public AMatrixHttpClient(String domain) { this(new MatrixClientContext().setDomain(domain)); } public AMatrixHttpClient(URL hsBaseUrl) { this(new MatrixClientContext().setHsBaseUrl(hsBaseUrl)); } protected AMatrixHttpClient(MatrixClientContext context) { this(context, new OkHttpClient.Builder(), new MatrixClientDefaults()); } protected AMatrixHttpClient(MatrixClientContext context, OkHttpClient.Builder client) { this(context, client, new MatrixClientDefaults()); } protected AMatrixHttpClient(MatrixClientContext context, OkHttpClient.Builder client, MatrixClientDefaults defaults) { this(context, client.connectTimeout(defaults.getConnectTimeout(), TimeUnit.MILLISECONDS) .readTimeout(5, TimeUnit.MINUTES).followRedirects(false).build()); } protected AMatrixHttpClient(MatrixClientContext context, OkHttpClient client) { this.context = context; this.client = client; } @Override public Optional<_AutoDiscoverySettings> discoverSettings() { if (StringUtils.isBlank(context.getDomain())) { throw new IllegalStateException("A non-empty Matrix domain must be set to discover the client settings"); } String hostname = context.getDomain().split(":")[0]; log.info("Performing .well-known auto-discovery for {}", hostname); URL url = new HttpUrl.Builder().scheme("https").host(hostname).addPathSegments(".well-known/matrix/client") .build().url(); String body = execute(new MatrixHttpRequest(new Request.Builder().get().url(url)).addIgnoredErrorCode(404)); if (StringUtils.isBlank(body)) { if (Objects.isNull(context.getHsBaseUrl())) { throw new IllegalStateException("No valid Homeserver base URL was found"); } // No .well-known data found // FIXME improve SDK so we can differentiate between not found and empty. // not found = skip // empty = failure return Optional.empty(); } log.info("Found body: {}", body); WellKnownAutoDiscoverySettings settings = new WellKnownAutoDiscoverySettings(body); log.info("Found .well-known data"); // TODO reconsider if and where we should check for an already present HS url in the context if (settings.getHsBaseUrls().isEmpty()) { throw new IllegalStateException("No valid Homeserver base URL was found"); } for (URL baseUrlCandidate : settings.getHsBaseUrls()) { context.setHsBaseUrl(baseUrlCandidate); try { if (!getHomeApiVersions().isEmpty()) { log.info("Found a valid HS at {}", getContext().getHsBaseUrl().toString()); break; } } catch (MatrixClientRequestException e) { log.warn("Error when trying to fetch {}: {}", baseUrlCandidate, e.getMessage()); } } for (URL baseUrlCandidate : settings.getIsBaseUrls()) { context.setIsBaseUrl(baseUrlCandidate); try { if (validateIsBaseUrl()) { log.info("Found a valid IS at {}", getContext().getIsBaseUrl().toString()); break; } } catch (MatrixClientRequestException e) { log.warn("Error when trying to fetch {}: {}", baseUrlCandidate, e.getMessage()); } } return Optional.of(settings); } @Override public MatrixClientContext getContext() { return context; } @Override public _MatrixHomeserver getHomeserver() { return context.getHomeserver(); } @Override public Optional getAccessToken() { return Optional.ofNullable(context.getToken()); } public String getAccessTokenOrThrow() { return getAccessToken() .orElseThrow(() -> new IllegalStateException("This method can only be used with a valid token.")); } @Override public List getHomeApiVersions() { String body = execute(new Request.Builder().get().url(getPath("client", "", "versions"))); return GsonUtil.asList(GsonUtil.parseObj(body), "versions", String.class); } @Override public boolean validateIsBaseUrl() { String body = execute(new Request.Builder().get().url(getIdentityPath("identity", "api", "v1"))); return "{}".equals(body); } protected String getUserId() { return getUser().orElseThrow(IllegalStateException::new).getId(); } @Override public Optional<_MatrixID> getUser() { return context.getUser(); } protected Request.Builder addAuthHeader(Request.Builder builder, String token) { builder.addHeader("Authorization", "Bearer " + token); return builder; } protected Request.Builder addAuthHeader(Request.Builder builder) { return addAuthHeader(builder, getAccessTokenOrThrow()); } protected String executeAuthenticated(Request.Builder builder, String token) { return execute(addAuthHeader(builder, token)); } protected String executeAuthenticated(Request.Builder builder) { return execute(addAuthHeader(builder)); } protected String executeAuthenticated(MatrixHttpRequest matrixRequest) { addAuthHeader(matrixRequest.getHttpRequest()); return execute(matrixRequest); } protected String execute(Request.Builder builder) { return execute(new MatrixHttpRequest(builder)); } protected String execute(MatrixHttpRequest matrixRequest) { log(matrixRequest.getHttpRequest()); try (Response response = client.newCall(matrixRequest.getHttpRequest().build()).execute()) { String body = response.body().string(); int responseStatus = response.code(); if (responseStatus == 200) { log.debug("Request successfully executed."); } else if (matrixRequest.getIgnoredErrorCodes().contains(responseStatus)) { log.debug("Error code ignored: " + responseStatus); return ""; } else { MatrixErrorInfo info = createErrorInfo(body, responseStatus); body = handleError(matrixRequest, responseStatus, info); } return body; } catch (IOException e) { throw new MatrixClientRequestException(e); } } protected MatrixHttpContentResult executeContentRequest(MatrixHttpRequest matrixRequest) { log(matrixRequest.getHttpRequest()); try (Response response = client.newCall(matrixRequest.getHttpRequest().build()).execute()) { int responseStatus = response.code(); MatrixHttpContentResult result; if (responseStatus == 200 || matrixRequest.getIgnoredErrorCodes().contains(responseStatus)) { log.debug("Request successfully executed."); result = new MatrixHttpContentResult(response); } else { String body = response.body().string(); MatrixErrorInfo info = createErrorInfo(body, responseStatus); result = handleErrorContentRequest(matrixRequest, responseStatus, info); } return result; } catch (IOException e) { throw new MatrixClientRequestException(e); } } /** * Default handling of errors. Can be overwritten by a custom implementation in inherited classes. * * @param matrixRequest * @param responseStatus * @param info * @return body of the response of a repeated call of the request, else this methods throws a * MatrixClientRequestException */ protected String handleError(MatrixHttpRequest matrixRequest, int responseStatus, MatrixErrorInfo info) { String message = String.format("Request failed: %s", responseStatus); if (Objects.nonNull(info)) { message = String.format("%s - %s - %s", message, info.getErrcode(), info.getError()); } if (responseStatus == 429) { return handleRateLimited(matrixRequest, info); } throw new MatrixClientRequestException(info, message); } /** * Default handling of rate limited calls. Can be overwritten by a custom implementation in inherited classes. * * @param matrixRequest * @param info * @return body of the response of a repeated call of the request, else this methods throws a * MatrixClientRequestException */ protected String handleRateLimited(MatrixHttpRequest matrixRequest, MatrixErrorInfo info) { throw new MatrixClientRequestException(info, "Request was rate limited."); // TODO Add default handling of rate limited call, i.e. repeated call after given time interval. // 1. Wait for timeout // 2. return execute(request) } protected MatrixHttpContentResult handleErrorContentRequest(MatrixHttpRequest matrixRequest, int responseStatus, MatrixErrorInfo info) { String message = String.format("Request failed with status code: %s", responseStatus); if (responseStatus == 429) { return handleRateLimitedContentRequest(matrixRequest, info); } throw new MatrixClientRequestException(info, message); } protected MatrixHttpContentResult handleRateLimitedContentRequest(MatrixHttpRequest matrixRequest, MatrixErrorInfo info) { throw new MatrixClientRequestRateLimitedException(info, "Request was rate limited."); // TODO Add default handling of rate limited call, i.e. repeated call after given time interval. // 1. Wait for timeout // 2. return execute(request) } protected Optional extractAsStringFromBody(String body, String jsonObjectName) { if (StringUtils.isEmpty(body)) { return Optional.empty(); } return GsonUtil.findString(jsonParser.parse(body).getAsJsonObject(), jsonObjectName); } private MatrixErrorInfo createErrorInfo(String body, int responseStatus) { MatrixErrorInfo info = null; try { info = gson.fromJson(body, MatrixErrorInfo.class); if (Objects.nonNull(info)) { log.debug("Request returned with an error. Status code: {}, errcode: {}, error: {}", responseStatus, info.getErrcode(), info.getError()); } } catch (JsonSyntaxException e) { log.debug("Unable to parse Matrix error info. Content was:\n{}", body); } return info; } private void log(Request.Builder req) { log.debug("Doing {} {}", req, req.toString()); } protected HttpUrl.Builder getHsBaseUrl() { return HttpUrl.get(context.getHsBaseUrl()).newBuilder(); } protected HttpUrl.Builder getIsBaseUrl() { return HttpUrl.get(context.getIsBaseUrl()).newBuilder(); } protected HttpUrl.Builder getPathBuilder(HttpUrl.Builder base, String... segments) { base.addPathSegment("_matrix"); for (String segment : segments) { base.addPathSegment(segment); } if (context.isVirtual()) { context.getUser().ifPresent(user -> base.addQueryParameter("user_id", user.getId())); } return base; } protected HttpUrl.Builder getPathBuilder(String... segments) { return getPathBuilder(getHsBaseUrl(), segments); } protected HttpUrl.Builder getIdentityPathBuilder(String... segments) { return getPathBuilder(getIsBaseUrl(), segments); } protected URL getPath(String... segments) { return getPathBuilder(segments).build().url(); } protected URL getIdentityPath(String... segments) { return getIdentityPathBuilder(segments).build().url(); } protected HttpUrl.Builder getClientPathBuilder(String... segments) { String[] base = { "client", "v3" }; segments = ArrayUtils.addAll(base, segments); return getPathBuilder(segments); } protected HttpUrl.Builder getMediaPathBuilder(String... segments) { String[] base = { "media", "v3" }; segments = ArrayUtils.addAll(base, segments); return getPathBuilder(segments); } protected URL getClientPath(String... segments) { return getClientPathBuilder(segments).build().url(); } protected URL getMediaPath(String... segments) { return getMediaPathBuilder(segments).build().url(); } protected RequestBody getJsonBody(Object o) { return RequestBody.create(MediaType.parse("application/json"), GsonUtil.get().toJson(o)); } protected Request.Builder request(URL url) { return new Request.Builder().url(url); } protected Request.Builder getRequest(URL url) { return request(url).get(); } }