Skip to content

Commit

Permalink
Release 0.2.1 (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
levimdmiller authored Feb 18, 2021
1 parent 908bf77 commit 4c84621
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 86 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</parent>
<groupId>ca.levimiller</groupId>
<artifactId>sms-bridge</artifactId>
<version>0.2.0</version>
<version>0.2.1</version>
<name>sms-bridge</name>
<description>Sms Bridge for Matrix</description>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ca.levimiller.smsbridge.rest;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import java.util.UUID;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/attachment")
@Api(value = "/attachment", tags = "Api for attachments")
public interface AttachmentController {

@GetMapping("/{media_uid}")
@ApiOperation("Returns the attachment file")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Success", response = String.class),
@ApiResponse(code = 400, message = "Request not valid")})
ResponseEntity<InputStreamResource> getAttachment(@PathVariable("media_uid") UUID mediaUid);
}
12 changes: 0 additions & 12 deletions src/main/java/ca/levimiller/smsbridge/rest/TwilioController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,8 @@
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import java.util.UUID;
import javax.validation.Valid;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -31,11 +26,4 @@ public interface TwilioController {
@ApiResponse(code = 201, message = "Created", response = String.class),
@ApiResponse(code = 400, message = "Request not valid")})
void createSms(@Valid @RequestBody TwilioSmsDto sms);

@GetMapping("/attachment/{media_uid}")
@ApiOperation("Returns the attachment file")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Success", response = String.class),
@ApiResponse(code = 400, message = "Request not valid")})
ResponseEntity<InputStreamResource> getAttachment(@PathVariable("media_uid") UUID mediaUid);
}
46 changes: 46 additions & 0 deletions src/main/java/ca/levimiller/smsbridge/rest/impl/AttachmentApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package ca.levimiller.smsbridge.rest.impl;

import ca.levimiller.smsbridge.data.db.MediaRepository;
import ca.levimiller.smsbridge.data.model.Media;
import ca.levimiller.smsbridge.error.NotFoundException;
import ca.levimiller.smsbridge.rest.AttachmentController;
import ca.levimiller.smsbridge.service.FileService;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.UUID;
import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class AttachmentApi implements AttachmentController {
private final MediaRepository mediaRepository;
private final FileService fileService;

@Inject
public AttachmentApi(
MediaRepository mediaRepository,
@Qualifier("matrixFileService") FileService fileService) {
this.mediaRepository = mediaRepository;
this.fileService = fileService;
}

@Override
public ResponseEntity<InputStreamResource> getAttachment(UUID mediaUid) {
Media media = mediaRepository.findDistinctByUid(mediaUid)
.orElseThrow(() -> new NotFoundException("Requested attachment not found."));
try {
return ResponseEntity.ok()
.contentType(MediaType.valueOf(media.getContentType()))
.body(new InputStreamResource(fileService.getFileStream(media.getUrl())));
} catch (URISyntaxException | IOException e) {
log.error("Error getting attachment from matrix", e);
throw new NotFoundException("Media url malformed or doesn't exist.");
}
}
}
32 changes: 1 addition & 31 deletions src/main/java/ca/levimiller/smsbridge/rest/impl/TwilioApi.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
package ca.levimiller.smsbridge.rest.impl;

import ca.levimiller.smsbridge.data.db.MediaRepository;
import ca.levimiller.smsbridge.data.dto.TwilioSmsDto;
import ca.levimiller.smsbridge.data.model.Media;
import ca.levimiller.smsbridge.data.model.Message;
import ca.levimiller.smsbridge.data.transformer.twilio.MessageTransformer;
import ca.levimiller.smsbridge.error.NotFoundException;
import ca.levimiller.smsbridge.rest.TwilioController;
import ca.levimiller.smsbridge.service.ChatService;
import ca.levimiller.smsbridge.service.FileService;
import ca.levimiller.smsbridge.service.MessageService;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.UUID;
import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;

@Slf4j
Expand All @@ -28,22 +18,16 @@ public class TwilioApi implements TwilioController {
private final ChatService chatService;
private final MessageTransformer messageTransformer;
private final MessageService messageService;
private final MediaRepository mediaRepository;
private final FileService fileService;

@Inject
public TwilioApi(
@Qualifier("matrixChatService")
ChatService chatService,
MessageTransformer messageTransformer,
MessageService messageService,
MediaRepository mediaRepository,
@Qualifier("matrixFileService") FileService fileService) {
MessageService messageService) {
this.chatService = chatService;
this.messageTransformer = messageTransformer;
this.messageService = messageService;
this.mediaRepository = mediaRepository;
this.fileService = fileService;
}

@Override
Expand All @@ -56,18 +40,4 @@ public void createSms(TwilioSmsDto sms) {
messageService.save(message);
chatService.sendMessage(message);
}

@Override
public ResponseEntity<InputStreamResource> getAttachment(UUID mediaUid) {
Media media = mediaRepository.findDistinctByUid(mediaUid)
.orElseThrow(() -> new NotFoundException("Requested attachment not found."));
try {
return ResponseEntity.ok()
.contentType(MediaType.valueOf(media.getContentType()))
.body(new InputStreamResource(fileService.getFileStream(media.getUrl())));
} catch (URISyntaxException | IOException e) {
log.error("Error getting attachment from matrix", e);
throw new NotFoundException("Media url malformed or doesn't exist.");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package ca.levimiller.smsbridge.security;

import ca.levimiller.smsbridge.error.ForbiddenException;
import ca.levimiller.smsbridge.error.UnauthorizedException;
import com.twilio.security.RequestValidator;
import java.io.IOException;
import java.util.Arrays;
Expand All @@ -16,7 +14,9 @@
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import liquibase.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
Expand All @@ -26,6 +26,7 @@
/**
* https://www.twilio.com/docs/usage/tutorials/how-to-secure-your-servlet-app-by-validating-incoming-twilio-requests
*/
@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@Qualifier("twilioFilter")
Expand All @@ -41,30 +42,34 @@ public TwilioAuthenticationFilter(RequestValidator requestValidator) {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
boolean isValidRequest = false;
if (request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;

// Concatenates the request URL with the query string
String pathAndQueryUrl = getRequestUrlAndQueryString(httpRequest);
log.debug("Authenticating X-Twilio-Signature for request: {}", pathAndQueryUrl);

// Extracts only the POST parameters and converts the parameters Map type
Map<String, String> postParams = extractPostParams(httpRequest);
String signatureHeader = httpRequest.getHeader("X-Twilio-Signature");
if (StringUtils.isEmpty(signatureHeader)) {
throw new UnauthorizedException();
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization header needed");
return;
}

isValidRequest = requestValidator.validate(
boolean isValidRequest = requestValidator.validate(
pathAndQueryUrl,
postParams,
signatureHeader);
}

if (isValidRequest) {
chain.doFilter(request, response);
} else {
throw new ForbiddenException();
if (!isValidRequest) {
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Bad Authorization header");
return;
}
}

chain.doFilter(request, response);
}

private Map<String, String> extractPostParams(HttpServletRequest request) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
Expand All @@ -34,27 +35,20 @@ public void configure(WebSecurity web) {

@Override
protected void configure(HttpSecurity http) throws Exception {
// disable csrf for twilio as it uses a generated token to verify the server.
// No need for csrf between back end servers. (no cookies/basic auth)
http.csrf()
.ignoringAntMatchers("/twilio/**")
.and()
.authorizeRequests()
.ignoringAntMatchers("/matrix/**", "/attachment/**", "/twilio/**");

http.antMatcher("/attachment/**")
.addFilterAfter(twilioAuthenticationFilter, AnonymousAuthenticationFilter.class);

http.antMatcher("/twilio/**")
.addFilterAfter(twilioAuthenticationFilter, AnonymousAuthenticationFilter.class);

http.authorizeRequests()
.antMatchers("/twilio/**")
.authenticated()
.and()
.httpBasic();

http.csrf()
.ignoringAntMatchers("/matrix/**");
}

@Bean
FilterRegistrationBean<Filter> twilioFilterRegistration() {
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();

registrationBean.setFilter(twilioAuthenticationFilter);
registrationBean.addUrlPatterns("/twilio/**");
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); //set precedence
return registrationBean;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public void sendAttachment(Media attachment) {
try {
// proxy matrix attachments so twilio has access
String attachmentUrl = new URIBuilder(hostedUrlService.getBaseUrl())
.setPath("/twilio/attachment/" + attachment.getUid())
.setPath("/attachment/" + attachment.getUid())
.toString();

// send attachment
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package ca.levimiller.smsbridge.security;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import ca.levimiller.smsbridge.error.ForbiddenException;
import ca.levimiller.smsbridge.error.UnauthorizedException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.ImmutableMap;
import com.twilio.security.RequestValidator;
Expand All @@ -18,15 +15,14 @@
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequestWrapper;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

@SpringBootTest
class TwilioAuthenticationFilterTest {
Expand All @@ -35,7 +31,7 @@ class TwilioAuthenticationFilterTest {
private final Filter twilioAuthenticationFilter;

private MockHttpServletRequest request;
private ServletResponse response;
private HttpServletResponse response;
private FilterChain filterChain;

@Autowired
Expand All @@ -52,7 +48,7 @@ void setUp() throws URISyntaxException, JsonProcessingException {
request.setParameter("param2", "post-param");
request.setQueryString("param1=query-param");

response = new MockHttpServletResponse();
response = mock(HttpServletResponse.class);
filterChain = mock(FilterChain.class);

when(requestValidator.validate("http://localhost/endpoint?param1=query-param",
Expand All @@ -62,16 +58,18 @@ void setUp() throws URISyntaxException, JsonProcessingException {

@Test
void testNotHttpServletRequest() throws IOException, ServletException {
assertThrows(ForbiddenException.class, () -> twilioAuthenticationFilter.doFilter(
new ServletRequestWrapper(request), response, filterChain));
verify(filterChain, times(0)).doFilter(request, response);
ServletRequestWrapper servletRequest = new ServletRequestWrapper(request);
twilioAuthenticationFilter.doFilter(servletRequest, response, filterChain);
verify(filterChain, times(1)).doFilter(servletRequest, response);
}

@Test
void testNoSignature() throws IOException, ServletException {
request.removeHeader("X-Twilio-Signature");
assertThrows(UnauthorizedException.class, () -> twilioAuthenticationFilter.doFilter(
request, response, filterChain));

twilioAuthenticationFilter.doFilter(request, response, filterChain);
verify(response, times(1))
.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization header needed");
verify(filterChain, times(0)).doFilter(request, response);
}

Expand Down Expand Up @@ -103,8 +101,9 @@ void testSuccess_NoQueryString() throws IOException, ServletException {
void testNotValid() throws IOException, ServletException {
request.setScheme("https");

assertThrows(ForbiddenException.class, () ->
twilioAuthenticationFilter.doFilter(request, response, filterChain));
twilioAuthenticationFilter.doFilter(request, response, filterChain);
verify(response, times(1))
.sendError(HttpServletResponse.SC_FORBIDDEN, "Bad Authorization header");
verify(requestValidator, times(1))
.validate("https://localhost:80/endpoint?param1=query-param",
ImmutableMap.of("param2", "post-param"), "twilio-hash");
Expand Down

0 comments on commit 4c84621

Please sign in to comment.