001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU General
007: * Public License Version 2 only ("GPL") or the Common Development and Distribution
008: * License("CDDL") (collectively, the "License"). You may not use this file except in
009: * compliance with the License. You can obtain a copy of the License at
010: * http://www.netbeans.org/cddl-gplv2.html or nbbuild/licenses/CDDL-GPL-2-CP. See the
011: * License for the specific language governing permissions and limitations under the
012: * License. When distributing the software, include this License Header Notice in
013: * each file and include the License file at nbbuild/licenses/CDDL-GPL-2-CP. Sun
014: * designates this particular file as subject to the "Classpath" exception as
015: * provided by Sun in the GPL Version 2 section of the License file that
016: * accompanied this code. If applicable, add the following below the License Header,
017: * with the fields enclosed by brackets [] replaced by your own identifying
018: * information: "Portions Copyrighted [year] [name of copyright owner]"
019: *
020: * Contributor(s):
021: *
022: * The Original Software is NetBeans. The Initial Developer of the Original Software
023: * is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun Microsystems, Inc. All
024: * Rights Reserved.
025: *
026: * If you wish your version of this file to be governed by only the CDDL or only the
027: * GPL Version 2, indicate your decision by adding "[Contributor] elects to include
028: * this software in this distribution under the [CDDL or GPL Version 2] license." If
029: * you do not indicate a single choice of license, a recipient has the option to
030: * distribute your version of this file under either the CDDL, the GPL Version 2 or
031: * to extend the choice of license to its licensees as provided above. However, if
032: * you add GPL Version 2 code and therefore, elected the GPL Version 2 license, then
033: * the option applies only if the new code is made subject to such option by the
034: * copyright holder.
035: */
036:
037: package org.netbeans.installer.sandbox.download.connection;
038:
039: import java.io.IOException;
040: import java.io.OutputStream;
041: import java.io.PushbackInputStream;
042: import java.io.UnsupportedEncodingException;
043: import java.net.Socket;
044: import java.net.URL;
045: import java.text.ParseException;
046: import java.text.SimpleDateFormat;
047: import java.util.Date;
048: import java.util.HashMap;
049: import java.util.List;
050: import java.util.Locale;
051: import java.util.Map;
052: import org.netbeans.installer.sandbox.download.Download;
053: import org.netbeans.installer.sandbox.download.DownloadManager;
054: import org.netbeans.installer.sandbox.download.DownloadOptions;
055: import org.netbeans.installer.sandbox.download.proxy.Proxy;
056: import org.netbeans.installer.sandbox.download.proxy.Proxy.ProxyType;
057: import org.netbeans.installer.utils.helper.ErrorLevel;
058: import org.netbeans.installer.utils.ErrorManager;
059: import org.netbeans.installer.utils.StringUtils;
060: import org.netbeans.installer.utils.exceptions.HTTPException;
061:
062: /**
063: *
064: * @author Kirill Sorokin
065: */
066: public class HTTPConnection extends Connection {
067: private URL url;
068: private long offset;
069: private long length;
070:
071: private DownloadOptions options;
072:
073: private String host;
074: private int port;
075: private String file;
076: private String parent;
077: private String referer;
078:
079: private String requestStatusLine;
080: private Map<String, String> requestFields;
081: private String requestHeader;
082:
083: private String authorizationString;
084: private String proxyAuthorizationString;
085:
086: private int responseCode;
087: private String responseStatusLine;
088: private Map<String, String> responseFields;
089: private String responseHeader;
090:
091: private int numberOfRedirects;
092:
093: private Socket socket;
094: private PushbackInputStream socketInput;
095: private OutputStream socketOutput;
096:
097: public HTTPConnection(URL anURL, long anOffset, long aLength,
098: DownloadOptions someOptions) {
099: // save the initial connection properties
100: offset = anOffset;
101: length = aLength;
102:
103: options = someOptions;
104:
105: // parse the url
106: parseURL(anURL);
107: }
108:
109: public void open() throws IOException {
110: boolean socksProxyUsed = false;
111: boolean httpProxyUsed = false;
112: boolean retryDirectly = false;
113:
114: List<Proxy> httpProxies = DownloadManager.getInstance()
115: .getProxies(ProxyType.HTTP);
116: List<Proxy> socksProxies = DownloadManager.getInstance()
117: .getProxies(ProxyType.SOCKS);
118:
119: if (!options.getBoolean(DownloadOptions.IGNORE_PROXIES)) {
120: if ((httpProxies.size() == 0) && (socksProxies.size() > 0)) {
121: socksProxyUsed = true;
122: System.setProperty("socksProxyHost", socksProxies
123: .get(0).getHost());
124: System.setProperty("socksProxyPort", Integer
125: .toString(socksProxies.get(0).getPort()));
126: }
127: if (httpProxies.size() > 0) {
128: Proxy httpProxy = httpProxies.get(0);
129:
130: if (!httpProxy.skipProxyForHost(host)) {
131: httpProxyUsed = true;
132: }
133: }
134: }
135:
136: try {
137: if (httpProxyUsed) {
138: socket = new Socket(httpProxies.get(0).getHost(),
139: httpProxies.get(0).getPort());
140: } else {
141: socket = new Socket(host, port);
142: }
143: } catch (IOException e) {
144: if (socksProxyUsed) {
145: ErrorManager.notify(ErrorLevel.WARNING,
146: "Could not connect through SOCKS proxy", e);
147: retryDirectly = true;
148: } else {
149: throw e;
150: }
151: }
152:
153: if (socksProxyUsed) {
154: System.getProperties().remove("socksProxyHost");
155: System.getProperties().remove("socksProxyPort");
156: }
157:
158: if (retryDirectly) {
159: socket = new Socket(host, port);
160: }
161:
162: socketOutput = socket.getOutputStream();
163: socketInput = new PushbackInputStream(socket.getInputStream(),
164: 1024);
165:
166: // init the request header
167: requestFields = new HashMap<String, String>();
168:
169: // init the response header
170: responseFields = new HashMap<String, String>();
171:
172: sendRequestHeader();
173: readResponseHeader();
174: }
175:
176: public void close() throws IOException {
177: if (socket != null) {
178: socket.close();
179:
180: socket = null;
181: }
182: }
183:
184: public int read(byte[] buffer) throws IOException {
185: ensureAvailability();
186:
187: return socketInput.read(buffer);
188: }
189:
190: public int available() throws IOException {
191: return socketInput.available();
192: }
193:
194: public boolean supportsRanges() {
195: for (String key : responseFields.keySet()) {
196: if (key.equalsIgnoreCase("Accept-Ranges")
197: && responseFields.get(key).contains("bytes")) {
198: return true;
199: }
200: }
201:
202: return false;
203: }
204:
205: public long getContentLength() {
206: for (String key : responseFields.keySet()) {
207: if (key.equalsIgnoreCase("Content-Length")) {
208: try {
209: return Integer.parseInt(responseFields.get(key));
210: } catch (NumberFormatException e) {
211: ErrorManager.notify(ErrorLevel.DEBUG,
212: "Invalid content length received", e);
213: }
214: }
215: }
216:
217: return -1;
218: }
219:
220: public Date getModificationDate() {
221: for (String key : responseFields.keySet()) {
222: if (key.equalsIgnoreCase("Last-Modified")) {
223: try {
224: return new SimpleDateFormat(
225: "EEE MMM d HH:mm:ss yyyy", Locale.US)
226: .parse(responseFields.get(key));
227: } catch (ParseException e) {
228: try {
229: return new SimpleDateFormat(
230: "EEEE, dd-MMM-yy HH:mm:ss zzz",
231: Locale.US).parse(responseFields
232: .get(key));
233: } catch (ParseException ex) {
234: try {
235: return new SimpleDateFormat(
236: "EEE, dd MMM yyyy HH:mm:ss zzz",
237: Locale.US).parse(responseFields
238: .get(key));
239: } catch (ParseException exe) {
240: ErrorManager
241: .notify(
242: ErrorLevel.DEBUG,
243: "Faield to parse the modification date",
244: e);
245: }
246: }
247: }
248: }
249: }
250:
251: return new Date();
252: }
253:
254: private void parseURL(URL anURL) {
255: url = anURL;
256: host = url.getHost();
257: port = (url.getPort() == UNDEFINED_PORT) ? url.getDefaultPort()
258: : url.getPort();
259: file = url.getFile().equals("") ? URL_PATH_SEPARATOR : url
260: .getFile();
261: parent = file.substring(0,
262: file.lastIndexOf(URL_PATH_SEPARATOR) + 1);
263: referer = url.getProtocol() + "://" + host + ":" + port
264: + parent;
265: }
266:
267: private void sendRequestHeader() throws IOException {
268: boolean httpProxyUsed = DownloadManager.getInstance()
269: .getProxies(ProxyType.HTTP).size() > 0;
270:
271: // init the request header
272: if (!httpProxyUsed) {
273: requestStatusLine = "GET " + file + " HTTP/1.1";
274: } else {
275: requestStatusLine = "GET " + url + " HTTP/1.1";
276: }
277:
278: requestFields = new HashMap<String, String>();
279: requestFields.put("User-Agent",
280: "NetBeans Installer Download Manager");
281: requestFields.put("Accept", "*/*");
282: requestFields.put("Referer", referer);
283: requestFields.put("Host", host);
284: requestFields.put("Pragma", "no-cache");
285: requestFields.put("Cache-Control", "no-cache");
286:
287: if ((offset != Download.ZERO_OFFSET)
288: || (length != Download.UNDEFINED_LENGTH)) {
289: String range = "bytes=" + offset + "-";
290: if (length != Download.UNDEFINED_LENGTH) {
291: range += offset + length;
292: }
293: requestFields.put("Range", range);
294: }
295:
296: if (authorizationString != null) {
297: requestFields.put("Authorization", authorizationString);
298: }
299: if (proxyAuthorizationString != null) {
300: requestFields.put("Proxy-Authorization",
301: proxyAuthorizationString);
302: }
303:
304: // construct the request string
305: String header = requestStatusLine;
306:
307: for (String field : requestFields.keySet()) {
308: header += StringUtils.CRLF + field + ": "
309: + requestFields.get(field);
310: }
311:
312: header += StringUtils.CRLFCRLF;
313:
314: socketOutput.write(header.getBytes("UTF-8"));
315:
316: requestHeader = header;
317: }
318:
319: private void readResponseHeader() throws IOException {
320: boolean headerRead = false;
321: byte[] buffer = new byte[1024];
322:
323: responseHeader = "";
324:
325: while (!headerRead) {
326: ensureAvailability();
327:
328: int read = socketInput.read(buffer);
329: String string = new String(buffer, 0, read, "ISO-8859-1");
330:
331: int endIndex = string.indexOf(StringUtils.CRLF
332: + StringUtils.CRLF);
333:
334: if (endIndex != -1) {
335: socketInput.unread(buffer, endIndex + 4, read
336: - endIndex - 4);
337: string = string.substring(0, endIndex + 4);
338: headerRead = true;
339: }
340:
341: responseHeader += string;
342: }
343:
344: String[] lines = StringUtils.rightTrim(responseHeader).split(
345: StringUtils.CRLF);
346:
347: responseStatusLine = lines[0];
348: responseCode = Integer
349: .parseInt(responseStatusLine.split(" ")[1]);
350:
351: for (int i = 1; i < lines.length; i++) {
352: int colon = lines[i].indexOf(":");
353:
354: String name = StringUtils.rightTrim(
355: lines[i].substring(0, colon)).toLowerCase();
356: String value = StringUtils.leftTrim(lines[i]
357: .substring(colon + 1));
358:
359: responseFields.put(name, value);
360: }
361:
362: // if the response code is from OK family - continue
363: if ((responseCode >= 200) && (responseCode < 300)) {
364: return;
365: }
366:
367: // otherwise go into a more detailed analysis
368: switch (responseCode) {
369: case 301:
370: case 302:
371: case 303:
372: if (numberOfRedirects <= 5) {
373: parseURL(new URL(responseFields.get("location")));
374: open();
375: return;
376: } else {
377: throw new HTTPException(
378: "Maximum number of redirects exceeded");
379: }
380: case 401:
381: if (authorizationString == null) {
382: constructAuthorizationString();
383: open();
384: return;
385: } else {
386: throw new HTTPException("Authorization failed");
387: }
388: case 407:
389: if (proxyAuthorizationString == null) {
390: constructProxyAuthorizationString();
391: open();
392: return;
393: } else {
394: throw new HTTPException("Proxy Authorization failed");
395: }
396: default:
397: throw new HTTPException("Unsupported response header: "
398: + responseStatusLine);
399: }
400: }
401:
402: private void ensureAvailability() throws IOException {
403: for (int timeout = 0; timeout <= connectionTimeout; timeout += DELAY) {
404: if (socketInput.available() > 0) {
405: return;
406: }
407:
408: try {
409: Thread.sleep(DELAY);
410: } catch (InterruptedException e) {
411: // do nothing
412: }
413: }
414:
415: throw new IOException("Connection timed out.");
416: }
417:
418: private void constructAuthorizationString() throws HTTPException {
419: String challenge = responseFields.get("WWW-Authenticate");
420:
421: if (challenge.startsWith("Basic")) {
422: String username = options
423: .getString(DownloadOptions.USERNAME);
424: String password = options
425: .getString(DownloadOptions.PASSWORD);
426:
427: if ((username == null) || (password == null)) {
428: throw new HTTPException(
429: "Authorization required, while either username or password were not supplied");
430: } else {
431: try {
432: authorizationString = "Basic "
433: + StringUtils.base64Encode(username + ":"
434: + password);
435: } catch (UnsupportedEncodingException e) {
436: throw new HTTPException(
437: "Cannot build the authorization string", e);
438: }
439: }
440: } else {
441: throw new HTTPException("Unsupported authorization scheme");
442: }
443: }
444:
445: private void constructProxyAuthorizationString()
446: throws HTTPException {
447: String challenge = responseFields.get("Proxy-Authenticate");
448:
449: if (challenge.startsWith("Basic")) {
450: List<Proxy> httpProxies = DownloadManager.getInstance()
451: .getProxies(ProxyType.HTTP);
452:
453: if (httpProxies.size() == 0) {
454: throw new HTTPException(
455: "Proxy authorization was required, while no proxies were registered");
456: }
457:
458: String username = httpProxies.get(0).getUsername();
459: String password = httpProxies.get(0).getPassword();
460:
461: if ((username == null) || (password == null)) {
462: throw new HTTPException(
463: "Proxy authorization required, while either username or password were not set");
464: } else {
465: try {
466: authorizationString = "Basic "
467: + StringUtils.base64Encode(username + ":"
468: + password);
469: } catch (UnsupportedEncodingException e) {
470: throw new HTTPException(
471: "Cannot build the authorization string", e);
472: }
473: }
474: } else {
475: throw new HTTPException("Unsupported authorization scheme");
476: }
477: }
478:
479: /////////////////////////////////////////////////////////////////////////////////
480: // Constants
481: private static final int DELAY = 25;
482: private static final int UNDEFINED_PORT = -1;
483: private static final String URL_PATH_SEPARATOR = "/";
484: }
|