diff options
-rw-r--r-- | pom.xml | 12 | ||||
-rw-r--r-- | src/main/java/com/juick/ActivityPubManager.java | 6 | ||||
-rw-r--r-- | src/main/java/com/juick/config/ActivityPubClientConfig.java | 78 | ||||
-rw-r--r-- | src/main/java/com/juick/config/ActivityPubClientErrorHandler.java | 42 | ||||
-rw-r--r-- | src/main/java/com/juick/config/AppConfig.java | 4 | ||||
-rw-r--r-- | src/main/java/com/juick/config/HttpClientConfig.java | 113 | ||||
-rw-r--r-- | src/main/java/com/juick/service/ActivityPubService.java | 75 | ||||
-rw-r--r-- | src/main/java/com/juick/service/WebfingerService.java | 82 | ||||
-rw-r--r-- | src/main/java/com/juick/util/ActivityPubRequestInterceptor.java | 22 | ||||
-rw-r--r-- | src/main/java/com/juick/www/api/activity/Profile.java | 222 | ||||
-rw-r--r-- | src/main/java/com/juick/www/api/webfinger/Resource.java | 3 | ||||
-rw-r--r-- | src/test/java/com/juick/server/tests/ServerTests.java | 186 |
12 files changed, 275 insertions, 570 deletions
@@ -82,8 +82,16 @@ </exclusions> </dependency> <dependency> - <groupId>org.apache.httpcomponents.client5</groupId> - <artifactId>httpclient5</artifactId> + <groupId>com.squareup.okhttp3</groupId> + <artifactId>okhttp</artifactId> + </dependency> + <dependency> + <groupId>com.squareup.okhttp3</groupId> + <artifactId>logging-interceptor</artifactId> + </dependency> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> diff --git a/src/main/java/com/juick/ActivityPubManager.java b/src/main/java/com/juick/ActivityPubManager.java index e9a04197..922ab002 100644 --- a/src/main/java/com/juick/ActivityPubManager.java +++ b/src/main/java/com/juick/ActivityPubManager.java @@ -97,7 +97,7 @@ public class ActivityPubManager implements ActivityListener, NotificationListene activityPubService.post(me, follower, accept); socialService.addFollower(followedUser, follower.getId()); logger.info("Follower added for {}", followedUser.getName()); - } catch (IOException | NoSuchAlgorithmException e) { + } catch (InterruptedException | IOException | NoSuchAlgorithmException e) { logger.info("activitypub exception", e); } } @@ -137,7 +137,7 @@ public class ActivityPubManager implements ActivityListener, NotificationListene delete.setObject(note); logger.info("Deletion to follower {}", follower.getId()); activityPubService.post(me, follower, delete); - } catch (IOException | NoSuchAlgorithmException e) { + } catch (InterruptedException | IOException | NoSuchAlgorithmException e) { logger.warn("activitypub exception", e); } catch (NoSuchElementException ex) { logger.warn("Can not find {}", acct); @@ -267,7 +267,7 @@ public class ActivityPubManager implements ActivityListener, NotificationListene create.setObject(note); try { activityPubService.post(me, to, create); - } catch (IOException | NoSuchAlgorithmException e) { + } catch (InterruptedException | IOException | NoSuchAlgorithmException e) { logger.warn("Delivery to {} failed: {}", to, e.getMessage()); } } else { diff --git a/src/main/java/com/juick/config/ActivityPubClientConfig.java b/src/main/java/com/juick/config/ActivityPubClientConfig.java deleted file mode 100644 index 7df8350e..00000000 --- a/src/main/java/com/juick/config/ActivityPubClientConfig.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2008-2023, Juick - * - * 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 <http://www.gnu.org/licenses/>. - */ - -package com.juick.config; - -import java.nio.charset.StandardCharsets; - -import javax.inject.Inject; - -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.convert.ConversionService; -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.http.converter.StringHttpMessageConverter; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.web.client.RestTemplate; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.juick.model.User; -import com.juick.service.SignatureService; -import com.juick.util.ActivityPubRequestInterceptor; -import com.juick.www.api.activity.model.objects.Actor; - -@Configuration -public class ActivityPubClientConfig { - @Inject - CloseableHttpClient httpClient; - @Inject - ObjectMapper jsonMapper; - @Inject - private ActivityPubClientErrorHandler activityPubClientErrorHandler; - @Inject - private SignatureService signatureService; - @Inject - private ConversionService conversionService; - @Inject - private User serviceUser; - - @Bean - ClientHttpRequestFactory clientHttpRequestFactory() { - var clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(); - clientHttpRequestFactory.setHttpClient(httpClient); - return clientHttpRequestFactory; - } - - @Bean - MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() { - MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); - converter.setObjectMapper(jsonMapper); - return converter; - } - - @Bean - RestTemplate restClient() { - RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory()); - restTemplate.getMessageConverters().add(0, mappingJacksonHttpMessageConverter()); - restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8)); - restTemplate.setErrorHandler(activityPubClientErrorHandler); - restTemplate.getInterceptors().add(new ActivityPubRequestInterceptor()); - return restTemplate; - } -} diff --git a/src/main/java/com/juick/config/ActivityPubClientErrorHandler.java b/src/main/java/com/juick/config/ActivityPubClientErrorHandler.java index 08b98c82..65b67110 100644 --- a/src/main/java/com/juick/config/ActivityPubClientErrorHandler.java +++ b/src/main/java/com/juick/config/ActivityPubClientErrorHandler.java @@ -18,33 +18,39 @@ package com.juick.config; import com.juick.service.activities.DeleteUserEvent; +import okhttp3.Interceptor; +import okhttp3.Response; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.stereotype.Component; -import org.springframework.web.client.DefaultResponseErrorHandler; -import javax.inject.Inject; import java.io.IOException; -import java.net.URI; -@Component -public class ActivityPubClientErrorHandler extends DefaultResponseErrorHandler { +public class ActivityPubClientErrorHandler implements Interceptor { private static final Logger logger = LoggerFactory.getLogger("ActivityPub"); - @Inject - private ApplicationEventPublisher applicationEventPublisher; + private final ApplicationEventPublisher applicationEventPublisher; + + public ActivityPubClientErrorHandler(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + @NotNull @Override - public void handleError(URI contextUri, HttpMethod method, ClientHttpResponse response) - throws IOException { - if (response.getStatusCode().equals(HttpStatus.GONE)) { - logger.warn("Server report {} is gone, deleting", contextUri.toASCIIString()); - applicationEventPublisher.publishEvent(new DeleteUserEvent(this, contextUri.toASCIIString())); - } else { - logger.warn("HTTP ERROR {} on {} : {}", response.getStatusCode().value(), - contextUri.toASCIIString(), response.getStatusText()); + public Response intercept(@NotNull Interceptor.Chain chain) throws IOException { + var request = chain.request(); + var response = chain.proceed(request); + var url = request.url(); + if (!response.isSuccessful()) { + if (response.code() == HttpStatus.GONE.value()) { + logger.warn("Server report {} is gone, deleting", url); + applicationEventPublisher.publishEvent(new DeleteUserEvent(this, url.toString())); + } else { + logger.warn("HTTP ERROR {} on {} : {}", response.code(), + url, response.body() != null ? response.body().string() : ""); + } } + return response; } } diff --git a/src/main/java/com/juick/config/AppConfig.java b/src/main/java/com/juick/config/AppConfig.java index c579c64c..32926bd1 100644 --- a/src/main/java/com/juick/config/AppConfig.java +++ b/src/main/java/com/juick/config/AppConfig.java @@ -34,7 +34,6 @@ import io.pebbletemplates.pebble.loader.Loader; import io.pebbletemplates.spring.extension.SpringExtension; import io.pebbletemplates.spring.servlet.PebbleViewResolver; -import org.apache.commons.codec.CharEncoding; import org.commonmark.ext.autolink.AutolinkExtension; import org.commonmark.node.Link; import org.commonmark.parser.Parser; @@ -49,6 +48,7 @@ import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; import org.springframework.web.servlet.resource.ResourceUrlProvider; +import java.nio.charset.StandardCharsets; import java.util.Collections; import javax.inject.Inject; @@ -173,7 +173,7 @@ public class AppConfig { PebbleViewResolver viewResolver = new PebbleViewResolver(pebbleEngine()); viewResolver.setPrefix("templates"); viewResolver.setSuffix(".html"); - viewResolver.setCharacterEncoding(CharEncoding.UTF_8); + viewResolver.setCharacterEncoding(StandardCharsets.UTF_8.name()); viewResolver.setExposeRequestAttributes(true); return viewResolver; } diff --git a/src/main/java/com/juick/config/HttpClientConfig.java b/src/main/java/com/juick/config/HttpClientConfig.java index 081e51dd..18b56293 100644 --- a/src/main/java/com/juick/config/HttpClientConfig.java +++ b/src/main/java/com/juick/config/HttpClientConfig.java @@ -17,110 +17,29 @@ package com.juick.config; -import java.util.concurrent.TimeUnit; - -import org.apache.hc.client5.http.ConnectionKeepAliveStrategy; -import org.apache.hc.client5.http.config.RequestConfig; -import org.apache.hc.client5.http.cookie.StandardCookieSpec; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; -import org.apache.hc.core5.http.HeaderElement; -import org.apache.hc.core5.http.HttpResponse; -import org.apache.hc.core5.http.message.BasicHeaderElementIterator; -import org.apache.hc.core5.http.protocol.HttpContext; -import org.apache.hc.core5.util.TimeValue; -import org.apache.hc.core5.util.Timeout; +import com.juick.util.ActivityPubRequestInterceptor; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.Scheduled; - -/** - * - Uses a connection pool to re-use connections and save overhead of creating connections. - * - Has a custom connection keep-alive strategy (to apply a default keep-alive if one isn't specified) - * - Starts an idle connection monitor to continuously clean up stale connections. - */ -@Configuration -public class HttpClientConfig { - - private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientConfig.class); - - // Determines the timeout in milliseconds until a connection is established. - private static final int CONNECT_TIMEOUT = 30000; - - // The timeout when requesting a connection from the connection manager. - private static final int REQUEST_TIMEOUT = 30000; - - // The timeout for waiting for data - private static final int SOCKET_TIMEOUT = 60000; - - private static final int MAX_TOTAL_CONNECTIONS = 50; - private static final int DEFAULT_KEEP_ALIVE_TIME_MILLIS = 20 * 1000; - private static final int CLOSE_IDLE_CONNECTION_WAIT_TIME_SECS = 30; - - @Bean - PoolingHttpClientConnectionManager poolingConnectionManager() { - PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager(); - poolingConnectionManager.setMaxTotal(MAX_TOTAL_CONNECTIONS); - return poolingConnectionManager; - } - - @Bean - ConnectionKeepAliveStrategy connectionKeepAliveStrategy() { - return new ConnectionKeepAliveStrategy() { - @Override - public TimeValue getKeepAliveDuration(HttpResponse response, HttpContext context) { - BasicHeaderElementIterator it = new BasicHeaderElementIterator - (response.headerIterator("Keep-Alive")); - while (it.hasNext()) { - HeaderElement he = it.next(); - String param = he.getName(); - String value = he.getValue(); - if (value != null && param.equalsIgnoreCase("timeout")) { - return TimeValue.of(Long.parseLong(value) * 1000, TimeUnit.MILLISECONDS); - } - } - return TimeValue.of(DEFAULT_KEEP_ALIVE_TIME_MILLIS, TimeUnit.MILLISECONDS); - } - }; - } +import javax.inject.Inject; +@Configuration +public class HttpClientConfig { + private final static Logger logger = LoggerFactory.getLogger("ActivityPub"); + @Inject + ApplicationEventPublisher applicationEventPublisher; @Bean - CloseableHttpClient httpClient() { - RequestConfig requestConfig = RequestConfig.custom() - .setCookieSpec(StandardCookieSpec.IGNORE) - .setConnectionRequestTimeout(Timeout.of(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS)) - .setConnectTimeout(Timeout.of(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)) - .setResponseTimeout(Timeout.of(SOCKET_TIMEOUT, TimeUnit.MILLISECONDS)).build(); - - return HttpClients.custom() - .setDefaultRequestConfig(requestConfig) - .setConnectionManager(poolingConnectionManager()) - .setKeepAliveStrategy(connectionKeepAliveStrategy()) + public OkHttpClient httpClient() { + return new OkHttpClient.Builder() + .addInterceptor(new HttpLoggingInterceptor(logger::debug) + .setLevel(HttpLoggingInterceptor.Level.HEADERS)) + .addInterceptor(new ActivityPubRequestInterceptor()) + .addInterceptor(new ActivityPubClientErrorHandler(applicationEventPublisher)) .build(); } - - @Bean - Runnable idleConnectionMonitor(final PoolingHttpClientConnectionManager connectionManager) { - return new Runnable() { - @Override - @Scheduled(fixedDelay = 10000) - public void run() { - try { - if (connectionManager != null) { - LOGGER.trace("run IdleConnectionMonitor - Closing expired and idle connections..."); - connectionManager.closeExpired(); - connectionManager.closeIdle(TimeValue.of(CLOSE_IDLE_CONNECTION_WAIT_TIME_SECS, TimeUnit.SECONDS)); - } else { - LOGGER.trace("run IdleConnectionMonitor - Http Client Connection manager is not initialised"); - } - } catch (Exception e) { - LOGGER.error("run IdleConnectionMonitor - Exception occurred. msg={}, e={}", e.getMessage(), e); - } - } - }; - } } diff --git a/src/main/java/com/juick/service/ActivityPubService.java b/src/main/java/com/juick/service/ActivityPubService.java index 9b93cab2..7eff123f 100644 --- a/src/main/java/com/juick/service/ActivityPubService.java +++ b/src/main/java/com/juick/service/ActivityPubService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008-2020, Juick + * Copyright (C) 2008-2023, Juick * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -17,16 +17,18 @@ package com.juick.service; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.juick.KeystoreManager; +import com.juick.model.AnonymousUser; import com.juick.model.User; import com.juick.service.activities.DeleteUserEvent; import com.juick.util.DateFormattersHolder; import com.juick.www.api.activity.model.Context; import com.juick.www.api.activity.model.objects.Actor; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.juick.KeystoreManager; -import com.juick.model.AnonymousUser; - -import org.apache.commons.codec.binary.Base64; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,20 +37,18 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.convert.ConversionService; import org.springframework.dao.DuplicateKeyException; -import org.springframework.http.*; +import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; +import org.tomitribe.auth.signatures.Base64; import org.tomitribe.auth.signatures.MissingRequiredHeaderException; import org.tomitribe.auth.signatures.Signature; import org.tomitribe.auth.signatures.Verifier; import javax.annotation.Nonnull; import javax.inject.Inject; - import java.io.IOException; import java.net.URI; import java.security.Key; @@ -56,7 +56,6 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; import java.time.Instant; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -71,7 +70,7 @@ public class ActivityPubService extends BaseJdbcService implements SocialService @Inject private UserService userService; @Inject - private RestTemplate restClient; + private OkHttpClient httpClient; @Inject private ObjectMapper jsonMapper; @Inject @@ -138,28 +137,26 @@ public class ActivityPubService extends BaseJdbcService implements SocialService try { String signatureString = signatureService.addSignature(from, host, "get", contextUri.getPath(), requestDate, ""); - HttpHeaders requestHeaders = new HttpHeaders(); - requestHeaders.add(HttpHeaders.DATE, requestDate); - requestHeaders.add(HttpHeaders.HOST, host); - requestHeaders.add("Signature", signatureString); - requestHeaders.setAccept(Collections.singletonList(MediaType.valueOf(Context.ACTIVITY_MEDIA_TYPE))); - HttpEntity<Void> activityRequest = new HttpEntity<>(requestHeaders); - var response = restClient.exchange(contextUri, HttpMethod.GET, activityRequest, Context.class); - if (response.getStatusCode().is2xxSuccessful()) { - var context = response.getBody(); - if (context == null) { - logger.warn("Cannot identify {}", contextUri); - return Optional.empty(); + var request = new Request.Builder() + .url(contextUri.toURL()) + .addHeader(HttpHeaders.DATE, requestDate) + .addHeader(HttpHeaders.HOST, host) + .addHeader("Signature", signatureString) + .addHeader(HttpHeaders.ACCEPT, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE) + .build(); + try (var response = httpClient.newCall(request).execute()) { + if (response.isSuccessful() && response.body() != null) { + var context = jsonMapper.readValue(response.body().string(), Context.class); + return Optional.of(context); } - return Optional.of(context); } - } catch (IOException | RestClientException e) { + } catch (IOException e) { logger.warn("HTTP Signature exception: {}", e.getMessage()); } return Optional.empty(); } - public HttpStatusCode post(Actor from, Actor to, Context data) throws IOException, NoSuchAlgorithmException { + public int post(Actor from, Actor to, Context data) throws IOException, NoSuchAlgorithmException, InterruptedException { UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(to.getInbox()); URI inbox = uriComponentsBuilder.build().toUri(); Instant now = Instant.now(); @@ -171,18 +168,20 @@ public class ActivityPubService extends BaseJdbcService implements SocialService final String digestHeader = "SHA-256=" + new String(Base64.encodeBase64(digest)); String signatureString = signatureService.addSignature(from, host, "post", inbox.getPath(), requestDate, digestHeader); - - HttpHeaders requestHeaders = new HttpHeaders(); - requestHeaders.add(HttpHeaders.CONTENT_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE); - requestHeaders.add(HttpHeaders.DATE, requestDate); - requestHeaders.add(HttpHeaders.HOST, host); - requestHeaders.add("Digest", digestHeader); - requestHeaders.add("Signature", signatureString); - HttpEntity<String> request = new HttpEntity<>(payload, requestHeaders); + var body = RequestBody.create(payload, MediaType.get(Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE)); + var request = new Request.Builder() + .url(inbox.toASCIIString()) + .post(body) + .addHeader(HttpHeaders.DATE, requestDate) + .addHeader(HttpHeaders.CONTENT_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE) + .addHeader("Digest", digestHeader) + .addHeader("Signature", signatureString) + .build(); logger.debug("Sending context to {}: {}", to.getId(), payload); - ResponseEntity<String> response = restClient.postForEntity(inbox, request, String.class); - logger.debug("Remote response: {} {}", response.getStatusCode(), response.getBody()); - return response.getStatusCode(); + try (var response = httpClient.newCall(request).execute()) { + logger.debug("Remote response: {} {}", response.code(), response.body()); + return response.code(); + } } public User verifyActor(String method, String path, Map<String, String> headers) { diff --git a/src/main/java/com/juick/service/WebfingerService.java b/src/main/java/com/juick/service/WebfingerService.java index dc978763..ca49bd51 100644 --- a/src/main/java/com/juick/service/WebfingerService.java +++ b/src/main/java/com/juick/service/WebfingerService.java @@ -1,57 +1,73 @@ -package com.juick.service; +/* + * Copyright (C) 2008-2023, Juick + * + * 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 <http://www.gnu.org/licenses/>. + */ -import java.net.URI; -import java.util.Collections; - -import javax.inject.Inject; +package com.juick.service; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.juick.www.api.webfinger.Resource; +import com.juick.www.api.webfinger.model.Account; +import com.juick.www.api.webfinger.model.Link; +import okhttp3.OkHttpClient; +import okhttp3.Request; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; - -import com.juick.www.api.webfinger.model.Account; -import com.juick.www.api.webfinger.model.Link; - import rocks.xmpp.addr.Jid; +import javax.inject.Inject; +import java.io.IOException; +import java.net.URI; +import java.util.Collections; + @Component public class WebfingerService { private static final Logger logger = LoggerFactory.getLogger("ActivityPub"); - - private final RestTemplate restClient; + private final OkHttpClient httpClient; + private final ObjectMapper jsonMapper; @Inject - public WebfingerService(final RestTemplate restClient) { - this.restClient = restClient; + public WebfingerService(final OkHttpClient httpClient, final ObjectMapper jsonMapper) { + this.httpClient = httpClient; + this.jsonMapper = jsonMapper; } public URI discoverAccountURI(String acct, MediaType linkType) { Jid acctId = Jid.of(acct); - URI resourceUri = UriComponentsBuilder.fromPath("/.well-known/webfinger").host(acctId.getDomain()) - .scheme("https").queryParam("resource", "acct:" + acct).build().toUri(); - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.valueOf("application/jrd+json"))); - HttpEntity<Void> webfingerRequest = new HttpEntity<>(headers); + var resourceUri = UriComponentsBuilder.fromPath("/.well-known/webfinger").host(acctId.getDomain()) + .scheme("https").queryParam("resource", "acct:" + acct).build().toUriString(); + var request = new Request.Builder() + .url(resourceUri) + .addHeader(HttpHeaders.ACCEPT, Resource.MEDIA_TYPE) + .build(); try { - ResponseEntity<Account> response = restClient.exchange(resourceUri, HttpMethod.GET, webfingerRequest, - Account.class); - if (response.getStatusCode().is2xxSuccessful()) { - var account = response.getBody(); - for (Link l : account.links()) { - if (l.rel().equals("self") && l.type().equals(linkType.toString())) { - return URI.create(l.href()); + try(var response = httpClient.newCall(request).execute()) { + if (response.isSuccessful() && response.body() != null) { + var account = jsonMapper.readValue(response.body().string(), Account.class); + for (Link l : account.links()) { + if (l.rel().equals("self") && l.type().equals(linkType.toString())) { + return URI.create(l.href()); + } } } } - } catch (RestClientException e) { - logger.warn("Cannot discover person {}: {}", acct, e.getMessage()); + } catch (IOException e) { + return URI.create(StringUtils.EMPTY); } return URI.create(StringUtils.EMPTY); } diff --git a/src/main/java/com/juick/util/ActivityPubRequestInterceptor.java b/src/main/java/com/juick/util/ActivityPubRequestInterceptor.java index 138a3d09..65720910 100644 --- a/src/main/java/com/juick/util/ActivityPubRequestInterceptor.java +++ b/src/main/java/com/juick/util/ActivityPubRequestInterceptor.java @@ -17,21 +17,21 @@ package com.juick.util; +import okhttp3.Interceptor; +import okhttp3.Response; +import org.jetbrains.annotations.NotNull; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpRequest; -import org.springframework.http.client.ClientHttpRequestExecution; -import org.springframework.http.client.ClientHttpRequestInterceptor; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.lang.NonNull; import java.io.IOException; -public class ActivityPubRequestInterceptor implements ClientHttpRequestInterceptor { - +public class ActivityPubRequestInterceptor implements Interceptor { + @NotNull @Override - public @NonNull ClientHttpResponse intercept(HttpRequest request, @NonNull byte[] body, - ClientHttpRequestExecution execution) throws IOException { - request.getHeaders().set(HttpHeaders.USER_AGENT, "Juick/2.x"); - return execution.execute(request, body); + public Response intercept(@NotNull Interceptor.Chain chain) throws IOException { + var original = chain.request(); + var request = original.newBuilder() + .addHeader(HttpHeaders.USER_AGENT, "Juick/2.x") + .method(original.method(), original.body()); + return chain.proceed(request.build()); } } diff --git a/src/main/java/com/juick/www/api/activity/Profile.java b/src/main/java/com/juick/www/api/activity/Profile.java index 19a28a39..7b440a6a 100644 --- a/src/main/java/com/juick/www/api/activity/Profile.java +++ b/src/main/java/com/juick/www/api/activity/Profile.java @@ -279,142 +279,144 @@ public class Profile { @CacheEvict(cacheNames = "profiles", key = "{ #visitor.uri }") @PostMapping(value = "/api/inbox", consumes = { Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITY_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE, MediaType.APPLICATION_JSON_VALUE }) - public ResponseEntity<CommandResult> processInbox(@Parameter(hidden = true) User visitor, InputStream inboxData) + public ResponseEntity<CommandResult> processInbox(@Parameter(hidden = true) User visitor, @RequestBody Context context) throws Exception { - String inbox = IOUtils.toString(inboxData, StandardCharsets.UTF_8); - Activity activity = jsonMapper.readValue(inbox, Activity.class); - if ((StringUtils.isNotEmpty(visitor.getUri().toString()) - && visitor.getUri().equals(URI.create(activity.getActor()))) || !visitor.isAnonymous()) { - if (activity instanceof Follow) { - Follow followRequest = (Follow) activity; - applicationEventPublisher.publishEvent(new FollowEvent(this, followRequest)); - return new ResponseEntity<>(CommandResult.fromString("Follow request accepted"), HttpStatus.ACCEPTED); - } - if (activity instanceof Undo) { - Context object = activity.getObject(); - if (object instanceof Follow) { - applicationEventPublisher.publishEvent( - new UndoFollowEvent(this, activity.getActor(), ((Activity) object).getObject().getId())); - return new ResponseEntity<>(CommandResult.fromString("Undo follow request accepted"), - HttpStatus.OK); - } else if (object instanceof Like || object instanceof Announce) { - applicationEventPublisher.publishEvent( - new UndoAnnounceEvent(this, activity.getActor(), ((Activity) object).getObject().getId())); - return new ResponseEntity<>(CommandResult.fromString("Undo like/announce request accepted"), - HttpStatus.OK); + if (context instanceof Activity activity) { + if ((StringUtils.isNotEmpty(visitor.getUri().toString()) + && visitor.getUri().equals(URI.create(activity.getActor()))) || !visitor.isAnonymous()) { + if (activity instanceof Follow) { + Follow followRequest = (Follow) activity; + applicationEventPublisher.publishEvent(new FollowEvent(this, followRequest)); + return new ResponseEntity<>(CommandResult.fromString("Follow request accepted"), HttpStatus.ACCEPTED); } - } - if (activity instanceof Create) { - if (activity.getObject() instanceof Note) { - Note note = (Note) activity.getObject(); - URI noteId = URI.create((String) note.getId()); - if (messagesService.replyExists(noteId)) { - return new ResponseEntity<>(CommandResult.fromString("Reply already exists"), HttpStatus.OK); - } else { - String inReplyTo = (String) note.getInReplyTo(); - if (StringUtils.isNotBlank(inReplyTo)) { - if (inReplyTo.startsWith(baseUri)) { - String postId = profileUriBuilder.postId(inReplyTo); - User user = new User(); - user.setUri(URI.create(activity.getActor())); - CommandResult result = commandsManager.processCommand(user, - String.format("#%s %s", postId, formatNote(note)), - URI.create(StringUtils.EMPTY)); - logger.info(jsonMapper.writeValueAsString(result)); - if (result.getNewMessage().isPresent()) { - messagesService.updateReplyUri(result.getNewMessage().get(), noteId); - return new ResponseEntity<>(result, HttpStatus.OK); - } else { - logger.warn("Invalid request: {}", inbox); - return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST); - } - } else { - Message reply = messagesService.getReplyByUri(inReplyTo); - if (reply != null) { + if (activity instanceof Undo) { + Context object = activity.getObject(); + if (object instanceof Follow) { + applicationEventPublisher.publishEvent( + new UndoFollowEvent(this, activity.getActor(), ((Activity) object).getObject().getId())); + return new ResponseEntity<>(CommandResult.fromString("Undo follow request accepted"), + HttpStatus.OK); + } else if (object instanceof Like || object instanceof Announce) { + applicationEventPublisher.publishEvent( + new UndoAnnounceEvent(this, activity.getActor(), ((Activity) object).getObject().getId())); + return new ResponseEntity<>(CommandResult.fromString("Undo like/announce request accepted"), + HttpStatus.OK); + } + } + if (activity instanceof Create) { + if (activity.getObject() instanceof Note) { + Note note = (Note) activity.getObject(); + URI noteId = URI.create(note.getId()); + if (messagesService.replyExists(noteId)) { + return new ResponseEntity<>(CommandResult.fromString("Reply already exists"), HttpStatus.OK); + } else { + String inReplyTo = note.getInReplyTo(); + if (StringUtils.isNotBlank(inReplyTo)) { + if (inReplyTo.startsWith(baseUri)) { + String postId = profileUriBuilder.postId(inReplyTo); User user = new User(); user.setUri(URI.create(activity.getActor())); CommandResult result = commandsManager.processCommand(user, - String.format("#%d/%d %s", reply.getMid(), reply.getRid(), - formatNote(note)), + String.format("#%s %s", postId, formatNote(note)), URI.create(StringUtils.EMPTY)); logger.info(jsonMapper.writeValueAsString(result)); if (result.getNewMessage().isPresent()) { messagesService.updateReplyUri(result.getNewMessage().get(), noteId); return new ResponseEntity<>(result, HttpStatus.OK); + } else { + logger.warn("Invalid request: {}", context.getId()); + return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST); + } + } else { + Message reply = messagesService.getReplyByUri(inReplyTo); + if (reply != null) { + User user = new User(); + user.setUri(URI.create(activity.getActor())); + CommandResult result = commandsManager.processCommand(user, + String.format("#%d/%d %s", reply.getMid(), reply.getRid(), + formatNote(note)), + URI.create(StringUtils.EMPTY)); + logger.info(jsonMapper.writeValueAsString(result)); + if (result.getNewMessage().isPresent()) { + messagesService.updateReplyUri(result.getNewMessage().get(), noteId); + return new ResponseEntity<>(result, HttpStatus.OK); + } } } + } else { + if (note.getTo().stream().anyMatch(recipient -> recipient.startsWith(baseUri))) { + logger.warn("Possible direct message from {}", note.getAttributedTo()); + applicationEventPublisher.publishEvent(new DirectMessageEvent(this, note)); + return new ResponseEntity<>(CommandResult.fromString("Message accepted"), + HttpStatus.ACCEPTED); + } } - } else { - if (note.getTo().stream().anyMatch(recipient -> recipient.startsWith(baseUri))) { - logger.warn("Possible direct message from {}", note.getAttributedTo()); - applicationEventPublisher.publishEvent(new DirectMessageEvent(this, note)); - return new ResponseEntity<>(CommandResult.fromString("Message accepted"), - HttpStatus.ACCEPTED); - } + logger.warn("Request with invalid recipient from {}", activity.getActor()); + return new ResponseEntity<>(CommandResult.fromString("Message accepted"), HttpStatus.ACCEPTED); } - logger.warn("Request with invalid recipient from {}", activity.getActor()); - return new ResponseEntity<>(CommandResult.fromString("Message accepted"), HttpStatus.ACCEPTED); } } - } - if (activity instanceof Delete) { - URI objectId = URI.create(activity.getObject().getId()); - if (messagesService.replyExists(objectId)) { - URI actor = URI.create(activity.getActor()); - messagesService.deleteReply(actor, objectId); + if (activity instanceof Delete) { + URI objectId = URI.create(activity.getObject().getId()); + if (messagesService.replyExists(objectId)) { + URI actor = URI.create(activity.getActor()); + messagesService.deleteReply(actor, objectId); + } + // accept all authenticated Delete activities + return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"), HttpStatus.ACCEPTED); } - // accept all authenticated Delete activities - return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"), HttpStatus.ACCEPTED); - } - if (activity instanceof Like || activity instanceof Announce) { - String messageUri = activity.getObject().getId(); - applicationEventPublisher.publishEvent(new AnnounceEvent(this, activity.getActor(), messageUri)); - return new ResponseEntity<>(CommandResult.fromString("Like/announce request accepted"), HttpStatus.OK); - } - if (activity instanceof Flag) { - URI actor = URI.create(activity.getActor()); - logger.info("{} flag some objects: {}", actor, activity.getObject()); - return new ResponseEntity<>(CommandResult.fromString("Report accepted"), HttpStatus.ACCEPTED); - } - if (activity instanceof Update) { - if (activity.getObject() instanceof Person - && activity.getActor().equals(activity.getObject().getId())) { - logger.info("{} update they profile"); - return new ResponseEntity<>(CommandResult.fromString("Update accepted"), HttpStatus.ACCEPTED); + if (activity instanceof Like || activity instanceof Announce) { + String messageUri = activity.getObject().getId(); + applicationEventPublisher.publishEvent(new AnnounceEvent(this, activity.getActor(), messageUri)); + return new ResponseEntity<>(CommandResult.fromString("Like/announce request accepted"), HttpStatus.OK); + } + if (activity instanceof Flag) { + URI actor = URI.create(activity.getActor()); + logger.info("{} flag some objects: {}", actor, activity.getObject()); + return new ResponseEntity<>(CommandResult.fromString("Report accepted"), HttpStatus.ACCEPTED); } - if (activity.getObject() instanceof Note) { - Note note = (Note) activity.getObject(); - logger.info("Got update to {}", note.getId()); - if (activity.getActor().equals(note.getAttributedTo())) { - Message reply = messagesService.getReplyByUri(note.getId()); - if (reply != null) { - if (messagesService.updateMessage(reply.getMid(), reply.getRid(), formatNote(note), true)) { - logger.info("{} update they message {}", activity.getActor(), note.getId()); + if (activity instanceof Update) { + if (activity.getObject() instanceof Person + && activity.getActor().equals(activity.getObject().getId())) { + logger.info("{} update they profile"); + return new ResponseEntity<>(CommandResult.fromString("Update accepted"), HttpStatus.ACCEPTED); + } + if (activity.getObject() instanceof Note) { + Note note = (Note) activity.getObject(); + logger.info("Got update to {}", note.getId()); + if (activity.getActor().equals(note.getAttributedTo())) { + Message reply = messagesService.getReplyByUri(note.getId()); + if (reply != null) { + if (messagesService.updateMessage(reply.getMid(), reply.getRid(), formatNote(note), true)) { + logger.info("{} update they message {}", activity.getActor(), note.getId()); + return new ResponseEntity<>(HttpStatus.ACCEPTED); + } + logger.warn("Unable to update {}", note.getId()); + return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); + } else { + logger.warn("Update not found: {}", note.getId()); return new ResponseEntity<>(HttpStatus.ACCEPTED); } - logger.warn("Unable to update {}", note.getId()); - return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); } else { - logger.warn("Update not found: {}", note.getId()); - return new ResponseEntity<>(HttpStatus.ACCEPTED); + logger.warn("Invalid Update: {}", jsonMapper.writeValueAsString(activity)); + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } - } else { - logger.warn("Invalid Update: {}", jsonMapper.writeValueAsString(activity)); - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } + if (activity instanceof Block) { + logger.info("{} blocks {} (room_full_of_people_who_care.jpg)", activity.getActor(), activity.getObject().getId()); + } + logger.warn("Unknown activity: {}", jsonMapper.writeValueAsString(activity)); + return new ResponseEntity<>(CommandResult.fromString("Unknown activity"), HttpStatus.NOT_IMPLEMENTED); } - if (activity instanceof Block) { - logger.info("{} blocks {} (room_full_of_people_who_care.jpg)", activity.getActor(), activity.getObject().getId()); - } - logger.warn("Unknown activity: {}", jsonMapper.writeValueAsString(activity)); - return new ResponseEntity<>(CommandResult.fromString("Unknown activity"), HttpStatus.NOT_IMPLEMENTED); - } - if (activity instanceof Delete) { - // Delete gone user - if (activity.getActor().equals(activity.getObject().getId())) { - return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"), HttpStatus.ACCEPTED); + if (activity instanceof Delete) { + // Delete gone user + if (activity.getActor().equals(activity.getObject().getId())) { + return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"), HttpStatus.ACCEPTED); + } } + } else { + return new ResponseEntity<>(CommandResult.fromString("Can't parse"), HttpStatus.BAD_REQUEST); } return new ResponseEntity<>(CommandResult.fromString("Can not authenticate"), HttpStatus.UNAUTHORIZED); } diff --git a/src/main/java/com/juick/www/api/webfinger/Resource.java b/src/main/java/com/juick/www/api/webfinger/Resource.java index 34878b69..3a6c36c4 100644 --- a/src/main/java/com/juick/www/api/webfinger/Resource.java +++ b/src/main/java/com/juick/www/api/webfinger/Resource.java @@ -38,6 +38,7 @@ import static com.juick.www.api.activity.model.Context.ACTIVITY_MEDIA_TYPE; @RestController public class Resource { + public static final String MEDIA_TYPE = "application/jrd+json"; @Inject private UserService userService; @Value("${web_domain:localhost}") @@ -46,7 +47,7 @@ public class Resource { private String baseUri; @GetMapping(value = "/.well-known/webfinger", produces = { - "application/jrd+json", MediaType.APPLICATION_JSON_VALUE }) + Resource.MEDIA_TYPE, MediaType.APPLICATION_JSON_VALUE }) public Account getWebResource(@RequestParam String resource) { if (resource.startsWith("acct:")) { try { diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index f01f58a7..5d149277 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -32,11 +32,9 @@ import com.jayway.jsonpath.JsonPath; import com.juick.*; import com.juick.model.Tag; import com.juick.model.*; -import com.juick.server.MockDeleteListener; import com.juick.server.MockNotificationListener; import com.juick.server.MockUpdateListener; import com.juick.service.*; -import com.juick.service.activities.DeleteUserEvent; import com.juick.service.activities.UpdateEvent; import com.juick.service.component.SystemEvent; import com.juick.test.util.MockUtils; @@ -67,6 +65,7 @@ import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Marshaller; import jakarta.xml.bind.Unmarshaller; +import okhttp3.OkHttpClient; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.IteratorUtils; import org.apache.commons.io.IOUtils; @@ -88,12 +87,10 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.http.*; -import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.web.MockHttpSession; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.TestPropertySource; -import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; @@ -103,11 +100,8 @@ import org.springframework.util.DigestUtils; import org.springframework.util.FileSystemUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.client.ResourceAccessException; -import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; -import org.tomitribe.auth.signatures.Base64; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; @@ -127,7 +121,6 @@ import java.io.*; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.*; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.spec.InvalidKeySpecException; @@ -142,7 +135,6 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.StreamSupport; -import static com.juick.www.api.activity.model.Context.ACTIVITY_MEDIA_TYPE; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.hamcrest.collection.IsEmptyCollection.empty; @@ -150,10 +142,6 @@ import static org.junit.jupiter.api.Assertions.*; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.test.util.AssertionErrors.assertNotEquals; -import static org.springframework.test.web.client.ExpectedCount.times; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; -import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; -import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -161,6 +149,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. /** * Created by vitalyster on 25.11.2016. */ + +// TODO: test deleted when GONE, test deleted when suspended, test incorrect certificates @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @TestPropertySource(properties = {"ios_app_id=12345678.com.juick.ExampleApp"}) @AutoConfigureMockMvc @@ -219,7 +209,7 @@ public class ServerTests { @Value("${web_domain:localhost}") private String webDomain; @Inject - private RestTemplate apClient; + private OkHttpClient apClient; @Value("classpath:snapshots/activity/testuser.json") private Resource testuserResponse; @@ -2056,9 +2046,9 @@ public class ServerTests { (Actor) activityPubService.get(URI.create("http://localhost:8080/u/freefd")).get(), (Actor) activityPubService.get(URI.create("http://localhost:8080/u/ugnich")).get(), create); - Message replyToExt = commandsManager - .processCommand(ugnich, String.format("#%d/1 PSSH YOBA ETO TI", msg.getMid()), emptyUri) - .getNewMessage() + var reply = commandsManager + .processCommand(ugnich, String.format("#%d/1 PSSH YOBA ETO TI", msg.getMid()), emptyUri); + var replyToExt = reply.getNewMessage() .get(); json = jsonMapper.writeValueAsString(Context.build( activityPubManager.makeNote( @@ -2076,98 +2066,13 @@ public class ServerTests { follow.setActor("http://localhost:8080/u/freefd"); follow.setObject(new Context("http://localhost:8080/u/ugnich")); var result = activityPubService.post(from, to, follow); - assertThat(result, is(HttpStatusCode.valueOf(202))); + assertThat(result, is(202)); String testuserResponseString = IOUtils.toString(testuserResponse.getInputStream(), StandardCharsets.UTF_8); Actor maliciousActor = jsonMapper.readValue(testuserResponseString, Actor.class); follow.setActor(maliciousActor.getId()); result = activityPubService.post(maliciousActor, to, follow); - assertThat(result, is(HttpStatusCode.valueOf(401))); - } - - @Test - @Order(1) - public void serviceSignatureAuth() throws Exception { - String meUri = "/api/me"; - Instant now = Instant.now(); - String requestDate = DateFormattersHolder.getHttpDateFormatter().format(now); - mockMvc.perform(get("/api/me").header("Date", requestDate)).andExpect(status().isUnauthorized()); - String testHost = "localhost:8080"; - Actor ugnichPerson = conversionService.convert(ugnich, Actor.class); - now = Instant.now(); - requestDate = DateFormattersHolder.getHttpDateFormatter().format(now); - String signatureString = signatureService.addSignature(ugnichPerson, testHost, "GET", meUri, - requestDate, - StringUtils.EMPTY); - MvcResult me = mockMvc.perform(get("/api/me") - .header("Host", testHost).header("Date", requestDate) - .header( "Signature", signatureString)).andExpect(status().isOk()).andReturn(); - User meUser = jsonMapper.readValue(me.getResponse().getContentAsString(), User.class); - assertThat(meUser, is(ugnich)); - String testuserResponseString = IOUtils.toString(testuserResponse.getInputStream(), - StandardCharsets.UTF_8); - ClientHttpRequestFactory originalRequestFactory = apClient.getRequestFactory(); - URI testuserUri = URI.create("https://example.com/u/testuser"); - URI testuserkeyUri = URI.create("https://example.com/u/testuser#main-key"); - URI testAppUri = URI.create("https://example.com/actor"); - URI testAppkeyUri = URI.create("https://example.com/actor#main-key"); - MockRestServiceServer restServiceServer = MockRestServiceServer.createServer(apClient); - restServiceServer.expect(times(4), requestTo(testuserUri)) - .andRespond(withSuccess(testuserResponseString, MediaType.APPLICATION_JSON)); - restServiceServer.expect(times(4), requestTo(testuserkeyUri)) - .andRespond(withSuccess(testuserResponseString, MediaType.APPLICATION_JSON)); - Person testuser = (Person) activityPubService.get(testuserUri).get(); - assertThat(testuser.getPublicKey().getPublicKeyPem(), is(testKeystoreManager.getPublicKeyPem())); - Instant now2 = Instant.now(); - String testRequestDate = DateFormattersHolder.getHttpDateFormatter().format(now2); - String inboxUri = "/api/inbox"; - var payload = IOUtils.toByteArray(testfollowRequest.getInputStream()); - byte[] digest = MessageDigest.getInstance("SHA-256").digest(payload); // (1) - String digestHeader = "SHA-256=" + new String(Base64.encodeBase64(digest)); - String testSignatureString = signatureService.addSignature(testuser, testHost, "POST", inboxUri, - testRequestDate, digestHeader, testKeystoreManager); - mockMvc.perform(post(inboxUri).header("Host", testHost).header("Date", testRequestDate) - .header("Digest", digestHeader).header("Signature", testSignatureString) - .contentType(Context.LD_JSON_MEDIA_TYPE).content(payload)) - .andExpect(status().isAccepted()); - mockMvc.perform(post(inboxUri).header("Host", "wronghost").header("Date", testRequestDate) - .header("Signature", testSignatureString).contentType(Context.LD_JSON_MEDIA_TYPE) - .content(IOUtils.toByteArray(testfollowRequest.getInputStream()))) - .andExpect(status().isUnauthorized()); - // digest required but not present - mockMvc.perform(post(inboxUri).header("Host", testHost).header("Date", testRequestDate) - .header("Signature", testSignatureString).contentType(Context.LD_JSON_MEDIA_TYPE) - .content(payload)) - .andExpect(status().isUnauthorized()); - apClient.setRequestFactory(originalRequestFactory); - } - - @Test - public void testFlaggingAsApplication() throws Exception { - var payload = IOUtils.toByteArray(flagPayload.getInputStream()); - var digest = MessageDigest.getInstance("SHA-256").digest(payload); // (1) - var digestHeader = "SHA-256=" + new String(Base64.encodeBase64(digest)); - var now2 = Instant.now(); - String inboxUri = "/api/inbox"; - String testHost = "localhost:8080"; - URI testAppUri = URI.create("https://example.com/actor"); - String testappResponseString = IOUtils.toString(testappResponse.getInputStream(), - StandardCharsets.UTF_8); - var testRequestDate = DateFormattersHolder.getHttpDateFormatter().format(now2); - ClientHttpRequestFactory originalRequestFactory = apClient.getRequestFactory(); - MockRestServiceServer restServiceServer = MockRestServiceServer.createServer(apClient); - restServiceServer.expect(times(2), requestTo(testAppUri)) - .andRespond(withSuccess(testappResponseString, MediaType.APPLICATION_JSON)); - Application testapp = (Application) activityPubService.get(testAppUri).get(); - assertThat(testapp.getPublicKey().getPublicKeyPem(), is(testKeystoreManager.getPublicKeyPem())); - var testSignatureString = signatureService.addSignature(testapp, testHost, "POST", inboxUri, - testRequestDate, - digestHeader, testKeystoreManager); - mockMvc.perform(post(inboxUri).header("Host", testHost).header("Date", testRequestDate) - .header("Signature", testSignatureString).header("Digest", digestHeader) - .contentType(Context.LD_JSON_MEDIA_TYPE).content(payload)) - .andExpect(status().isAccepted()); - apClient.setRequestFactory(originalRequestFactory); + assertThat(result, is(401)); } @Test @@ -2354,79 +2259,6 @@ public class ServerTests { } @Test - public void federatedUserDeletionFlowWhenItIsGone() throws Exception { - String deleteJsonStr = IOUtils.toString(new ClassPathResource("delete_user.json").getURI(), - StandardCharsets.UTF_8); - Delete delete = jsonMapper.readValue(deleteJsonStr, Delete.class); - ClientHttpRequestFactory originalRequestFactory = apClient.getRequestFactory(); - MockRestServiceServer restServiceServer = MockRestServiceServer.createServer(apClient); - restServiceServer.expect(times(2), requestTo(delete.getObject().getId())) - .andRespond(withStatus(HttpStatus.GONE)); - restServiceServer.expect(requestTo(delete.getObject().getId())).andRespond(response -> { - throw new ResourceAccessException("Connection reset"); - }); - mockMvc.perform(post("/api/inbox").contentType(ACTIVITY_MEDIA_TYPE).content(deleteJsonStr)) - .andExpect(status().isAccepted()); - mockMvc.perform(post("/api/inbox").contentType(ACTIVITY_MEDIA_TYPE).content(deleteJsonStr).header( - "Signature", - "keyId=\"https://example.com/users/deleted#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"wHoU91JJBsIYcR1W1/57B0oG98t5Aa/TvGPw1B8KQlAp5KhpePnOzD1MZRgivBx7YKO6eYwDx+AX9dn6tjlAvzRLygv21H6UoDZFihWzeE1HM8pY2Pe4EhUgYBN0YuiKUi7W4TS9bDRAJ5vGNPUWATe+2o5Jcbux5cZYXFKKYbLBLD+/IlqPdHA2IXLZ52HFVVfBkPH5sSklV6XJtD/PHLK9R/I9w/mUpj9moUPQu44rR7KvxiGNuHla3vfDtJbkBqLMdScX91EG8373AulXPUiCCF7R2lJB0fFQedm2nSbcwBoJ32GEyOyOPFgPKG5zd9Fd5TfB1pmA8ZIE0sChfA==\"")) - .andExpect(status().isAccepted()); - apClient.setRequestFactory(originalRequestFactory); - } - - @MockBean - private MockDeleteListener deleteListener; - @Captor - protected ArgumentCaptor<DeleteUserEvent> deleteEventCaptor; - - @Test - public void federatedUserDeletionFlowWhenItIsSuspended() throws Exception { - String deleteJsonStr = IOUtils.toString(testDeleteRequest.getInputStream(), StandardCharsets.UTF_8); - Delete delete = jsonMapper.readValue(deleteJsonStr, Delete.class); - ClientHttpRequestFactory originalRequestFactory = apClient.getRequestFactory(); - MockRestServiceServer restServiceServer = MockRestServiceServer.createServer(apClient); - restServiceServer.expect(times(2), requestTo(delete.getObject().getId())) - .andRespond(withSuccess( - IOUtils.toString(testSuspendedUserResponse.getInputStream(), - StandardCharsets.UTF_8), - MediaType.APPLICATION_JSON)); - Person testuser = (Person) activityPubService.get(URI.create(delete.getObject().getId())).get(); - Instant now = Instant.now(); - String testRequestDate = DateFormattersHolder.getHttpDateFormatter().format(now); - String inboxUri = "/api/inbox"; - byte[] digest = MessageDigest.getInstance("SHA-256").digest(deleteJsonStr.getBytes()); - String digestHeader = "SHA-256=" + new String(Base64.encodeBase64(digest)); - String testSignatureString = signatureService.addSignature(testuser, "localhost", "POST", inboxUri, - testRequestDate, digestHeader, testKeystoreManager); - mockMvc.perform(post(inboxUri).contentType(ACTIVITY_MEDIA_TYPE).content(deleteJsonStr) - .header("Host", "localhost").header("Date", testRequestDate) - .header("Digest", digestHeader) - .header("Signature", testSignatureString)).andExpect(status().isAccepted()); - apClient.setRequestFactory(originalRequestFactory); - Mockito.verify(deleteListener, Mockito.times(1)).onApplicationEvent(deleteEventCaptor.capture()); - DeleteUserEvent receivedEvent = deleteEventCaptor.getValue(); - assertThat(receivedEvent.getUserUri(), is(testuser.getId())); - } - - @Test - @Order(2) - public void handleIncorrectCertificates() throws Exception { - String deleteJsonStr = IOUtils.toString(new ClassPathResource("delete_user.json").getURI(), - StandardCharsets.UTF_8); - Delete delete = jsonMapper.readValue(deleteJsonStr, Delete.class); - ClientHttpRequestFactory originalRequestFactory = apClient.getRequestFactory(); - MockRestServiceServer restServiceServer = MockRestServiceServer.createServer(apClient); - restServiceServer.expect(requestTo(delete.getObject().getId())).andRespond(response -> { - throw new ResourceAccessException("Connection reset"); - }); - mockMvc.perform(post("/api/inbox").contentType(ACTIVITY_MEDIA_TYPE).content(deleteJsonStr).header( - "Signature", - "keyId=\"https://example.com/users/deleted#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"wHoU91JJBsIYcR1W1/57B0oG98t5Aa/TvGPw1B8KQlAp5KhpePnOzD1MZRgivBx7YKO6eYwDx+AX9dn6tjlAvzRLygv21H6UoDZFihWzeE1HM8pY2Pe4EhUgYBN0YuiKUi7W4TS9bDRAJ5vGNPUWATe+2o5Jcbux5cZYXFKKYbLBLD+/IlqPdHA2IXLZ52HFVVfBkPH5sSklV6XJtD/PHLK9R/I9w/mUpj9moUPQu44rR7KvxiGNuHla3vfDtJbkBqLMdScX91EG8373AulXPUiCCF7R2lJB0fFQedm2nSbcwBoJ32GEyOyOPFgPKG5zd9Fd5TfB1pmA8ZIE0sChfA==\"")) - .andExpect(status().isAccepted()); - apClient.setRequestFactory(originalRequestFactory); - } - - @Test public void legacyAvatarEndpoint() throws Exception { mockMvc.perform(get("/api/avatar").param("uname", "unknown")).andExpect(status().isOk()) .andExpect(content().bytes(IOUtils.toByteArray(defaultAvatar.getInputStream()))); |