5 package com.google.appinventor.components.runtime.util;
6 import java.io.BufferedReader;
7 import java.io.ByteArrayInputStream;
9 import java.io.FileInputStream;
10 import java.io.IOException;
11 import java.io.InputStream;
12 import java.io.InputStreamReader;
13 import java.io.OutputStream;
14 import java.io.PrintStream;
15 import java.io.PrintWriter;
16 import java.net.ServerSocket;
17 import java.net.Socket;
18 import java.net.URLEncoder;
19 import java.util.concurrent.SynchronousQueue;
20 import java.util.concurrent.ThreadFactory;
21 import java.util.concurrent.ThreadPoolExecutor;
22 import java.util.concurrent.TimeUnit;
23 import java.util.Date;
24 import java.util.Enumeration;
25 import java.util.Vector;
26 import java.util.Hashtable;
27 import java.util.Locale;
28 import java.util.Properties;
29 import java.util.StringTokenizer;
30 import java.util.TimeZone;
32 import java.io.ByteArrayOutputStream;
33 import java.io.FileOutputStream;
35 import android.util.Log;
84 private static final String LOG_TAG =
"AppInvHTTPD";
104 public Response serve( String uri, String method, Properties header, Properties parms, Properties files, Socket mySocket )
106 myOut.println( method +
" '" + uri +
"' " );
108 Enumeration e = header.propertyNames();
109 while ( e.hasMoreElements())
111 String value = (String)e.nextElement();
112 myOut.println(
" HDR: '" + value +
"' = '" +
113 header.getProperty( value ) +
"'" );
115 e = parms.propertyNames();
116 while ( e.hasMoreElements())
118 String value = (String)e.nextElement();
119 myOut.println(
" PRM: '" + value +
"' = '" +
120 parms.getProperty( value ) +
"'" );
122 e = files.propertyNames();
123 while ( e.hasMoreElements())
125 String value = (String)e.nextElement();
126 myOut.println(
" UPLOADED: '" + value +
"' = '" +
127 files.getProperty( value ) +
"'" );
130 return serveFile( uri, header, myRootDir,
true );
167 this.
data =
new ByteArrayInputStream( txt.getBytes(
"UTF-8"));
169 catch ( java.io.UnsupportedEncodingException uee )
171 uee.printStackTrace();
180 header.put( name, value );
202 public Properties
header =
new Properties();
208 public static final String
210 HTTP_PARTIALCONTENT =
"206 Partial Content",
211 HTTP_RANGE_NOT_SATISFIABLE =
"416 Requested Range Not Satisfiable",
212 HTTP_REDIRECT =
"301 Moved Permanently",
213 HTTP_NOTMODIFIED =
"304 Not Modified",
214 HTTP_FORBIDDEN =
"403 Forbidden",
215 HTTP_NOTFOUND =
"404 Not Found",
216 HTTP_BADREQUEST =
"400 Bad Request",
217 HTTP_INTERNALERROR =
"500 Internal Server Error",
218 HTTP_NOTIMPLEMENTED =
"501 Not Implemented";
223 public static final String
225 MIME_HTML =
"text/html",
226 MIME_DEFAULT_BINARY =
"application/octet-stream",
227 MIME_XML =
"text/xml";
233 private static final int REPL_STACK_SIZE = 256*1024;
240 public NanoHTTPD(
int port, File wwwroot )
throws IOException
243 this.myRootDir = wwwroot;
244 myServerSocket =
new ServerSocket( myTcpPort );
245 myThread =
new Thread(
new Runnable()
252 new HTTPSession( myServerSocket.accept());
254 catch ( IOException ioe )
258 myThread.setDaemon(
true );
269 myServerSocket.close();
272 catch ( IOException ioe ) {}
273 catch ( InterruptedException e ) {}
280 public static void main( String[] args )
282 myOut.println(
"NanoHTTPD 1.25 (C) 2001,2005-2011 Jarno Elonen and (C) 2010 Konstantinos Togias\n" +
283 "(Command line options: [-p port] [-d root-dir] [--licence])\n" );
287 File wwwroot =
new File(
".").getAbsoluteFile();
290 for (
int i=0; i<args.length; ++i )
291 if(args[i].equalsIgnoreCase(
"-p"))
292 port = Integer.parseInt( args[i+1] );
293 else if(args[i].equalsIgnoreCase(
"-d"))
294 wwwroot =
new File( args[i+1] ).getAbsoluteFile();
295 else if ( args[i].toLowerCase().endsWith(
"licence" ))
297 myOut.println( LICENCE +
"\n" );
305 catch( IOException ioe )
307 myErr.println(
"Couldn't start server:\n" + ioe );
311 myOut.println(
"Now serving files in port " + port +
" from \"" + wwwroot +
"\"" );
312 myOut.println(
"Hit Enter to stop.\n" );
314 try { System.in.read(); }
catch( Throwable t ) {}
317 private class myThreadFactory
implements ThreadFactory {
319 public Thread newThread(Runnable r) {
320 Thread retval =
new Thread(
new ThreadGroup(
"biggerstack"), r,
"HTTPD Session", REPL_STACK_SIZE);
321 retval.setDaemon(
true);
333 private ThreadPoolExecutor myExecutor =
new ThreadPoolExecutor(2, 10, 5,
334 TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new myThreadFactory());
340 private class HTTPSession
implements Runnable
342 public HTTPSession( Socket s )
345 Log.d(LOG_TAG,
"NanoHTTPD: getPoolSize() = " + myExecutor.getPoolSize());
346 myExecutor.execute(
this);
353 InputStream is = mySocket.getInputStream();
354 if ( is ==
null)
return;
360 byte[] buf =
new byte[bufsize];
361 int rlen = is.read(buf, 0, bufsize);
362 if (rlen <= 0)
return;
365 ByteArrayInputStream hbis =
new ByteArrayInputStream(buf, 0, rlen);
366 BufferedReader hin =
new BufferedReader(
new InputStreamReader( hbis ));
367 Properties pre =
new Properties();
368 Properties parms =
new Properties();
369 Properties header =
new Properties();
370 Properties files =
new Properties();
373 decodeHeader(hin, pre, parms, header);
374 String method = pre.getProperty(
"method");
375 String uri = pre.getProperty(
"uri");
377 long size = 0x7FFFFFFFFFFFFFFFl;
378 String contentLength = header.getProperty(
"content-length");
379 if (contentLength !=
null)
381 try { size = Integer.parseInt(contentLength); }
382 catch (NumberFormatException ex) {}
388 boolean sbfound =
false;
389 while (splitbyte < rlen)
391 if (buf[splitbyte] ==
'\r' && buf[++splitbyte] ==
'\n' && buf[++splitbyte] ==
'\r' && buf[++splitbyte] ==
'\n') {
401 if ( method.equalsIgnoreCase(
"PUT" ) )
403 File tmpfile = File.createTempFile(
"upload",
"bin");
404 tmpfile.deleteOnExit();
405 f =
new FileOutputStream(tmpfile);
406 files.put(
"content", tmpfile.getAbsolutePath());
410 f =
new ByteArrayOutputStream();
412 if (splitbyte < rlen) f.write(buf, splitbyte, rlen-splitbyte);
420 if (splitbyte < rlen)
421 size -= rlen - splitbyte +1;
422 else if (!sbfound || size == 0x7FFFFFFFFFFFFFFFl)
427 while ( rlen >= 0 && size > 0 )
429 rlen = is.read(buf, 0, 512);
432 f.write(buf, 0, rlen);
437 if ( method.equalsIgnoreCase(
"POST" ))
440 byte [] fbuf = ((ByteArrayOutputStream)f).toByteArray();
443 ByteArrayInputStream bin =
new ByteArrayInputStream(fbuf);
444 BufferedReader in =
new BufferedReader(
new InputStreamReader(bin));
446 String contentType =
"";
447 String contentTypeHeader = header.getProperty(
"content-type");
448 StringTokenizer st =
new StringTokenizer( contentTypeHeader ,
"; " );
449 if ( st.hasMoreTokens()) {
450 contentType = st.nextToken();
453 if (contentType.equalsIgnoreCase(
"multipart/form-data"))
456 if ( !st.hasMoreTokens())
457 sendError( HTTP_BADREQUEST,
"BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html" );
458 String boundaryExp = st.nextToken();
459 st =
new StringTokenizer( boundaryExp ,
"=" );
460 if (st.countTokens() != 2)
461 sendError( HTTP_BADREQUEST,
"BAD REQUEST: Content type is multipart/form-data but boundary syntax error. Usage: GET /example/file.html" );
463 String boundary = st.nextToken();
465 decodeMultipartData(boundary, fbuf, in, parms, files);
470 String postLine =
"";
471 char pbuf[] =
new char[512];
472 int read = in.read(pbuf);
473 while ( read >= 0 && !postLine.endsWith(
"\r\n") )
475 postLine += String.valueOf(pbuf, 0, read);
476 read = in.read(pbuf);
478 postLine = postLine.trim();
479 decodeParms( postLine, parms );
483 else if ( method.equalsIgnoreCase(
"PUT " ) )
489 Response r =
serve( uri, method, header, parms, files, mySocket );
491 sendError( HTTP_INTERNALERROR,
"SERVER INTERNAL ERROR: Serve() returned a null response." );
493 sendResponse( r.status, r.mimeType, r.header, r.data );
497 catch ( IOException ioe )
501 sendError( HTTP_INTERNALERROR,
"SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
503 catch ( Throwable t ) {}
505 catch ( InterruptedException ie )
515 private void decodeHeader(BufferedReader in, Properties pre, Properties parms, Properties header)
516 throws InterruptedException
520 String inLine = in.readLine();
521 if (inLine ==
null)
return;
522 StringTokenizer st =
new StringTokenizer( inLine );
523 if ( !st.hasMoreTokens())
524 sendError( HTTP_BADREQUEST,
"BAD REQUEST: Syntax error. Usage: GET /example/file.html" );
526 String method = st.nextToken();
527 pre.put(
"method", method);
529 if ( !st.hasMoreTokens())
530 sendError( HTTP_BADREQUEST,
"BAD REQUEST: Missing URI. Usage: GET /example/file.html" );
532 String uri = st.nextToken();
535 int qmi = uri.indexOf(
'?' );
538 decodeParms( uri.substring( qmi+1 ), parms );
539 uri = decodePercent( uri.substring( 0, qmi ));
541 else uri = decodePercent(uri);
547 if ( st.hasMoreTokens())
549 String line = in.readLine();
550 while ( line !=
null && line.trim().length() > 0 )
552 int p = line.indexOf(
':' );
554 header.put( line.substring(0,p).trim().toLowerCase(), line.substring(p+1).trim());
555 line = in.readLine();
561 catch ( IOException ioe )
563 sendError( HTTP_INTERNALERROR,
"SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
571 private void decodeMultipartData(String boundary,
byte[] fbuf, BufferedReader in, Properties parms, Properties files)
572 throws InterruptedException
576 int[] bpositions = getBoundaryPositions(fbuf,boundary.getBytes());
577 int boundarycount = 1;
578 String mpline = in.readLine();
579 while ( mpline !=
null )
581 if (mpline.indexOf(boundary) == -1)
582 sendError( HTTP_BADREQUEST,
"BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html" );
584 Properties item =
new Properties();
585 mpline = in.readLine();
586 while (mpline !=
null && mpline.trim().length() > 0)
588 int p = mpline.indexOf(
':' );
590 item.put( mpline.substring(0,p).trim().toLowerCase(), mpline.substring(p+1).trim());
591 mpline = in.readLine();
595 String contentDisposition = item.getProperty(
"content-disposition");
596 if (contentDisposition ==
null)
598 sendError( HTTP_BADREQUEST,
"BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html" );
600 StringTokenizer st =
new StringTokenizer( contentDisposition ,
"; " );
601 Properties disposition =
new Properties();
602 while ( st.hasMoreTokens())
604 String token = st.nextToken();
605 int p = token.indexOf(
'=' );
607 disposition.put( token.substring(0,p).trim().toLowerCase(), token.substring(p+1).trim());
609 String pname = disposition.getProperty(
"name");
610 pname = pname.substring(1,pname.length()-1);
613 if (item.getProperty(
"content-type") ==
null) {
614 while (mpline !=
null && mpline.indexOf(boundary) == -1)
616 mpline = in.readLine();
619 int d = mpline.indexOf(boundary);
623 value+=mpline.substring(0,d-2);
629 if (boundarycount> bpositions.length)
630 sendError( HTTP_INTERNALERROR,
"Error processing request" );
631 int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount-2]);
632 String path = saveTmpFile(fbuf, offset, bpositions[boundarycount-1]-offset-4);
633 files.put(pname, path);
634 value = disposition.getProperty(
"filename");
635 value = value.substring(1,value.length()-1);
637 mpline = in.readLine();
638 }
while (mpline !=
null && mpline.indexOf(boundary) == -1);
640 parms.put(pname, value);
644 catch ( IOException ioe )
646 sendError( HTTP_INTERNALERROR,
"SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
653 public int[] getBoundaryPositions(
byte[] b,
byte[] boundary)
657 Vector matchbytes =
new Vector();
658 for (
int i=0; i<b.length; i++)
660 if (b[i] == boundary[matchcount])
665 if (matchcount==boundary.length)
667 matchbytes.addElement(
new Integer(matchbyte));
679 int[] ret =
new int[matchbytes.size()];
680 for (
int i=0; i < ret.length; i++)
682 ret[i] = ((Integer)matchbytes.elementAt(i)).intValue();
692 private String saveTmpFile(
byte[] b,
int offset,
int len)
697 String tmpdir = System.getProperty(
"java.io.tmpdir");
699 File temp = File.createTempFile(
"NanoHTTPD",
"",
new File(tmpdir));
700 OutputStream fstream =
new FileOutputStream(temp);
701 fstream.write(b, offset, len);
703 path = temp.getAbsolutePath();
704 }
catch (Exception e) {
705 myErr.println(
"Error: " + e.getMessage());
716 private int stripMultipartHeaders(
byte[] b,
int offset)
719 for (i=offset; i<b.length; i++)
721 if (b[i] ==
'\r' && b[++i] ==
'\n' && b[++i] ==
'\r' && b[++i] ==
'\n')
731 private String decodePercent( String str )
throws InterruptedException
735 StringBuffer sb =
new StringBuffer();
736 for(
int i=0; i<str.length(); i++ )
738 char c = str.charAt( i );
745 sb.append((
char)Integer.parseInt( str.substring(i+1,i+3), 16 ));
753 return sb.toString();
757 sendError( HTTP_BADREQUEST,
"BAD REQUEST: Bad percent-encoding." );
769 private void decodeParms( String parms, Properties p )
770 throws InterruptedException
775 StringTokenizer st =
new StringTokenizer( parms,
"&" );
776 while ( st.hasMoreTokens())
778 String e = st.nextToken();
779 int sep = e.indexOf(
'=' );
781 p.put( decodePercent( e.substring( 0, sep )).trim(),
782 decodePercent( e.substring( sep+1 )));
790 private void sendError( String status, String msg )
throws InterruptedException
792 sendResponse( status,
MIME_PLAINTEXT,
null,
new ByteArrayInputStream( msg.getBytes()));
793 throw new InterruptedException();
799 private void sendResponse( String status, String mime, Properties header, InputStream data )
803 if ( status ==
null )
804 throw new Error(
"sendResponse(): Status can't be null." );
806 OutputStream out = mySocket.getOutputStream();
807 PrintWriter pw =
new PrintWriter( out );
808 pw.print(
"HTTP/1.0 " + status +
" \r\n");
811 pw.print(
"Content-Type: " + mime +
"\r\n");
813 if ( header ==
null || header.getProperty(
"Date" ) == null )
814 pw.print(
"Date: " + gmtFrmt.format(
new Date()) +
"\r\n");
816 if ( header !=
null )
818 Enumeration e = header.keys();
819 while ( e.hasMoreElements())
821 String key = (String)e.nextElement();
822 String value = header.getProperty( key );
823 pw.print( key +
": " + value +
"\r\n");
832 int pending = data.available();
833 byte[] buff =
new byte[theBufferSize];
836 int read = data.read( buff, 0, ( (pending>theBufferSize) ? theBufferSize : pending ));
837 if (read <= 0)
break;
838 out.write( buff, 0, read );
847 catch( IOException ioe )
850 try { mySocket.close(); }
catch( Throwable t ) {}
854 private Socket mySocket;
861 private String encodeUri( String uri )
864 StringTokenizer st =
new StringTokenizer( uri,
"/ ",
true );
865 while ( st.hasMoreTokens())
867 String tok = st.nextToken();
868 if ( tok.equals(
"/" ))
870 else if ( tok.equals(
" " ))
874 newUri += URLEncoder.encode( tok );
882 private int myTcpPort;
883 private final ServerSocket myServerSocket;
884 private Thread myThread;
885 private File myRootDir;
896 boolean allowDirectoryListing )
901 if ( !homeDir.isDirectory())
903 "INTERNAL ERRROR: serveFile(): given homeDir is not a directory." );
908 uri = uri.trim().replace( File.separatorChar,
'/' );
909 if ( uri.indexOf(
'?' ) >= 0 )
910 uri = uri.substring(0, uri.indexOf(
'?' ));
913 if ( uri.startsWith(
".." ) || uri.endsWith(
".." ) || uri.indexOf(
"../" ) >= 0 )
915 "FORBIDDEN: Won't serve ../ for security reasons." );
918 File f =
new File( homeDir, uri );
919 if ( res ==
null && !f.exists())
921 "Error 404, file not found." );
924 if ( res ==
null && f.isDirectory())
928 if ( !uri.endsWith(
"/" ))
931 res =
new Response( HTTP_REDIRECT, MIME_HTML,
932 "<html><body>Redirected: <a href=\"" + uri +
"\">" +
933 uri +
"</a></body></html>");
940 if (
new File( f,
"index.html" ).exists())
941 f =
new File( homeDir, uri +
"/index.html" );
942 else if (
new File( f,
"index.htm" ).exists())
943 f =
new File( homeDir, uri +
"/index.htm" );
945 else if ( allowDirectoryListing && f.canRead() )
947 String[] files = f.list();
948 String msg =
"<html><body><h1>Directory " + uri +
"</h1><br/>";
950 if ( uri.length() > 1 )
952 String u = uri.substring( 0, uri.length()-1 );
953 int slash = u.lastIndexOf(
'/' );
954 if ( slash >= 0 && slash < u.length())
955 msg +=
"<b><a href=\"" + uri.substring(0, slash+1) +
"\">..</a></b><br/>";
960 for (
int i=0; i<files.length; ++i )
962 File curFile =
new File( f, files[i] );
963 boolean dir = curFile.isDirectory();
970 msg +=
"<a href=\"" + encodeUri( uri + files[i] ) +
"\">" +
974 if ( curFile.isFile())
976 long len = curFile.length();
977 msg +=
" <font size=2>(";
979 msg += len +
" bytes";
980 else if ( len < 1024 * 1024 )
981 msg += len/1024 +
"." + (len%1024/10%100) +
" KB";
983 msg += len/(1024*1024) +
"." + len%(1024*1024)/10%100 +
" MB";
988 if ( dir ) msg +=
"</b>";
991 msg +=
"</body></html>";
997 "FORBIDDEN: No directory listing." );
1008 int dot = f.getCanonicalPath().lastIndexOf(
'.' );
1010 mime = (String)theMimeTypes.get( f.getCanonicalPath().substring( dot + 1 ).toLowerCase());
1012 mime = MIME_DEFAULT_BINARY;
1015 String etag = Integer.toHexString((f.getAbsolutePath() + f.lastModified() +
"" + f.length()).hashCode());
1020 String range = header.getProperty(
"range" );
1021 if ( range !=
null )
1023 if ( range.startsWith(
"bytes=" ))
1025 range = range.substring(
"bytes=".length());
1026 int minus = range.indexOf(
'-' );
1030 startFrom = Long.parseLong( range.substring( 0, minus ));
1031 endAt = Long.parseLong( range.substring( minus+1 ));
1034 catch ( NumberFormatException nfe ) {}
1039 long fileLen = f.length();
1040 if (range !=
null && startFrom >= 0)
1042 if ( startFrom >= fileLen)
1045 res.
addHeader(
"Content-Range",
"bytes 0-0/" + fileLen);
1052 long newLen = endAt - startFrom + 1;
1053 if ( newLen < 0 ) newLen = 0;
1055 final long dataLen = newLen;
1056 FileInputStream fis =
new FileInputStream( f ) {
1057 public int available()
throws IOException {
return (
int)dataLen; }
1059 fis.skip( startFrom );
1061 res =
new Response( HTTP_PARTIALCONTENT, mime, fis );
1062 res.
addHeader(
"Content-Length",
"" + dataLen);
1063 res.
addHeader(
"Content-Range",
"bytes " + startFrom +
"-" + endAt +
"/" + fileLen);
1069 if (etag.equals(header.getProperty(
"if-none-match")))
1070 res =
new Response( HTTP_NOTMODIFIED, mime,
"");
1074 res.
addHeader(
"Content-Length",
"" + fileLen);
1080 catch( IOException ioe )
1085 res.
addHeader(
"Accept-Ranges",
"bytes");
1092 private static Hashtable theMimeTypes =
new Hashtable();
1095 StringTokenizer st =
new StringTokenizer(
1107 "m3u audio/mpeg-url " +
1110 "flv video/x-flv " +
1111 "mov video/quicktime " +
1112 "swf application/x-shockwave-flash " +
1113 "js application/javascript "+
1114 "pdf application/pdf "+
1115 "doc application/msword "+
1116 "ogg application/x-ogg "+
1117 "zip application/octet-stream "+
1118 "exe application/octet-stream "+
1119 "class application/octet-stream " );
1120 while ( st.hasMoreTokens())
1121 theMimeTypes.put( st.nextToken(), st.nextToken());
1124 private static int theBufferSize = 16 * 1024;
1127 protected static PrintStream
myOut = System.out;
1128 protected static PrintStream
myErr = System.err;
1133 private static java.text.SimpleDateFormat gmtFrmt;
1136 gmtFrmt =
new java.text.SimpleDateFormat(
"E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
1137 gmtFrmt.setTimeZone(TimeZone.getTimeZone(
"GMT"));
1143 private static final String LICENCE =
1144 "Copyright (C) 2001,2005-2011 by Jarno Elonen <elonen@iki.fi>\n"+
1145 "and Copyright (C) 2010 by Konstantinos Togias <info@ktogias.gr>\n"+
1147 "Redistribution and use in source and binary forms, with or without\n"+
1148 "modification, are permitted provided that the following conditions\n"+
1151 "Redistributions of source code must retain the above copyright notice,\n"+
1152 "this list of conditions and the following disclaimer. Redistributions in\n"+
1153 "binary form must reproduce the above copyright notice, this list of\n"+
1154 "conditions and the following disclaimer in the documentation and/or other\n"+
1155 "materials provided with the distribution. The name of the author may not\n"+
1156 "be used to endorse or promote products derived from this software without\n"+
1157 "specific prior written permission. \n"+
1159 "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"+
1160 "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n"+
1161 "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n"+
1162 "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"+
1163 "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n"+
1164 "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n"+
1165 "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"+
1166 "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"+
1167 "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"+
1168 "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";