package won.owner.web.rest;
import org.apache.commons.httpclient.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestTemplate;
import won.owner.model.User;
import won.owner.model.UserNeed;
import won.owner.service.impl.WONUserDetailService;
import won.protocol.rest.LinkedDataRestBridge;
import won.protocol.rest.RDFMediaType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
/**
* User: ypanchenko
* Date: 03.09.2015
*
* This controller at Owner server-side serves as a bridge for Owner client-side to obtain linked data from a Node:
* because the linked data on a Node can have restricted access based on WebID, only Owner server-side can provide the
* client's certificate as proof of having the private key from client's published WebID. Because of this, Owner
* client-side has to ask Owner-server side to query Node for it, instead of querying directly from Owner client-side.
*/
@Controller
@RequestMapping("/rest/linked-data")
public class BridgeForLinkedDataController {
@Autowired
private WONUserDetailService wonUserDetailService;
@Autowired
private LinkedDataRestBridge linkedDataRestBridge;
final Logger logger = LoggerFactory.getLogger(getClass());
/* //for some reason this cannot be used as parameter in restTemplate.execute()
private final ResponseExtractor httpResponseResponseExtractor = new ResponseExtractor<ClientHttpResponse>()
{
@Override
public ClientHttpResponse extractData(final ClientHttpResponse response)
throws IOException {
return response;
}
};*/
@RequestMapping(
value = "/",
method = RequestMethod.GET,
produces={"*/*"}
)
public void fetchResource(
@RequestParam("uri") String resourceUri,
@RequestParam(value ="requester", required = false) String requesterWebId,
final HttpServletResponse response, final HttpServletRequest request) throws IOException {
// prepare restTestmplate that can deal with webID certificate
RestTemplate restTemplate = null;
//no webID requested? - don't use one!
if (requesterWebId == null) {
restTemplate = linkedDataRestBridge.getRestTemplate();
} else {
//check if the requesterWebID actually is an URI
try {
new URI(requesterWebId);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Parameter 'requester' must be a URI. Actual value was: '" +
requesterWebId + "'");
}
//check if the currently logged in user owns that webid:
if (currentUserHasIdentity(requesterWebId)) {
//yes: let them use it
restTemplate = linkedDataRestBridge.getRestTemplate(requesterWebId);
} else {
//no: that's fishy, but we let them make the request without the webid
restTemplate = linkedDataRestBridge.getRestTemplate();
}
}
// prepare headers to be passed in request for linked data resource
final HttpHeaders requestHeaders = extractLinkedDataRequestRelevantHeaders(request);
restTemplate.execute(
URI.create(resourceUri),
HttpMethod.valueOf(request.getMethod()),
new RequestCallback() {
@Override
public void doWithRequest(final ClientHttpRequest request)
throws IOException {
request.getHeaders()
.setAll(requestHeaders.toSingleValueMap());
}
},
new ResponseExtractor<Object>() {
@Override
public ClientHttpResponse extractData(final ClientHttpResponse originalResponse)
throws IOException {
prepareBridgeResponseOutputStream(originalResponse, response);
//we don't really need to return anything, so we don't
return null;
}
});
// by this point, the response is constructed and is ready to be returned to the client
}
private void prepareBridgeResponseOutputStream(final ClientHttpResponse originalResponse, final HttpServletResponse response)
throws IOException {
// create response headers
MediaType originalResponseMediaType = originalResponse.getHeaders().getContentType();
if (originalResponseMediaType == null) {
// No content-type header: we assume there is no body, as in our application this only happens
// with 304 NOT MODIFIED responses. We don't copy the body to the response. We log a debug message though
logger.debug("no Content-Type header found in response from server. Assuming no body, not attempting to copy " +
"body");
copyLinkedDataResponseRelevantHeaders(originalResponse.getHeaders(), response);
response.setStatus(originalResponse.getRawStatusCode());
} else {
if (RDFMediaType.isRDFMediaType(originalResponseMediaType)) {
copyLinkedDataResponseRelevantHeaders(originalResponse.getHeaders(), response);
response.setStatus(originalResponse.getRawStatusCode());
// create response body
copyResponseBody(originalResponse, response);
// close response output stream
} else {
//the content type is not an RDF media type. We don't like to handle such requests. indicate this with
//the BAD GATEWAY response status
copyResponseBody(originalResponse, response);
response.setStatus(HttpStatus.SC_BAD_GATEWAY);
/*response.getOutputStream().print("The nodes' response was of type " + originalResponseMediaType
+ ". For security reasons the owner-server only forwards responses of the following types " +
RDFMediaType.rdfMediaTypes.toString());*/
}
}
response.getOutputStream().flush();
response.getOutputStream().close();
}
private void copyResponseBody(final ClientHttpResponse fromResponse, final HttpServletResponse toResponse)
throws IOException {
InputStream is = fromResponse.getBody();
if (is != null) {
org.apache.commons.io.IOUtils.copy(is, toResponse.getOutputStream());
}
}
/**
* Currently copies all the headers from the given headers to the headers of the response.
* TODO check with spec if any other headers should not be copied
* (e.g , it seems Transfer-Encoding should not be copied by proxies, see https://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.4.6)
*
* @param fromHeaders
* @param toResponse
*/
private void copyLinkedDataResponseRelevantHeaders(final HttpHeaders fromHeaders, final HttpServletResponse toResponse) {
for (String headerName : fromHeaders.keySet()) {
for (String headerValue : fromHeaders.get(headerName)) {
if ((headerName != "Transfer-Encoding") || (headerValue != "chunked")) {
//we allow all transfer codings except chunked, because we don't do chunking here!
toResponse.addHeader(headerName, headerValue);
}
}
}
}
/**
* Extract all headers that are relevant in a request for linked data resource
* @param request
* @return
*/
private HttpHeaders extractLinkedDataRequestRelevantHeaders(final HttpServletRequest request) {
HttpHeaders headers = new HttpHeaders();
copyHeader(HttpHeaders.ACCEPT, request, headers);
copyHeader("Prefer", request, headers);
copyHeader(HttpHeaders.ACCEPT_LANGUAGE, request, headers);
copyHeader(HttpHeaders.ACCEPT_ENCODING, request, headers);
copyHeader(HttpHeaders.USER_AGENT, request, headers);
copyHeader(HttpHeaders.CACHE_CONTROL, request, headers);
copyHeader(HttpHeaders.IF_NONE_MATCH, request, headers);
return headers;
}
private void copyHeader(final String headerName, final HttpServletRequest fromRequest, final HttpHeaders toHeaders) {
Enumeration<String> values = fromRequest.getHeaders(headerName);
while (values.hasMoreElements()) {
toHeaders.add(headerName, values.nextElement());
}
}
/**
* Check if the current user has the claimed identity represented by web-id of the need.
* I.e. if the identity is that of the need that belongs to the user - return true, otherwise - false.
*
* @param requesterWebId
* @return
*/
private boolean currentUserHasIdentity(final String requesterWebId) {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
User user = (User) wonUserDetailService.loadUserByUsername(username);
Set<URI> needUris = getUserNeedUris(user);
if (needUris.contains(URI.create(requesterWebId))) {
return true;
}
return false;
}
private Set<URI> getUserNeedUris(final User user) {
Set<URI> needUris = new HashSet<>();
for (UserNeed userNeed : user.getUserNeeds()) {
needUris.add(userNeed.getUri());
}
return needUris;
}
}