sikuli-server 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sikuli-server.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Andrew Vos
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Sikuli::Server
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'sikuli-server'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install sikuli-server
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,2 @@
1
+ bin/*
2
+ lib/*
@@ -0,0 +1,3 @@
1
+ mkdir bin
2
+ javac -d bin -sourcepath src -cp lib/gson-2.2.2.jar src/*.java
3
+ java -cp bin:lib/gson-2.2.2.jar Main
@@ -0,0 +1,2 @@
1
+ Manifest-Version: 1.0
2
+ Main-Class: SikuliServer.src.Main
@@ -0,0 +1,16 @@
1
+ import java.io.IOException;
2
+ import java.lang.InterruptedException;
3
+
4
+ public class Main {
5
+ public static void main (String[] args) throws IOException
6
+ {
7
+ SikuliServer server = new SikuliServer();
8
+ while (server.getServerRunning())
9
+ {
10
+ try {
11
+ Thread.sleep(1);
12
+ }
13
+ catch (InterruptedException exception) {}
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,1117 @@
1
+ import java.io.ByteArrayInputStream;
2
+ import java.io.File;
3
+ import java.io.FileInputStream;
4
+ import java.io.IOException;
5
+ import java.io.InputStream;
6
+ import java.io.InputStreamReader;
7
+ import java.io.OutputStream;
8
+ import java.io.PrintStream;
9
+ import java.io.PrintWriter;
10
+ import java.io.BufferedReader;
11
+ import java.net.ServerSocket;
12
+ import java.net.Socket;
13
+ import java.net.URLEncoder;
14
+ import java.util.Date;
15
+ import java.util.Enumeration;
16
+ import java.util.Vector;
17
+ import java.util.Hashtable;
18
+ import java.util.Locale;
19
+ import java.util.Properties;
20
+ import java.util.StringTokenizer;
21
+ import java.util.TimeZone;
22
+ import java.io.ByteArrayOutputStream;
23
+ import java.io.FileOutputStream;
24
+
25
+ /**
26
+ * A simple, tiny, nicely embeddable HTTP 1.0 (partially 1.1) server in Java
27
+ *
28
+ * <p> NanoHTTPD version 1.25,
29
+ * Copyright &copy; 2001,2005-2012 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/)
30
+ * and Copyright &copy; 2010 Konstantinos Togias (info@ktogias.gr, http://ktogias.gr)
31
+ *
32
+ * <p><b>Features + limitations: </b><ul>
33
+ *
34
+ * <li> Only one Java file </li>
35
+ * <li> Java 1.1 compatible </li>
36
+ * <li> Released as open source, Modified BSD licence </li>
37
+ * <li> No fixed config files, logging, authorization etc. (Implement yourself if you need them.) </li>
38
+ * <li> Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25) </li>
39
+ * <li> Supports both dynamic content and file serving </li>
40
+ * <li> Supports file upload (since version 1.2, 2010) </li>
41
+ * <li> Supports partial content (streaming)</li>
42
+ * <li> Supports ETags</li>
43
+ * <li> Never caches anything </li>
44
+ * <li> Doesn't limit bandwidth, request time or simultaneous connections </li>
45
+ * <li> Default code serves files and shows all HTTP parameters and headers</li>
46
+ * <li> File server supports directory listing, index.html and index.htm</li>
47
+ * <li> File server supports partial content (streaming)</li>
48
+ * <li> File server supports ETags</li>
49
+ * <li> File server does the 301 redirection trick for directories without '/'</li>
50
+ * <li> File server supports simple skipping for files (continue download) </li>
51
+ * <li> File server serves also very long files without memory overhead </li>
52
+ * <li> Contains a built-in list of most common mime types </li>
53
+ * <li> All header names are converted lowercase so they don't vary between browsers/clients </li>
54
+ *
55
+ * </ul>
56
+ *
57
+ * <p><b>Ways to use: </b><ul>
58
+ *
59
+ * <li> Run as a standalone app, serves files and shows requests</li>
60
+ * <li> Subclass serve() and embed to your own program </li>
61
+ * <li> Call serveFile() from serve() with your own base directory </li>
62
+ *
63
+ * </ul>
64
+ *
65
+ * See the end of the source file for distribution license
66
+ * (Modified BSD licence)
67
+ */
68
+ public class NanoHTTPD
69
+ {
70
+ // ==================================================
71
+ // API parts
72
+ // ==================================================
73
+
74
+ /**
75
+ * Override this to customize the server.<p>
76
+ *
77
+ * (By default, this delegates to serveFile() and allows directory listing.)
78
+ *
79
+ * @param uri Percent-decoded URI without parameters, for example "/index.cgi"
80
+ * @param method "GET", "POST" etc.
81
+ * @param parms Parsed, percent decoded parameters from URI and, in case of POST, data.
82
+ * @param header Header entries, percent decoded
83
+ * @return HTTP response, see class Response for details
84
+ */
85
+ public Response serve( String uri, String method, Properties header, Properties parms, Properties files )
86
+ {
87
+ myOut.println( method + " '" + uri + "' " );
88
+
89
+ Enumeration e = header.propertyNames();
90
+ while ( e.hasMoreElements())
91
+ {
92
+ String value = (String)e.nextElement();
93
+ myOut.println( " HDR: '" + value + "' = '" +
94
+ header.getProperty( value ) + "'" );
95
+ }
96
+ e = parms.propertyNames();
97
+ while ( e.hasMoreElements())
98
+ {
99
+ String value = (String)e.nextElement();
100
+ myOut.println( " PRM: '" + value + "' = '" +
101
+ parms.getProperty( value ) + "'" );
102
+ }
103
+ e = files.propertyNames();
104
+ while ( e.hasMoreElements())
105
+ {
106
+ String value = (String)e.nextElement();
107
+ myOut.println( " UPLOADED: '" + value + "' = '" +
108
+ files.getProperty( value ) + "'" );
109
+ }
110
+
111
+ return serveFile( uri, header, myRootDir, true );
112
+ }
113
+
114
+ /**
115
+ * HTTP response.
116
+ * Return one of these from serve().
117
+ */
118
+ public class Response
119
+ {
120
+ /**
121
+ * Default constructor: response = HTTP_OK, data = mime = 'null'
122
+ */
123
+ public Response()
124
+ {
125
+ this.status = HTTP_OK;
126
+ }
127
+
128
+ /**
129
+ * Basic constructor.
130
+ */
131
+ public Response( String status, String mimeType, InputStream data )
132
+ {
133
+ this.status = status;
134
+ this.mimeType = mimeType;
135
+ this.data = data;
136
+ }
137
+
138
+ /**
139
+ * Convenience method that makes an InputStream out of
140
+ * given text.
141
+ */
142
+ public Response( String status, String mimeType, String txt )
143
+ {
144
+ this.status = status;
145
+ this.mimeType = mimeType;
146
+ try
147
+ {
148
+ this.data = new ByteArrayInputStream( txt.getBytes("UTF-8"));
149
+ }
150
+ catch ( java.io.UnsupportedEncodingException uee )
151
+ {
152
+ uee.printStackTrace();
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Adds given line to the header.
158
+ */
159
+ public void addHeader( String name, String value )
160
+ {
161
+ header.put( name, value );
162
+ }
163
+
164
+ /**
165
+ * HTTP status code after processing, e.g. "200 OK", HTTP_OK
166
+ */
167
+ public String status;
168
+
169
+ /**
170
+ * MIME type of content, e.g. "text/html"
171
+ */
172
+ public String mimeType;
173
+
174
+ /**
175
+ * Data of the response, may be null.
176
+ */
177
+ public InputStream data;
178
+
179
+ /**
180
+ * Headers for the HTTP response. Use addHeader()
181
+ * to add lines.
182
+ */
183
+ public Properties header = new Properties();
184
+ }
185
+
186
+ /**
187
+ * Some HTTP response status codes
188
+ */
189
+ public static final String
190
+ HTTP_OK = "200 OK",
191
+ HTTP_PARTIALCONTENT = "206 Partial Content",
192
+ HTTP_RANGE_NOT_SATISFIABLE = "416 Requested Range Not Satisfiable",
193
+ HTTP_REDIRECT = "301 Moved Permanently",
194
+ HTTP_NOTMODIFIED = "304 Not Modified",
195
+ HTTP_FORBIDDEN = "403 Forbidden",
196
+ HTTP_NOTFOUND = "404 Not Found",
197
+ HTTP_BADREQUEST = "400 Bad Request",
198
+ HTTP_INTERNALERROR = "500 Internal Server Error",
199
+ HTTP_NOTIMPLEMENTED = "501 Not Implemented";
200
+
201
+ /**
202
+ * Common mime types for dynamic content
203
+ */
204
+ public static final String
205
+ MIME_PLAINTEXT = "text/plain",
206
+ MIME_HTML = "text/html",
207
+ MIME_DEFAULT_BINARY = "application/octet-stream",
208
+ MIME_XML = "text/xml";
209
+
210
+ // ==================================================
211
+ // Socket & server code
212
+ // ==================================================
213
+
214
+ /**
215
+ * Starts a HTTP server to given port.<p>
216
+ * Throws an IOException if the socket is already in use
217
+ */
218
+ public NanoHTTPD( int port, File wwwroot ) throws IOException
219
+ {
220
+ myTcpPort = port;
221
+ this.myRootDir = wwwroot;
222
+ myServerSocket = new ServerSocket( myTcpPort );
223
+ myThread = new Thread( new Runnable()
224
+ {
225
+ public void run()
226
+ {
227
+ try
228
+ {
229
+ while( true )
230
+ new HTTPSession( myServerSocket.accept());
231
+ }
232
+ catch ( IOException ioe )
233
+ {}
234
+ }
235
+ });
236
+ myThread.setDaemon( true );
237
+ myThread.start();
238
+ }
239
+
240
+ /**
241
+ * Stops the server.
242
+ */
243
+ public void stop()
244
+ {
245
+ try
246
+ {
247
+ myServerSocket.close();
248
+ myThread.join();
249
+ }
250
+ catch ( IOException ioe ) {}
251
+ catch ( InterruptedException e ) {}
252
+ }
253
+
254
+
255
+ /**
256
+ * Starts as a standalone file server and waits for Enter.
257
+ */
258
+ public static void main( String[] args )
259
+ {
260
+ myOut.println( "NanoHTTPD 1.25 (C) 2001,2005-2011 Jarno Elonen and (C) 2010 Konstantinos Togias\n" +
261
+ "(Command line options: [-p port] [-d root-dir] [--licence])\n" );
262
+
263
+ // Defaults
264
+ int port = 80;
265
+ File wwwroot = new File(".").getAbsoluteFile();
266
+
267
+ // Show licence if requested
268
+ for ( int i=0; i<args.length; ++i )
269
+ if(args[i].equalsIgnoreCase("-p"))
270
+ port = Integer.parseInt( args[i+1] );
271
+ else if(args[i].equalsIgnoreCase("-d"))
272
+ wwwroot = new File( args[i+1] ).getAbsoluteFile();
273
+ else if ( args[i].toLowerCase().endsWith( "licence" ))
274
+ {
275
+ myOut.println( LICENCE + "\n" );
276
+ break;
277
+ }
278
+
279
+ try
280
+ {
281
+ new NanoHTTPD( port, wwwroot );
282
+ }
283
+ catch( IOException ioe )
284
+ {
285
+ myErr.println( "Couldn't start server:\n" + ioe );
286
+ System.exit( -1 );
287
+ }
288
+
289
+ myOut.println( "Now serving files in port " + port + " from \"" + wwwroot + "\"" );
290
+ myOut.println( "Hit Enter to stop.\n" );
291
+
292
+ try { System.in.read(); } catch( Throwable t ) {}
293
+ }
294
+
295
+ /**
296
+ * Handles one session, i.e. parses the HTTP request
297
+ * and returns the response.
298
+ */
299
+ private class HTTPSession implements Runnable
300
+ {
301
+ public HTTPSession( Socket s )
302
+ {
303
+ mySocket = s;
304
+ Thread t = new Thread( this );
305
+ t.setDaemon( true );
306
+ t.start();
307
+ }
308
+
309
+ public void run()
310
+ {
311
+ try
312
+ {
313
+ InputStream is = mySocket.getInputStream();
314
+ if ( is == null) return;
315
+
316
+ // Read the first 8192 bytes.
317
+ // The full header should fit in here.
318
+ // Apache's default header limit is 8KB.
319
+ int bufsize = 8192;
320
+ byte[] buf = new byte[bufsize];
321
+ int rlen = is.read(buf, 0, bufsize);
322
+ if (rlen <= 0) return;
323
+
324
+ // Create a BufferedReader for parsing the header.
325
+ ByteArrayInputStream hbis = new ByteArrayInputStream(buf, 0, rlen);
326
+ BufferedReader hin = new BufferedReader( new InputStreamReader( hbis ));
327
+ Properties pre = new Properties();
328
+ Properties parms = new Properties();
329
+ Properties header = new Properties();
330
+ Properties files = new Properties();
331
+
332
+ // Decode the header into parms and header java properties
333
+ decodeHeader(hin, pre, parms, header);
334
+ String method = pre.getProperty("method");
335
+ String uri = pre.getProperty("uri");
336
+
337
+ long size = 0x7FFFFFFFFFFFFFFFl;
338
+ String contentLength = header.getProperty("content-length");
339
+ if (contentLength != null)
340
+ {
341
+ try { size = Integer.parseInt(contentLength); }
342
+ catch (NumberFormatException ex) {}
343
+ }
344
+
345
+ // We are looking for the byte separating header from body.
346
+ // It must be the last byte of the first two sequential new lines.
347
+ int splitbyte = 0;
348
+ boolean sbfound = false;
349
+ while (splitbyte < rlen)
350
+ {
351
+ if (buf[splitbyte] == '\r' && buf[++splitbyte] == '\n' && buf[++splitbyte] == '\r' && buf[++splitbyte] == '\n') {
352
+ sbfound = true;
353
+ break;
354
+ }
355
+ splitbyte++;
356
+ }
357
+ splitbyte++;
358
+
359
+ // Write the part of body already read to ByteArrayOutputStream f
360
+ ByteArrayOutputStream f = new ByteArrayOutputStream();
361
+ if (splitbyte < rlen) f.write(buf, splitbyte, rlen-splitbyte);
362
+
363
+ // While Firefox sends on the first read all the data fitting
364
+ // our buffer, Chrome and Opera sends only the headers even if
365
+ // there is data for the body. So we do some magic here to find
366
+ // out whether we have already consumed part of body, if we
367
+ // have reached the end of the data to be sent or we should
368
+ // expect the first byte of the body at the next read.
369
+ if (splitbyte < rlen)
370
+ size -= rlen - splitbyte +1;
371
+ else if (!sbfound || size == 0x7FFFFFFFFFFFFFFFl)
372
+ size = 0;
373
+
374
+ // Now read all the body and write it to f
375
+ buf = new byte[512];
376
+ while ( rlen >= 0 && size > 0 )
377
+ {
378
+ rlen = is.read(buf, 0, 512);
379
+ size -= rlen;
380
+ if (rlen > 0)
381
+ f.write(buf, 0, rlen);
382
+ }
383
+
384
+ // Get the raw body as a byte []
385
+ byte [] fbuf = f.toByteArray();
386
+
387
+ // Create a BufferedReader for easily reading it as string.
388
+ ByteArrayInputStream bin = new ByteArrayInputStream(fbuf);
389
+ BufferedReader in = new BufferedReader( new InputStreamReader(bin));
390
+
391
+ // If the method is POST, there may be parameters
392
+ // in data section, too, read it:
393
+ if ( method.equalsIgnoreCase( "POST" ))
394
+ {
395
+ String contentType = "";
396
+ String contentTypeHeader = header.getProperty("content-type");
397
+ StringTokenizer st = new StringTokenizer( contentTypeHeader , "; " );
398
+ if ( st.hasMoreTokens()) {
399
+ contentType = st.nextToken();
400
+ }
401
+
402
+ if (contentType.equalsIgnoreCase("multipart/form-data"))
403
+ {
404
+ // Handle multipart/form-data
405
+ if ( !st.hasMoreTokens())
406
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html" );
407
+ String boundaryExp = st.nextToken();
408
+ st = new StringTokenizer( boundaryExp , "=" );
409
+ if (st.countTokens() != 2)
410
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary syntax error. Usage: GET /example/file.html" );
411
+ st.nextToken();
412
+ String boundary = st.nextToken();
413
+
414
+ decodeMultipartData(boundary, fbuf, in, parms, files);
415
+ }
416
+ else
417
+ {
418
+ // Handle application/x-www-form-urlencoded
419
+ String postLine = "";
420
+ char pbuf[] = new char[512];
421
+ int read = in.read(pbuf);
422
+ while ( read >= 0 && !postLine.endsWith("\r\n") )
423
+ {
424
+ postLine += String.valueOf(pbuf, 0, read);
425
+ read = in.read(pbuf);
426
+ }
427
+ postLine = postLine.trim();
428
+ decodeParms( postLine, parms );
429
+ }
430
+ }
431
+
432
+ if ( method.equalsIgnoreCase( "PUT" ))
433
+ files.put("content", saveTmpFile( fbuf, 0, f.size()));
434
+
435
+ // Ok, now do the serve()
436
+ Response r = serve( uri, method, header, parms, files );
437
+ if ( r == null )
438
+ sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response." );
439
+ else
440
+ sendResponse( r.status, r.mimeType, r.header, r.data );
441
+
442
+ in.close();
443
+ is.close();
444
+ }
445
+ catch ( IOException ioe )
446
+ {
447
+ try
448
+ {
449
+ sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
450
+ }
451
+ catch ( Throwable t ) {}
452
+ }
453
+ catch ( InterruptedException ie )
454
+ {
455
+ // Thrown by sendError, ignore and exit the thread.
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Decodes the sent headers and loads the data into
461
+ * java Properties' key - value pairs
462
+ **/
463
+ private void decodeHeader(BufferedReader in, Properties pre, Properties parms, Properties header)
464
+ throws InterruptedException
465
+ {
466
+ try {
467
+ // Read the request line
468
+ String inLine = in.readLine();
469
+ if (inLine == null) return;
470
+ StringTokenizer st = new StringTokenizer( inLine );
471
+ if ( !st.hasMoreTokens())
472
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html" );
473
+
474
+ String method = st.nextToken();
475
+ pre.put("method", method);
476
+
477
+ if ( !st.hasMoreTokens())
478
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html" );
479
+
480
+ String uri = st.nextToken();
481
+
482
+ // Decode parameters from the URI
483
+ int qmi = uri.indexOf( '?' );
484
+ if ( qmi >= 0 )
485
+ {
486
+ decodeParms( uri.substring( qmi+1 ), parms );
487
+ uri = decodePercent( uri.substring( 0, qmi ));
488
+ }
489
+ else uri = decodePercent(uri);
490
+
491
+ // If there's another token, it's protocol version,
492
+ // followed by HTTP headers. Ignore version but parse headers.
493
+ // NOTE: this now forces header names lowercase since they are
494
+ // case insensitive and vary by client.
495
+ if ( st.hasMoreTokens())
496
+ {
497
+ String line = in.readLine();
498
+ while ( line != null && line.trim().length() > 0 )
499
+ {
500
+ int p = line.indexOf( ':' );
501
+ if ( p >= 0 )
502
+ header.put( line.substring(0,p).trim().toLowerCase(), line.substring(p+1).trim());
503
+ line = in.readLine();
504
+ }
505
+ }
506
+
507
+ pre.put("uri", uri);
508
+ }
509
+ catch ( IOException ioe )
510
+ {
511
+ sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Decodes the Multipart Body data and put it
517
+ * into java Properties' key - value pairs.
518
+ **/
519
+ private void decodeMultipartData(String boundary, byte[] fbuf, BufferedReader in, Properties parms, Properties files)
520
+ throws InterruptedException
521
+ {
522
+ try
523
+ {
524
+ int[] bpositions = getBoundaryPositions(fbuf,boundary.getBytes());
525
+ int boundarycount = 1;
526
+ String mpline = in.readLine();
527
+ while ( mpline != null )
528
+ {
529
+ if (mpline.indexOf(boundary) == -1)
530
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html" );
531
+ boundarycount++;
532
+ Properties item = new Properties();
533
+ mpline = in.readLine();
534
+ while (mpline != null && mpline.trim().length() > 0)
535
+ {
536
+ int p = mpline.indexOf( ':' );
537
+ if (p != -1)
538
+ item.put( mpline.substring(0,p).trim().toLowerCase(), mpline.substring(p+1).trim());
539
+ mpline = in.readLine();
540
+ }
541
+ if (mpline != null)
542
+ {
543
+ String contentDisposition = item.getProperty("content-disposition");
544
+ if (contentDisposition == null)
545
+ {
546
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html" );
547
+ }
548
+ StringTokenizer st = new StringTokenizer( contentDisposition , "; " );
549
+ Properties disposition = new Properties();
550
+ while ( st.hasMoreTokens())
551
+ {
552
+ String token = st.nextToken();
553
+ int p = token.indexOf( '=' );
554
+ if (p!=-1)
555
+ disposition.put( token.substring(0,p).trim().toLowerCase(), token.substring(p+1).trim());
556
+ }
557
+ String pname = disposition.getProperty("name");
558
+ pname = pname.substring(1,pname.length()-1);
559
+
560
+ String value = "";
561
+ if (item.getProperty("content-type") == null) {
562
+ while (mpline != null && mpline.indexOf(boundary) == -1)
563
+ {
564
+ mpline = in.readLine();
565
+ if ( mpline != null)
566
+ {
567
+ int d = mpline.indexOf(boundary);
568
+ if (d == -1)
569
+ value+=mpline;
570
+ else
571
+ value+=mpline.substring(0,d-2);
572
+ }
573
+ }
574
+ }
575
+ else
576
+ {
577
+ if (boundarycount> bpositions.length)
578
+ sendError( HTTP_INTERNALERROR, "Error processing request" );
579
+ int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount-2]);
580
+ String path = saveTmpFile(fbuf, offset, bpositions[boundarycount-1]-offset-4);
581
+ files.put(pname, path);
582
+ value = disposition.getProperty("filename");
583
+ value = value.substring(1,value.length()-1);
584
+ do {
585
+ mpline = in.readLine();
586
+ } while (mpline != null && mpline.indexOf(boundary) == -1);
587
+ }
588
+ parms.put(pname, value);
589
+ }
590
+ }
591
+ }
592
+ catch ( IOException ioe )
593
+ {
594
+ sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Find the byte positions where multipart boundaries start.
600
+ **/
601
+ public int[] getBoundaryPositions(byte[] b, byte[] boundary)
602
+ {
603
+ int matchcount = 0;
604
+ int matchbyte = -1;
605
+ Vector matchbytes = new Vector();
606
+ for (int i=0; i<b.length; i++)
607
+ {
608
+ if (b[i] == boundary[matchcount])
609
+ {
610
+ if (matchcount == 0)
611
+ matchbyte = i;
612
+ matchcount++;
613
+ if (matchcount==boundary.length)
614
+ {
615
+ matchbytes.addElement(new Integer(matchbyte));
616
+ matchcount = 0;
617
+ matchbyte = -1;
618
+ }
619
+ }
620
+ else
621
+ {
622
+ i -= matchcount;
623
+ matchcount = 0;
624
+ matchbyte = -1;
625
+ }
626
+ }
627
+ int[] ret = new int[matchbytes.size()];
628
+ for (int i=0; i < ret.length; i++)
629
+ {
630
+ ret[i] = ((Integer)matchbytes.elementAt(i)).intValue();
631
+ }
632
+ return ret;
633
+ }
634
+
635
+ /**
636
+ * Retrieves the content of a sent file and saves it
637
+ * to a temporary file.
638
+ * The full path to the saved file is returned.
639
+ **/
640
+ private String saveTmpFile(byte[] b, int offset, int len)
641
+ {
642
+ String path = "";
643
+ if (len > 0)
644
+ {
645
+ String tmpdir = System.getProperty("java.io.tmpdir");
646
+ try {
647
+ File temp = File.createTempFile("NanoHTTPD", "", new File(tmpdir));
648
+ OutputStream fstream = new FileOutputStream(temp);
649
+ fstream.write(b, offset, len);
650
+ fstream.close();
651
+ path = temp.getAbsolutePath();
652
+ } catch (Exception e) { // Catch exception if any
653
+ myErr.println("Error: " + e.getMessage());
654
+ }
655
+ }
656
+ return path;
657
+ }
658
+
659
+
660
+ /**
661
+ * It returns the offset separating multipart file headers
662
+ * from the file's data.
663
+ **/
664
+ private int stripMultipartHeaders(byte[] b, int offset)
665
+ {
666
+ int i = 0;
667
+ for (i=offset; i<b.length; i++)
668
+ {
669
+ if (b[i] == '\r' && b[++i] == '\n' && b[++i] == '\r' && b[++i] == '\n')
670
+ break;
671
+ }
672
+ return i+1;
673
+ }
674
+
675
+ /**
676
+ * Decodes the percent encoding scheme. <br/>
677
+ * For example: "an+example%20string" -> "an example string"
678
+ */
679
+ private String decodePercent( String str ) throws InterruptedException
680
+ {
681
+ try
682
+ {
683
+ StringBuffer sb = new StringBuffer();
684
+ for( int i=0; i<str.length(); i++ )
685
+ {
686
+ char c = str.charAt( i );
687
+ switch ( c )
688
+ {
689
+ case '+':
690
+ sb.append( ' ' );
691
+ break;
692
+ case '%':
693
+ sb.append((char)Integer.parseInt( str.substring(i+1,i+3), 16 ));
694
+ i += 2;
695
+ break;
696
+ default:
697
+ sb.append( c );
698
+ break;
699
+ }
700
+ }
701
+ return sb.toString();
702
+ }
703
+ catch( Exception e )
704
+ {
705
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding." );
706
+ return null;
707
+ }
708
+ }
709
+
710
+ /**
711
+ * Decodes parameters in percent-encoded URI-format
712
+ * ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and
713
+ * adds them to given Properties. NOTE: this doesn't support multiple
714
+ * identical keys due to the simplicity of Properties -- if you need multiples,
715
+ * you might want to replace the Properties with a Hashtable of Vectors or such.
716
+ */
717
+ private void decodeParms( String parms, Properties p )
718
+ throws InterruptedException
719
+ {
720
+ if ( parms == null )
721
+ return;
722
+
723
+ StringTokenizer st = new StringTokenizer( parms, "&" );
724
+ while ( st.hasMoreTokens())
725
+ {
726
+ String e = st.nextToken();
727
+ int sep = e.indexOf( '=' );
728
+ if ( sep >= 0 )
729
+ p.put( decodePercent( e.substring( 0, sep )).trim(),
730
+ decodePercent( e.substring( sep+1 )));
731
+ }
732
+ }
733
+
734
+ /**
735
+ * Returns an error message as a HTTP response and
736
+ * throws InterruptedException to stop further request processing.
737
+ */
738
+ private void sendError( String status, String msg ) throws InterruptedException
739
+ {
740
+ sendResponse( status, MIME_PLAINTEXT, null, new ByteArrayInputStream( msg.getBytes()));
741
+ throw new InterruptedException();
742
+ }
743
+
744
+ /**
745
+ * Sends given response to the socket.
746
+ */
747
+ private void sendResponse( String status, String mime, Properties header, InputStream data )
748
+ {
749
+ try
750
+ {
751
+ if ( status == null )
752
+ throw new Error( "sendResponse(): Status can't be null." );
753
+
754
+ OutputStream out = mySocket.getOutputStream();
755
+ PrintWriter pw = new PrintWriter( out );
756
+ pw.print("HTTP/1.0 " + status + " \r\n");
757
+
758
+ if ( mime != null )
759
+ pw.print("Content-Type: " + mime + "\r\n");
760
+
761
+ if ( header == null || header.getProperty( "Date" ) == null )
762
+ pw.print( "Date: " + gmtFrmt.format( new Date()) + "\r\n");
763
+
764
+ if ( header != null )
765
+ {
766
+ Enumeration e = header.keys();
767
+ while ( e.hasMoreElements())
768
+ {
769
+ String key = (String)e.nextElement();
770
+ String value = header.getProperty( key );
771
+ pw.print( key + ": " + value + "\r\n");
772
+ }
773
+ }
774
+
775
+ pw.print("\r\n");
776
+ pw.flush();
777
+
778
+ if ( data != null )
779
+ {
780
+ int pending = data.available(); // This is to support partial sends, see serveFile()
781
+ byte[] buff = new byte[theBufferSize];
782
+ while (pending>0)
783
+ {
784
+ int read = data.read( buff, 0, ( (pending>theBufferSize) ? theBufferSize : pending ));
785
+ if (read <= 0) break;
786
+ out.write( buff, 0, read );
787
+ pending -= read;
788
+ }
789
+ }
790
+ out.flush();
791
+ out.close();
792
+ if ( data != null )
793
+ data.close();
794
+ }
795
+ catch( IOException ioe )
796
+ {
797
+ // Couldn't write? No can do.
798
+ try { mySocket.close(); } catch( Throwable t ) {}
799
+ }
800
+ }
801
+
802
+ private Socket mySocket;
803
+ }
804
+
805
+ /**
806
+ * URL-encodes everything between "/"-characters.
807
+ * Encodes spaces as '%20' instead of '+'.
808
+ */
809
+ private String encodeUri( String uri )
810
+ {
811
+ String newUri = "";
812
+ StringTokenizer st = new StringTokenizer( uri, "/ ", true );
813
+ while ( st.hasMoreTokens())
814
+ {
815
+ String tok = st.nextToken();
816
+ if ( tok.equals( "/" ))
817
+ newUri += "/";
818
+ else if ( tok.equals( " " ))
819
+ newUri += "%20";
820
+ else
821
+ {
822
+ newUri += URLEncoder.encode( tok );
823
+ // For Java 1.4 you'll want to use this instead:
824
+ // try { newUri += URLEncoder.encode( tok, "UTF-8" ); } catch ( java.io.UnsupportedEncodingException uee ) {}
825
+ }
826
+ }
827
+ return newUri;
828
+ }
829
+
830
+ private int myTcpPort;
831
+ private final ServerSocket myServerSocket;
832
+ private Thread myThread;
833
+ private File myRootDir;
834
+
835
+ // ==================================================
836
+ // File server code
837
+ // ==================================================
838
+
839
+ /**
840
+ * Serves file from homeDir and its' subdirectories (only).
841
+ * Uses only URI, ignores all headers and HTTP parameters.
842
+ */
843
+ public Response serveFile( String uri, Properties header, File homeDir,
844
+ boolean allowDirectoryListing )
845
+ {
846
+ Response res = null;
847
+
848
+ // Make sure we won't die of an exception later
849
+ if ( !homeDir.isDirectory())
850
+ res = new Response( HTTP_INTERNALERROR, MIME_PLAINTEXT,
851
+ "INTERNAL ERRROR: serveFile(): given homeDir is not a directory." );
852
+
853
+ if ( res == null )
854
+ {
855
+ // Remove URL arguments
856
+ uri = uri.trim().replace( File.separatorChar, '/' );
857
+ if ( uri.indexOf( '?' ) >= 0 )
858
+ uri = uri.substring(0, uri.indexOf( '?' ));
859
+
860
+ // Prohibit getting out of current directory
861
+ if ( uri.startsWith( ".." ) || uri.endsWith( ".." ) || uri.indexOf( "../" ) >= 0 )
862
+ res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
863
+ "FORBIDDEN: Won't serve ../ for security reasons." );
864
+ }
865
+
866
+ File f = new File( homeDir, uri );
867
+ if ( res == null && !f.exists())
868
+ res = new Response( HTTP_NOTFOUND, MIME_PLAINTEXT,
869
+ "Error 404, file not found." );
870
+
871
+ // List the directory, if necessary
872
+ if ( res == null && f.isDirectory())
873
+ {
874
+ // Browsers get confused without '/' after the
875
+ // directory, send a redirect.
876
+ if ( !uri.endsWith( "/" ))
877
+ {
878
+ uri += "/";
879
+ res = new Response( HTTP_REDIRECT, MIME_HTML,
880
+ "<html><body>Redirected: <a href=\"" + uri + "\">" +
881
+ uri + "</a></body></html>");
882
+ res.addHeader( "Location", uri );
883
+ }
884
+
885
+ if ( res == null )
886
+ {
887
+ // First try index.html and index.htm
888
+ if ( new File( f, "index.html" ).exists())
889
+ f = new File( homeDir, uri + "/index.html" );
890
+ else if ( new File( f, "index.htm" ).exists())
891
+ f = new File( homeDir, uri + "/index.htm" );
892
+ // No index file, list the directory if it is readable
893
+ else if ( allowDirectoryListing && f.canRead() )
894
+ {
895
+ String[] files = f.list();
896
+ String msg = "<html><body><h1>Directory " + uri + "</h1><br/>";
897
+
898
+ if ( uri.length() > 1 )
899
+ {
900
+ String u = uri.substring( 0, uri.length()-1 );
901
+ int slash = u.lastIndexOf( '/' );
902
+ if ( slash >= 0 && slash < u.length())
903
+ msg += "<b><a href=\"" + uri.substring(0, slash+1) + "\">..</a></b><br/>";
904
+ }
905
+
906
+ if (files!=null)
907
+ {
908
+ for ( int i=0; i<files.length; ++i )
909
+ {
910
+ File curFile = new File( f, files[i] );
911
+ boolean dir = curFile.isDirectory();
912
+ if ( dir )
913
+ {
914
+ msg += "<b>";
915
+ files[i] += "/";
916
+ }
917
+
918
+ msg += "<a href=\"" + encodeUri( uri + files[i] ) + "\">" +
919
+ files[i] + "</a>";
920
+
921
+ // Show file size
922
+ if ( curFile.isFile())
923
+ {
924
+ long len = curFile.length();
925
+ msg += " &nbsp;<font size=2>(";
926
+ if ( len < 1024 )
927
+ msg += len + " bytes";
928
+ else if ( len < 1024 * 1024 )
929
+ msg += len/1024 + "." + (len%1024/10%100) + " KB";
930
+ else
931
+ msg += len/(1024*1024) + "." + len%(1024*1024)/10%100 + " MB";
932
+
933
+ msg += ")</font>";
934
+ }
935
+ msg += "<br/>";
936
+ if ( dir ) msg += "</b>";
937
+ }
938
+ }
939
+ msg += "</body></html>";
940
+ res = new Response( HTTP_OK, MIME_HTML, msg );
941
+ }
942
+ else
943
+ {
944
+ res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
945
+ "FORBIDDEN: No directory listing." );
946
+ }
947
+ }
948
+ }
949
+
950
+ try
951
+ {
952
+ if ( res == null )
953
+ {
954
+ // Get MIME type from file name extension, if possible
955
+ String mime = null;
956
+ int dot = f.getCanonicalPath().lastIndexOf( '.' );
957
+ if ( dot >= 0 )
958
+ mime = (String)theMimeTypes.get( f.getCanonicalPath().substring( dot + 1 ).toLowerCase());
959
+ if ( mime == null )
960
+ mime = MIME_DEFAULT_BINARY;
961
+
962
+ // Calculate etag
963
+ String etag = Integer.toHexString((f.getAbsolutePath() + f.lastModified() + "" + f.length()).hashCode());
964
+
965
+ // Support (simple) skipping:
966
+ long startFrom = 0;
967
+ long endAt = -1;
968
+ String range = header.getProperty( "range" );
969
+ if ( range != null )
970
+ {
971
+ if ( range.startsWith( "bytes=" ))
972
+ {
973
+ range = range.substring( "bytes=".length());
974
+ int minus = range.indexOf( '-' );
975
+ try {
976
+ if ( minus > 0 )
977
+ {
978
+ startFrom = Long.parseLong( range.substring( 0, minus ));
979
+ endAt = Long.parseLong( range.substring( minus+1 ));
980
+ }
981
+ }
982
+ catch ( NumberFormatException nfe ) {}
983
+ }
984
+ }
985
+
986
+ // Change return code and add Content-Range header when skipping is requested
987
+ long fileLen = f.length();
988
+ if (range != null && startFrom >= 0)
989
+ {
990
+ if ( startFrom >= fileLen)
991
+ {
992
+ res = new Response( HTTP_RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "" );
993
+ res.addHeader( "Content-Range", "bytes 0-0/" + fileLen);
994
+ res.addHeader( "ETag", etag);
995
+ }
996
+ else
997
+ {
998
+ if ( endAt < 0 )
999
+ endAt = fileLen-1;
1000
+ long newLen = endAt - startFrom + 1;
1001
+ if ( newLen < 0 ) newLen = 0;
1002
+
1003
+ final long dataLen = newLen;
1004
+ FileInputStream fis = new FileInputStream( f ) {
1005
+ public int available() throws IOException { return (int)dataLen; }
1006
+ };
1007
+ fis.skip( startFrom );
1008
+
1009
+ res = new Response( HTTP_PARTIALCONTENT, mime, fis );
1010
+ res.addHeader( "Content-Length", "" + dataLen);
1011
+ res.addHeader( "Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
1012
+ res.addHeader( "ETag", etag);
1013
+ }
1014
+ }
1015
+ else
1016
+ {
1017
+ if (etag.equals(header.getProperty("if-none-match")))
1018
+ res = new Response( HTTP_NOTMODIFIED, mime, "");
1019
+ else
1020
+ {
1021
+ res = new Response( HTTP_OK, mime, new FileInputStream( f ));
1022
+ res.addHeader( "Content-Length", "" + fileLen);
1023
+ res.addHeader( "ETag", etag);
1024
+ }
1025
+ }
1026
+ }
1027
+ }
1028
+ catch( IOException ioe )
1029
+ {
1030
+ res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed." );
1031
+ }
1032
+
1033
+ res.addHeader( "Accept-Ranges", "bytes"); // Announce that the file server accepts partial content requestes
1034
+ return res;
1035
+ }
1036
+
1037
+ /**
1038
+ * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE
1039
+ */
1040
+ private static Hashtable theMimeTypes = new Hashtable();
1041
+ static
1042
+ {
1043
+ StringTokenizer st = new StringTokenizer(
1044
+ "css text/css "+
1045
+ "htm text/html "+
1046
+ "html text/html "+
1047
+ "xml text/xml "+
1048
+ "txt text/plain "+
1049
+ "asc text/plain "+
1050
+ "gif image/gif "+
1051
+ "jpg image/jpeg "+
1052
+ "jpeg image/jpeg "+
1053
+ "png image/png "+
1054
+ "mp3 audio/mpeg "+
1055
+ "m3u audio/mpeg-url " +
1056
+ "mp4 video/mp4 " +
1057
+ "ogv video/ogg " +
1058
+ "flv video/x-flv " +
1059
+ "mov video/quicktime " +
1060
+ "swf application/x-shockwave-flash " +
1061
+ "js application/javascript "+
1062
+ "pdf application/pdf "+
1063
+ "doc application/msword "+
1064
+ "ogg application/x-ogg "+
1065
+ "zip application/octet-stream "+
1066
+ "exe application/octet-stream "+
1067
+ "class application/octet-stream " );
1068
+ while ( st.hasMoreTokens())
1069
+ theMimeTypes.put( st.nextToken(), st.nextToken());
1070
+ }
1071
+
1072
+ private static int theBufferSize = 16 * 1024;
1073
+
1074
+ // Change these if you want to log to somewhere else than stdout
1075
+ protected static PrintStream myOut = System.out;
1076
+ protected static PrintStream myErr = System.err;
1077
+
1078
+ /**
1079
+ * GMT date formatter
1080
+ */
1081
+ private static java.text.SimpleDateFormat gmtFrmt;
1082
+ static
1083
+ {
1084
+ gmtFrmt = new java.text.SimpleDateFormat( "E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
1085
+ gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
1086
+ }
1087
+
1088
+ /**
1089
+ * The distribution licence
1090
+ */
1091
+ private static final String LICENCE =
1092
+ "Copyright (C) 2001,2005-2011 by Jarno Elonen <elonen@iki.fi>\n"+
1093
+ "and Copyright (C) 2010 by Konstantinos Togias <info@ktogias.gr>\n"+
1094
+ "\n"+
1095
+ "Redistribution and use in source and binary forms, with or without\n"+
1096
+ "modification, are permitted provided that the following conditions\n"+
1097
+ "are met:\n"+
1098
+ "\n"+
1099
+ "Redistributions of source code must retain the above copyright notice,\n"+
1100
+ "this list of conditions and the following disclaimer. Redistributions in\n"+
1101
+ "binary form must reproduce the above copyright notice, this list of\n"+
1102
+ "conditions and the following disclaimer in the documentation and/or other\n"+
1103
+ "materials provided with the distribution. The name of the author may not\n"+
1104
+ "be used to endorse or promote products derived from this software without\n"+
1105
+ "specific prior written permission. \n"+
1106
+ " \n"+
1107
+ "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"+
1108
+ "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n"+
1109
+ "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n"+
1110
+ "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"+
1111
+ "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n"+
1112
+ "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n"+
1113
+ "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"+
1114
+ "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"+
1115
+ "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"+
1116
+ "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
1117
+ }