1 /* 2 * Copyright (C) 2009-2010, Google Inc. and others 3 * 4 * This program and the accompanying materials are made available under the 5 * terms of the Eclipse Distribution License v. 1.0 which is available at 6 * https://www.eclipse.org/org/documents/edl-v10.php. 7 * 8 * SPDX-License-Identifier: BSD-3-Clause 9 */ 10 11 package org.eclipse.jgit.http.server; 12 13 import static javax.servlet.http.HttpServletResponse.SC_PARTIAL_CONTENT; 14 import static javax.servlet.http.HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE; 15 import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT_RANGES; 16 import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_LENGTH; 17 import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_RANGE; 18 import static org.eclipse.jgit.util.HttpSupport.HDR_IF_RANGE; 19 import static org.eclipse.jgit.util.HttpSupport.HDR_RANGE; 20 21 import java.io.EOFException; 22 import java.io.File; 23 import java.io.FileNotFoundException; 24 import java.io.IOException; 25 import java.io.OutputStream; 26 import java.io.RandomAccessFile; 27 import java.text.MessageFormat; 28 import java.time.Instant; 29 import java.util.Enumeration; 30 31 import javax.servlet.http.HttpServletRequest; 32 import javax.servlet.http.HttpServletResponse; 33 34 import org.eclipse.jgit.lib.ObjectId; 35 import org.eclipse.jgit.util.FS; 36 37 /** 38 * Dumps a file over HTTP GET (or its information via HEAD). 39 * <p> 40 * Supports a single byte range requested via {@code Range} HTTP header. This 41 * feature supports a dumb client to resume download of a larger object file. 42 */ 43 final class FileSender { 44 private final File path; 45 46 private final RandomAccessFile source; 47 48 private final Instant lastModified; 49 50 private final long fileLen; 51 52 private long pos; 53 54 private long end; 55 FileSender(File path)56 FileSender(File path) throws FileNotFoundException { 57 this.path = path; 58 this.source = new RandomAccessFile(path, "r"); 59 60 try { 61 this.lastModified = FS.DETECTED.lastModifiedInstant(path); 62 this.fileLen = source.getChannel().size(); 63 this.end = fileLen; 64 } catch (IOException e) { 65 try { 66 source.close(); 67 } catch (IOException closeError) { 68 // Ignore any error closing the stream. 69 } 70 71 final FileNotFoundException r; 72 r = new FileNotFoundException(MessageFormat.format(HttpServerText.get().cannotGetLengthOf, path)); 73 r.initCause(e); 74 throw r; 75 } 76 } 77 close()78 void close() { 79 try { 80 source.close(); 81 } catch (IOException e) { 82 // Ignore close errors on a read-only stream. 83 } 84 } 85 getLastModified()86 Instant getLastModified() { 87 return lastModified; 88 } 89 getTailChecksum()90 String getTailChecksum() throws IOException { 91 final int n = 20; 92 final byte[] buf = new byte[n]; 93 source.seek(fileLen - n); 94 source.readFully(buf, 0, n); 95 return ObjectId.fromRaw(buf).getName(); 96 } 97 serve(final HttpServletRequest req, final HttpServletResponse rsp, final boolean sendBody)98 void serve(final HttpServletRequest req, final HttpServletResponse rsp, 99 final boolean sendBody) throws IOException { 100 if (!initRangeRequest(req, rsp)) { 101 rsp.sendError(SC_REQUESTED_RANGE_NOT_SATISFIABLE); 102 return; 103 } 104 105 rsp.setHeader(HDR_ACCEPT_RANGES, "bytes"); 106 rsp.setHeader(HDR_CONTENT_LENGTH, Long.toString(end - pos)); 107 108 if (sendBody) { 109 try (OutputStream out = rsp.getOutputStream()) { 110 final byte[] buf = new byte[4096]; 111 source.seek(pos); 112 while (pos < end) { 113 final int r = (int) Math.min(buf.length, end - pos); 114 final int n = source.read(buf, 0, r); 115 if (n < 0) { 116 throw new EOFException(MessageFormat.format(HttpServerText.get().unexpectedeOFOn, path)); 117 } 118 out.write(buf, 0, n); 119 pos += n; 120 } 121 out.flush(); 122 } 123 } 124 } 125 initRangeRequest(final HttpServletRequest req, final HttpServletResponse rsp)126 private boolean initRangeRequest(final HttpServletRequest req, 127 final HttpServletResponse rsp) throws IOException { 128 final Enumeration<String> rangeHeaders = getRange(req); 129 if (!rangeHeaders.hasMoreElements()) { 130 // No range headers, the request is fine. 131 return true; 132 } 133 134 final String range = rangeHeaders.nextElement(); 135 if (rangeHeaders.hasMoreElements()) { 136 // To simplify the code we support only one range. 137 return false; 138 } 139 140 final int eq = range.indexOf('='); 141 final int dash = range.indexOf('-'); 142 if (eq < 0 || dash < 0 || !range.startsWith("bytes=")) { 143 return false; 144 } 145 146 final String ifRange = req.getHeader(HDR_IF_RANGE); 147 if (ifRange != null && !getTailChecksum().equals(ifRange)) { 148 // If the client asked us to verify the ETag and its not 149 // what they expected we need to send the entire content. 150 return true; 151 } 152 153 try { 154 if (eq + 1 == dash) { 155 // "bytes=-500" means last 500 bytes 156 pos = Long.parseLong(range.substring(dash + 1)); 157 pos = fileLen - pos; 158 } else { 159 // "bytes=500-" (position 500 to end) 160 // "bytes=500-1000" (position 500 to 1000) 161 pos = Long.parseLong(range.substring(eq + 1, dash)); 162 if (dash < range.length() - 1) { 163 end = Long.parseLong(range.substring(dash + 1)); 164 end++; // range was inclusive, want exclusive 165 } 166 } 167 } catch (NumberFormatException e) { 168 // We probably hit here because of a non-digit such as 169 // "," appearing at the end of the first range telling 170 // us there is a second range following. To simplify 171 // the code we support only one range. 172 return false; 173 } 174 175 if (end > fileLen) { 176 end = fileLen; 177 } 178 if (pos >= end) { 179 return false; 180 } 181 182 rsp.setStatus(SC_PARTIAL_CONTENT); 183 rsp.setHeader(HDR_CONTENT_RANGE, "bytes " + pos + "-" + (end - 1) + "/" 184 + fileLen); 185 source.seek(pos); 186 return true; 187 } 188 getRange(HttpServletRequest req)189 private static Enumeration<String> getRange(HttpServletRequest req) { 190 return req.getHeaders(HDR_RANGE); 191 } 192 } 193