aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/juick/service/ActivityPubService.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/juick/service/ActivityPubService.java')
-rw-r--r--src/main/java/com/juick/service/ActivityPubService.java158
1 files changed, 157 insertions, 1 deletions
diff --git a/src/main/java/com/juick/service/ActivityPubService.java b/src/main/java/com/juick/service/ActivityPubService.java
index c45b0c13..f161be7b 100644
--- a/src/main/java/com/juick/service/ActivityPubService.java
+++ b/src/main/java/com/juick/service/ActivityPubService.java
@@ -18,24 +18,71 @@
package com.juick.service;
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 org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
+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.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
+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;
+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;
@Repository
public class ActivityPubService extends BaseJdbcService implements SocialService {
+
+ private static final Logger logger = LoggerFactory.getLogger("ActivityPub");
+
@Value("${ap_base_uri:http://localhost:8080/}")
private String baseUri;
@Inject
private UserService userService;
+ @Inject
+ private RestTemplate restClient;
+ @Inject
+ private ObjectMapper jsonMapper;
+ @Inject
+ private SignatureService signatureService;
+ @Inject
+ private ApplicationEventPublisher applicationEventPublisher;
+ @Inject
+ private KeystoreManager keystoreManager;
+ @Inject
+ private User serviceUser;
+ @Inject
+ private ConversionService conversionService;
@Transactional(readOnly = true)
@Override
@@ -53,7 +100,8 @@ public class ActivityPubService extends BaseJdbcService implements SocialService
@Transactional(readOnly = true)
@Override
public @Nonnull List<String> getFollowers(User user) {
- return getJdbcTemplate().queryForList("SELECT acct FROM followers WHERE user_id=?", String.class, user.getUid());
+ return getJdbcTemplate().queryForList("SELECT acct FROM followers WHERE user_id=?", String.class,
+ user.getUid());
}
@Transactional
@@ -78,4 +126,112 @@ public class ActivityPubService extends BaseJdbcService implements SocialService
public void removeAccount(String acct) {
getJdbcTemplate().update("DELETE FROM followers WHERE acct=?", acct);
}
+
+ @Cacheable("profiles")
+ public Optional<Context> get(URI contextUri) {
+ Instant now = Instant.now();
+ String requestDate = DateFormattersHolder.getHttpDateFormatter().format(now);
+ String host = contextUri.getPort() > 0 ? String.format("%s:%d", contextUri.getHost(), contextUri.getPort())
+ : contextUri.getHost();
+ var from = conversionService.convert(serviceUser, Actor.class);
+ try {
+ String signatureString = signatureService.addSignature(from, host, "get", contextUri.getPath(), requestDate,
+ "");
+ HttpHeaders requestHeaders = new HttpHeaders();
+ requestHeaders.add("Date", requestDate);
+ requestHeaders.add("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();
+ }
+ return Optional.of(context);
+ }
+ } 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 {
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(to.getInbox());
+ URI inbox = uriComponentsBuilder.build().toUri();
+ Instant now = Instant.now();
+ String requestDate = DateFormattersHolder.getHttpDateFormatter().format(now);
+ String host = inbox.getPort() > 0 ? String.format("%s:%d", inbox.getHost(), inbox.getPort()) : inbox.getHost();
+ var finalContext = Context.build(data);
+ var payload = jsonMapper.writeValueAsString(finalContext);
+ final byte[] digest = MessageDigest.getInstance("SHA-256").digest(payload.getBytes()); // (1)
+ 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("Content-Type", Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE);
+ requestHeaders.add("Date", requestDate);
+ requestHeaders.add("Host", host);
+ requestHeaders.add("Digest", digestHeader);
+ requestHeaders.add("Signature", signatureString);
+ HttpEntity<String> request = new HttpEntity<>(payload, requestHeaders);
+ 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();
+ }
+
+ public User verifyActor(String method, String path, Map<String, String> headers) {
+ String signatureString = headers.get("signature");
+ if (StringUtils.isNotEmpty(signatureString)) {
+ try {
+ Signature signature = Signature.fromString(signatureString);
+ var keyId = UriComponentsBuilder.fromUriString(signature.getKeyId()).fragment(null).build().toUri();
+ var user = getUserByAccountUri(keyId.toASCIIString());
+ Key key = null;
+ Actor actor = null;
+ if (!user.isAnonymous()) {
+ // local user
+ key = keystoreManager.getPublicKey();
+ } else {
+ var context = get(keyId);
+ if (context.isPresent()) {
+ actor = (Actor) context.get();
+ key = KeystoreManager.publicKeyOf(actor);
+ }
+ }
+ if (key != null) {
+ Verifier verifier = new Verifier(key, signature);
+ try {
+ boolean result = verifier.verify(method.toLowerCase(), path, headers);
+ if (result) {
+ if (!user.isAnonymous()) {
+ return user;
+ } else {
+ if (actor != null) {
+ User person = new User();
+ person.setUri(URI.create(actor.getId()));
+ if (actor.isSuspended()) {
+ logger.info("{} is suspended, deleting", actor.getId());
+ applicationEventPublisher
+ .publishEvent(new DeleteUserEvent(this, actor.getId()));
+ }
+ return person;
+ }
+ }
+ }
+ } catch (NoSuchAlgorithmException | SignatureException | MissingRequiredHeaderException
+ | IOException e) {
+ logger.warn("Verification error for {}: {}", signature.getKeyId(), e.getMessage());
+ }
+ }
+ } catch (Exception ex) {
+
+ }
+ }
+ return AnonymousUser.INSTANCE;
+ }
}