/* * Copyright 2014 Red Hat, Inc. and/or its affiliates. * * 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. * See the License for the specific language governing permissions and * limitations under the License. */ package org.guvnor.m2repo.backend.server.helpers; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.RandomAccessFile; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.zip.GZIPOutputStream; import javax.inject.Inject; import javax.servlet.ServletContext; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.guvnor.m2repo.backend.server.GuvnorM2Repository; import org.guvnor.m2repo.service.M2RepoService; public class HttpGetHelper { private static final int DEFAULT_BUFFER_SIZE = 10240; private static final long DEFAULT_EXPIRE_TIME = 604800000L; //1 week. private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES"; @Inject private M2RepoService m2RepoService; @Inject private GuvnorM2Repository repository; public void handle( final HttpServletRequest request, final HttpServletResponse response, final ServletContext context ) throws IOException { String requestedFile = request.getPathInfo(); if ( requestedFile == null ) { response.sendError( HttpServletResponse.SC_NOT_FOUND ); return; } requestedFile = URLDecoder.decode( requestedFile, "UTF-8" ); //File traversal check: final File mavenRootDir = new File( repository.getM2RepositoryRootDir() ); final String canonicalDirPath = mavenRootDir.getCanonicalPath() + File.separator; final String canonicalEntryPath = new File( mavenRootDir, requestedFile ).getCanonicalPath(); if ( !canonicalEntryPath.startsWith( canonicalDirPath ) ) { response.sendError( HttpServletResponse.SC_NOT_FOUND ); return; } requestedFile = canonicalEntryPath.substring( canonicalDirPath.length() ); final File file = new File( mavenRootDir, requestedFile ); if ( !file.exists() ) { response.sendError( HttpServletResponse.SC_NOT_FOUND ); return; } // Process the ETag String fileName = file.getName(); long length = file.length(); long lastModified = file.lastModified(); String eTag = fileName + "_" + length + "_" + lastModified; String ifNoneMatch = request.getHeader( "If-None-Match" ); if ( ifNoneMatch != null && matches( ifNoneMatch, eTag ) ) { response.setHeader( "ETag", eTag ); // Required in 304. response.sendError( HttpServletResponse.SC_NOT_MODIFIED ); return; } long ifModifiedSince = request.getDateHeader( "If-Modified-Since" ); if ( ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified ) { response.setHeader( "ETag", eTag ); // Required in 304. response.sendError( HttpServletResponse.SC_NOT_MODIFIED ); return; } String ifMatch = request.getHeader( "If-Match" ); if ( ifMatch != null && !matches( ifMatch, eTag ) ) { response.sendError( HttpServletResponse.SC_PRECONDITION_FAILED ); return; } long ifUnmodifiedSince = request.getDateHeader( "If-Unmodified-Since" ); if ( ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified ) { response.sendError( HttpServletResponse.SC_PRECONDITION_FAILED ); return; } Range full = new Range( 0, length - 1, length ); List<Range> ranges = new ArrayList<Range>(); String contentType = context.getMimeType( fileName ); boolean acceptsGzip = false; String disposition = "inline"; if ( contentType == null ) { contentType = "application/octet-stream"; } if ( contentType.startsWith( "text" ) ) { String acceptEncoding = request.getHeader( "Accept-Encoding" ); acceptsGzip = acceptEncoding != null && accepts( acceptEncoding, "gzip" ); contentType += ";charset=UTF-8"; } else if ( !contentType.startsWith( "image" ) ) { String accept = request.getHeader( "Accept" ); disposition = accept != null && accepts( accept, contentType ) ? "inline" : "attachment"; } //Response. response.reset(); response.setBufferSize( DEFAULT_BUFFER_SIZE ); response.setHeader( "Content-Disposition", disposition + ";filename=\"" + fileName + "\"" ); response.setHeader( "Accept-Ranges", "bytes" ); response.setHeader( "ETag", eTag ); response.setDateHeader( "Last-Modified", lastModified ); response.setDateHeader( "Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME ); RandomAccessFile input = null; OutputStream output = null; try { input = new RandomAccessFile( file, "r" ); output = response.getOutputStream(); if ( ranges.isEmpty() || ranges.get( 0 ) == full ) { Range r = full; response.setContentType( contentType ); response.setHeader( "Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total ); if ( acceptsGzip ) { response.setHeader( "Content-Encoding", "gzip" ); output = new GZIPOutputStream( output, DEFAULT_BUFFER_SIZE ); } else { response.setHeader( "Content-Length", String.valueOf( r.length ) ); } copyRange( input, output, r.start, r.length ); } else if ( ranges.size() == 1 ) { Range r = ranges.get( 0 ); response.setContentType( contentType ); response.setHeader( "Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total ); response.setHeader( "Content-Length", String.valueOf( r.length ) ); response.setStatus( HttpServletResponse.SC_PARTIAL_CONTENT ); // 206. copyRange( input, output, r.start, r.length ); } else { response.setContentType( "multipart/byteranges; boundary=" + MULTIPART_BOUNDARY ); response.setStatus( HttpServletResponse.SC_PARTIAL_CONTENT ); // 206. ServletOutputStream sos = (ServletOutputStream) output; for ( Range r : ranges ) { sos.println(); sos.println( "--" + MULTIPART_BOUNDARY ); sos.println( "Content-Type: " + contentType ); sos.println( "Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total ); copyRange( input, output, r.start, r.length ); } sos.println(); sos.println( "--" + MULTIPART_BOUNDARY + "--" ); } } finally { if ( output != null ) { output.close(); } if ( input != null ) { input.close(); } } } private static boolean accepts( final String acceptHeader, final String toAccept ) { String[] acceptValues = acceptHeader.split( "\\s*(,|;)\\s*" ); Arrays.sort( acceptValues ); return Arrays.binarySearch( acceptValues, toAccept ) > -1 || Arrays.binarySearch( acceptValues, toAccept.replaceAll( "/.*$", "/*" ) ) > -1 || Arrays.binarySearch( acceptValues, "*/*" ) > -1; } private static boolean matches( final String matchHeader, final String toMatch ) { String[] matchValues = matchHeader.split( "\\s*,\\s*" ); Arrays.sort( matchValues ); return Arrays.binarySearch( matchValues, toMatch ) > -1 || Arrays.binarySearch( matchValues, "*" ) > -1; } private static void copyRange( final RandomAccessFile input, final OutputStream output, final long start, final long length ) throws IOException { byte[] buffer = new byte[ DEFAULT_BUFFER_SIZE ]; int read; if ( input.length() == length ) { // Write full range. while ( ( read = input.read( buffer ) ) > 0 ) { output.write( buffer, 0, read ); } } else { // Write partial range. input.seek( start ); long toRead = length; while ( ( read = input.read( buffer ) ) > 0 ) { if ( ( toRead -= read ) > 0 ) { output.write( buffer, 0, read ); } else { output.write( buffer, 0, (int) toRead + read ); break; } } } } protected class Range { long start; long end; long length; long total; public Range( long start, long end, long total ) { this.start = start; this.end = end; this.length = end - start + 1; this.total = total; } } }