/** * 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.admin; import com.github.ambry.config.AdminConfig; import com.github.ambry.messageformat.BlobInfo; import com.github.ambry.messageformat.BlobProperties; 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.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 Admin that doesn't do any validations, but just * sets the respective headers on response. */ class AdminSecurityService implements SecurityService { private boolean isOpen; private final AdminConfig adminConfig; private final AdminMetrics adminMetrics; public AdminSecurityService(AdminConfig adminConfig, AdminMetrics adminMetrics) { this.adminConfig = adminConfig; this.adminMetrics = adminMetrics; isOpen = true; } @Override public Future<Void> processRequest(RestRequest restRequest, Callback<Void> callback) { Exception exception = null; adminMetrics.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"); } } FutureResult<Void> futureResult = new FutureResult<Void>(); if (callback != null) { callback.onCompletion(null, exception); } futureResult.done(null, exception); adminMetrics.securityServiceProcessRequestTimeInMs.update(System.currentTimeMillis() - startTimeMs); return futureResult; } @Override public Future<Void> processResponse(RestRequest restRequest, RestResponseChannel responseChannel, BlobInfo blobInfo, Callback<Void> callback) { Exception exception = null; adminMetrics.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 { responseChannel.setHeader(RestUtils.Headers.DATE, new GregorianCalendar().getTime()); RestMethod restMethod = restRequest.getRestMethod(); switch (restMethod) { case HEAD: responseChannel.setStatus(ResponseStatus.Ok); responseChannel.setHeader(RestUtils.Headers.LAST_MODIFIED, new Date(blobInfo.getBlobProperties().getCreationTimeInMs())); setHeadResponseHeaders(blobInfo, responseChannel); break; case GET: responseChannel.setStatus(ResponseStatus.Ok); RestUtils.SubResource subResource = RestUtils.getBlobSubResource(restRequest); if (subResource == null) { Long ifModifiedSinceMs = getIfModifiedSinceMs(restRequest); responseChannel.setHeader(RestUtils.Headers.LAST_MODIFIED, new Date(blobInfo.getBlobProperties().getCreationTimeInMs())); if (ifModifiedSinceMs != null && RestUtils.toSecondsPrecisionInMs(blobInfo.getBlobProperties().getCreationTimeInMs()) <= ifModifiedSinceMs) { responseChannel.setStatus(ResponseStatus.NotModified); setCacheHeaders(blobInfo.getBlobProperties().isPrivate(), responseChannel); } else { setGetBlobResponseHeaders(responseChannel, blobInfo); } } else { switch (subResource) { case BlobInfo: setBlobPropertiesHeaders(blobInfo.getBlobProperties(), responseChannel); // no break on purpose. case UserMetadata: responseChannel.setHeader(RestUtils.Headers.LAST_MODIFIED, new Date(blobInfo.getBlobProperties().getCreationTimeInMs())); break; } } 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); } adminMetrics.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. * @throws RestServiceException if there was any problem setting the headers. */ private void setHeadResponseHeaders(BlobInfo blobInfo, RestResponseChannel restResponseChannel) throws RestServiceException { BlobProperties blobProperties = blobInfo.getBlobProperties(); restResponseChannel.setHeader(RestUtils.Headers.CONTENT_LENGTH, blobProperties.getBlobSize()); if (blobProperties.getContentType() != null) { restResponseChannel.setHeader(RestUtils.Headers.CONTENT_TYPE, blobProperties.getContentType()); } setBlobPropertiesHeaders(blobProperties, restResponseChannel); } /** * Sets the required headers in the response. * @param blobInfo the {@link BlobInfo} to refer to while setting headers. * @throws RestServiceException if there was any problem setting the headers. */ private void setGetBlobResponseHeaders(RestResponseChannel restResponseChannel, BlobInfo blobInfo) throws RestServiceException { BlobProperties blobProperties = blobInfo.getBlobProperties(); restResponseChannel.setHeader(RestUtils.Headers.BLOB_SIZE, blobProperties.getBlobSize()); if (blobProperties.getBlobSize() < adminConfig.adminChunkedGetResponseThresholdInBytes) { restResponseChannel.setHeader(RestUtils.Headers.CONTENT_LENGTH, blobProperties.getBlobSize()); } 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() + adminConfig.adminCacheValiditySeconds * Time.MsPerSec)); restResponseChannel.setHeader(RestUtils.Headers.CACHE_CONTROL, "max-age=" + adminConfig.adminCacheValiditySeconds); } } /** * 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()); } } }