All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.milton.http.http11.DefaultHttp11ResponseHandler Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package io.milton.http.http11;

import io.milton.http.Auth;
import io.milton.http.AuthenticationService;
import io.milton.http.ExternalIdentityProvider;
import io.milton.http.Range;
import io.milton.http.Request;
import io.milton.http.Response;
import io.milton.http.Response.Status;
import io.milton.http.entity.BufferingGetableResourceEntity;
import io.milton.http.entity.GetableResourceEntity;
import io.milton.http.entity.PartialEntity;
import io.milton.http.exceptions.BadRequestException;
import io.milton.http.exceptions.NotAuthorizedException;
import io.milton.http.exceptions.NotFoundException;
import io.milton.resource.BufferingControlResource;
import io.milton.resource.GetableResource;
import io.milton.resource.Resource;

import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;

import io.milton.servlet.ServletRequest;
import io.milton.servlet.ServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;

/**
 *
 */
public class DefaultHttp11ResponseHandler implements Http11ResponseHandler, Bufferable {

	private static final Logger log = LoggerFactory.getLogger(DefaultHttp11ResponseHandler.class);
	private static final String miltonVerson;

	static {
		Properties props = new Properties();
		try {
			props.load(DefaultHttp11ResponseHandler.class.getResourceAsStream("/milton.properties"));
		} catch (IOException ex) {
			log.warn("Failed lot load milton properties file", ex);
		}
		miltonVerson = props.getProperty("milton.version");
	}

	public enum BUFFERING {

		always,
		never,
		whenNeeded
	}

	private final AuthenticationService authenticationService;
	private final ETagGenerator eTagGenerator;
	private final ContentGenerator contentGenerator;
	private CacheControlHelper cacheControlHelper = new DefaultCacheControlHelper();
	private int maxMemorySize = 100000;
	private BUFFERING buffering;
	private String multipartBoundary = UUID.randomUUID().toString();

	public DefaultHttp11ResponseHandler(AuthenticationService authenticationService, ETagGenerator eTagGenerator, ContentGenerator contentGenerator) {
		this.authenticationService = authenticationService;
		this.eTagGenerator = eTagGenerator;
		this.contentGenerator = contentGenerator;
	}

	/**
	 * Defaults to io.milton.http.http11.DefaultCacheControlHelper
	 *
	 * @return
	 */
	public CacheControlHelper getCacheControlHelper() {
		return cacheControlHelper;
	}

	public void setCacheControlHelper(CacheControlHelper cacheControlHelper) {
		this.cacheControlHelper = cacheControlHelper;
	}

	@Override
	public String generateEtag(Resource r) {
		return eTagGenerator.generateEtag(r);
	}

	@Override
	public void respondWithOptions(Resource resource, Response response, Request request, List methodsAllowed) {
		setRespondCommonHeaders(response, resource, Status.SC_OK, request.getAuthorization());
		response.setAllowHeader(methodsAllowed);
		response.setContentLengthHeader((long) 0); // Note that setting content length must be done last for tomcat5	
	}

	@Override
	public void respondNotFound(Response response, Request request) {
		response.setStatus(Response.Status.SC_NOT_FOUND);
		response.setContentTypeHeader("text/html");
		contentGenerator.generate(null, request, response, Status.SC_NOT_FOUND);
	}

	@Override
	public void respondUnauthorised(Resource resource, Response response, Request request) {
		if (authenticationService.canUseExternalAuth(resource, request)) {
			log.info("respondUnauthorised: use external authentication");
			initiateExternalAuth(resource, request, response);
		} else {
			Auth auth = request.getAuthorization();
			if (auth == null || auth.getTag() == null) {
				log.info("respondUnauthorised: no authenticated user, so return status: " + Response.Status.SC_UNAUTHORIZED);
				response.setStatus(Response.Status.SC_UNAUTHORIZED);
				List challenges = authenticationService.getChallenges(resource, request);
				response.setAuthenticateHeader(challenges);

			} else {
				log.info("respondUnauthorised: request has an authenticated user, so return status: " + Response.Status.SC_FORBIDDEN);
				response.setStatus(Response.Status.SC_FORBIDDEN);

			}
		}
	}

	@Override
	public void respondMethodNotImplemented(Resource resource, Response response, Request request) {
		response.setStatus(Response.Status.SC_NOT_IMPLEMENTED);
		contentGenerator.generate(resource, request, response, Status.SC_NOT_IMPLEMENTED);
	}

	@Override
	public void respondMethodNotAllowed(Resource res, Response response, Request request) {
		log.debug("method not allowed. handler: " + this.getClass().getName() + " resource: " + res.getClass().getName());
		response.setStatus(Response.Status.SC_METHOD_NOT_ALLOWED);
		contentGenerator.generate(res, request, response, Status.SC_METHOD_NOT_ALLOWED);
	}

	/**
	 *
	 * @param resource
	 * @param response
	 * @param request
	 * @param message - optional message to output in the body content
	 */
	@Override
	public void respondConflict(Resource resource, Response response, Request request, String message) {
		log.debug("respondConflict");
		response.setStatus(Response.Status.SC_CONFLICT);
		contentGenerator.generate(resource, request, response, Status.SC_CONFLICT);
	}

	@Override
	public void respondServerError(Request request, Response response, String reason) {
		response.setStatus(Status.SC_INTERNAL_SERVER_ERROR);
		contentGenerator.generate(null, request, response, Status.SC_INTERNAL_SERVER_ERROR);
	}

	@Override
	public void respondRedirect(Response response, Request request, String redirectUrl) {
		if (redirectUrl == null) {
			throw new NullPointerException("redirectUrl cannot be null");
		}
		log.trace("respondRedirect");
		// delegate to the response, because this can be server dependent
		try {
			ServletRequest.getRequest().getRequestDispatcher(redirectUrl.substring(redirectUrl.lastIndexOf("/"))).forward(ServletRequest.getRequest(), ServletResponse.getResponse());
		} catch (ServletException e) {


		} catch (IOException e) {
			e.printStackTrace();
		}

//		response.sendRedirect(redirectUrl);
//        response.setStatus(Response.Status.SC_MOVED_TEMPORARILY);
//        response.setLocationHeader(redirectUrl);
	}

	@Override
	public void respondExpectationFailed(Response response, Request request) {
		response.setStatus(Response.Status.SC_EXPECTATION_FAILED);
	}

	@Override
	public void respondCreated(Resource resource, Response response, Request request) {
//        log.debug( "respondCreated" );
		setRespondCommonHeaders(response, resource, Status.SC_CREATED, request.getAuthorization());
	}

	@Override
	public void respondNoContent(Resource resource, Response response, Request request) {
//        log.debug( "respondNoContent" );
		//response.setStatus(Response.Status.SC_OK);
		// see comments in http://www.ettrema.com:8080/browse/MIL-87
		setRespondCommonHeaders(response, resource, Status.SC_NO_CONTENT, request.getAuthorization());
	}

	@Override
	public void respondPartialContent(GetableResource resource, Response response, Request request, Map params, Range range) throws NotAuthorizedException, BadRequestException, NotFoundException {
		log.debug("respondPartialContent: " + range.getStart() + " - " + range.getFinish());
		response.setStatus(Response.Status.SC_PARTIAL_CONTENT);
		long st = range.getStart() == null ? 0 : range.getStart();
		long fn;
		Long cl = resource.getContentLength();
		if (range.getFinish() == null) {
			if (cl != null) {
				fn = cl - 1; // position is one less then length
			} else {
				log.warn("Couldnt calculate range end position because the resource is not reporting a content length, and no end position was requested by the client: " + resource.getName() + " - " + resource.getClass());
				fn = -1;
			}
		} else {
			if (cl != null && cl < range.getFinish()) {
				fn = cl - 1;
			} else if (cl == null) {
				log.warn("Couldnt calculate range end position because the resource is not reporting a content length, and no end position was requested by the client: " + resource.getName() + " - " + resource.getClass());
				fn = -1;
			} else {
				fn = range.getFinish();
			}
		}
		response.setContentRangeHeader(st, fn, cl);
		long contentLength = fn - st + 1;
		response.setDateHeader(new Date());
		String etag = eTagGenerator.generateEtag(resource);
		if (etag != null) {
			response.setEtag(etag);
		}
		//String acc = request.getAcceptHeader();
//		String ct = resource.getContentType(acc);
//		if (ct != null) {
//			response.setContentTypeHeader(ct);
//		}
		response.setContentLengthHeader(contentLength);
		response.setEntity(new GetableResourceEntity(resource, range, params, null));
	}

	/**
	 * Send a partial content response with multiple ranges
	 *
	 * @param resource
	 * @param response
	 * @param request
	 * @param params
	 * @param ranges
	 * @throws NotAuthorizedException
	 * @throws BadRequestException
	 * @throws NotFoundException
	 */
	@Override
	public void respondPartialContent(GetableResource resource, Response response, Request request, Map params, List ranges) throws NotAuthorizedException, BadRequestException, NotFoundException {
		log.debug("respondPartialContent - multiple ranges");
		response.setStatus(Response.Status.SC_PARTIAL_CONTENT);
		response.setAcceptRanges("bytes");
		response.setDateHeader(new Date());
		String etag = eTagGenerator.generateEtag(resource);
		if (etag != null) {
			response.setEtag(etag);
		}
		response.setContentTypeHeader("multipart/byteranges; boundary=" + multipartBoundary);
		String acc = request.getAcceptHeader();
		String ct = resource.getContentType(acc);

		response.setEntity(
				new PartialEntity(resource, ranges, params, ct, multipartBoundary)
		);

	}

	@Override
	public void respondHead(Resource resource, Response response, Request request) {
		//setRespondContentCommonHeaders(response, resource, Response.Status.SC_NO_CONTENT, request.getAuthorization());
		setRespondContentCommonHeaders(response, resource, Response.Status.SC_OK, request.getAuthorization());
		if (!(resource instanceof GetableResource)) {
			return;
		}
		GetableResource gr = (GetableResource) resource;
		String acc = request.getAcceptHeader();
		String ct = gr.getContentType(acc);
		if (ct != null) {
			ct = pickBestContentType(ct);
			if (ct != null) {
				response.setContentTypeHeader(ct);
			}
		}
		Long contentLength = gr.getContentLength();
		if (contentLength != null) {
			response.setContentLengthHeader(contentLength);
		} else {
			log.trace("No content length is available for HEAD request");
		}
	}

	@Override
	public void respondContent(Resource resource, Response response, Request request, Map params) throws NotAuthorizedException, BadRequestException, NotFoundException {
		log.debug("respondContent: " + resource.getClass());
		Auth auth = request.getAuthorization();
		setRespondContentCommonHeaders(response, resource, auth);
		if (resource instanceof GetableResource) {
			GetableResource gr = (GetableResource) resource;
			String acc = request.getAcceptHeader();
			String ct = gr.getContentType(acc);
			if (ct != null) {
				ct = pickBestContentType(ct);
				response.setContentTypeHeader(ct);
			}
			cacheControlHelper.setCacheControl(gr, response, request.getAuthorization());

			Long contentLength = gr.getContentLength();
			Boolean doBuffering = null;
			if (resource instanceof BufferingControlResource) {
				BufferingControlResource bcr = (BufferingControlResource) resource;
				doBuffering = bcr.isBufferingRequired();
			}
			if (doBuffering == null) {
				if (buffering == null || buffering == BUFFERING.whenNeeded) {
					doBuffering = (contentLength == null); // if no content length then we buffer content to find content length
				} else {
					doBuffering = (buffering == BUFFERING.always); // if not null or whenNeeded then buffering is explicitly enabled or disabled
				}
			}
			if (!doBuffering) {
				log.trace("sending content with known content length: " + contentLength);
				if (contentLength != null) {
					response.setContentLengthHeader(contentLength);
				}
				response.setEntity(new GetableResourceEntity(gr, params, ct));
			} else {
				BufferingGetableResourceEntity e = new BufferingGetableResourceEntity(gr, params, ct, contentLength, getMaxMemorySize());
				response.setEntity(e);
			}
		}
	}

	@Override
	public void respondNotModified(GetableResource resource, Response response, Request request) {
		log.trace("respondNotModified");
		response.setStatus(Response.Status.SC_NOT_MODIFIED);
		response.setDateHeader(new Date());
		String etag = eTagGenerator.generateEtag(resource);
		if (etag != null) {
			response.setEtag(etag);
		}

		// Note that we use a simpler modified date handling here then when
		// responding with content, because in a not-modified situation the
		// modified date MUST be that of the actual resource
		Date modDate = resource.getModifiedDate();
		response.setLastModifiedHeader(modDate);

		cacheControlHelper.setCacheControl(resource, response, request.getAuthorization());
	}

	protected void setRespondContentCommonHeaders(Response response, Resource resource, Auth auth) {
		setRespondContentCommonHeaders(response, resource, Response.Status.SC_OK, auth);
	}

	protected void setRespondContentCommonHeaders(Response response, Resource resource, Response.Status status, Auth auth) {
		setRespondCommonHeaders(response, resource, status, auth);
		setModifiedDate(response, resource, auth);
	}

	protected void setRespondCommonHeaders(Response response, Resource resource, Response.Status status, Auth auth) {
		response.setStatus(status);
		response.setNonStandardHeader("Server", "milton.io-" + miltonVerson);
		response.setDateHeader(new Date());
		response.setNonStandardHeader("Accept-Ranges", "bytes");
		String etag = eTagGenerator.generateEtag(resource);
		if (etag != null) {
			response.setEtag(etag);
		}
	}

	/**
	 * The modified date response header is used by the client for content
	 * caching. It seems obvious that if we have a modified date on the resource
	 * we should set it. BUT, because of the interaction with max-age we should
	 * always set it to the current date if we have max-age The problem, is that
	 * if we find that a condition GET has an expired mod-date (based on maxAge)
	 * then we want to respond with content (even if our mod-date hasnt changed.
	 * But if we use the actual mod-date in that case, then the browser will
	 * continue to use the old mod-date, so will forever more respond with
	 * content. So we send a mod-date of now to ensure that future requests will
	 * be given a 304 not modified.*
	 *
	 * @param response
	 * @param resource
	 * @param auth
	 */
	public static void setModifiedDate(Response response, Resource resource, Auth auth) {
		Date modDate = resource.getModifiedDate();
		if (modDate != null) {
			// HACH - see if this helps IE
			response.setLastModifiedHeader(modDate);
//            if (resource instanceof GetableResource) {
//                GetableResource gr = (GetableResource) resource;
//                Long maxAge = gr.getMaxAgeSeconds(auth);
//                if (maxAge != null && maxAge > 0) {
//                    log.trace("setModifiedDate: has a modified date and a positive maxAge, so adjust modDate");
//                    long tm = System.currentTimeMillis() - 60000; // modified 1 minute ago
//                    modDate = new Date(tm); // have max-age, so use current date
//                }
//            }
//            response.setLastModifiedHeader(modDate);
		}
	}

	@Override
	public void respondBadRequest(Resource resource, Response response, Request request) {
		response.setStatus(Response.Status.SC_BAD_REQUEST);
	}

	@Override
	public void respondForbidden(Resource resource, Response response, Request request) {
		response.setStatus(Response.Status.SC_FORBIDDEN);
	}

	@Override
	public void respondDeleteFailed(Request request, Response response, Resource resource, Status status) {
		response.setStatus(status);
	}

	@Override
	public void respondPreconditionFailed(Request request, Response response, Resource resource) {
		response.setStatus(Status.SC_PRECONDITION_FAILED);
	}

	public AuthenticationService getAuthenticationService() {
		return authenticationService;
	}

	/**
	 * Maximum size of data to hold in memory per request when buffering output
	 * data.
	 *
	 * @return
	 */
	public int getMaxMemorySize() {
		return maxMemorySize;
	}

	public void setMaxMemorySize(int maxMemorySize) {
		this.maxMemorySize = maxMemorySize;
	}

	@Override
	public BUFFERING getBuffering() {
		return buffering;
	}

	@Override
	public void setBuffering(BUFFERING buffering) {
		this.buffering = buffering;
	}

	/**
	 * Sometimes we'll get a content type list, such as image/jpeg,image/pjpeg
	 *
	 * In this case we should pick the first in the list
	 *
	 * @param ct
	 * @return
	 */
	private String pickBestContentType(String ct) {
		if (ct == null) {
			return null;
		} else if (ct.contains(",")) {
			return ct.split(",")[0];
		} else {
			return ct;
		}
	}

	public void initiateExternalAuth(Resource resource, Request request, Response response) {
		ExternalIdentityProvider eip = getSelectedIP(request);
		if (eip == null) {
			// means that the user needs to select an identity provider, so generate appropriate page
		} else {
			eip.initiateExternalAuth(resource, request, response);
		}
	}

	private ExternalIdentityProvider getSelectedIP(Request request) {
		List list = authenticationService.getExternalIdentityProviders();
		if (list.size() == 1) {
			return list.get(0);
		} else {
			String ipName = request.getParams().get("_ip");
			if (ipName != null && ipName.length() > 0) {
				for (ExternalIdentityProvider eip : list) {
					if (ipName.equals(eip.getName())) {
						return eip;
					}

				}
			}
			return null;
		}
	}

	public ContentGenerator getContentGenerator() {
		return contentGenerator;
	}

	public String getMultipartBoundary() {
		return multipartBoundary;
	}

	public void setMultipartBoundary(String multipartBoundary) {
		this.multipartBoundary = multipartBoundary;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy