View Javadoc

1   package com.ebay.carad.os.vitalsigns.util;
2   
3   import java.io.*;
4   import java.util.*;
5   import java.net.*;
6   
7   /***
8    * A simple, tiny, nicely embeddable HTTP 1.0 server in Java
9    *
10   * <p> NanoHTTPD version 1.1,
11   * Copyright &copy; 2001,2005-2007 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/)
12   *
13   * <p><b>Features + limitations: </b><ul>
14   *
15   *    <li> Only one Java file </li>
16   *    <li> Java 1.1 compatible </li>
17   *    <li> Released as open source, Modified BSD licence </li>
18   *    <li> No fixed config files, logging, authorization etc. (Implement yourself if you need them.) </li>
19   *    <li> Supports parameter parsing of GET and POST methods </li>
20   *    <li> Supports both dynamic content and file serving </li>
21   *    <li> Never caches anything </li>
22   *    <li> Doesn't limit bandwidth, request time or simultaneous connections </li>
23   *    <li> Default code serves files and shows all HTTP parameters and headers</li>
24   *    <li> File server supports directory listing, index.html and index.htm </li>
25   *    <li> File server does the 301 redirection trick for directories without '/'</li>
26   *    <li> File server supports simple skipping for files (continue download) </li>
27   *    <li> File server uses current directory as a web root </li>
28   *    <li> File server serves also very long files without memory overhead </li>
29   *    <li> Contains a built-in list of most common mime types </li>
30   *    <li> All header names are converted lowercase so they don't vary between browsers/clients </li>
31   *
32   * </ul>
33   *
34   * <p><b>Ways to use: </b><ul>
35   *
36   *    <li> Run as a standalone app, serves files from current directory and shows requests</li>
37   *    <li> Subclass serve() and embed to your own program </li>
38   *    <li> Call serveFile() from serve() with your own base directory </li>
39   *
40   * </ul>
41   *
42   * See the end of the source file for distribution license
43   * (Modified BSD licence)
44   */
45  public class NanoHTTPD
46  {
47      // ==================================================
48      // API parts
49      // ==================================================
50  
51      /***
52       * Override this to customize the server.<p>
53       *
54       * (By default, this delegates to serveFile() and allows directory listing.)
55       *
56       * @parm uri    Percent-decoded URI without parameters, for example "/index.cgi"
57       * @parm method "GET", "POST" etc.
58       * @parm parms  Parsed, percent decoded parameters from URI and, in case of POST, data.
59       * @parm header Header entries, percent decoded
60       * @return HTTP response, see class Response for details
61       */
62      public Response serve( String uri, String method, Properties header, Properties parms )
63      {
64  //        System.out.println( method + " '" + uri + "' " );
65  
66  //        Enumeration e = header.propertyNames();
67  //        while ( e.hasMoreElements())
68  //        {
69  //            String value = (String)e.nextElement();
70  //            System.out.println( "  HDR: '" + value + "' = '" +
71  //                                header.getProperty( value ) + "'" );
72  //        }
73  //        e = parms.propertyNames();
74  //        while ( e.hasMoreElements())
75  //        {
76  //            String value = (String)e.nextElement();
77  //            System.out.println( "  PRM: '" + value + "' = '" +
78  //                                parms.getProperty( value ) + "'" );
79  //        }
80  
81          return serveFile( uri, header, myFileDir, true );
82      }
83  
84      /***
85       * HTTP response.
86       * Return one of these from serve().
87       */
88      public class Response
89      {
90          /***
91           * Default constructor: response = HTTP_OK, data = mime = 'null'
92           */
93          public Response()
94          {
95              this.status = HTTP_OK;
96          }
97  
98          /***
99           * Basic constructor.
100          */
101         public Response( String status, String mimeType, InputStream data )
102         {
103             this.status = status;
104             this.mimeType = mimeType;
105             this.data = data;
106         }
107 
108         /***
109          * Convenience method that makes an InputStream out of
110          * given text.
111          */
112         public Response( String status, String mimeType, String txt )
113         {
114             this.status = status;
115             this.mimeType = mimeType;
116             this.data = new ByteArrayInputStream( txt.getBytes());
117         }
118 
119         /***
120          * Adds given line to the header.
121          */
122         public void addHeader( String name, String value )
123         {
124             header.put( name, value );
125         }
126 
127         /***
128          * HTTP status code after processing, e.g. "200 OK", HTTP_OK
129          */
130         public String status;
131 
132         /***
133          * MIME type of content, e.g. "text/html"
134          */
135         public String mimeType;
136 
137         /***
138          * Data of the response, may be null.
139          */
140         public InputStream data;
141 
142         /***
143          * Headers for the HTTP response. Use addHeader()
144          * to add lines.
145          */
146         public Properties header = new Properties();
147     }
148 
149     /***
150      * Some HTTP response status codes
151      */
152     public static final String
153         HTTP_OK = "200 OK",
154         HTTP_REDIRECT = "301 Moved Permanently",
155         HTTP_FORBIDDEN = "403 Forbidden",
156         HTTP_NOTFOUND = "404 Not Found",
157         HTTP_BADREQUEST = "400 Bad Request",
158         HTTP_INTERNALERROR = "500 Internal Server Error",
159         HTTP_NOTIMPLEMENTED = "501 Not Implemented";
160 
161     /***
162      * Common mime types for dynamic content
163      */
164     public static final String
165         MIME_PLAINTEXT = "text/plain",
166         MIME_HTML = "text/html",
167         MIME_DEFAULT_BINARY = "application/octet-stream";
168 
169     // ==================================================
170     // Socket & server code
171     // ==================================================
172 
173     /***
174      * Starts a HTTP server to given port.<p>
175      * Throws an IOException if the socket is already in use
176      */
177     public NanoHTTPD( int port ) throws IOException
178     {
179         myTcpPort = port;
180 
181         final ServerSocket ss = new ServerSocket( myTcpPort );
182         Thread t = new Thread( new Runnable()
183             {
184                 public void run()
185                 {
186                     try
187                     {
188                         while( true )
189                             new HTTPSession( ss.accept());
190                     }
191                     catch ( IOException ioe )
192                     {}
193                 }
194             });
195         t.setDaemon( true );
196         t.start();
197     }
198     
199     public NanoHTTPD(int port, String directory) throws IOException {
200         this(port);
201         myFileDir = new File(directory);
202         System.out.println( "Now serving files in port " + port + " from \"" +
203                 myFileDir.getAbsolutePath() + "\"" );
204     }
205 
206     /***
207      * Starts as a standalone file server and waits for Enter.
208      */
209     public static void main( String[] args )
210     {
211         System.out.println( "NanoHTTPD 1.1 (C) 2001,2005-2007 Jarno Elonen\n" +
212                             "(Command line options: [port] [--licence])\n" );
213 
214         // Show licence if requested
215         int lopt = -1;
216         for ( int i=0; i<args.length; ++i )
217         if ( args[i].toLowerCase().endsWith( "licence" ))
218         {
219             lopt = i;
220             System.out.println( LICENCE + "\n" );
221         }
222 
223         // Change port if requested
224         int port = 80;
225         if ( args.length > 0 && lopt != 0 )
226             port = Integer.parseInt( args[0] );
227 
228         if ( args.length > 1 &&
229              args[1].toLowerCase().endsWith( "licence" ))
230                 System.out.println( LICENCE + "\n" );
231 
232         NanoHTTPD nh = null;
233         try
234         {
235             nh = new NanoHTTPD( port );
236         }
237         catch( IOException ioe )
238         {
239             System.err.println( "Couldn't start server:\n" + ioe );
240             System.exit( -1 );
241         }
242         nh.myFileDir = new File("");
243 
244         System.out.println( "Now serving files in port " + port + " from \"" +
245                             new File("").getAbsolutePath() + "\"" );
246         //System.out.println( "Hit Enter to stop.\n" );
247 
248         //try { System.in.read(); } catch( Throwable t ) {};
249     }
250 
251     /***
252      * Handles one session, i.e. parses the HTTP request
253      * and returns the response.
254      */
255     private class HTTPSession implements Runnable
256     {
257         public HTTPSession( Socket s )
258         {
259             mySocket = s;
260             Thread t = new Thread( this );
261             t.setDaemon( true );
262             t.start();
263         }
264 
265         public void run()
266         {
267             try
268             {
269                 InputStream is = mySocket.getInputStream();
270                 if ( is == null) return;
271                 BufferedReader in = new BufferedReader( new InputStreamReader( is ));
272 
273                 // Read the request line
274                 StringTokenizer st = new StringTokenizer( in.readLine());
275                 if ( !st.hasMoreTokens())
276                     sendError( HTTP_BADREQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html" );
277 
278                 String method = st.nextToken();
279 
280                 if ( !st.hasMoreTokens())
281                     sendError( HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html" );
282 
283                 String uri = decodePercent( st.nextToken());
284 
285                 // Decode parameters from the URI
286                 Properties parms = new Properties();
287                 int qmi = uri.indexOf( '?' );
288                 if ( qmi >= 0 )
289                 {
290                     decodeParms( uri.substring( qmi+1 ), parms );
291                     uri = decodePercent( uri.substring( 0, qmi ));
292                 }
293 
294                 // If there's another token, it's protocol version,
295                 // followed by HTTP headers. Ignore version but parse headers.
296                 // NOTE: this now forces header names uppercase since they are
297                 // case insensitive and vary by client.
298                 Properties header = new Properties();
299                 if ( st.hasMoreTokens())
300                 {
301                     String line = in.readLine();
302                     while ( line.trim().length() > 0 )
303                     {
304                         int p = line.indexOf( ':' );
305                         header.put( line.substring(0,p).trim().toLowerCase(), line.substring(p+1).trim());
306                         line = in.readLine();
307                     }
308                 }
309 
310                 // If the method is POST, there may be parameters
311                 // in data section, too, read it:
312                 if ( method.equalsIgnoreCase( "POST" ))
313                 {
314                     long size = 0x7FFFFFFFFFFFFFFFl;
315                     String contentLength = header.getProperty("content-length");
316                     if (contentLength != null)
317                     {
318                         try { size = Integer.parseInt(contentLength); }
319                         catch (NumberFormatException ex) {}
320                     }
321                     String postLine = "";
322                     char buf[] = new char[512];
323                     int read = in.read(buf);
324                     while ( read >= 0 && size > 0 && !postLine.endsWith("\r\n") )
325                     {
326                         size -= read;
327                         postLine += String.valueOf(buf, 0, read);
328                         if ( size > 0 )
329                             read = in.read(buf);
330                     }
331                     postLine = postLine.trim();
332                     decodeParms( postLine, parms );
333                 }
334 
335                 // Ok, now do the serve()
336                 Response r = serve( uri, method, header, parms );
337                 if ( r == null )
338                     sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response." );
339                 else
340                     sendResponse( r.status, r.mimeType, r.header, r.data );
341 
342                 in.close();
343             }
344             catch ( IOException ioe )
345             {
346                 try
347                 {
348                     sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
349                 }
350                 catch ( Throwable t ) {}
351             }
352             catch ( InterruptedException ie )
353             {
354                 // Thrown by sendError, ignore and exit the thread.
355             }
356         }
357 
358         /***
359          * Decodes the percent encoding scheme. <br/>
360          * For example: "an+example%20string" -> "an example string"
361          */
362         private String decodePercent( String str ) throws InterruptedException
363         {
364             try
365             {
366                 StringBuffer sb = new StringBuffer();
367                 for( int i=0; i<str.length(); i++ )
368                 {
369                     char c = str.charAt( i );
370                     switch ( c )
371                     {
372                         case '+':
373                             sb.append( ' ' );
374                             break;
375                         case '%':
376                             sb.append((char)Integer.parseInt( str.substring(i+1,i+3), 16 ));
377                             i += 2;
378                             break;
379                         default:
380                             sb.append( c );
381                             break;
382                     }
383                 }
384                 return new String( sb.toString().getBytes());
385             }
386             catch( Exception e )
387             {
388                 sendError( HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding." );
389                 return null;
390             }
391         }
392 
393         /***
394          * Decodes parameters in percent-encoded URI-format
395          * ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and
396          * adds them to given Properties.
397          */
398         private void decodeParms( String parms, Properties p )
399             throws InterruptedException
400         {
401             if ( parms == null )
402                 return;
403 
404             StringTokenizer st = new StringTokenizer( parms, "&" );
405             while ( st.hasMoreTokens())
406             {
407                 String e = st.nextToken();
408                 int sep = e.indexOf( '=' );
409                 if ( sep >= 0 )
410                     p.put( decodePercent( e.substring( 0, sep )).trim(),
411                            decodePercent( e.substring( sep+1 )));
412             }
413         }
414 
415         /***
416          * Returns an error message as a HTTP response and
417          * throws InterruptedException to stop furhter request processing.
418          */
419         private void sendError( String status, String msg ) throws InterruptedException
420         {
421             sendResponse( status, MIME_PLAINTEXT, null, new ByteArrayInputStream( msg.getBytes()));
422             throw new InterruptedException();
423         }
424 
425         /***
426          * Sends given response to the socket.
427          */
428         private void sendResponse( String status, String mime, Properties header, InputStream data )
429         {
430             try
431             {
432                 if ( status == null )
433                     throw new Error( "sendResponse(): Status can't be null." );
434 
435                 OutputStream out = mySocket.getOutputStream();
436                 PrintWriter pw = new PrintWriter( out );
437                 pw.print("HTTP/1.0 " + status + " \r\n");
438 
439                 if ( mime != null )
440                     pw.print("Content-Type: " + mime + "\r\n");
441 
442                 if ( header == null || header.getProperty( "Date" ) == null )
443                     pw.print( "Date: " + gmtFrmt.format( new Date()) + "\r\n");
444 
445                 if ( header != null )
446                 {
447                     Enumeration e = header.keys();
448                     while ( e.hasMoreElements())
449                     {
450                         String key = (String)e.nextElement();
451                         String value = header.getProperty( key );
452                         pw.print( key + ": " + value + "\r\n");
453                     }
454                 }
455 
456                 pw.print("\r\n");
457                 pw.flush();
458 
459                 if ( data != null )
460                 {
461                     byte[] buff = new byte[2048];
462                     while (true)
463                     {
464                         int read = data.read( buff, 0, 2048 );
465                         if (read <= 0)
466                             break;
467                         out.write( buff, 0, read );
468                     }
469                 }
470                 out.flush();
471                 out.close();
472                 if ( data != null )
473                     data.close();
474             }
475             catch( IOException ioe )
476             {
477                 // Couldn't write? No can do.
478                 try { mySocket.close(); } catch( Throwable t ) {}
479             }
480         }
481 
482         private Socket mySocket;
483     };
484 
485     /***
486      * URL-encodes everything between "/"-characters.
487      * Encodes spaces as '%20' instead of '+'.
488      */
489     private String encodeUri( String uri )
490     {
491         String newUri = "";
492         StringTokenizer st = new StringTokenizer( uri, "/ ", true );
493         while ( st.hasMoreTokens())
494         {
495             String tok = st.nextToken();
496             if ( tok.equals( "/" ))
497                 newUri += "/";
498             else if ( tok.equals( " " ))
499                 newUri += "%20";
500             else
501             {
502                 // newUri += URLEncoder.encode( tok );
503                 // For Java 1.4 you'll want to use this instead:
504                 try { newUri += URLEncoder.encode( tok, "UTF-8" ); } catch ( UnsupportedEncodingException uee ) {}
505             }
506         }
507         return newUri;
508     }
509 
510     private int myTcpPort;
511     File myFileDir;
512 
513     // ==================================================
514     // File server code
515     // ==================================================
516 
517     /***
518      * Serves file from homeDir and its' subdirectories (only).
519      * Uses only URI, ignores all headers and HTTP parameters.
520      */
521     public Response serveFile( String uri, Properties header, File homeDir,
522                                boolean allowDirectoryListing )
523     {
524         // Make sure we won't die of an exception later
525         if ( !homeDir.isDirectory())
526             return new Response( HTTP_INTERNALERROR, MIME_PLAINTEXT,
527                                  "INTERNAL ERRROR: serveFile(): given homeDir is not a directory." );
528 
529         // Remove URL arguments
530         uri = uri.trim().replace( File.separatorChar, '/' );
531         if ( uri.indexOf( '?' ) >= 0 )
532             uri = uri.substring(0, uri.indexOf( '?' ));
533 
534         // Prohibit getting out of current directory
535         if ( uri.startsWith( ".." ) || uri.endsWith( ".." ) || uri.indexOf( "../" ) >= 0 )
536             return new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
537                                  "FORBIDDEN: Won't serve ../ for security reasons." );
538 
539         File f = new File( homeDir, uri );
540         if ( !f.exists())
541             return new Response( HTTP_NOTFOUND, MIME_PLAINTEXT,
542                                  "Error 404, file not found." );
543 
544         // List the directory, if necessary
545         if ( f.isDirectory())
546         {
547             // Browsers get confused without '/' after the
548             // directory, send a redirect.
549             if ( !uri.endsWith( "/" ))
550             {
551                 uri += "/";
552                 Response r = new Response( HTTP_REDIRECT, MIME_HTML,
553                                            "<html><body>Redirected: <a href=\"" + uri + "\">" +
554                                            uri + "</a></body></html>");
555                 r.addHeader( "Location", uri );
556                 return r;
557             }
558 
559             // First try index.html and index.htm
560             if ( new File( f, "index.html" ).exists())
561                 f = new File( homeDir, uri + "/index.html" );
562             else if ( new File( f, "index.htm" ).exists())
563                 f = new File( homeDir, uri + "/index.htm" );
564 
565             // No index file, list the directory
566             else if ( allowDirectoryListing )
567             {
568                 String[] files = f.list();
569                 String msg = "<html><body><h1>Directory " + uri + "</h1><br/>";
570 
571                 if ( uri.length() > 1 )
572                 {
573                     String u = uri.substring( 0, uri.length()-1 );
574                     int slash = u.lastIndexOf( '/' );
575                     if ( slash >= 0 && slash  < u.length())
576                         msg += "<b><a href=\"" + uri.substring(0, slash+1) + "\">..</a></b><br/>";
577                 }
578 
579                 for ( int i=0; i<files.length; ++i )
580                 {
581                     File curFile = new File( f, files[i] );
582                     boolean dir = curFile.isDirectory();
583                     if ( dir )
584                     {
585                         msg += "<b>";
586                         files[i] += "/";
587                     }
588 
589                     msg += "<a href=\"" + encodeUri( uri + files[i] ) + "\">" +
590                            files[i] + "</a>";
591 
592                     // Show file size
593                     if ( curFile.isFile())
594                     {
595                         long len = curFile.length();
596                         msg += " &nbsp;<font size=2>(";
597                         if ( len < 1024 )
598                             msg += curFile.length() + " bytes";
599                         else if ( len < 1024 * 1024 )
600                             msg += curFile.length()/1024 + "." + (curFile.length()%1024/10%100) + " KB";
601                         else
602                             msg += curFile.length()/(1024*1024) + "." + curFile.length()%(1024*1024)/10%100 + " MB";
603 
604                         msg += ")</font>";
605                     }
606                     msg += "<br/>";
607                     if ( dir ) msg += "</b>";
608                 }
609                 return new Response( HTTP_OK, MIME_HTML, msg );
610             }
611             else
612             {
613                 return new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
614                                  "FORBIDDEN: No directory listing." );
615             }
616         }
617 
618         try
619         {
620             // Get MIME type from file name extension, if possible
621             String mime = null;
622             int dot = f.getCanonicalPath().lastIndexOf( '.' );
623             if ( dot >= 0 )
624                 mime = (String)theMimeTypes.get( f.getCanonicalPath().substring( dot + 1 ).toLowerCase());
625             if ( mime == null )
626                 mime = MIME_DEFAULT_BINARY;
627 
628             // Support (simple) skipping:
629             long startFrom = 0;
630             String range = header.getProperty( "Range" );
631             if ( range != null )
632             {
633                 if ( range.startsWith( "bytes=" ))
634                 {
635                     range = range.substring( "bytes=".length());
636                     int minus = range.indexOf( '-' );
637                     if ( minus > 0 )
638                         range = range.substring( 0, minus );
639                     try {
640                         startFrom = Long.parseLong( range );
641                     }
642                     catch ( NumberFormatException nfe ) {}
643                 }
644             }
645 
646             FileInputStream fis = new FileInputStream( f );
647             fis.skip( startFrom );
648             Response r = new Response( HTTP_OK, mime, fis );
649             r.addHeader( "Content-length", "" + (f.length() - startFrom));
650             r.addHeader( "Content-range", "" + startFrom + "-" +
651                         (f.length()-1) + "/" + f.length());
652             return r;
653         }
654         catch( IOException ioe )
655         {
656             return new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed." );
657         }
658     }
659 
660     /***
661      * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE
662      */
663     private static Hashtable theMimeTypes = new Hashtable();
664     static
665     {
666         StringTokenizer st = new StringTokenizer(
667             "htm        text/html "+
668             "html       text/html "+
669             "txt        text/plain "+
670             "asc        text/plain "+
671             "gif        image/gif "+
672             "jpg        image/jpeg "+
673             "jpeg       image/jpeg "+
674             "png        image/png "+
675             "mp3        audio/mpeg "+
676             "m3u        audio/mpeg-url " +
677             "pdf        application/pdf "+
678             "doc        application/msword "+
679             "ogg        application/x-ogg "+
680             "zip        application/octet-stream "+
681             "exe        application/octet-stream "+
682             "class      application/octet-stream " );
683         while ( st.hasMoreTokens())
684             theMimeTypes.put( st.nextToken(), st.nextToken());
685     }
686 
687     /***
688      * GMT date formatter
689      */
690     private static java.text.SimpleDateFormat gmtFrmt;
691     static
692     {
693         gmtFrmt = new java.text.SimpleDateFormat( "E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
694         gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
695     }
696 
697     /***
698      * The distribution licence
699      */
700     public static final String LICENCE =
701         "Copyright (C) 2001,2005 by Jarno Elonen <elonen@iki.fi>\n"+
702         "\n"+
703         "Redistribution and use in source and binary forms, with or without\n"+
704         "modification, are permitted provided that the following conditions\n"+
705         "are met:\n"+
706         "\n"+
707         "Redistributions of source code must retain the above copyright notice,\n"+
708         "this list of conditions and the following disclaimer. Redistributions in\n"+
709         "binary form must reproduce the above copyright notice, this list of\n"+
710         "conditions and the following disclaimer in the documentation and/or other\n"+
711         "materials provided with the distribution. The name of the author may not\n"+
712         "be used to endorse or promote products derived from this software without\n"+
713         "specific prior written permission. \n"+
714         " \n"+
715         "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"+
716         "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n"+
717         "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n"+
718         "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"+
719         "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n"+
720         "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n"+
721         "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"+
722         "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"+
723         "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"+
724         "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
725 }