From 9d493ac966db2e016f73efcdcfb65dfb10bf8114 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Wed, 12 Dec 2018 15:12:27 +0300 Subject: UserInfo -> User --- src/main/java/com/juick/server/configuration/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main/java/com/juick/server/configuration/SecurityConfig.java') diff --git a/src/main/java/com/juick/server/configuration/SecurityConfig.java b/src/main/java/com/juick/server/configuration/SecurityConfig.java index f53cc531..16b61172 100644 --- a/src/main/java/com/juick/server/configuration/SecurityConfig.java +++ b/src/main/java/com/juick/server/configuration/SecurityConfig.java @@ -98,7 +98,7 @@ public class SecurityConfig { .authorizeRequests() .antMatchers(HttpMethod.OPTIONS).permitAll() .antMatchers("/api/", "/api/messages", "/api/messages/discussions", "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk", - "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", "/api/_google", "/api/signup", "/api/inbox", "/api/u/**", "/.well-known/webfinger", "/.well-known/x-nodeinfo2", "/rss/**", "/api/events").permitAll() + "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", "/api/_google", "/api/signup", "/api/inbox", "/api/u/**", "/.well-known/webfinger", "/.well-known/x-nodeinfo2", "/rss/**", "/api/events", "/api/info/**").permitAll() .anyRequest().hasRole("USER") .and() .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY) -- cgit v1.2.3 From 8268ccc461608d45bdf60e58ccf49256e8cc993c Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Mon, 17 Dec 2018 11:58:50 +0300 Subject: CORS for ActivityPub endpoints --- .../juick/server/configuration/SecurityConfig.java | 30 ++++++++++++---------- .../java/com/juick/server/tests/ServerTests.java | 5 ++++ 2 files changed, 21 insertions(+), 14 deletions(-) (limited to 'src/main/java/com/juick/server/configuration/SecurityConfig.java') diff --git a/src/main/java/com/juick/server/configuration/SecurityConfig.java b/src/main/java/com/juick/server/configuration/SecurityConfig.java index 16b61172..7145e9d5 100644 --- a/src/main/java/com/juick/server/configuration/SecurityConfig.java +++ b/src/main/java/com/juick/server/configuration/SecurityConfig.java @@ -69,6 +69,20 @@ public class SecurityConfig { public UserDetailsService userDetailsService() { return new JuickUserDetailsService(userService); } + @Bean + static CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowedOrigins(Collections.singletonList("*")); + configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "OPTIONS", "DELETE")); + configuration.setAllowedHeaders(Collections.singletonList("*")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/api/**", configuration); + source.registerCorsConfiguration("/u/**", configuration); + source.registerCorsConfiguration("/n/**", configuration); + return source; + } @Configuration @Order(1) @@ -98,7 +112,7 @@ public class SecurityConfig { .authorizeRequests() .antMatchers(HttpMethod.OPTIONS).permitAll() .antMatchers("/api/", "/api/messages", "/api/messages/discussions", "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk", - "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", "/api/_google", "/api/signup", "/api/inbox", "/api/u/**", "/.well-known/webfinger", "/.well-known/x-nodeinfo2", "/rss/**", "/api/events", "/api/info/**").permitAll() + "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", "/api/_google", "/api/signup", "/api/inbox", "/api/events", "/api/info/**").permitAll() .anyRequest().hasRole("USER") .and() .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY) @@ -122,19 +136,6 @@ public class SecurityConfig { return new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED); } - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - - configuration.setAllowedOrigins(Collections.singletonList("*")); - configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "OPTIONS", "DELETE")); - configuration.setAllowedHeaders(Collections.singletonList("*")); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/api/**", configuration); - - return source; - } @Override public void configure(WebSecurity web) { web.debug(false); @@ -182,6 +183,7 @@ public class SecurityConfig { .anyRequest().permitAll() .and() .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY) + .and().cors().configurationSource(corsConfigurationSource()) .and().sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .invalidSessionUrl("/") diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index d1cdac8b..5902220f 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -488,6 +488,11 @@ public class ServerTests { .header("Origin", "http://api.example.net")) .andExpect(status().isOk()) .andExpect(header().string("Access-Control-Allow-Origin", "*")); + mockMvc.perform( + get("/u/ugnich") + .header("Origin", "http://api.example.net")) + .andExpect(status().isOk()) + .andExpect(header().string("Access-Control-Allow-Origin", "*")); } @Test -- cgit v1.2.3 From 809ef60e18bb8ab7c95db93b7777f3c0ffb30872 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Thu, 20 Dec 2018 09:41:32 +0300 Subject: HTTPSignatureAuthenticationFilter --- .../java/com/juick/server/KeystoreManager.java | 9 +-- .../java/com/juick/server/SignatureManager.java | 60 +++++++++++------ .../com/juick/server/api/activity/Profile.java | 26 ++----- .../server/configuration/BaseWebConfiguration.java | 10 +++ .../juick/server/configuration/SecurityConfig.java | 5 ++ .../HTTPSignatureAuthenticationFilter.java | 68 +++++++++++++++++++ .../configuration/TestActivityConfiguration.java | 19 ++++++ .../java/com/juick/server/tests/ServerTests.java | 75 +++++++++++++++++++-- src/test/resources/mocks/activity/testfollow.json | 15 +++++ src/test/resources/mocks/activity/testuser.json | 27 ++++++++ src/test/resources/test.p12 | Bin 0 -> 2386 bytes 11 files changed, 260 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java create mode 100644 src/test/java/com/juick/server/configuration/TestActivityConfiguration.java create mode 100644 src/test/resources/mocks/activity/testfollow.json create mode 100644 src/test/resources/mocks/activity/testuser.json create mode 100644 src/test/resources/test.p12 (limited to 'src/main/java/com/juick/server/configuration/SecurityConfig.java') diff --git a/src/main/java/com/juick/server/KeystoreManager.java b/src/main/java/com/juick/server/KeystoreManager.java index 67a24f11..3ae7b866 100644 --- a/src/main/java/com/juick/server/KeystoreManager.java +++ b/src/main/java/com/juick/server/KeystoreManager.java @@ -19,20 +19,17 @@ import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; import java.util.stream.Collectors; -@Component public class KeystoreManager { private static final Logger logger = LoggerFactory.getLogger("com.juick.server"); - @Value("${keystore:juick.p12}") - private String keystore; - @Value("${keystore_password:secret}") + private String keystorePassword; private KeyStore ks; private KeyManagerFactory kmf; - @PostConstruct - public void init() { + public KeystoreManager(String keystore, String keystorePassword) { + this.keystorePassword = keystorePassword; try (InputStream ksIs = new FileInputStream(keystore)) { ks = KeyStore.getInstance("PKCS12"); ks.load(ksIs, keystorePassword.toCharArray()); diff --git a/src/main/java/com/juick/server/SignatureManager.java b/src/main/java/com/juick/server/SignatureManager.java index 9ecdaad5..23f5c37a 100644 --- a/src/main/java/com/juick/server/SignatureManager.java +++ b/src/main/java/com/juick/server/SignatureManager.java @@ -2,6 +2,7 @@ package com.juick.server; import com.fasterxml.jackson.databind.ObjectMapper; import com.juick.User; +import com.juick.model.AnonymousUser; import com.juick.server.api.activity.model.Context; import com.juick.server.api.activity.model.objects.Person; import com.juick.server.api.webfinger.model.Account; @@ -11,7 +12,6 @@ import com.juick.util.DateFormattersHolder; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; @@ -53,28 +53,43 @@ public class SignatureManager { URI inbox = uriComponentsBuilder.build().toUri(); Instant now = Instant.now(); String requestDate = DateFormattersHolder.getHttpDateFormatter().format(now); - Signature templateSignature = new Signature(from.getPublicKey().getId(), "rsa-sha256", null, - "(request-target)", "host", "date"); - Signer signer = new Signer(keystoreManager.getPrivateKey(), templateSignature); - Map headers = new HashMap<>(); - headers.put("host", inbox.getHost()); - headers.put("date", requestDate); - Signature signature = signer.sign("POST", inbox.getPath(), headers); + String host = inbox.getPort() > 0 ? String.format("%s:%d", inbox.getHost(), inbox.getPort()) : inbox.getHost(); + String signatureString = addSignature(from, host, "POST", inbox.getPath(), requestDate); + HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.add("Content-Type", Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE); requestHeaders.add("Date", requestDate); - requestHeaders.add("Signature", signature.toString().substring(10)); + requestHeaders.add("Host", host); + requestHeaders.add("Signature", signatureString); HttpEntity request = new HttpEntity<>(Context.build(data), requestHeaders); - //boolean valid = verifySignature(Signature.fromString(requestHeaders.getFirst("Signature")), - // keystoreManager.getPublicKey(), "POST", inbox.getPath(), headers); logger.info("Sending context: {}", jsonMapper.writeValueAsString(data)); logger.info("Request date: {}", requestDate); ResponseEntity response = apClient.postForEntity(inbox, request, Void.class); logger.info("accepted follower: {}", response.getStatusCodeValue()); + } + + public String addSignature(Person from, String host, String method, String path, String dateString) throws IOException { + return addSignature(from, host, method, path, dateString, keystoreManager); + } + public String addSignature(Person from, String host, String method, String path, String dateString, KeystoreManager keystoreManager) throws IOException { + Signature templateSignature = new Signature(from.getPublicKey().getId(), "rsa-sha256", null, + "(request-target)", "host", "date"); + Map headers = new HashMap<>(); + headers.put("host", host); + headers.put("date", dateString); + 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 headers) throws IOException { - Signature signature = Signature.fromString(headers.get("signature")); + String signatureString = headers.get("signature"); + if (StringUtils.isEmpty(signatureString)) { + return AnonymousUser.INSTANCE; + } + Signature signature = Signature.fromString(signatureString); Optional context = getContext(URI.create(signature.getKeyId())); if (context.isPresent() && context.get() instanceof Person) { Person person = (Person) context.get(); @@ -84,12 +99,16 @@ public class SignatureManager { try { boolean result = verifier.verify(method, path, headers); logger.info("signature is valid: {}", result); - User user = new User(); - user.setUri(URI.create(person.getId())); - if (key.equals(keystoreManager.getPublicKey())) { - return userService.getUserByName(person.getName()); + if (result) { + User user = new User(); + user.setUri(URI.create(person.getId())); + if (key.equals(keystoreManager.getPublicKey())) { + return userService.getUserByName(person.getName()); + } + return user; + } else { + return AnonymousUser.INSTANCE; } - return user; } catch (NoSuchAlgorithmException | SignatureException | IOException e) { throw new IOException("Invalid signature"); } @@ -110,9 +129,12 @@ public class SignatureManager { return Optional.empty(); } public Optional discoverPerson(String acct) { - Jid acctId = Jid.of(acct); + String[] accountParts = acct.split(":", 2); + String account = accountParts[0]; + int port = accountParts.length > 1 ? Integer.valueOf(accountParts[1]) : 80; + Jid acctId = Jid.of(account); URI resourceUri = UriComponentsBuilder.fromUriString( - String.format("https://%s/.well-known/webfinger?resource=acct:%s", acctId.getDomain(), acct)).build().toUri(); + String.format("http://%s:%d/.well-known/webfinger?resource=acct:%s", acctId.getDomain(), port, account)).build().toUri(); Account acctData = apClient.getForEntity(resourceUri, Account.class).getBody(); if (acctData != null) { for (Link l : acctData.getLinks()) { diff --git a/src/main/java/com/juick/server/api/activity/Profile.java b/src/main/java/com/juick/server/api/activity/Profile.java index 2614cded..404f0f84 100644 --- a/src/main/java/com/juick/server/api/activity/Profile.java +++ b/src/main/java/com/juick/server/api/activity/Profile.java @@ -44,6 +44,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -252,30 +253,11 @@ public class Profile { } @PostMapping(value = "/api/inbox", consumes = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE}) - public ResponseEntity processInbox(@RequestBody Activity activity, - @RequestHeader(name = "Host") String host, - @RequestHeader(name = "Date") String date, - @RequestHeader(name = "Digest", required = false) String digest, - @RequestHeader(name = "Content-Type") String contentType, - @RequestHeader(name = "User-Agent", required = false) String userAgent, - @RequestHeader(name = "Accept-Encoding", required = false) String acceptEncoding, - @RequestHeader(name = "Signature", required = false) String signature) throws Exception { - UriComponents componentsBuilder = ServletUriComponentsBuilder.fromCurrentRequestUri().build(); - Map headers = new HashMap<>(); - headers.put("host", host.split(":", 2)[0]); - headers.put("date", date); - headers.put("digest", digest); - headers.put("content-type", contentType); - headers.put("user-agent", userAgent); - headers.put("accept-encoding", acceptEncoding); - headers.put("signature", signature); - User signedUser = signatureManager.verifySignature( "POST", - componentsBuilder.getPath(), headers); - if ((StringUtils.isNotEmpty(signedUser.getUri().toString()) && signedUser.getUri().equals(URI.create(activity.getActor()))) || !signedUser.isAnonymous()) { + public ResponseEntity processInbox(@RequestBody Activity activity) throws Exception { + User visitor = UserUtils.getCurrentUser(); + if ((StringUtils.isNotEmpty(visitor.getUri().toString()) && visitor.getUri().equals(URI.create(activity.getActor()))) || !visitor.isAnonymous()) { if (activity instanceof Follow) { Follow followRequest = (Follow) activity; - String actor = followRequest.getActor(); - Person follower = (Person) signatureManager.getContext(URI.create(actor)).orElseThrow(HttpBadRequestException::new); applicationEventPublisher.publishEvent( new FollowEvent(this, followRequest)); return new ResponseEntity<>(HttpStatus.ACCEPTED); diff --git a/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java b/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java index 6a2a8142..16693995 100644 --- a/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java +++ b/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java @@ -17,6 +17,8 @@ package com.juick.server.configuration; +import com.juick.server.KeystoreManager; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.SchedulingConfigurer; @@ -36,6 +38,10 @@ import java.util.concurrent.Executors; @Configuration public class BaseWebConfiguration implements WebMvcConfigurer, SchedulingConfigurer { + @Value("${keystore:juick.p12}") + private String keystore; + @Value("${keystore_password:secret}") + private String keystorePassword; @Override public void configurePathMatch(PathMatchConfigurer configurer) { @@ -61,4 +67,8 @@ public class BaseWebConfiguration implements WebMvcConfigurer, SchedulingConfigu public ExecutorService executorService() { return Executors.newCachedThreadPool(); } + @Bean + public KeystoreManager keystoreManager() { + return new KeystoreManager(keystore, keystorePassword); + } } diff --git a/src/main/java/com/juick/server/configuration/SecurityConfig.java b/src/main/java/com/juick/server/configuration/SecurityConfig.java index 7145e9d5..d2d3ab13 100644 --- a/src/main/java/com/juick/server/configuration/SecurityConfig.java +++ b/src/main/java/com/juick/server/configuration/SecurityConfig.java @@ -17,7 +17,9 @@ package com.juick.server.configuration; +import com.juick.server.SignatureManager; import com.juick.service.UserService; +import com.juick.service.security.HTTPSignatureAuthenticationFilter; import com.juick.service.security.HashParamAuthenticationFilter; import com.juick.service.security.JuickUserDetailsService; import com.juick.service.security.deprecated.RequestParamHashRememberMeServices; @@ -93,6 +95,8 @@ public class SecurityConfig { private String webDomain; @Resource private UserService userService; + @Resource + private SignatureManager signatureManager; ApiConfig() { super(true); } @@ -109,6 +113,7 @@ public class SecurityConfig { protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/api/**") .addFilterBefore(apiAuthenticationFilter(), BasicAuthenticationFilter.class) + .addFilterBefore(new HTTPSignatureAuthenticationFilter(signatureManager, userService), BasicAuthenticationFilter.class) .authorizeRequests() .antMatchers(HttpMethod.OPTIONS).permitAll() .antMatchers("/api/", "/api/messages", "/api/messages/discussions", "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk", diff --git a/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java b/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java new file mode 100644 index 00000000..8332fc8c --- /dev/null +++ b/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java @@ -0,0 +1,68 @@ +package com.juick.service.security; + +import com.juick.User; +import com.juick.server.SignatureManager; +import com.juick.service.UserService; +import com.juick.service.security.entities.JuickUser; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.annotation.Nonnull; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; + +public class HTTPSignatureAuthenticationFilter extends OncePerRequestFilter { + + private final SignatureManager signatureManager; + private final UserService userService; + + + public HTTPSignatureAuthenticationFilter( + final SignatureManager signatureManager, + final UserService userService) { + this.signatureManager = signatureManager; + this.userService = userService; + } + @Override + protected void doFilterInternal(@Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response, @Nonnull FilterChain filterChain) throws IOException, ServletException { + if (authenticationIsRequired()) { + Map headers = Collections.list(request.getHeaderNames()) + .stream() + .collect(Collectors.toMap(String::toLowerCase, request::getHeader)); + User user = signatureManager.verifySignature(request.getMethod(), request.getRequestURI(), headers); + if (!user.isAnonymous()) { + String userUri = user.getUri().toString(); + if (userUri.length() == 0) { + User userWithPassword = userService.getUserByName(user.getName()); + userWithPassword.setAuthHash(userService.getHashByUID(userWithPassword.getUid())); + Authentication authentication = new UsernamePasswordAuthenticationToken(userWithPassword.getName(), userWithPassword.getCredentials()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } else { + Authentication authentication = new AnonymousAuthenticationToken(userUri, user, Collections.singletonList(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + } + + filterChain.doFilter(request, response); + } + + private boolean authenticationIsRequired() { + Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication(); + + return existingAuth == null || + !existingAuth.isAuthenticated() || + existingAuth instanceof AnonymousAuthenticationToken; + } +} diff --git a/src/test/java/com/juick/server/configuration/TestActivityConfiguration.java b/src/test/java/com/juick/server/configuration/TestActivityConfiguration.java new file mode 100644 index 00000000..5daf4900 --- /dev/null +++ b/src/test/java/com/juick/server/configuration/TestActivityConfiguration.java @@ -0,0 +1,19 @@ +package com.juick.server.configuration; + +import com.juick.server.KeystoreManager; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; + +import java.io.IOException; + +@Configuration +public class TestActivityConfiguration { + @Value("classpath:test.p12") + Resource keystoreFile; + @Bean + public KeystoreManager testKeystoreManager() throws IOException { + return new KeystoreManager(keystoreFile.getFile().getAbsolutePath(), "secret"); + } +} diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index fa2e2ce9..fedbaba0 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -65,10 +65,13 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; 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.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -76,6 +79,7 @@ import org.springframework.util.DigestUtils; import org.springframework.util.FileSystemUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import org.w3c.dom.Document; @@ -118,6 +122,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.client.ExpectedCount.times; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -130,9 +137,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @TestPropertySource(properties = { "broken_ssl_hosts=localhost,serverstorageisfull.tld", "ios_app_id=12345678.com.juick.ExampleApp", - "xmppbot_jid=juick@localhost/Juick", - "hostname=localhost", - "componentname=localhost", "spring.jackson.default-property-inclusion=non_default" }) @AutoConfigureMockMvc @@ -174,10 +178,6 @@ public class ServerTests { private ServerManager serverManager; @Inject private KeystoreManager keystoreManager; - @Value("${hostname:localhost}") - private Jid jid; - @Value("${xmppbot_jid:juick@localhost}") - private Jid botJid; @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") private String tmpDir; @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}") @@ -192,6 +192,18 @@ public class ServerTests { private ActivityPubManager activityPubManager; @Inject private WebApp webApp; + @Inject + private RestTemplate apClient; + + @Value("classpath:mocks/activity/testuser.json") + private Resource testuserResponse; + @Value("classpath:mocks/activity/testfollow.json") + private Resource testfollowRequest; + + @Inject + private KeystoreManager testKeystoreManager; + + private MockRestServiceServer restServiceServer; private static User ugnich, freefd, juick; static String ugnichName, ugnichPassword, freefdName, freefdPassword, juickName, juickPassword; @@ -1699,6 +1711,55 @@ public class ServerTests { signatureManager.post(from, to, follow); } @Test + public void serviceSignatureAuth() throws Exception { + String meUri = "/api/me"; + String testHost = "localhost:8080"; + Person ugnichPerson = (Person) signatureManager.discoverPerson("ugnich@localhost:8080").get(); + Instant now = Instant.now(); + String requestDate = DateFormattersHolder.getHttpDateFormatter().format(now); + String signatureString = signatureManager.addSignature(ugnichPerson, testHost, "GET", meUri, requestDate); + MvcResult me = mockMvc.perform(get("/api/me") + .header("Host", testHost) + .header("Date", requestDate) + .header("Signature", signatureString)) + .andExpect(status().isOk()) + .andReturn(); + User meUser = jsonMapper.readValue(me.getResponse().getContentAsString(), User.class); + assertThat(meUser, is(ugnich)); + String testuserResponseString = IOUtils.toString(testuserResponse.getInputStream(), StandardCharsets.UTF_8); + ClientHttpRequestFactory originalRequestFactory = apClient.getRequestFactory(); + restServiceServer = MockRestServiceServer.bindTo(apClient).build(); + URI testuserUri = URI.create("https://example.com/u/testuser"); + URI testuserkeyUri = URI.create("https://example.com/u/testuser#main-key"); + restServiceServer.expect(times(3), requestTo(testuserUri)) + .andRespond(withSuccess(testuserResponseString, MediaType.APPLICATION_JSON_UTF8)); + restServiceServer.expect(times(3), requestTo(testuserkeyUri)) + .andRespond(withSuccess(testuserResponseString, MediaType.APPLICATION_JSON_UTF8)); + Person testuser = (Person)signatureManager.getContext(testuserUri).get(); + Assert.assertThat(testuser.getPublicKey().getPublicKeyPem(), is(testKeystoreManager.getPublicKeyPem())); + Instant now2 = Instant.now(); + String testRequestDate = DateFormattersHolder.getHttpDateFormatter().format(now2); + String inboxUri = "/api/inbox"; + String testSignatureString = + signatureManager.addSignature(testuser, testHost, "POST", + inboxUri, testRequestDate, testKeystoreManager); + mockMvc.perform(post(inboxUri) + .header("Host", testHost) + .header("Date", testRequestDate) + .header("Signature", testSignatureString) + .contentType(Context.LD_JSON_MEDIA_TYPE) + .content(IOUtils.toByteArray(testfollowRequest.getInputStream()))) + .andExpect(status().isAccepted()); + mockMvc.perform(post(inboxUri) + .header("Host", "wronghost") + .header("Date", testRequestDate) + .header("Signature", testSignatureString) + .contentType(Context.LD_JSON_MEDIA_TYPE) + .content(IOUtils.toByteArray(testfollowRequest.getInputStream()))) + .andExpect(status().isUnauthorized()); + apClient.setRequestFactory(originalRequestFactory); + } + @Test public void hostmeta() throws Exception { MvcResult result = mockMvc.perform(get("/.well-known/host-meta")) .andExpect(status().isOk()).andReturn(); diff --git a/src/test/resources/mocks/activity/testfollow.json b/src/test/resources/mocks/activity/testfollow.json new file mode 100644 index 00000000..e308e52e --- /dev/null +++ b/src/test/resources/mocks/activity/testfollow.json @@ -0,0 +1,15 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value" + } + ], + "id": "https://example.com/12345678", + "type": "Follow", + "actor": "https://example.com/u/testuser", + "object": "http://localhost:8080/u/ugnich" +} \ No newline at end of file diff --git a/src/test/resources/mocks/activity/testuser.json b/src/test/resources/mocks/activity/testuser.json new file mode 100644 index 00000000..95fc2aa9 --- /dev/null +++ b/src/test/resources/mocks/activity/testuser.json @@ -0,0 +1,27 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value" + } + ], + "id": "https://example.com/u/testuser", + "type": "Person", + "following": "https://example.com/u/testuser/following", + "followers": "https://example.com/u/testuser/followers", + "inbox": "https://example.com/u/testuser/inbox", + "outbox": "https://example.com/u/testuser/outbox", + "preferredUsername": "testuser", + "url": "https://example.com/@testuser", + "publicKey": { + "id": "https://example.com/u/testuser#main-key", + "owner": "https://example.com/u/testuser", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiHKRdKFFeT4P/MVlNbxC\nbbgXOkEdeQzvJB/wAJgSYbUwm9SzNFzttePQXk3/MWoK2awWUInZTduVHsWt8zU7\nO3d9PAW6YH6L1oDkjgMLAb9aUWV2ClQWMwsn88WKK9Rb1WOmd8BrXjPfmeFK2ypQ\n9eg8aKpH36WAXiiaTDfBupBZ0Ki2+E87BrWxpbUeDC1dkV+zbl8BMm7X0rp+reoC\nYUWMcjQMzhMmQOXUd4zwJIDPZDMdF4beq/y6WPSUTVgjs4kPDS1HT60ATnsUqyPE\n6tuGxG4j0msb4TTre87PKxMU5YPOxSiqNL0O/3u9/2shVPpjDa/uy9W+VaeBHbFm\nSQIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "endpoints": { + "sharedInbox": "https://example.com/inbox" + } +} \ No newline at end of file diff --git a/src/test/resources/test.p12 b/src/test/resources/test.p12 new file mode 100644 index 00000000..7f7457eb Binary files /dev/null and b/src/test/resources/test.p12 differ -- cgit v1.2.3 From 7f937bdffea9b0219f4fce079ccbad8d233a03b9 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Tue, 19 Feb 2019 16:09:06 +0300 Subject: allow avatar endpoint for anonymous --- src/main/java/com/juick/server/configuration/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main/java/com/juick/server/configuration/SecurityConfig.java') diff --git a/src/main/java/com/juick/server/configuration/SecurityConfig.java b/src/main/java/com/juick/server/configuration/SecurityConfig.java index d2d3ab13..c5cb4a55 100644 --- a/src/main/java/com/juick/server/configuration/SecurityConfig.java +++ b/src/main/java/com/juick/server/configuration/SecurityConfig.java @@ -116,7 +116,7 @@ public class SecurityConfig { .addFilterBefore(new HTTPSignatureAuthenticationFilter(signatureManager, userService), BasicAuthenticationFilter.class) .authorizeRequests() .antMatchers(HttpMethod.OPTIONS).permitAll() - .antMatchers("/api/", "/api/messages", "/api/messages/discussions", "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk", + .antMatchers("/api/", "/api/messages", "/api/avatar", "/api/messages/discussions", "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk", "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", "/api/_google", "/api/signup", "/api/inbox", "/api/events", "/api/info/**").permitAll() .anyRequest().hasRole("USER") .and() -- cgit v1.2.3 From e9a3361b31ae49a801b03bd2950e38896b1f3863 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Sun, 24 Feb 2019 19:29:36 +0300 Subject: /.well-known/nodeinfo --- .../java/com/juick/server/api/xnodeinfo2/Info.java | 35 ++++++++++++++++++++-- .../server/api/xnodeinfo2/model/NodeInfo.java | 24 ++++++++++++++- .../juick/server/api/xnodeinfo2/model/Server.java | 6 ++++ .../server/api/xnodeinfo2/model/ServiceInfo.java | 3 ++ .../juick/server/api/xnodeinfo2/model/Usage.java | 3 ++ .../server/api/xnodeinfo2/model/UserStats.java | 3 ++ .../juick/server/configuration/SecurityConfig.java | 7 +++-- .../java/com/juick/server/tests/ServerTests.java | 25 ++++++++++++++++ 8 files changed, 101 insertions(+), 5 deletions(-) (limited to 'src/main/java/com/juick/server/configuration/SecurityConfig.java') diff --git a/src/main/java/com/juick/server/api/xnodeinfo2/Info.java b/src/main/java/com/juick/server/api/xnodeinfo2/Info.java index 36e36bdd..7ecd02cb 100644 --- a/src/main/java/com/juick/server/api/xnodeinfo2/Info.java +++ b/src/main/java/com/juick/server/api/xnodeinfo2/Info.java @@ -1,15 +1,22 @@ package com.juick.server.api.xnodeinfo2; +import com.cliqset.xrd.Link; +import com.cliqset.xrd.XRD; +import com.fasterxml.jackson.annotation.JsonView; import com.juick.server.api.xnodeinfo2.model.*; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponentsBuilder; import javax.inject.Inject; +import java.net.URI; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Arrays; +import java.util.Collections; @RestController public class Info { @@ -18,9 +25,9 @@ public class Info { @Inject private JdbcTemplate jdbcTemplate; - @GetMapping("/.well-known/x-nodeinfo2") - public NodeInfo showNodeInfo() { + private NodeInfo getCurrentNodeInfo(String version) { NodeInfo nodeInfo = new NodeInfo(); + nodeInfo.setVersion(version); Server server = new Server(); server.setBaseUrl(baseUri); server.setName("Juick"); @@ -47,4 +54,28 @@ public class Info { nodeInfo.setUsage(usage); return nodeInfo; } + + @GetMapping(value = "/.well-known/x-nodeinfo2", consumes = MediaType.APPLICATION_JSON_VALUE) + @JsonView(NodeInfo.XNodeInfoView.class) + public NodeInfo showXNodeInfo() { + return getCurrentNodeInfo("1.0"); + } + + @GetMapping(value = "/.well-known/nodeinfo", consumes = MediaType.APPLICATION_JSON_VALUE) + public XRD getNodeInfoLinks() { + Link nodeinfo = new Link(); + nodeinfo.setRel(URI.create("http://nodeinfo.diaspora.software/ns/schema/2.0")); + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(baseUri); + uriComponentsBuilder.replacePath("/api/nodeinfo/2.0"); + nodeinfo.setHref(uriComponentsBuilder.build().toUri()); + XRD xrd = new XRD(); + xrd.setLinks(Collections.singletonList(nodeinfo)); + return xrd; + } + + @GetMapping(value = "/api/nodeinfo/2.0", consumes = MediaType.APPLICATION_JSON_VALUE) + @JsonView(NodeInfo.NodeInfoView.class) + public NodeInfo showNodeInfo() { + return getCurrentNodeInfo("2.0"); + } } diff --git a/src/main/java/com/juick/server/api/xnodeinfo2/model/NodeInfo.java b/src/main/java/com/juick/server/api/xnodeinfo2/model/NodeInfo.java index 06fe354f..44bd4325 100644 --- a/src/main/java/com/juick/server/api/xnodeinfo2/model/NodeInfo.java +++ b/src/main/java/com/juick/server/api/xnodeinfo2/model/NodeInfo.java @@ -1,27 +1,43 @@ package com.juick.server.api.xnodeinfo2.model; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; + import java.util.List; public class NodeInfo { + private String version; + private Server server; private List protocols; private ServiceInfo services; + @JsonView({XNodeInfoView.class, NodeInfoView.class}) public String getVersion() { - return "1.0"; + return version; + } + + public void setVersion(String version) { + this.version = version; } + @JsonView(XNodeInfoView.class) public Server getServer() { return server; } + @JsonView(NodeInfoView.class) + public Server getSoftware() { + return server; + } public void setServer(Server server) { this.server = server; } + @JsonView({XNodeInfoView.class, NodeInfoView.class}) public List getProtocols() { return protocols; } @@ -30,6 +46,7 @@ public class NodeInfo { this.protocols = protocols; } + @JsonView({XNodeInfoView.class, NodeInfoView.class}) public ServiceInfo getServices() { return services; } @@ -38,12 +55,14 @@ public class NodeInfo { this.services = services; } + @JsonView({XNodeInfoView.class, NodeInfoView.class}) public boolean getOpenRegistrations() { return true; } private Usage usage; + @JsonView({XNodeInfoView.class, NodeInfoView.class}) public Usage getUsage() { return usage; } @@ -51,4 +70,7 @@ public class NodeInfo { public void setUsage(Usage usage) { this.usage = usage; } + + public interface NodeInfoView {} + public interface XNodeInfoView {} } diff --git a/src/main/java/com/juick/server/api/xnodeinfo2/model/Server.java b/src/main/java/com/juick/server/api/xnodeinfo2/model/Server.java index a772d268..08c1a696 100644 --- a/src/main/java/com/juick/server/api/xnodeinfo2/model/Server.java +++ b/src/main/java/com/juick/server/api/xnodeinfo2/model/Server.java @@ -1,11 +1,14 @@ package com.juick.server.api.xnodeinfo2.model; +import com.fasterxml.jackson.annotation.JsonView; + public class Server { private String baseUrl; private String name; private String software; private String version; + @JsonView(NodeInfo.XNodeInfoView.class) public String getBaseUrl() { return baseUrl; } @@ -14,6 +17,7 @@ public class Server { this.baseUrl = baseUrl; } + @JsonView({NodeInfo.NodeInfoView.class, NodeInfo.XNodeInfoView.class}) public String getName() { return name; } @@ -22,6 +26,7 @@ public class Server { this.name = name; } + @JsonView(NodeInfo.XNodeInfoView.class) public String getSoftware() { return software; } @@ -30,6 +35,7 @@ public class Server { this.software = software; } + @JsonView({NodeInfo.NodeInfoView.class, NodeInfo.XNodeInfoView.class}) public String getVersion() { return version; } diff --git a/src/main/java/com/juick/server/api/xnodeinfo2/model/ServiceInfo.java b/src/main/java/com/juick/server/api/xnodeinfo2/model/ServiceInfo.java index 5b6d2baa..0114488f 100644 --- a/src/main/java/com/juick/server/api/xnodeinfo2/model/ServiceInfo.java +++ b/src/main/java/com/juick/server/api/xnodeinfo2/model/ServiceInfo.java @@ -1,7 +1,10 @@ package com.juick.server.api.xnodeinfo2.model; +import com.fasterxml.jackson.annotation.JsonView; + import java.util.List; +@JsonView({NodeInfo.NodeInfoView.class, NodeInfo.XNodeInfoView.class}) public class ServiceInfo { private List inbound; private List outbound; diff --git a/src/main/java/com/juick/server/api/xnodeinfo2/model/Usage.java b/src/main/java/com/juick/server/api/xnodeinfo2/model/Usage.java index e04ea48b..f270296f 100644 --- a/src/main/java/com/juick/server/api/xnodeinfo2/model/Usage.java +++ b/src/main/java/com/juick/server/api/xnodeinfo2/model/Usage.java @@ -1,5 +1,8 @@ package com.juick.server.api.xnodeinfo2.model; +import com.fasterxml.jackson.annotation.JsonView; + +@JsonView({NodeInfo.NodeInfoView.class, NodeInfo.XNodeInfoView.class}) public class Usage { private UserStats users; private int localPosts; diff --git a/src/main/java/com/juick/server/api/xnodeinfo2/model/UserStats.java b/src/main/java/com/juick/server/api/xnodeinfo2/model/UserStats.java index 515661e3..cca31be6 100644 --- a/src/main/java/com/juick/server/api/xnodeinfo2/model/UserStats.java +++ b/src/main/java/com/juick/server/api/xnodeinfo2/model/UserStats.java @@ -1,5 +1,8 @@ package com.juick.server.api.xnodeinfo2.model; +import com.fasterxml.jackson.annotation.JsonView; + +@JsonView({NodeInfo.NodeInfoView.class, NodeInfo.XNodeInfoView.class}) public class UserStats { private int total; private int activeHalfyear; diff --git a/src/main/java/com/juick/server/configuration/SecurityConfig.java b/src/main/java/com/juick/server/configuration/SecurityConfig.java index c5cb4a55..df0da16e 100644 --- a/src/main/java/com/juick/server/configuration/SecurityConfig.java +++ b/src/main/java/com/juick/server/configuration/SecurityConfig.java @@ -116,8 +116,11 @@ public class SecurityConfig { .addFilterBefore(new HTTPSignatureAuthenticationFilter(signatureManager, userService), BasicAuthenticationFilter.class) .authorizeRequests() .antMatchers(HttpMethod.OPTIONS).permitAll() - .antMatchers("/api/", "/api/messages", "/api/avatar", "/api/messages/discussions", "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk", - "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", "/api/_google", "/api/signup", "/api/inbox", "/api/events", "/api/info/**").permitAll() + .antMatchers("/api/", "/api/messages", "/api/avatar", "/api/messages/discussions", + "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk", + "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", + "/api/_google", "/api/signup", "/api/inbox", "/api/events", "/api/info/**", + "/api/nodeinfo/2.0").permitAll() .anyRequest().hasRole("USER") .and() .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY) diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index a509b36d..5c421f07 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -19,6 +19,7 @@ package com.juick.server.tests; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.gargoylesoftware.htmlunit.CookieManager; import com.gargoylesoftware.htmlunit.WebClient; @@ -1961,4 +1962,28 @@ public class ServerTests { Follow follow = jsonMapper.readValue(followData, Follow.class); assertThat(follow.getActor(), is("https://ussr.win/channel/zlax")); } + @Test + public void nodeinfo() throws Exception { + MvcResult nodeinfoXRD = mockMvc.perform(get("/.well-known/nodeinfo") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + JsonNode node = jsonMapper.readTree(nodeinfoXRD.getResponse().getContentAsString()); + assertThat(node.get("links"), notNullValue()); + String nodeinfoUrl = node.get("links").get(0).get("href").textValue(); + MvcResult nodeinfoData = mockMvc.perform(get(nodeinfoUrl) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + JsonNode nodeinfo = jsonMapper.readTree(nodeinfoData.getResponse().getContentAsString()); + assertThat(nodeinfo.get("software"), notNullValue()); + assertThat(nodeinfo.get("server"), nullValue()); + MvcResult xnodeinfoData = mockMvc.perform(get("/.well-known/x-nodeinfo2") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + JsonNode xnodeinfo = jsonMapper.readTree(xnodeinfoData.getResponse().getContentAsString()); + assertThat(xnodeinfo.get("server"), notNullValue()); + assertThat(xnodeinfo.get("software"), nullValue()); + } } -- cgit v1.2.3