xref: /JGit/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/FileSender.java (revision 5c5f7c6b146b24f2bd4afae1902df85ad6e57ea3)
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