diff options
Diffstat (limited to 'src/main/java/com/juick')
10 files changed, 256 insertions, 391 deletions
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 { |