/** * Copyright 2016 LinkedIn Corp. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ package com.github.ambry.frontend; import com.github.ambry.config.FrontendConfig; import com.github.ambry.messageformat.BlobInfo; import com.github.ambry.messageformat.BlobProperties; import com.github.ambry.protocol.GetOption; import com.github.ambry.rest.ResponseStatus; import com.github.ambry.rest.RestMethod; import com.github.ambry.rest.RestRequest; import com.github.ambry.rest.RestResponseChannel; import com.github.ambry.rest.RestServiceErrorCode; import com.github.ambry.rest.RestServiceException; import com.github.ambry.rest.RestUtils; import com.github.ambry.rest.SecurityService; import com.github.ambry.router.Callback; import com.github.ambry.router.FutureResult; import com.github.ambry.router.GetBlobOptions; import com.github.ambry.utils.Pair; import com.github.ambry.utils.Time; import com.github.ambry.utils.Utils; import java.util.Date; import java.util.GregorianCalendar; import java.util.concurrent.Future; /** * Default implementation of {@link SecurityService} for Ambry that doesn't do any validations, but just * sets the respective headers on response. */ class AmbrySecurityService implements SecurityService { private boolean isOpen; private final FrontendConfig frontendConfig; private final FrontendMetrics frontendMetrics; public AmbrySecurityService(FrontendConfig frontendConfig, FrontendMetrics frontendMetrics) { this.frontendConfig = frontendConfig; this.frontendMetrics = frontendMetrics; isOpen = true; } @Override public Future<Void> processRequest(RestRequest restRequest, Callback<Void> callback) { Exception exception = null; frontendMetrics.securityServiceProcessRequestRate.mark(); long startTimeMs = System.currentTimeMillis(); if (!isOpen) { exception = new RestServiceException("SecurityService is closed", RestServiceErrorCode.ServiceUnavailable); } else { if (restRequest == null) { throw new IllegalArgumentException("RestRequest is null"); } RestMethod restMethod = restRequest.getRestMethod(); switch (restMethod) { case GET: RestUtils.SubResource subresource = RestUtils.getBlobSubResource(restRequest); if (subresource != null) { switch (subresource) { case BlobInfo: case UserMetadata: break; default: exception = new RestServiceException("Sub-resource [" + subresource + "] not allowed for GET", RestServiceErrorCode.BadRequest); } } break; } } FutureResult<Void> futureResult = new FutureResult<Void>(); if (callback != null) { callback.onCompletion(null, exception); } futureResult.done(null, exception); frontendMetrics.securityServiceProcessRequestTimeInMs.update(System.currentTimeMillis() - startTimeMs); return futureResult; } @Override public Future<Void> processResponse(RestRequest restRequest, RestResponseChannel responseChannel, BlobInfo blobInfo, Callback<Void> callback) { Exception exception = null; frontendMetrics.securityServiceProcessResponseRate.mark(); long startTimeMs = System.currentTimeMillis(); FutureResult<Void> futureResult = new FutureResult<Void>(); if (!isOpen) { exception = new RestServiceException("SecurityService is closed", RestServiceErrorCode.ServiceUnavailable); } else { if (restRequest == null || responseChannel == null || blobInfo == null) { throw new IllegalArgumentException("One of the required params is null"); } try { GetBlobOptions options; responseChannel.setHeader(RestUtils.Headers.DATE, new GregorianCalendar().getTime()); RestMethod restMethod = restRequest.getRestMethod(); switch (restMethod) { case HEAD: options = RestUtils.buildGetBlobOptions(restRequest.getArgs(), null, GetOption.None); responseChannel.setStatus(options.getRange() == null ? ResponseStatus.Ok : ResponseStatus.PartialContent); responseChannel.setHeader(RestUtils.Headers.LAST_MODIFIED, new Date(blobInfo.getBlobProperties().getCreationTimeInMs())); setHeadResponseHeaders(blobInfo, options, responseChannel); break; case GET: responseChannel.setStatus(ResponseStatus.Ok); RestUtils.SubResource subResource = RestUtils.getBlobSubResource(restRequest); responseChannel.setHeader(RestUtils.Headers.LAST_MODIFIED, new Date(blobInfo.getBlobProperties().getCreationTimeInMs())); if (subResource == null) { Long ifModifiedSinceMs = getIfModifiedSinceMs(restRequest); if (ifModifiedSinceMs != null && RestUtils.toSecondsPrecisionInMs(blobInfo.getBlobProperties().getCreationTimeInMs()) <= ifModifiedSinceMs) { responseChannel.setStatus(ResponseStatus.NotModified); setCacheHeaders(blobInfo.getBlobProperties().isPrivate(), responseChannel); } else { options = RestUtils.buildGetBlobOptions(restRequest.getArgs(), null, GetOption.None); if (options.getRange() != null) { responseChannel.setStatus(ResponseStatus.PartialContent); } setGetBlobResponseHeaders(blobInfo, options, responseChannel); } } else { if (subResource.equals(RestUtils.SubResource.BlobInfo)) { setBlobPropertiesHeaders(blobInfo.getBlobProperties(), responseChannel); } } break; case POST: responseChannel.setStatus(ResponseStatus.Created); responseChannel.setHeader(RestUtils.Headers.CONTENT_LENGTH, 0); responseChannel.setHeader(RestUtils.Headers.CREATION_TIME, new Date(blobInfo.getBlobProperties().getCreationTimeInMs())); break; default: exception = new RestServiceException("Cannot process response for request with method " + restMethod, RestServiceErrorCode.InternalServerError); } } catch (RestServiceException e) { exception = e; } } futureResult.done(null, exception); if (callback != null) { callback.onCompletion(null, exception); } frontendMetrics.securityServiceProcessResponseTimeInMs.update(System.currentTimeMillis() - startTimeMs); return futureResult; } @Override public void close() { isOpen = false; } /** * Fetches the {@link RestUtils.Headers#IF_MODIFIED_SINCE} value in epoch time if present * @param restRequest the {@link RestRequest} that needs to be parsed * @return the {@link RestUtils.Headers#IF_MODIFIED_SINCE} value in epoch time if present */ private Long getIfModifiedSinceMs(RestRequest restRequest) { if (restRequest.getArgs().get(RestUtils.Headers.IF_MODIFIED_SINCE) != null) { return RestUtils.getTimeFromDateString((String) restRequest.getArgs().get(RestUtils.Headers.IF_MODIFIED_SINCE)); } return null; } /** * Sets the required headers in the HEAD response. * @param blobInfo the {@link BlobInfo} to refer to while setting headers. * @param options the {@link GetBlobOptions} associated with the request. * @param restResponseChannel the {@link RestResponseChannel} to set headers on. * @throws RestServiceException if there was any problem setting the headers. */ private void setHeadResponseHeaders(BlobInfo blobInfo, GetBlobOptions options, RestResponseChannel restResponseChannel) throws RestServiceException { BlobProperties blobProperties = blobInfo.getBlobProperties(); if (blobProperties.getContentType() != null) { restResponseChannel.setHeader(RestUtils.Headers.CONTENT_TYPE, blobProperties.getContentType()); } restResponseChannel.setHeader(RestUtils.Headers.ACCEPT_RANGES, RestUtils.BYTE_RANGE_UNITS); long contentLength = blobProperties.getBlobSize(); if (options.getRange() != null) { Pair<String, Long> rangeAndLength = RestUtils.buildContentRangeAndLength(options.getRange(), contentLength); restResponseChannel.setHeader(RestUtils.Headers.CONTENT_RANGE, rangeAndLength.getFirst()); contentLength = rangeAndLength.getSecond(); } restResponseChannel.setHeader(RestUtils.Headers.CONTENT_LENGTH, contentLength); setBlobPropertiesHeaders(blobProperties, restResponseChannel); } /** * Sets the required headers in the response. * @param blobInfo the {@link BlobInfo} to refer to while setting headers. * @param options the {@link GetBlobOptions} associated with the request. * @param restResponseChannel the {@link RestResponseChannel} to set headers on. * @throws RestServiceException if there was any problem setting the headers. */ private void setGetBlobResponseHeaders(BlobInfo blobInfo, GetBlobOptions options, RestResponseChannel restResponseChannel) throws RestServiceException { BlobProperties blobProperties = blobInfo.getBlobProperties(); restResponseChannel.setHeader(RestUtils.Headers.BLOB_SIZE, blobProperties.getBlobSize()); restResponseChannel.setHeader(RestUtils.Headers.ACCEPT_RANGES, RestUtils.BYTE_RANGE_UNITS); long contentLength = blobProperties.getBlobSize(); if (options.getRange() != null) { Pair<String, Long> rangeAndLength = RestUtils.buildContentRangeAndLength(options.getRange(), contentLength); restResponseChannel.setHeader(RestUtils.Headers.CONTENT_RANGE, rangeAndLength.getFirst()); contentLength = rangeAndLength.getSecond(); } if (contentLength < frontendConfig.frontendChunkedGetResponseThresholdInBytes) { restResponseChannel.setHeader(RestUtils.Headers.CONTENT_LENGTH, contentLength); } if (blobProperties.getContentType() != null) { restResponseChannel.setHeader(RestUtils.Headers.CONTENT_TYPE, blobProperties.getContentType()); // Ensure browsers do not execute html with embedded exploits. if (blobProperties.getContentType().equals("text/html")) { restResponseChannel.setHeader("Content-Disposition", "attachment"); } } setCacheHeaders(blobProperties.isPrivate(), restResponseChannel); } /** * Sets headers that provide directions to proxies and caches. * @param isPrivate whether the blob is private. * @param restResponseChannel the channel that the response will be sent over * @throws RestServiceException if there is any problem setting the headers */ private void setCacheHeaders(boolean isPrivate, RestResponseChannel restResponseChannel) throws RestServiceException { if (isPrivate) { restResponseChannel.setHeader(RestUtils.Headers.EXPIRES, restResponseChannel.getHeader(RestUtils.Headers.DATE)); restResponseChannel.setHeader(RestUtils.Headers.CACHE_CONTROL, "private, no-cache, no-store, proxy-revalidate"); restResponseChannel.setHeader(RestUtils.Headers.PRAGMA, "no-cache"); } else { restResponseChannel.setHeader(RestUtils.Headers.EXPIRES, new Date(System.currentTimeMillis() + frontendConfig.frontendCacheValiditySeconds * Time.MsPerSec)); restResponseChannel.setHeader(RestUtils.Headers.CACHE_CONTROL, "max-age=" + frontendConfig.frontendCacheValiditySeconds); } } /** * Sets the blob properties in the headers of the response. * @param blobProperties the {@link BlobProperties} that need to be set in the headers. * @param restResponseChannel the {@link RestResponseChannel} that is used for sending the response. * @throws RestServiceException if there are any problems setting the header. */ private void setBlobPropertiesHeaders(BlobProperties blobProperties, RestResponseChannel restResponseChannel) throws RestServiceException { restResponseChannel.setHeader(RestUtils.Headers.BLOB_SIZE, blobProperties.getBlobSize()); restResponseChannel.setHeader(RestUtils.Headers.SERVICE_ID, blobProperties.getServiceId()); restResponseChannel.setHeader(RestUtils.Headers.CREATION_TIME, new Date(blobProperties.getCreationTimeInMs())); restResponseChannel.setHeader(RestUtils.Headers.PRIVATE, blobProperties.isPrivate()); if (blobProperties.getTimeToLiveInSeconds() != Utils.Infinite_Time) { restResponseChannel.setHeader(RestUtils.Headers.TTL, Long.toString(blobProperties.getTimeToLiveInSeconds())); } if (blobProperties.getContentType() != null) { restResponseChannel.setHeader(RestUtils.Headers.AMBRY_CONTENT_TYPE, blobProperties.getContentType()); } if (blobProperties.getOwnerId() != null) { restResponseChannel.setHeader(RestUtils.Headers.OWNER_ID, blobProperties.getOwnerId()); } } }