View Javadoc
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  
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  
78  	void close() {
79  		try {
80  			source.close();
81  		} catch (IOException e) {
82  			// Ignore close errors on a read-only stream.
83  		}
84  	}
85  
86  	Instant getLastModified() {
87  		return lastModified;
88  	}
89  
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  
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 
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 
189 	private static Enumeration<String> getRange(HttpServletRequest req) {
190 		return req.getHeaders(HDR_RANGE);
191 	}
192 }