001: /*
002: *
003: *
004: * Copyright 1990-2007 Sun Microsystems, Inc. All Rights Reserved.
005: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License version
009: * 2 only, as published by the Free Software Foundation.
010: *
011: * This program is distributed in the hope that it will be useful, but
012: * WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * General Public License version 2 for more details (a copy is
015: * included at /legal/license.txt).
016: *
017: * You should have received a copy of the GNU General Public License
018: * version 2 along with this work; if not, write to the Free Software
019: * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA
021: *
022: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
023: * Clara, CA 95054 or visit www.sun.com if you need additional
024: * information or have any questions.
025: */
026:
027: package com.sun.midp.installer;
028:
029: import java.io.InputStream;
030: import java.io.OutputStream;
031: import java.io.ByteArrayOutputStream;
032: import java.io.IOException;
033:
034: import javax.microedition.io.ConnectionNotFoundException;
035: import javax.microedition.io.Connector;
036: import javax.microedition.io.Connection;
037: import javax.microedition.io.HttpConnection;
038:
039: import com.sun.midp.io.j2me.storage.RandomAccessStream;
040: import com.sun.midp.io.Base64;
041: import com.sun.midp.io.HttpUrl;
042: import com.sun.midp.io.Util;
043:
044: import com.sun.midp.log.Logging;
045: import com.sun.midp.log.LogChannels;
046:
047: /**
048: * An Installer allowing to install a midlet suite from an http server.
049: * If the midlet suite is given by a descriptor file, the jar URL
050: * specified in the descriptor must have an "http" or "https" scheme.
051: */
052: public class HttpInstaller extends Installer {
053: /** Max number of bytes to download at one time (1K). */
054: private static final int MAX_DL_SIZE = 1024;
055:
056: /** Tag that signals that the HTTP server supports basic auth. */
057: private static final String BASIC_TAG = "basic";
058:
059: /** HTTP connection to close when we stop the installation. */
060: private HttpConnection httpConnection;
061:
062: /** HTTP stream to close when we stop the installation. */
063: private InputStream httpInputStream;
064:
065: /**
066: * Constructor of the HttpInstaller.
067: */
068: public HttpInstaller() {
069: super ();
070: }
071:
072: /**
073: * Downloads an application descriptor file from the given URL.
074: *
075: * @return a byte array representation of the file or null if not found
076: *
077: * @exception IOException is thrown if any error prevents the download
078: * of the JAD
079: */
080: protected byte[] downloadJAD() throws IOException {
081: String[] encoding = new String[1];
082: ByteArrayOutputStream bos = new ByteArrayOutputStream(
083: MAX_DL_SIZE);
084: String[] acceptableTypes = { JAD_MT };
085: String[] extraFieldKeys = new String[3];
086: String[] extraFieldValues = new String[3];
087: String locale;
088: String prof = System.getProperty(MICROEDITION_PROFILES);
089: int space = prof.indexOf(' ');
090: if (space != -1) {
091: prof = prof.substring(0, space);
092: }
093:
094: extraFieldKeys[0] = "User-Agent";
095: extraFieldValues[0] = "Profile/" + prof + " Configuration/"
096: + System.getProperty(MICROEDITION_CONFIG);
097:
098: extraFieldKeys[1] = "Accept-Charset";
099: extraFieldValues[1] = "UTF-8, ISO-8859-1";
100:
101: /* locale can be null */
102: locale = System.getProperty(MICROEDITION_LOCALE);
103: if (locale != null) {
104: extraFieldKeys[2] = "Accept-Language";
105: extraFieldValues[2] = locale;
106: }
107:
108: state.beginTransferDataStatus = DOWNLOADING_JAD;
109: state.transferStatus = DOWNLOADED_1K_OF_JAD;
110:
111: /*
112: * Do not send the list of acceptable types because some servers
113: * will send a 406 if the URL is to a JAR. It is better to
114: * reject the resource at the client after check the media-type so
115: * if the type is JAR a JAR only install can be performed.
116: */
117: downloadResource(info.jadUrl, extraFieldKeys, extraFieldValues,
118: acceptableTypes, false, false, bos, encoding,
119: InvalidJadException.INVALID_JAD_URL,
120: InvalidJadException.JAD_SERVER_NOT_FOUND,
121: InvalidJadException.JAD_NOT_FOUND,
122: InvalidJadException.INVALID_JAD_TYPE);
123:
124: state.jadEncoding = encoding[0];
125: return bos.toByteArray();
126: }
127:
128: /**
129: * Downloads an application archive file from the given URL into the
130: * given file. Automatically handle re-tries.
131: *
132: * @param filename name of the file to write. This file resides
133: * in the storage area of the given application
134: *
135: * @return size of the JAR
136: *
137: * @exception IOException is thrown if any error prevents the download
138: * of the JAR
139: */
140: protected int downloadJAR(String filename) throws IOException {
141: HttpUrl parsedUrl;
142: String url;
143: String[] acceptableTypes = { JAR_MT_1, JAR_MT_2 };
144: String[] extraFieldKeys = new String[3];
145: String[] extraFieldValues = new String[3];
146: int jarSize;
147: String locale;
148: String prof;
149: int space;
150: RandomAccessStream jarOutputStream = null;
151: OutputStream outputStream = null;
152:
153: parsedUrl = new HttpUrl(info.jarUrl);
154: if (parsedUrl.authority == null && info.jadUrl != null) {
155: // relative URL, add the JAD URL as the base
156: try {
157: parsedUrl.addBaseUrl(info.jadUrl);
158: } catch (IOException e) {
159: postInstallMsgBackToProvider(OtaNotifier.INVALID_JAD_MSG);
160: throw new InvalidJadException(
161: InvalidJadException.INVALID_JAR_URL);
162: }
163:
164: url = parsedUrl.toString();
165:
166: // The JAR URL saved to storage MUST be absolute
167: info.jarUrl = url;
168: } else {
169: url = info.jarUrl;
170: }
171:
172: jarOutputStream = new RandomAccessStream();
173: jarOutputStream.connect(filename,
174: RandomAccessStream.READ_WRITE_TRUNCATE);
175: outputStream = jarOutputStream.openOutputStream();
176:
177: prof = System.getProperty(MICROEDITION_PROFILES);
178: space = prof.indexOf(' ');
179: if (space != -1) {
180: prof = prof.substring(0, space);
181: }
182:
183: extraFieldKeys[0] = "User-Agent";
184: extraFieldValues[0] = "Profile/" + prof + " Configuration/"
185: + System.getProperty(MICROEDITION_CONFIG);
186:
187: extraFieldKeys[1] = "Accept-Charset";
188: extraFieldValues[1] = "UTF-8, ISO-8859-1";
189:
190: /* locale can be null */
191: locale = System.getProperty(MICROEDITION_LOCALE);
192: if (locale != null) {
193: extraFieldKeys[2] = "Accept-Language";
194: extraFieldValues[2] = locale;
195: }
196:
197: try {
198: state.beginTransferDataStatus = DOWNLOADING_JAR;
199: state.transferStatus = DOWNLOADED_1K_OF_JAR;
200: jarSize = downloadResource(url, extraFieldKeys,
201: extraFieldValues, acceptableTypes, true, true,
202: outputStream, null,
203: InvalidJadException.INVALID_JAR_URL,
204: InvalidJadException.JAR_SERVER_NOT_FOUND,
205: InvalidJadException.JAR_NOT_FOUND,
206: InvalidJadException.INVALID_JAR_TYPE);
207: return jarSize;
208: } catch (InvalidJadException ije) {
209: switch (ije.getReason()) {
210: case InvalidJadException.INVALID_JAR_URL:
211: case InvalidJadException.JAR_SERVER_NOT_FOUND:
212: case InvalidJadException.JAR_NOT_FOUND:
213: case InvalidJadException.INVALID_JAR_TYPE:
214: postInstallMsgBackToProvider(OtaNotifier.INVALID_JAR_MSG);
215: break;
216:
217: default:
218: // for safety/completeness.
219: if (Logging.REPORT_LEVEL <= Logging.ERROR) {
220: Logging.report(Logging.ERROR, LogChannels.LC_AMS,
221: "Installer InvalidJadException: "
222: + ije.getMessage());
223: }
224: break;
225: }
226:
227: throw ije;
228: } finally {
229: try {
230: jarOutputStream.disconnect();
231: } catch (Exception e) {
232: if (Logging.REPORT_LEVEL <= Logging.WARNING) {
233: Logging.report(Logging.WARNING, LogChannels.LC_AMS,
234: "disconnect threw a Exception");
235: }
236: }
237: }
238: }
239:
240: /**
241: * Downloads an resource from the given URL into the output stream.
242: *
243: * @param url location of the resource to download
244: * @param extraFieldKeys keys to the extra fields to put in the request
245: * @param extraFieldValues values to the extra fields to put in the request
246: * @param acceptableTypes list of acceptable media types for this resource,
247: * there must be at least one
248: * @param sendAcceptableTypes if true the list of acceptable media types
249: * for this resource will be sent in the request
250: * @param allowNoMediaType if true it is not consider an error if
251: * the media type is not in the response
252: * for this resource will be sent in the request
253: * @param output output stream to write the resource to
254: * @param encoding an array to receive the character encoding of resource,
255: * can be null
256: * @param invalidURLCode reason code to use when the URL is invalid
257: * @param serverNotFoundCode reason code to use when the server is not
258: * found
259: * @param resourceNotFoundCode reason code to use when the resource is not
260: * found on the server
261: * @param invalidMediaTypeCode reason code to use when the media type of
262: * the resource is not valid
263: *
264: * @return size of the resource
265: *
266: * @exception IOException is thrown if any error prevents the download
267: * of the resource
268: */
269: private int downloadResource(String url, String[] extraFieldKeys,
270: String[] extraFieldValues, String[] acceptableTypes,
271: boolean sendAcceptableTypes, boolean allowNoMediaType,
272: OutputStream output, String[] encoding, int invalidURLCode,
273: int serverNotFoundCode, int resourceNotFoundCode,
274: int invalidMediaTypeCode) throws IOException {
275: Connection conn = null;
276: StringBuffer acceptField;
277: int responseCode;
278: String retryAfterField;
279: int retryInterval;
280: String mediaType;
281:
282: try {
283: for (;;) {
284: try {
285: conn = Connector.open(url, Connector.READ);
286: } catch (IllegalArgumentException e) {
287: throw new InvalidJadException(invalidURLCode, url);
288: } catch (ConnectionNotFoundException e) {
289: // protocol not found
290: throw new InvalidJadException(invalidURLCode, url);
291: }
292:
293: if (!(conn instanceof HttpConnection)) {
294: // only HTTP or HTTPS are supported
295: throw new InvalidJadException(invalidURLCode, url);
296: }
297:
298: httpConnection = (HttpConnection) conn;
299:
300: if (extraFieldKeys != null) {
301: for (int i = 0; i < extraFieldKeys.length
302: && extraFieldKeys[i] != null; i++) {
303: httpConnection.setRequestProperty(
304: extraFieldKeys[i], extraFieldValues[i]);
305: }
306: }
307:
308: // 256 is given to avoid resizing without adding lengths
309: acceptField = new StringBuffer(256);
310:
311: if (sendAcceptableTypes) {
312: // there must be one or more acceptable media types
313: acceptField.append(acceptableTypes[0]);
314: for (int i = 1; i < acceptableTypes.length; i++) {
315: acceptField.append(", ");
316: acceptField.append(acceptableTypes[i]);
317: }
318: } else {
319: /* Send at least a wildcard to satisfy WAP gateways. */
320: acceptField.append("*/*");
321: }
322:
323: httpConnection.setRequestProperty("Accept", acceptField
324: .toString());
325: httpConnection.setRequestMethod(HttpConnection.GET);
326:
327: if (state.username != null && state.password != null) {
328: httpConnection.setRequestProperty("Authorization",
329: formatAuthCredentials(state.username,
330: state.password));
331: }
332:
333: if (state.proxyUsername != null
334: && state.proxyPassword != null) {
335: httpConnection.setRequestProperty(
336: "Proxy-Authorization",
337: formatAuthCredentials(state.proxyUsername,
338: state.proxyPassword));
339: }
340:
341: try {
342: responseCode = httpConnection.getResponseCode();
343: } catch (IOException ioe) {
344: if (httpConnection.getHost() == null) {
345: throw new InvalidJadException(invalidURLCode,
346: url);
347: }
348:
349: throw new InvalidJadException(serverNotFoundCode,
350: url);
351: }
352:
353: if (responseCode != HttpConnection.HTTP_UNAVAILABLE) {
354: break;
355: }
356:
357: retryAfterField = httpConnection
358: .getHeaderField("Retry-After");
359: if (retryAfterField == null) {
360: break;
361: }
362:
363: try {
364: /*
365: * see if the retry interval is in seconds, and
366: * not an absolute date
367: */
368: retryInterval = Integer.parseInt(retryAfterField);
369: if (retryInterval > 0) {
370: if (retryInterval > 60) {
371: // only wait 1 min
372: retryInterval = 60;
373: }
374:
375: Thread.sleep(retryInterval * 1000);
376: }
377: } catch (InterruptedException ie) {
378: // ignore thread interrupt
379: break;
380: } catch (NumberFormatException ne) {
381: // ignore bad format
382: break;
383: }
384:
385: httpConnection.close();
386:
387: if (state.stopInstallation) {
388: postInstallMsgBackToProvider(OtaNotifier.USER_CANCELLED_MSG);
389: throw new IOException("stopped");
390: }
391: } // end for
392:
393: if (responseCode == HttpConnection.HTTP_NOT_FOUND) {
394: throw new InvalidJadException(resourceNotFoundCode);
395: }
396:
397: if (responseCode == HttpConnection.HTTP_NOT_ACCEPTABLE) {
398: throw new InvalidJadException(invalidMediaTypeCode, "");
399: }
400:
401: if (responseCode == HttpConnection.HTTP_UNAUTHORIZED) {
402: // automatically throws the correct exception
403: checkIfBasicAuthSupported(httpConnection
404: .getHeaderField("WWW-Authenticate"));
405:
406: state.exception = new InvalidJadException(
407: InvalidJadException.UNAUTHORIZED);
408: return 0;
409: }
410:
411: if (responseCode == HttpConnection.HTTP_PROXY_AUTH) {
412: // automatically throws the correct exception
413: checkIfBasicAuthSupported(httpConnection
414: .getHeaderField("WWW-Authenticate"));
415:
416: state.exception = new InvalidJadException(
417: InvalidJadException.PROXY_AUTH);
418: return 0;
419: }
420:
421: if (responseCode != HttpConnection.HTTP_OK) {
422: throw new IOException("Failed to download " + url
423: + " HTTP response code: " + responseCode);
424: }
425:
426: mediaType = Util.getHttpMediaType(httpConnection.getType());
427:
428: if (mediaType != null) {
429: boolean goodType = false;
430:
431: for (int i = 0; i < acceptableTypes.length; i++) {
432: if (mediaType.equals(acceptableTypes[i])) {
433: goodType = true;
434: break;
435: }
436: }
437:
438: if (!goodType) {
439: throw new InvalidJadException(invalidMediaTypeCode,
440: mediaType);
441: }
442: } else if (!allowNoMediaType) {
443: throw new InvalidJadException(invalidMediaTypeCode, "");
444: }
445:
446: if (encoding != null) {
447: encoding[0] = getCharset(httpConnection.getType());
448: }
449:
450: httpInputStream = httpConnection.openInputStream();
451: return transferData(httpInputStream, output, MAX_DL_SIZE);
452: } finally {
453: // Close the streams or connections this method opened.
454: try {
455: httpInputStream.close();
456: } catch (Exception e) {
457: if (Logging.REPORT_LEVEL <= Logging.WARNING) {
458: Logging.report(Logging.WARNING, LogChannels.LC_AMS,
459: "stream close threw an Exception");
460: }
461: }
462:
463: try {
464: conn.close();
465: } catch (Exception e) {
466: if (Logging.REPORT_LEVEL <= Logging.WARNING) {
467: Logging.report(Logging.WARNING, LogChannels.LC_AMS,
468: "connection close threw an Exception");
469: }
470: }
471: }
472: }
473:
474: /**
475: * Compares two URLs for equality in sense that they have the same
476: * scheme, host and path.
477: *
478: * @param url1 the first URL for comparision
479: * @param url1 the second URL for comparision
480: *
481: * @return true if the scheme, host and path of the first given url
482: * is identical to the scheme, host and path of the second
483: * given url; false otherwise
484: */
485: protected boolean isSameUrl(String url1, String url2) {
486: HttpUrl newUrl;
487: HttpUrl originalUrl;
488:
489: try {
490: newUrl = new HttpUrl(url1);
491: originalUrl = new HttpUrl(url2);
492:
493: if (newUrl.scheme.equals(originalUrl.scheme)
494: && newUrl.host.equals(originalUrl.host)
495: && newUrl.path.equals(originalUrl.path)) {
496: return true;
497: }
498: } catch (NullPointerException npe) {
499: // no match, fall through
500: }
501:
502: return false;
503: }
504:
505: /**
506: * Checks to make sure the HTTP server will support Basic authentication.
507: *
508: * @param wwwAuthField WWW-Authenticate field from the response header
509: *
510: * @exception InvalidJadException if server does not support Basic
511: * authentication
512: */
513: private void checkIfBasicAuthSupported(String wwwAuthField)
514: throws InvalidJadException {
515: if (wwwAuthField != null) {
516: wwwAuthField = wwwAuthField.trim();
517:
518: if (wwwAuthField.regionMatches(true, 0, BASIC_TAG, 0,
519: BASIC_TAG.length())) {
520: return;
521: }
522: }
523:
524: throw new InvalidJadException(InvalidJadException.CANNOT_AUTH);
525: }
526:
527: /**
528: * Parses out the charset from the content-type field.
529: * The charset parameter is after the ';' in the content-type field.
530: *
531: * @param contentType value of the content-type field
532: *
533: * @return charset
534: */
535: private static String getCharset(String contentType) {
536: int start;
537: int end;
538:
539: if (contentType == null) {
540: return null;
541: }
542:
543: start = contentType.indexOf("charset");
544: if (start < 0) {
545: return null;
546: }
547:
548: start = contentType.indexOf('=', start);
549: if (start < 0) {
550: return null;
551: }
552:
553: // start past the '='
554: start++;
555:
556: end = contentType.indexOf(';', start);
557: if (end < 0) {
558: end = contentType.length();
559: }
560:
561: return contentType.substring(start, end).trim();
562: }
563:
564: /**
565: * Formats the username and password for HTTP basic authentication
566: * according RFC 2617.
567: *
568: * @param username for HTTP authentication
569: * @param password for HTTP authentication
570: *
571: * @return properly formated basic authentication credential
572: */
573: private static String formatAuthCredentials(String username,
574: String password) {
575: byte[] data = new byte[username.length() + password.length()
576: + 1];
577: int j = 0;
578:
579: for (int i = 0; i < username.length(); i++, j++) {
580: data[j] = (byte) username.charAt(i);
581: }
582:
583: data[j] = (byte) ':';
584: j++;
585:
586: for (int i = 0; i < password.length(); i++, j++) {
587: data[j] = (byte) password.charAt(i);
588: }
589:
590: return "Basic " + Base64.encode(data, 0, data.length);
591: }
592:
593: /**
594: * Stops the installation. If installer is not installing then this
595: * method has no effect. This will cause the install method to
596: * throw an IOException if the install is not writing the suite
597: * to storage which is the point of no return.
598: *
599: * @return true if the install will stop, false if it is too late
600: */
601: public boolean stopInstalling() {
602:
603: boolean res = super .stopInstalling();
604: if (!res) {
605: return res;
606: }
607:
608: try {
609: httpInputStream.close();
610: } catch (Exception e) {
611: if (Logging.REPORT_LEVEL <= Logging.WARNING) {
612: Logging.report(Logging.WARNING, LogChannels.LC_AMS,
613: "stream close threw an Exception");
614: }
615: }
616:
617: try {
618: httpConnection.close();
619: } catch (Exception e) {
620: if (Logging.REPORT_LEVEL <= Logging.WARNING) {
621: Logging.report(Logging.WARNING, LogChannels.LC_AMS,
622: "stream close threw an Exception");
623: }
624: }
625:
626: return true;
627: }
628:
629: }
|