aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2023-01-05 11:00:50 +0300
committerGravatar Vitaly Takmazov2023-01-05 20:58:47 +0300
commitcdd03aa64548810591e043fb59a287a1b36c92ba (patch)
tree665ad1e3f1162d0be76c95a814ec4500bcf5ce55
parent120b26c55069f89cc60ef862514d5cf09566f348 (diff)
ActivityPub: signed GET requests, fix Signature verification
-rw-r--r--src/main/java/com/juick/ActivityPubManager.java48
-rw-r--r--src/main/java/com/juick/SignatureManager.java200
-rw-r--r--src/main/java/com/juick/config/ActivityPubClientConfig.java61
-rw-r--r--src/main/java/com/juick/config/ActivityPubClientErrorHandler.java1
-rw-r--r--src/main/java/com/juick/config/ActivityPubConfig.java38
-rw-r--r--src/main/java/com/juick/config/AppConfig.java5
-rw-r--r--src/main/java/com/juick/config/SecurityConfig.java17
-rw-r--r--src/main/java/com/juick/service/ActivityPubService.java158
-rw-r--r--src/main/java/com/juick/service/SignatureService.java57
-rw-r--r--src/main/java/com/juick/service/WebfingerService.java58
-rw-r--r--src/main/java/com/juick/service/security/BearerTokenAuthenticationFilter.java7
-rw-r--r--src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java43
-rw-r--r--src/main/java/com/juick/util/ActivityPubRequestInterceptor.java2
-rw-r--r--src/main/java/com/juick/www/api/Users.java7
-rw-r--r--src/main/java/com/juick/www/controllers/Compat.java16
-rw-r--r--src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java1
-rw-r--r--src/test/java/com/juick/server/tests/ServerTests.java84
17 files changed, 451 insertions, 352 deletions
diff --git a/src/main/java/com/juick/ActivityPubManager.java b/src/main/java/com/juick/ActivityPubManager.java
index a487afb1..db8a1acb 100644
--- a/src/main/java/com/juick/ActivityPubManager.java
+++ b/src/main/java/com/juick/ActivityPubManager.java
@@ -19,12 +19,13 @@ package com.juick;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.json.JsonMapper;
import com.juick.model.Message;
import com.juick.model.Reaction;
import com.juick.model.User;
+import com.juick.service.ActivityPubService;
import com.juick.service.MessagesService;
import com.juick.service.SocialService;
+import com.juick.service.WebfingerService;
import com.juick.service.activities.*;
import com.juick.service.component.NotificationListener;
import com.juick.service.component.PingEvent;
@@ -45,6 +46,7 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.convert.ConversionService;
+import org.springframework.http.MediaType;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
@@ -62,7 +64,9 @@ import java.util.stream.Collectors;
public class ActivityPubManager implements ActivityListener, NotificationListener {
private static final Logger logger = LoggerFactory.getLogger("ActivityPub");
@Inject
- private SignatureManager signatureManager;
+ private ActivityPubService activityPubService;
+ @Inject
+ private WebfingerService webfingerService;
@Inject
private SocialService socialService;
@Inject
@@ -84,12 +88,12 @@ public class ActivityPubManager implements ActivityListener, NotificationListene
if (!followedUser.isAnonymous()) {
// automatically accept follower requests
Actor me = conversionService.convert(followedUser, Actor.class);
- Actor follower = (Actor) signatureManager.getContext(URI.create(followEvent.getRequest().getActor())).get();
+ Actor follower = (Actor) activityPubService.get(URI.create(followEvent.getRequest().getActor())).get();
Accept accept = new Accept();
accept.setActor(me.getId());
accept.setObject(followEvent.getRequest());
try {
- signatureManager.post(me, follower, accept);
+ activityPubService.post(me, follower, accept);
socialService.addFollower(followedUser, follower.getId());
logger.info("Follower added for {}", followedUser.getName());
} catch (IOException | NoSuchAlgorithmException e) {
@@ -124,14 +128,14 @@ public class ActivityPubManager implements ActivityListener, NotificationListene
Actor me = conversionService.convert(user, Actor.class);
socialService.getFollowers(user).forEach(acct -> {
try {
- Actor follower = (Actor) signatureManager.getContext(URI.create(acct)).orElseThrow();
+ Actor follower = (Actor) activityPubService.get(URI.create(acct)).orElseThrow();
Delete delete = new Delete();
delete.setId(note.getId());
delete.setActor(me.getId());
delete.setPublished(note.getPublished());
delete.setObject(note);
logger.info("Deletion to follower {}", follower.getId());
- signatureManager.post(me, follower, delete);
+ activityPubService.post(me, follower, delete);
} catch (IOException | NoSuchAlgorithmException e) {
logger.warn("activitypub exception", e);
} catch (NoSuchElementException ex) {
@@ -196,12 +200,12 @@ public class ActivityPubManager implements ActivityListener, NotificationListene
private void activityToFollowers(User user, Actor from, Activity activity) {
socialService.getFollowers(user).forEach(acct -> {
- var context = signatureManager.getContext(URI.create(acct));
+ var context = activityPubService.get(URI.create(acct));
context.ifPresentOrElse((follower) -> {
if (follower instanceof Actor) {
var to = (Actor) follower;
try {
- signatureManager.post(from, to, activity);
+ activityPubService.post(from, to, activity);
} catch (Exception e) {
logger.warn("Delivery to {} failed: {}", to, e.getMessage());
}
@@ -251,7 +255,7 @@ public class ActivityPubManager implements ActivityListener, NotificationListene
subscribers.addAll(note.getCc());
subscribers.forEach(acct -> {
if (!acct.equals(profileUriBuilder.followersUri(user))) {
- var context = signatureManager.getContext(URI.create(acct));
+ var context = activityPubService.get(URI.create(acct));
context.ifPresentOrElse((follower) -> {
if (follower instanceof Actor) {
var to = (Actor) follower;
@@ -261,7 +265,7 @@ public class ActivityPubManager implements ActivityListener, NotificationListene
create.setPublished(note.getPublished());
create.setObject(note);
try {
- signatureManager.post(me, to, create);
+ activityPubService.post(me, to, create);
} catch (IOException | NoSuchAlgorithmException e) {
logger.warn("Delivery to {} failed: {}", to, e.getMessage());
}
@@ -308,10 +312,10 @@ public class ActivityPubManager implements ActivityListener, NotificationListene
note.setTags(msg.getTags().stream().map(t -> new Hashtag(profileUriBuilder.tagUri(t), t.getName()))
.collect(Collectors.toList()));
if (msg.getReplyToUri() != null && msg.getReplyToUri().toASCIIString().length() > 0) {
- Optional<Context> noteContext = signatureManager.getContext(msg.getReplyToUri());
+ Optional<Context> noteContext = activityPubService.get(msg.getReplyToUri());
if (noteContext.isPresent()) {
Note activity = (Note) noteContext.get();
- Optional<Context> personContext = signatureManager.getContext(URI.create(activity.getAttributedTo()));
+ Optional<Context> personContext = activityPubService.get(URI.create(activity.getAttributedTo()));
if (personContext.isPresent()) {
Actor person = (Actor) personContext.get();
note.getTags().add(new Mention(person.getUrl(), person.getPreferredUsername()));
@@ -324,13 +328,17 @@ public class ActivityPubManager implements ActivityListener, NotificationListene
}
MessageUtils.getGlobalMentions(msg).forEach(m -> {
// @user@server.tld -> user@server.tld
- Optional<Context> personContext = signatureManager.discoverPerson(m.substring(1));
- if (personContext.isPresent()) {
- Actor person = (Actor) personContext.get();
- note.getTags().add(new Mention(person.getUrl(), person.getPreferredUsername()));
- List<String> cc = new ArrayList<>(note.getCc());
- cc.add(person.getId());
- note.setCc(cc);
+ var personURI = webfingerService.discoverAccountURI(m.substring(1),
+ MediaType.valueOf(Context.ACTIVITY_MEDIA_TYPE));
+ if (!personURI.toASCIIString().isEmpty()) {
+ var personContext = activityPubService.get(personURI);
+ if (personContext.isPresent()) {
+ Actor person = (Actor) personContext.get();
+ note.getTags().add(new Mention(person.getUrl(), person.getPreferredUsername()));
+ List<String> cc = new ArrayList<>(note.getCc());
+ cc.add(person.getId());
+ note.setCc(cc);
+ }
}
});
note.setSensitive(MessageUtils.isSensitive(msg));
@@ -371,7 +379,7 @@ public class ActivityPubManager implements ActivityListener, NotificationListene
}
public User actorToUser(URI uri) throws HttpBadRequestException, JsonProcessingException {
- var context = signatureManager.getContext(uri);
+ var context = activityPubService.get(uri);
if (context.isPresent() && context.get() instanceof Actor actor) {
User user = new User();
user.setUri(URI.create(actor.getId()));
diff --git a/src/main/java/com/juick/SignatureManager.java b/src/main/java/com/juick/SignatureManager.java
deleted file mode 100644
index 792cc8cd..00000000
--- a/src/main/java/com/juick/SignatureManager.java
+++ /dev/null
@@ -1,200 +0,0 @@
-/*
- * Copyright (C) 2008-2022, 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;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.juick.model.AnonymousUser;
-import com.juick.model.User;
-import com.juick.service.UserService;
-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.juick.www.api.webfinger.model.Account;
-import com.juick.www.api.webfinger.model.Link;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.cache.annotation.Cacheable;
-import org.springframework.context.ApplicationEventPublisher;
-import org.springframework.http.*;
-import org.springframework.web.client.RestClientException;
-import org.springframework.web.client.RestTemplate;
-import org.springframework.web.util.UriComponentsBuilder;
-import org.tomitribe.auth.signatures.Base64;
-import org.tomitribe.auth.signatures.*;
-import rocks.xmpp.addr.Jid;
-
-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.*;
-
-import static com.juick.www.api.activity.model.Context.ACTIVITY_MEDIA_TYPE;
-
-public class SignatureManager {
- private static final Logger logger = LoggerFactory.getLogger("ActivityPub");
- @Inject
- private KeystoreManager keystoreManager;
- @Inject
- private ObjectMapper jsonMapper;
- @Inject
- private UserService userService;
- @Inject
- private RestTemplate apClient;
- @Inject
- private ApplicationEventPublisher applicationEventPublisher;
-
- public void 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 = 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<Void> response = apClient.postForEntity(inbox, request, Void.class);
- logger.debug("Remote response: {}", response.getStatusCode());
- }
-
- public String addSignature(Actor from, String host, String method, String path, String dateString,
- String digestHeader) throws IOException {
- return addSignature(from, host, method, path, dateString, digestHeader, keystoreManager);
- }
-
- public String addSignature(Actor from, String host, String method, String path, String dateString,
- String digestHeader, KeystoreManager keystoreManager) throws IOException {
- List<String> requiredHeaders = StringUtils.isEmpty(digestHeader)
- ? Arrays.asList("(request-target)", "host", "date")
- : Arrays.asList("(request-target)", "host", "date", "digest");
- Signature templateSignature = new Signature(from.getPublicKey().getId(), "rsa-sha256", null, requiredHeaders);
- Map<String, String> headers = new HashMap<>();
- headers.put("host", host);
- headers.put("date", dateString);
- if (StringUtils.isNotEmpty(digestHeader)) {
- headers.put("digest", digestHeader);
- }
- Signer signer = new Signer(keystoreManager.getPrivateKey(), templateSignature);
- Signature signature = signer.sign(method, path, headers);
- // remove "Signature: " from result
- return signature.toString().substring(10);
- }
-
- public User verifySignature(String method, String path, Map<String, String> headers) {
- String signatureString = headers.get("signature");
- Signature signature = Signature.fromString(signatureString);
- var keyId = UriComponentsBuilder.fromUriString(signature.getKeyId()).fragment(null).build().toUri();
- var context = getContext(keyId);
- if (context.isPresent() && context.get() instanceof Actor actor) {
- Key key = KeystoreManager.publicKeyOf(actor);
- if (key != null) {
- Verifier verifier = new Verifier(key, signature);
- try {
- boolean result = verifier.verify(method, path, headers);
- if (result) {
- User user = new User();
- user.setUri(URI.create(actor.getId()));
- if (key.equals(keystoreManager.getPublicKey())) {
- return userService.getUserByName(actor.getName());
- }
- if (actor.isSuspended()) {
- logger.info("{} is suspended, deleting", actor.getId());
- applicationEventPublisher.publishEvent(new DeleteUserEvent(this, actor.getId()));
- }
- return user;
- } else {
- return AnonymousUser.INSTANCE;
- }
- } catch (NoSuchAlgorithmException | SignatureException | MissingRequiredHeaderException
- | IOException e) {
- logger.warn("Verification error for {}: {}", signature.getKeyId(), e.getMessage());
- }
- } else {
- logger.warn("Public key missing for {}", actor.getId());
- }
- } else {
- logger.warn("Public key error for {}", signature.getKeyId());
- }
- return AnonymousUser.INSTANCE;
- }
-
- @Cacheable("profiles")
- public Optional<Context> getContext(URI contextUri) {
- try {
- HttpHeaders headers = new HttpHeaders();
- headers.setAccept(Collections.singletonList(MediaType.valueOf(ACTIVITY_MEDIA_TYPE)));
- HttpEntity<Void> activityRequest = new HttpEntity<>(headers);
- var response = apClient.exchange(contextUri, HttpMethod.GET, activityRequest, Context.class);
- if (response.getStatusCode().is2xxSuccessful() && response.hasBody()) {
- var context = response.getBody();
- if (context == null) {
- logger.warn("Cannot identify {}", contextUri);
- return Optional.empty();
- }
- return Optional.of(context);
- }
- } catch (Exception e) {
- logger.warn("{}", e.getMessage());
- }
- return Optional.empty();
- }
-
- public Optional<Context> discoverPerson(String acct) {
- 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);
- try {
- ResponseEntity<Account> response = apClient.exchange(resourceUri, HttpMethod.GET, webfingerRequest,
- Account.class);
- if (response.getStatusCode().is2xxSuccessful()) {
- Account acctData = response.getBody();
- if (acctData != null) {
- for (Link l : acctData.links()) {
- if (l.rel().equals("self") && l.type().equals(ACTIVITY_MEDIA_TYPE)) {
- return getContext(URI.create(l.href()));
- }
- }
- }
- }
- } catch (RestClientException e) {
- logger.warn("Cannot discover person {}: {}", acct, e.getMessage());
- return Optional.empty();
- }
- return Optional.empty();
- }
-}
diff --git a/src/main/java/com/juick/config/ActivityPubClientConfig.java b/src/main/java/com/juick/config/ActivityPubClientConfig.java
new file mode 100644
index 00000000..940071e7
--- /dev/null
+++ b/src/main/java/com/juick/config/ActivityPubClientConfig.java
@@ -0,0 +1,61 @@
+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 243a4e32..b3aedf1f 100644
--- a/src/main/java/com/juick/config/ActivityPubClientErrorHandler.java
+++ b/src/main/java/com/juick/config/ActivityPubClientErrorHandler.java
@@ -18,7 +18,6 @@
package com.juick.config;
import com.juick.service.activities.DeleteUserEvent;
-import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
diff --git a/src/main/java/com/juick/config/ActivityPubConfig.java b/src/main/java/com/juick/config/ActivityPubConfig.java
index 9a626d12..b18dc71c 100644
--- a/src/main/java/com/juick/config/ActivityPubConfig.java
+++ b/src/main/java/com/juick/config/ActivityPubConfig.java
@@ -20,29 +20,18 @@ package com.juick.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.juick.ActivityPubManager;
import com.juick.KeystoreManager;
-import com.juick.util.ActivityPubRequestInterceptor;
import com.juick.www.WebApp;
import com.juick.www.api.activity.converters.UserToActorConverter;
import com.juick.www.api.activity.helpers.ProfileUriBuilder;
-import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-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 javax.inject.Inject;
-import java.nio.charset.StandardCharsets;
-
@Configuration
public class ActivityPubConfig {
@Inject
- ActivityPubClientErrorHandler activityPubClientErrorHandler;
- @Inject
ObjectMapper jsonMapper;
@Inject
KeystoreManager keystoreManager;
@@ -52,37 +41,10 @@ public class ActivityPubConfig {
private String baseUri;
@Bean
- MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() {
- MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
- converter.setObjectMapper(jsonMapper);
- return converter;
- }
-
- @Bean
ActivityPubManager activityPubManager() {
return new ActivityPubManager();
}
- @Inject
- CloseableHttpClient httpClient;
-
- @Bean
- ClientHttpRequestFactory clientHttpRequestFactory() {
- var clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
- clientHttpRequestFactory.setHttpClient(httpClient);
- return clientHttpRequestFactory;
- }
-
- @Bean
- RestTemplate apClient() {
- 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;
- }
-
@Bean
UserToActorConverter userToActorConverter() {
return new UserToActorConverter(profileUriBuilder(), keystoreManager, webApp);
diff --git a/src/main/java/com/juick/config/AppConfig.java b/src/main/java/com/juick/config/AppConfig.java
index d54bc675..ff92de1f 100644
--- a/src/main/java/com/juick/config/AppConfig.java
+++ b/src/main/java/com/juick/config/AppConfig.java
@@ -20,6 +20,7 @@ package com.juick.config;
import com.juick.*;
import com.juick.model.User;
import com.juick.service.HelpService;
+import com.juick.service.SignatureService;
import com.juick.service.StorageService;
import com.juick.service.FileSystemStorageService;
import com.juick.service.UserService;
@@ -158,8 +159,8 @@ public class AppConfig {
}
@Bean
- SignatureManager signatureManager() {
- return new SignatureManager();
+ SignatureService signatureManager() {
+ return new SignatureService(keystoreManager());
}
@Bean
diff --git a/src/main/java/com/juick/config/SecurityConfig.java b/src/main/java/com/juick/config/SecurityConfig.java
index d60abe00..d3f89eef 100644
--- a/src/main/java/com/juick/config/SecurityConfig.java
+++ b/src/main/java/com/juick/config/SecurityConfig.java
@@ -18,7 +18,7 @@
package com.juick.config;
import com.juick.KeystoreManager;
-import com.juick.SignatureManager;
+import com.juick.service.ActivityPubService;
import com.juick.service.UserService;
import com.juick.service.security.BearerTokenAuthenticationFilter;
import com.juick.service.security.HTTPSignatureAuthenticationFilter;
@@ -30,8 +30,6 @@ import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -53,7 +51,6 @@ import org.springframework.security.oauth2.server.authorization.config.annotatio
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.AuthenticationEntryPoint;
-import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.*;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
@@ -65,7 +62,6 @@ import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import javax.inject.Inject;
-import java.io.IOException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
@@ -107,7 +103,7 @@ public class SecurityConfig {
}
@Inject
- private SignatureManager signatureManager;
+ private ActivityPubService activityPubService;
@Bean
HashParamAuthenticationFilter apiAuthenticationFilter() {
@@ -195,11 +191,11 @@ public class SecurityConfig {
}
@Bean
- @Order(2)
+ @Order(Ordered.HIGHEST_PRECEDENCE + 1)
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
- http.securityMatcher("/api/**")
+ http.securityMatcher("/api/**", "/u/**", "/n/**")
.addFilterBefore(apiAuthenticationFilter(), BasicAuthenticationFilter.class)
- .addFilterBefore(new HTTPSignatureAuthenticationFilter(signatureManager, userService),
+ .addFilterBefore(new HTTPSignatureAuthenticationFilter(activityPubService, userService),
BasicAuthenticationFilter.class)
.authorizeHttpRequests(requests -> requests
.requestMatchers(HttpMethod.OPTIONS).permitAll()
@@ -210,7 +206,7 @@ public class SecurityConfig {
"/api/skypebotendpoint", "/api/_fblogin",
"/api/_vklogin", "/api/_tglogin",
"/api/_google", "/api/_applelogin", "/api/signup",
- "/api/inbox", "/api/events", "/api/u/",
+ "/api/inbox", "/api/events", "/u/**", "/n/**",
"/api/info/**", "/api/v1/apps", "/api/v1/instance",
"/api/nodeinfo/2.0", "/oauth/**")
.permitAll()
@@ -251,6 +247,7 @@ public class SecurityConfig {
return handler;
}
@Bean
+ @Order(Ordered.HIGHEST_PRECEDENCE + 2)
SecurityFilterChain wwwChain(HttpSecurity http) throws Exception {
http.addFilterBefore(wwwAuthenticationFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests(authorize -> authorize
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;
+ }
}
diff --git a/src/main/java/com/juick/service/SignatureService.java b/src/main/java/com/juick/service/SignatureService.java
new file mode 100644
index 00000000..12d9d67b
--- /dev/null
+++ b/src/main/java/com/juick/service/SignatureService.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2008-2022, 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.service;
+
+import com.juick.KeystoreManager;
+import com.juick.www.api.activity.model.objects.Actor;
+import org.apache.commons.lang3.StringUtils;
+import org.tomitribe.auth.signatures.*;
+import java.io.IOException;
+import java.util.*;
+
+public class SignatureService {
+
+ private KeystoreManager keystoreManager;
+
+ public SignatureService(KeystoreManager keystoreManager) {
+ this.keystoreManager = keystoreManager;
+ }
+
+ public String addSignature(Actor from, String host, String method, String path, String dateString,
+ String digestHeader) throws IOException {
+ return addSignature(from, host, method, path, dateString, digestHeader, keystoreManager);
+ }
+
+ public String addSignature(Actor from, String host, String method, String path, String dateString,
+ String digestHeader, KeystoreManager keystoreManager) throws IOException {
+ List<String> requiredHeaders = StringUtils.isEmpty(digestHeader)
+ ? Arrays.asList("(request-target)", "host", "date")
+ : Arrays.asList("(request-target)", "host", "date", "digest");
+ Signature templateSignature = new Signature(from.getPublicKey().getId(), "rsa-sha256", null, requiredHeaders);
+ Map<String, String> headers = new HashMap<>();
+ headers.put("host", host);
+ headers.put("date", dateString);
+ if (StringUtils.isNotEmpty(digestHeader)) {
+ headers.put("digest", digestHeader);
+ }
+ Signer signer = new Signer(keystoreManager.getPrivateKey(), templateSignature);
+ Signature signature = signer.sign(method.toLowerCase(), path, headers);
+ // remove "Signature: " from result
+ return signature.toString().substring(10);
+ }
+}
diff --git a/src/main/java/com/juick/service/WebfingerService.java b/src/main/java/com/juick/service/WebfingerService.java
new file mode 100644
index 00000000..dc978763
--- /dev/null
+++ b/src/main/java/com/juick/service/WebfingerService.java
@@ -0,0 +1,58 @@
+package com.juick.service;
+
+import java.net.URI;
+import java.util.Collections;
+
+import javax.inject.Inject;
+
+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.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;
+
+@Component
+public class WebfingerService {
+ private static final Logger logger = LoggerFactory.getLogger("ActivityPub");
+
+ private final RestTemplate restClient;
+ @Inject
+ public WebfingerService(final RestTemplate restClient) {
+ this.restClient = restClient;
+ }
+ 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);
+ 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());
+ }
+ }
+ }
+ } catch (RestClientException e) {
+ logger.warn("Cannot discover person {}: {}", acct, e.getMessage());
+ }
+ return URI.create(StringUtils.EMPTY);
+ }
+}
diff --git a/src/main/java/com/juick/service/security/BearerTokenAuthenticationFilter.java b/src/main/java/com/juick/service/security/BearerTokenAuthenticationFilter.java
index 2e96a594..f4e73b12 100644
--- a/src/main/java/com/juick/service/security/BearerTokenAuthenticationFilter.java
+++ b/src/main/java/com/juick/service/security/BearerTokenAuthenticationFilter.java
@@ -19,7 +19,6 @@ package com.juick.service.security;
import com.juick.service.UserService;
import com.juick.service.security.entities.JuickUser;
-import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import jakarta.servlet.FilterChain;
@@ -52,9 +51,9 @@ public class BearerTokenAuthenticationFilter extends BaseAuthenticationFilter {
}
@Override
- protected void doFilterInternal(@Nonnull HttpServletRequest request,
- @Nonnull HttpServletResponse response,
- @Nonnull FilterChain filterChain) throws ServletException, IOException {
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
if (authenticationIsRequired()) {
var headers = Collections.list(request.getHeaderNames())
.stream()
diff --git a/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java b/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java
index 5f6a730e..a851ef36 100644
--- a/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java
+++ b/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java
@@ -17,8 +17,8 @@
package com.juick.service.security;
-import com.juick.SignatureManager;
import com.juick.model.User;
+import com.juick.service.ActivityPubService;
import com.juick.service.UserService;
import com.juick.service.security.entities.JuickUser;
import jakarta.servlet.FilterChain;
@@ -31,7 +31,6 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
-import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
@@ -39,39 +38,37 @@ import java.util.stream.Collectors;
public class HTTPSignatureAuthenticationFilter extends BaseAuthenticationFilter {
- private final SignatureManager signatureManager;
+ private final ActivityPubService signatureManager;
private final UserService userService;
public HTTPSignatureAuthenticationFilter(
- final SignatureManager signatureManager,
+ final ActivityPubService activityPubService,
final UserService userService) {
- this.signatureManager = signatureManager;
+ this.signatureManager = activityPubService;
this.userService = userService;
}
@Override
- protected void doFilterInternal(@Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response,
- @Nonnull FilterChain filterChain) throws IOException, ServletException {
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+ FilterChain filterChain) throws IOException, ServletException {
if (authenticationIsRequired()) {
Map<String, String> headers = Collections.list(request.getHeaderNames())
.stream()
.collect(Collectors.toMap(String::toLowerCase, request::getHeader));
- if (StringUtils.isNotEmpty(headers.get("signature"))) {
- User user = signatureManager.verifySignature(request.getMethod(), request.getRequestURI(), headers);
- String userUri = user.getUri().toString();
- if (!user.isAnonymous() || userUri.length() > 0) {
- if (userUri.length() == 0) {
- User userWithPassword = userService.getUserByName(user.getName());
- userWithPassword.setAuthHash(userService.getHashByUID(userWithPassword.getUid()));
- Authentication authentication = new UsernamePasswordAuthenticationToken(
- new JuickUser(user), userWithPassword.getCredentials(), JuickUser.USER_AUTHORITY);
- SecurityContextHolder.getContext().setAuthentication(authentication);
- } else {
- // anonymous must have with uri
- Authentication authentication = new AnonymousAuthenticationToken(userUri,
- new JuickUser(user), JuickUser.ANONYMOUS_AUTHORITY);
- SecurityContextHolder.getContext().setAuthentication(authentication);
- }
+ var user = signatureManager.verifyActor(request.getMethod(), request.getRequestURI(), headers);
+ String userUri = user.getUri().toString();
+ if (!user.isAnonymous() || userUri.length() > 0) {
+ if (userUri.length() == 0) {
+ User userWithPassword = userService.getUserByName(user.getName());
+ userWithPassword.setAuthHash(userService.getHashByUID(userWithPassword.getUid()));
+ Authentication authentication = new UsernamePasswordAuthenticationToken(
+ new JuickUser(user), userWithPassword.getCredentials(), JuickUser.USER_AUTHORITY);
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ } else {
+ // anonymous must have with uri
+ Authentication authentication = new AnonymousAuthenticationToken(userUri,
+ new JuickUser(user), JuickUser.ANONYMOUS_AUTHORITY);
+ SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
diff --git a/src/main/java/com/juick/util/ActivityPubRequestInterceptor.java b/src/main/java/com/juick/util/ActivityPubRequestInterceptor.java
index 1876cc90..138a3d09 100644
--- a/src/main/java/com/juick/util/ActivityPubRequestInterceptor.java
+++ b/src/main/java/com/juick/util/ActivityPubRequestInterceptor.java
@@ -30,7 +30,7 @@ public class ActivityPubRequestInterceptor implements ClientHttpRequestIntercept
@Override
public @NonNull ClientHttpResponse intercept(HttpRequest request, @NonNull byte[] body,
- ClientHttpRequestExecution execution) throws IOException {
+ ClientHttpRequestExecution execution) throws IOException {
request.getHeaders().set(HttpHeaders.USER_AGENT, "Juick/2.x");
return execution.execute(request, body);
}
diff --git a/src/main/java/com/juick/www/api/Users.java b/src/main/java/com/juick/www/api/Users.java
index f7c24d8d..124632d0 100644
--- a/src/main/java/com/juick/www/api/Users.java
+++ b/src/main/java/com/juick/www/api/Users.java
@@ -72,11 +72,6 @@ public class Users {
@Inject
private ApplicationEventPublisher applicationEventPublisher;
- @RequestMapping(value = "/api/auth", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
- public String getAuthToken(@ModelAttribute User visitor) {
- return userService.getHashByUID(visitor.getUid());
- }
-
@RequestMapping(value = "/api/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public List<User> doGetUsers(
@ModelAttribute User visitor,
@@ -105,7 +100,7 @@ public class Users {
SecureUser me = new SecureUser();
me.setUid(visitor.getUid());
me.setName(visitor.getName());
- me.setAuthHash(getAuthToken(visitor));
+ me.setAuthHash(userService.getHashByUID(visitor.getUid()));
List<Integer> unread = messagesService.getUnread(visitor);
me.setUnread(unread);
me.setUnreadCount(unread.size());
diff --git a/src/main/java/com/juick/www/controllers/Compat.java b/src/main/java/com/juick/www/controllers/Compat.java
index 936a8e5c..300a0855 100644
--- a/src/main/java/com/juick/www/controllers/Compat.java
+++ b/src/main/java/com/juick/www/controllers/Compat.java
@@ -17,8 +17,11 @@
package com.juick.www.controllers;
-import com.juick.SignatureManager;
+import com.juick.service.ActivityPubService;
+import com.juick.service.WebfingerService;
import com.juick.util.HttpNotFoundException;
+import com.juick.www.api.activity.model.Context;
+import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -30,7 +33,9 @@ import javax.inject.Inject;
@Controller
public class Compat {
@Inject
- private SignatureManager signatureManager;
+ private WebfingerService webfingerService;
+ @Inject
+ private ActivityPubService activityPubService;
@GetMapping("/share")
public RedirectView share(@RequestParam String text, RedirectAttributes attributes) {
attributes.addAttribute("body", text);
@@ -38,9 +43,10 @@ public class Compat {
}
@GetMapping("/mention")
public RedirectView mention(@RequestParam String username) {
- var profile = signatureManager.discoverPerson(username);
- if (profile.isPresent()) {
- return new RedirectView(profile.get().getUrl());
+ var uri = webfingerService.discoverAccountURI(username, MediaType.valueOf(Context.ACTIVITY_MEDIA_TYPE));
+ if (!uri.toASCIIString().isEmpty()) {
+ var context = activityPubService.get(uri).orElseThrow(HttpNotFoundException::new);
+ return new RedirectView(context.getUrl());
}
throw new HttpNotFoundException();
}
diff --git a/src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java b/src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java
index 14faaa5d..bf4b72fd 100644
--- a/src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java
+++ b/src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java
@@ -25,7 +25,6 @@ import io.pebbletemplates.pebble.template.PebbleTemplate;
import java.util.Collection;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
/**
* Created by vitalyster on 23.05.2017.
diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java
index 51ffb876..04a0802a 100644
--- a/src/test/java/com/juick/server/tests/ServerTests.java
+++ b/src/test/java/com/juick/server/tests/ServerTests.java
@@ -64,6 +64,7 @@ import io.pebbletemplates.pebble.PebbleEngine;
import io.pebbletemplates.pebble.error.PebbleException;
import io.pebbletemplates.pebble.template.PebbleTemplate;
import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
@@ -85,11 +86,14 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.ApplicationEventPublisher;
+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.http.client.MockClientHttpRequest;
+import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.TestPropertySource;
@@ -203,7 +207,9 @@ public class ServerTests {
@Value("${ios_app_id:}")
private String appId;
@Inject
- private SignatureManager signatureManager;
+ private ActivityPubService activityPubService;
+ @Inject
+ private SignatureService signatureService;
@Inject
private ActivityPubManager activityPubManager;
@Inject
@@ -212,8 +218,6 @@ public class ServerTests {
private WebApp webApp;
@Inject
private RestTemplate apClient;
- @Inject
- private Profile profileController;
@Value("classpath:snapshots/activity/testuser.json")
private Resource testuserResponse;
@@ -274,6 +278,8 @@ public class ServerTests {
private User serviceUser;
@Inject
private User archiveUser;
+ @Inject
+ private ConversionService conversionService;
private static User ugnich, freefd;
static String ugnichName, ugnichPassword, freefdName, freefdPassword;
@@ -527,9 +533,6 @@ public class ServerTests {
public void testAllUnAuthorized() throws Exception {
mockMvc.perform(get("/api/")).andExpect(status().isMovedPermanently());
- mockMvc.perform(get("/api/auth")).andExpect(status().isUnauthorized())
- .andExpect(header().exists("WwW-Authenticate"));
-
mockMvc.perform(get("/api/home")).andExpect(status().isUnauthorized());
mockMvc.perform(get("/api/messages/recommended")).andExpect(status().isUnauthorized());
@@ -672,20 +675,6 @@ public class ServerTests {
}
@Test
- public void performRequestsWithIssuedToken() throws Exception {
- String ugnichHash = userService.getHashByUID(ugnich.getUid());
- mockMvc.perform(get("/api/home")).andExpect(status().isUnauthorized());
- mockMvc.perform(get("/api/auth")).andExpect(status().isUnauthorized());
- mockMvc.perform(get("/api/auth").with(httpBasic(ugnichName, "wrongpassword")))
- .andExpect(status().isUnauthorized());
- MvcResult result = mockMvc.perform(get("/api/auth").with(httpBasic(ugnichName, ugnichPassword)))
- .andExpect(status().isOk()).andReturn();
- String authHash = result.getResponse().getContentAsString();
- assertThat(authHash, equalTo(ugnichHash));
- mockMvc.perform(get("/api/home").param("hash", ugnichHash)).andExpect(status().isOk());
- }
-
- @Test
public void registerForNotificationsTests() throws Exception {
String token = "123456";
ExternalToken registration = new ExternalToken(null, "apns", token, null);
@@ -1592,7 +1581,7 @@ public class ServerTests {
String userName = "oldschooluser";
String userPassword = "";
userService.createUser(userName, userPassword);
- mockMvc.perform(get("/api/auth").with(httpBasic(userName, userPassword)))
+ mockMvc.perform(get("/api/me").with(httpBasic(userName, userPassword)))
.andExpect(status().isUnauthorized());
mockMvc.perform(
post("/login")
@@ -2060,9 +2049,9 @@ public class ServerTests {
create.setId(replyNote.getId());
create.setActor("http://localhost:8080/u/freefd");
create.setObject(replyNote);
- signatureManager.post(
- (Actor) signatureManager.getContext(URI.create("http://localhost:8080/u/freefd")).get(),
- (Actor) signatureManager.getContext(URI.create("http://localhost:8080/u/ugnich")).get(),
+ activityPubService.post(
+ (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)
@@ -2077,13 +2066,20 @@ public class ServerTests {
}
@Test
- public void signingSpec() throws IOException, NoSuchAlgorithmException {
- Actor from = (Actor) signatureManager.getContext(URI.create("http://localhost:8080/u/freefd")).get();
- Actor to = (Actor) signatureManager.getContext(URI.create("http://localhost:8080/u/ugnich")).get();
+ public void signingSpec() throws Exception {
+ Actor from = (Actor) activityPubService.get(URI.create("http://localhost:8080/u/freefd")).get();
+ Actor to = (Actor) activityPubService.get(URI.create("http://localhost:8080/u/ugnich")).get();
Follow follow = new Follow();
follow.setActor("http://localhost:8080/u/freefd");
follow.setObject(new Context("http://localhost:8080/u/ugnich"));
- signatureManager.post(from, to, follow);
+ var result = activityPubService.post(from, to, follow);
+ assertThat(result, is(HttpStatusCode.valueOf(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
@@ -2093,15 +2089,15 @@ public class ServerTests {
Instant now = Instant.now();
String requestDate = DateFormattersHolder.getHttpDateFormatter().format(now);
mockMvc.perform(get("/api/me").header("Date", requestDate)).andExpect(status().isUnauthorized());
- String testHost = "localhost";
- Actor ugnichPerson = profileController.getUser("ugnich");
+ String testHost = "localhost:8080";
+ Actor ugnichPerson = conversionService.convert(ugnich, Actor.class);
now = Instant.now();
requestDate = DateFormattersHolder.getHttpDateFormatter().format(now);
- String signatureString = signatureManager.addSignature(ugnichPerson, testHost, "GET", meUri,
+ 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();
+ .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(),
@@ -2116,7 +2112,7 @@ public class ServerTests {
.andRespond(withSuccess(testuserResponseString, MediaType.APPLICATION_JSON));
restServiceServer.expect(times(4), requestTo(testuserkeyUri))
.andRespond(withSuccess(testuserResponseString, MediaType.APPLICATION_JSON));
- Person testuser = (Person) signatureManager.getContext(testuserUri).get();
+ Person testuser = (Person) activityPubService.get(testuserUri).get();
assertThat(testuser.getPublicKey().getPublicKeyPem(), is(testKeystoreManager.getPublicKeyPem()));
Instant now2 = Instant.now();
String testRequestDate = DateFormattersHolder.getHttpDateFormatter().format(now2);
@@ -2124,7 +2120,7 @@ public class ServerTests {
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 = signatureManager.addSignature(testuser, testHost, "POST", inboxUri,
+ 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)
@@ -2149,7 +2145,7 @@ public class ServerTests {
var digestHeader = "SHA-256=" + new String(Base64.encodeBase64(digest));
var now2 = Instant.now();
String inboxUri = "/api/inbox";
- String testHost = "localhost";
+ String testHost = "localhost:8080";
URI testAppUri = URI.create("https://example.com/actor");
String testappResponseString = IOUtils.toString(testappResponse.getInputStream(),
StandardCharsets.UTF_8);
@@ -2158,9 +2154,9 @@ public class ServerTests {
MockRestServiceServer restServiceServer = MockRestServiceServer.createServer(apClient);
restServiceServer.expect(times(2), requestTo(testAppUri))
.andRespond(withSuccess(testappResponseString, MediaType.APPLICATION_JSON));
- Application testapp = (Application) signatureManager.getContext(testAppUri).get();
+ Application testapp = (Application) activityPubService.get(testAppUri).get();
assertThat(testapp.getPublicKey().getPublicKeyPem(), is(testKeystoreManager.getPublicKeyPem()));
- var testSignatureString = signatureManager.addSignature(testapp, "localhost", "POST", inboxUri,
+ var testSignatureString = signatureService.addSignature(testapp, testHost, "POST", inboxUri,
testRequestDate,
digestHeader, testKeystoreManager);
mockMvc.perform(post(inboxUri).header("Host", testHost).header("Date", testRequestDate)
@@ -2390,18 +2386,18 @@ public class ServerTests {
IOUtils.toString(testSuspendedUserResponse.getInputStream(),
StandardCharsets.UTF_8),
MediaType.APPLICATION_JSON));
- Person testuser = (Person) signatureManager.getContext(URI.create(delete.getObject().getId())).get();
+ 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 = signatureManager.addSignature(testuser, "localhost", "POST", inboxUri,
+ 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());
+ .header("Signature", testSignatureString)).andExpect(status().isAccepted());
apClient.setRequestFactory(originalRequestFactory);
Mockito.verify(deleteListener, Mockito.times(1)).onApplicationEvent(deleteEventCaptor.capture());
DeleteUserEvent receivedEvent = deleteEventCaptor.getValue();
@@ -2737,6 +2733,14 @@ public class ServerTests {
.andExpect(redirectedUrlPattern("**/settings?continue")).andReturn();
mockMvc.perform(securedResourceAccess.session(session)).andExpect(status().isOk());
}
+
+ @Test
+ @Transactional
+ public void signedGetTest() throws Exception {
+ var protectedAccount = "http://localhost:8080/u/ugnich";
+ var context = activityPubService.get(URI.create(protectedAccount));
+ assertThat(context.get().getId(), is(protectedAccount));
+ }
/*
@Test
public void tokenAuth() throws Exception {