├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── AndroidHttpServer.iml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── server │ │ └── http │ │ └── android │ │ └── androidhttpserver │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── server │ │ └── http │ │ └── android │ │ └── androidhttpserver │ │ ├── MainActivity.java │ │ └── server │ │ ├── MyServer.java │ │ └── NanoHTTPD.java │ └── res │ ├── layout │ └── activity_main.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | /captures 8 | ======= 9 | # Built application files 10 | *.apk 11 | *.ap_ 12 | 13 | # Files for the Dalvik VM 14 | *.dex 15 | 16 | # Java class files 17 | *.class 18 | 19 | # Generated files 20 | bin/ 21 | gen/ 22 | 23 | # Gradle files 24 | .gradle/ 25 | build/ 26 | /*/build/ 27 | 28 | # Local configuration file (sdk path, etc) 29 | local.properties 30 | 31 | # Proguard folder generated by Eclipse 32 | proguard/ 33 | 34 | # Log Files 35 | *.log 36 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | AndroidHttpServer -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 1.8 27 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AndroidHttpServer.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andrei Visan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidHttpServer 2 | An HTTP server inside your Android application using NanoHTTPD. 3 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | applicationId "server.http.android.androidhttpserver" 9 | minSdkVersion 15 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | compile 'com.android.support:appcompat-v7:22.2.0' 25 | } 26 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/andrei/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/server/http/android/androidhttpserver/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package server.http.android.androidhttpserver; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/server/http/android/androidhttpserver/MainActivity.java: -------------------------------------------------------------------------------- 1 | package server.http.android.androidhttpserver; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.Menu; 6 | import android.view.MenuItem; 7 | 8 | import java.io.IOException; 9 | 10 | import server.http.android.androidhttpserver.server.MyServer; 11 | 12 | 13 | public class MainActivity extends AppCompatActivity { 14 | 15 | private MyServer server; 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_main); 21 | } 22 | 23 | @Override 24 | public boolean onCreateOptionsMenu(Menu menu) { 25 | getMenuInflater().inflate(R.menu.menu_main, menu); 26 | return true; 27 | } 28 | 29 | @Override 30 | public boolean onOptionsItemSelected(MenuItem item) { 31 | int id = item.getItemId(); 32 | if (id == R.id.action_settings) { 33 | return true; 34 | } 35 | return super.onOptionsItemSelected(item); 36 | } 37 | 38 | @Override 39 | public void onResume() { 40 | super.onResume(); 41 | try { 42 | server = new MyServer(); 43 | } catch (IOException e) { 44 | e.printStackTrace(); 45 | } 46 | } 47 | 48 | @Override 49 | public void onPause() { 50 | super.onPause(); 51 | if(server != null) { 52 | server.stop(); 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/server/http/android/androidhttpserver/server/MyServer.java: -------------------------------------------------------------------------------- 1 | package server.http.android.androidhttpserver.server; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * Created by andrei on 7/30/15. 7 | */ 8 | public class MyServer extends NanoHTTPD { 9 | private final static int PORT = 8080; 10 | 11 | public MyServer() throws IOException { 12 | super(PORT); 13 | start(); 14 | System.out.println( "\nRunning! Point your browers to http://localhost:8080/ \n" ); 15 | } 16 | 17 | @Override 18 | public Response serve(IHTTPSession session) { 19 | String msg = "

Hello server

\n"; 20 | msg += "

We serve " + session.getUri() + " !

"; 21 | return newFixedLengthResponse( msg + "\n" ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/server/http/android/androidhttpserver/server/NanoHTTPD.java: -------------------------------------------------------------------------------- 1 | package server.http.android.androidhttpserver.server; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.BufferedWriter; 5 | import java.io.ByteArrayInputStream; 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.Closeable; 8 | import java.io.DataOutput; 9 | import java.io.DataOutputStream; 10 | import java.io.File; 11 | import java.io.FileOutputStream; 12 | import java.io.FilterOutputStream; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.io.InputStreamReader; 16 | import java.io.OutputStream; 17 | import java.io.OutputStreamWriter; 18 | import java.io.PrintWriter; 19 | import java.io.PushbackInputStream; 20 | import java.io.RandomAccessFile; 21 | import java.io.UnsupportedEncodingException; 22 | import java.net.InetAddress; 23 | import java.net.InetSocketAddress; 24 | import java.net.ServerSocket; 25 | import java.net.Socket; 26 | import java.net.SocketException; 27 | import java.net.SocketTimeoutException; 28 | import java.net.URLDecoder; 29 | import java.nio.ByteBuffer; 30 | import java.nio.channels.FileChannel; 31 | import java.nio.charset.Charset; 32 | import java.security.KeyStore; 33 | import java.text.SimpleDateFormat; 34 | import java.util.ArrayList; 35 | import java.util.Calendar; 36 | import java.util.Collections; 37 | import java.util.Date; 38 | import java.util.HashMap; 39 | import java.util.Iterator; 40 | import java.util.List; 41 | import java.util.Locale; 42 | import java.util.Map; 43 | import java.util.StringTokenizer; 44 | import java.util.TimeZone; 45 | import java.util.logging.Level; 46 | import java.util.logging.Logger; 47 | import java.util.regex.Matcher; 48 | import java.util.regex.Pattern; 49 | import java.util.zip.GZIPOutputStream; 50 | 51 | import javax.net.ssl.KeyManager; 52 | import javax.net.ssl.KeyManagerFactory; 53 | import javax.net.ssl.SSLContext; 54 | import javax.net.ssl.SSLServerSocket; 55 | import javax.net.ssl.SSLServerSocketFactory; 56 | import javax.net.ssl.TrustManagerFactory; 57 | 58 | import server.http.android.androidhttpserver.server.NanoHTTPD.Response.IStatus; 59 | import server.http.android.androidhttpserver.server.NanoHTTPD.Response.Status; 60 | 61 | /* 62 | * #%L 63 | * NanoHttpd-Core 64 | * %% 65 | * Copyright (C) 2012 - 2015 nanohttpd 66 | * %% 67 | * Redistribution and use in source and binary forms, with or without modification, 68 | * are permitted provided that the following conditions are met: 69 | * 70 | * 1. Redistributions of source code must retain the above copyright notice, this 71 | * list of conditions and the following disclaimer. 72 | * 73 | * 2. Redistributions in binary form must reproduce the above copyright notice, 74 | * this list of conditions and the following disclaimer in the documentation 75 | * and/or other materials provided with the distribution. 76 | * 77 | * 3. Neither the name of the nanohttpd nor the names of its contributors 78 | * may be used to endorse or promote products derived from this software without 79 | * specific prior written permission. 80 | * 81 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 82 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 83 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 84 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 85 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 86 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 87 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 88 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 89 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 90 | * OF THE POSSIBILITY OF SUCH DAMAGE. 91 | * #L% 92 | */ 93 | 94 | /** 95 | * A simple, tiny, nicely embeddable HTTP server in Java 96 | *

97 | *

98 | * NanoHTTPD 99 | *

100 | * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 101 | * 2010 by Konstantinos Togias 102 | *

103 | *

104 | *

105 | * Features + limitations: 106 | *

133 | *

134 | *

135 | * How to use: 136 | *

141 | *

142 | * See the separate "LICENSE.md" file for the distribution license (Modified BSD 143 | * licence) 144 | */ 145 | public abstract class NanoHTTPD { 146 | 147 | /** 148 | * Pluggable strategy for asynchronously executing requests. 149 | */ 150 | public interface AsyncRunner { 151 | 152 | void closeAll(); 153 | 154 | void closed(ClientHandler clientHandler); 155 | 156 | void exec(ClientHandler code); 157 | } 158 | 159 | /** 160 | * The runnable that will be used for every new client connection. 161 | */ 162 | public class ClientHandler implements Runnable { 163 | 164 | private final InputStream inputStream; 165 | 166 | private final Socket acceptSocket; 167 | 168 | private ClientHandler(InputStream inputStream, Socket acceptSocket) { 169 | this.inputStream = inputStream; 170 | this.acceptSocket = acceptSocket; 171 | } 172 | 173 | public void close() { 174 | safeClose(this.inputStream); 175 | safeClose(this.acceptSocket); 176 | } 177 | 178 | @Override 179 | public void run() { 180 | OutputStream outputStream = null; 181 | try { 182 | outputStream = this.acceptSocket.getOutputStream(); 183 | TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create(); 184 | HTTPSession session = new HTTPSession(tempFileManager, this.inputStream, outputStream, this.acceptSocket.getInetAddress()); 185 | while (!this.acceptSocket.isClosed()) { 186 | session.execute(); 187 | } 188 | } catch (Exception e) { 189 | // When the socket is closed by the client, 190 | // we throw our own SocketException 191 | // to break the "keep alive" loop above. If 192 | // the exception was anything other 193 | // than the expected SocketException OR a 194 | // SocketTimeoutException, print the 195 | // stacktrace 196 | if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) { 197 | NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e); 198 | } 199 | } finally { 200 | safeClose(outputStream); 201 | safeClose(this.inputStream); 202 | safeClose(this.acceptSocket); 203 | NanoHTTPD.this.asyncRunner.closed(this); 204 | } 205 | } 206 | } 207 | 208 | public static class Cookie { 209 | 210 | public static String getHTTPTime(int days) { 211 | Calendar calendar = Calendar.getInstance(); 212 | SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); 213 | dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); 214 | calendar.add(Calendar.DAY_OF_MONTH, days); 215 | return dateFormat.format(calendar.getTime()); 216 | } 217 | 218 | private final String n, v, e; 219 | 220 | public Cookie(String name, String value) { 221 | this(name, value, 30); 222 | } 223 | 224 | public Cookie(String name, String value, int numDays) { 225 | this.n = name; 226 | this.v = value; 227 | this.e = getHTTPTime(numDays); 228 | } 229 | 230 | public Cookie(String name, String value, String expires) { 231 | this.n = name; 232 | this.v = value; 233 | this.e = expires; 234 | } 235 | 236 | public String getHTTPHeader() { 237 | String fmt = "%s=%s; expires=%s"; 238 | return String.format(fmt, this.n, this.v, this.e); 239 | } 240 | } 241 | 242 | /** 243 | * Provides rudimentary support for cookies. Doesn't support 'path', 244 | * 'secure' nor 'httpOnly'. Feel free to improve it and/or add unsupported 245 | * features. 246 | * 247 | * @author LordFokas 248 | */ 249 | public class CookieHandler implements Iterable { 250 | 251 | private final HashMap cookies = new HashMap(); 252 | 253 | private final ArrayList queue = new ArrayList(); 254 | 255 | public CookieHandler(Map httpHeaders) { 256 | String raw = httpHeaders.get("cookie"); 257 | if (raw != null) { 258 | String[] tokens = raw.split(";"); 259 | for (String token : tokens) { 260 | String[] data = token.trim().split("="); 261 | if (data.length == 2) { 262 | this.cookies.put(data[0], data[1]); 263 | } 264 | } 265 | } 266 | } 267 | 268 | /** 269 | * Set a cookie with an expiration date from a month ago, effectively 270 | * deleting it on the client side. 271 | * 272 | * @param name 273 | * The cookie name. 274 | */ 275 | public void delete(String name) { 276 | set(name, "-delete-", -30); 277 | } 278 | 279 | @Override 280 | public Iterator iterator() { 281 | return this.cookies.keySet().iterator(); 282 | } 283 | 284 | /** 285 | * Read a cookie from the HTTP Headers. 286 | * 287 | * @param name 288 | * The cookie's name. 289 | * @return The cookie's value if it exists, null otherwise. 290 | */ 291 | public String read(String name) { 292 | return this.cookies.get(name); 293 | } 294 | 295 | public void set(Cookie cookie) { 296 | this.queue.add(cookie); 297 | } 298 | 299 | /** 300 | * Sets a cookie. 301 | * 302 | * @param name 303 | * The cookie's name. 304 | * @param value 305 | * The cookie's value. 306 | * @param expires 307 | * How many days until the cookie expires. 308 | */ 309 | public void set(String name, String value, int expires) { 310 | this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); 311 | } 312 | 313 | /** 314 | * Internally used by the webserver to add all queued cookies into the 315 | * Response's HTTP Headers. 316 | * 317 | * @param response 318 | * The Response object to which headers the queued cookies 319 | * will be added. 320 | */ 321 | public void unloadQueue(Response response) { 322 | for (Cookie cookie : this.queue) { 323 | response.addHeader("Set-Cookie", cookie.getHTTPHeader()); 324 | } 325 | } 326 | } 327 | 328 | /** 329 | * Default threading strategy for NanoHTTPD. 330 | *

331 | *

332 | * By default, the server spawns a new Thread for every incoming request. 333 | * These are set to daemon status, and named according to the request 334 | * number. The name is useful when profiling the application. 335 | *

336 | */ 337 | public static class DefaultAsyncRunner implements AsyncRunner { 338 | 339 | private long requestCount; 340 | 341 | private final List running = Collections.synchronizedList(new ArrayList()); 342 | 343 | /** 344 | * @return a list with currently running clients. 345 | */ 346 | public List getRunning() { 347 | return running; 348 | } 349 | 350 | @Override 351 | public void closeAll() { 352 | // copy of the list for concurrency 353 | for (ClientHandler clientHandler : new ArrayList(this.running)) { 354 | clientHandler.close(); 355 | } 356 | } 357 | 358 | @Override 359 | public void closed(ClientHandler clientHandler) { 360 | this.running.remove(clientHandler); 361 | } 362 | 363 | @Override 364 | public void exec(ClientHandler clientHandler) { 365 | ++this.requestCount; 366 | Thread t = new Thread(clientHandler); 367 | t.setDaemon(true); 368 | t.setName("NanoHttpd Request Processor (#" + this.requestCount + ")"); 369 | this.running.add(clientHandler); 370 | t.start(); 371 | } 372 | } 373 | 374 | /** 375 | * Default strategy for creating and cleaning up temporary files. 376 | *

377 | *

378 | * By default, files are created by File.createTempFile() in 379 | * the directory specified. 380 | *

381 | */ 382 | public static class DefaultTempFile implements TempFile { 383 | 384 | private final File file; 385 | 386 | private final OutputStream fstream; 387 | 388 | public DefaultTempFile(String tempdir) throws IOException { 389 | this.file = File.createTempFile("NanoHTTPD-", "", new File(tempdir)); 390 | this.fstream = new FileOutputStream(this.file); 391 | } 392 | 393 | @Override 394 | public void delete() throws Exception { 395 | safeClose(this.fstream); 396 | if (!this.file.delete()) { 397 | throw new Exception("could not delete temporary file"); 398 | } 399 | } 400 | 401 | @Override 402 | public String getName() { 403 | return this.file.getAbsolutePath(); 404 | } 405 | 406 | @Override 407 | public OutputStream open() throws Exception { 408 | return this.fstream; 409 | } 410 | } 411 | 412 | /** 413 | * Default strategy for creating and cleaning up temporary files. 414 | *

415 | *

416 | * This class stores its files in the standard location (that is, wherever 417 | * java.io.tmpdir points to). Files are added to an internal 418 | * list, and deleted when no longer needed (that is, when 419 | * clear() is invoked at the end of processing a request). 420 | *

421 | */ 422 | public static class DefaultTempFileManager implements TempFileManager { 423 | 424 | private final String tmpdir; 425 | 426 | private final List tempFiles; 427 | 428 | public DefaultTempFileManager() { 429 | this.tmpdir = System.getProperty("java.io.tmpdir"); 430 | this.tempFiles = new ArrayList(); 431 | } 432 | 433 | @Override 434 | public void clear() { 435 | for (TempFile file : this.tempFiles) { 436 | try { 437 | file.delete(); 438 | } catch (Exception ignored) { 439 | NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored); 440 | } 441 | } 442 | this.tempFiles.clear(); 443 | } 444 | 445 | @Override 446 | public TempFile createTempFile() throws Exception { 447 | DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir); 448 | this.tempFiles.add(tempFile); 449 | return tempFile; 450 | } 451 | } 452 | 453 | /** 454 | * Default strategy for creating and cleaning up temporary files. 455 | */ 456 | private class DefaultTempFileManagerFactory implements TempFileManagerFactory { 457 | 458 | @Override 459 | public TempFileManager create() { 460 | return new DefaultTempFileManager(); 461 | } 462 | } 463 | 464 | private static final String CONTENT_DISPOSITION_REGEX = "([ |\t]*Content-Disposition[ |\t]*:)(.*)"; 465 | 466 | private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile(CONTENT_DISPOSITION_REGEX, Pattern.CASE_INSENSITIVE); 467 | 468 | private static final String CONTENT_TYPE_REGEX = "([ |\t]*content-type[ |\t]*:)(.*)"; 469 | 470 | private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(CONTENT_TYPE_REGEX, Pattern.CASE_INSENSITIVE); 471 | 472 | private static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX = "[ |\t]*([a-zA-Z]*)[ |\t]*=[ |\t]*['|\"]([^\"^']*)['|\"]"; 473 | 474 | private static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN = Pattern.compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX); 475 | 476 | protected class HTTPSession implements IHTTPSession { 477 | 478 | public static final int BUFSIZE = 8192; 479 | 480 | private final TempFileManager tempFileManager; 481 | 482 | private final OutputStream outputStream; 483 | 484 | private final PushbackInputStream inputStream; 485 | 486 | private int splitbyte; 487 | 488 | private int rlen; 489 | 490 | private String uri; 491 | 492 | private Method method; 493 | 494 | private Map parms; 495 | 496 | private Map headers; 497 | 498 | private CookieHandler cookies; 499 | 500 | private String queryParameterString; 501 | 502 | private String remoteIp; 503 | 504 | private String protocolVersion; 505 | 506 | public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { 507 | this.tempFileManager = tempFileManager; 508 | this.inputStream = new PushbackInputStream(inputStream, HTTPSession.BUFSIZE); 509 | this.outputStream = outputStream; 510 | } 511 | 512 | public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { 513 | this.tempFileManager = tempFileManager; 514 | this.inputStream = new PushbackInputStream(inputStream, HTTPSession.BUFSIZE); 515 | this.outputStream = outputStream; 516 | this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString(); 517 | this.headers = new HashMap(); 518 | } 519 | 520 | /** 521 | * Decodes the sent headers and loads the data into Key/value pairs 522 | */ 523 | private void decodeHeader(BufferedReader in, Map pre, Map parms, Map headers) throws ResponseException { 524 | try { 525 | // Read the request line 526 | String inLine = in.readLine(); 527 | if (inLine == null) { 528 | return; 529 | } 530 | 531 | StringTokenizer st = new StringTokenizer(inLine); 532 | if (!st.hasMoreTokens()) { 533 | throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); 534 | } 535 | 536 | pre.put("method", st.nextToken()); 537 | 538 | if (!st.hasMoreTokens()) { 539 | throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html"); 540 | } 541 | 542 | String uri = st.nextToken(); 543 | 544 | // Decode parameters from the URI 545 | int qmi = uri.indexOf('?'); 546 | if (qmi >= 0) { 547 | decodeParms(uri.substring(qmi + 1), parms); 548 | uri = decodePercent(uri.substring(0, qmi)); 549 | } else { 550 | uri = decodePercent(uri); 551 | } 552 | 553 | // If there's another token, its protocol version, 554 | // followed by HTTP headers. 555 | // NOTE: this now forces header names lower case since they are 556 | // case insensitive and vary by client. 557 | if (st.hasMoreTokens()) { 558 | protocolVersion = st.nextToken(); 559 | } else { 560 | protocolVersion = "HTTP/1.1"; 561 | NanoHTTPD.LOG.log(Level.FINE, "no protocol version specified, strange. Assuming HTTP/1.1."); 562 | } 563 | String line = in.readLine(); 564 | while (line != null && line.trim().length() > 0) { 565 | int p = line.indexOf(':'); 566 | if (p >= 0) { 567 | headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim()); 568 | } 569 | line = in.readLine(); 570 | } 571 | 572 | pre.put("uri", uri); 573 | } catch (IOException ioe) { 574 | throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); 575 | } 576 | } 577 | 578 | /** 579 | * Decodes the Multipart Body data and put it into Key/Value pairs. 580 | */ 581 | private void decodeMultipartFormData(String boundary, ByteBuffer fbuf, Map parms, Map files) throws ResponseException { 582 | try { 583 | int[] boundary_idxs = getBoundaryPositions(fbuf, boundary.getBytes()); 584 | if (boundary_idxs.length < 2) { 585 | throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings."); 586 | } 587 | 588 | final int MAX_HEADER_SIZE = 1024; 589 | byte[] part_header_buff = new byte[MAX_HEADER_SIZE]; 590 | for (int bi = 0; bi < boundary_idxs.length - 1; bi++) { 591 | fbuf.position(boundary_idxs[bi]); 592 | int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE; 593 | fbuf.get(part_header_buff, 0, len); 594 | ByteArrayInputStream bais = new ByteArrayInputStream(part_header_buff, 0, len); 595 | BufferedReader in = new BufferedReader(new InputStreamReader(bais, Charset.forName("US-ASCII"))); 596 | 597 | // First line is boundary string 598 | String mpline = in.readLine(); 599 | if (!mpline.contains(boundary)) { 600 | throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary."); 601 | } 602 | 603 | String part_name = null, file_name = null, content_type = null; 604 | // Parse the reset of the header lines 605 | mpline = in.readLine(); 606 | while (mpline != null && mpline.trim().length() > 0) { 607 | Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(mpline); 608 | if (matcher.matches()) { 609 | String attributeString = matcher.group(2); 610 | matcher = CONTENT_DISPOSITION_ATTRIBUTE_PATTERN.matcher(attributeString); 611 | while (matcher.find()) { 612 | String key = matcher.group(1); 613 | if (key.equalsIgnoreCase("name")) { 614 | part_name = matcher.group(2); 615 | } else if (key.equalsIgnoreCase("filename")) { 616 | file_name = matcher.group(2); 617 | } 618 | } 619 | } 620 | matcher = CONTENT_TYPE_PATTERN.matcher(mpline); 621 | if (matcher.matches()) { 622 | content_type = matcher.group(2).trim(); 623 | } 624 | mpline = in.readLine(); 625 | } 626 | 627 | // Read the part data 628 | int part_header_len = len - (int) in.skip(MAX_HEADER_SIZE); 629 | if (part_header_len >= len - 4) { 630 | throw new ResponseException(Response.Status.INTERNAL_ERROR, "Multipart header size exceeds MAX_HEADER_SIZE."); 631 | } 632 | int part_data_start = boundary_idxs[bi] + part_header_len; 633 | int part_data_end = boundary_idxs[bi + 1] - 4; 634 | 635 | fbuf.position(part_data_start); 636 | if (content_type == null) { 637 | // Read the part into a string 638 | byte[] data_bytes = new byte[part_data_end - part_data_start]; 639 | fbuf.get(data_bytes); 640 | parms.put(part_name, new String(data_bytes)); 641 | } else { 642 | // Read it into a file 643 | String path = saveTmpFile(fbuf, part_data_start, part_data_end - part_data_start); 644 | if (!files.containsKey(part_name)) { 645 | files.put(part_name, path); 646 | } else { 647 | int count = 2; 648 | while (files.containsKey(part_name + count)) { 649 | count++; 650 | } 651 | files.put(part_name + count, path); 652 | } 653 | parms.put(part_name, file_name); 654 | } 655 | } 656 | } catch (ResponseException re) { 657 | throw re; 658 | } catch (Exception e) { 659 | throw new ResponseException(Response.Status.INTERNAL_ERROR, e.toString()); 660 | } 661 | } 662 | 663 | /** 664 | * Decodes parameters in percent-encoded URI-format ( e.g. 665 | * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given 666 | * Map. NOTE: this doesn't support multiple identical keys due to the 667 | * simplicity of Map. 668 | */ 669 | private void decodeParms(String parms, Map p) { 670 | if (parms == null) { 671 | this.queryParameterString = ""; 672 | return; 673 | } 674 | 675 | this.queryParameterString = parms; 676 | StringTokenizer st = new StringTokenizer(parms, "&"); 677 | while (st.hasMoreTokens()) { 678 | String e = st.nextToken(); 679 | int sep = e.indexOf('='); 680 | if (sep >= 0) { 681 | p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1))); 682 | } else { 683 | p.put(decodePercent(e).trim(), ""); 684 | } 685 | } 686 | } 687 | 688 | @Override 689 | public void execute() throws IOException { 690 | Response r = null; 691 | try { 692 | // Read the first 8192 bytes. 693 | // The full header should fit in here. 694 | // Apache's default header limit is 8KB. 695 | // Do NOT assume that a single read will get the entire header 696 | // at once! 697 | byte[] buf = new byte[HTTPSession.BUFSIZE]; 698 | this.splitbyte = 0; 699 | this.rlen = 0; 700 | 701 | int read = -1; 702 | try { 703 | read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE); 704 | } catch (Exception e) { 705 | safeClose(this.inputStream); 706 | safeClose(this.outputStream); 707 | throw new SocketException("NanoHttpd Shutdown"); 708 | } 709 | if (read == -1) { 710 | // socket was been closed 711 | safeClose(this.inputStream); 712 | safeClose(this.outputStream); 713 | throw new SocketException("NanoHttpd Shutdown"); 714 | } 715 | while (read > 0) { 716 | this.rlen += read; 717 | this.splitbyte = findHeaderEnd(buf, this.rlen); 718 | if (this.splitbyte > 0) { 719 | break; 720 | } 721 | read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen); 722 | } 723 | 724 | if (this.splitbyte < this.rlen) { 725 | this.inputStream.unread(buf, this.splitbyte, this.rlen - this.splitbyte); 726 | } 727 | 728 | this.parms = new HashMap(); 729 | if (null == this.headers) { 730 | this.headers = new HashMap(); 731 | } else { 732 | this.headers.clear(); 733 | } 734 | 735 | if (null != this.remoteIp) { 736 | this.headers.put("remote-addr", this.remoteIp); 737 | this.headers.put("http-client-ip", this.remoteIp); 738 | } 739 | 740 | // Create a BufferedReader for parsing the header. 741 | BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen))); 742 | 743 | // Decode the header into parms and header java properties 744 | Map pre = new HashMap(); 745 | decodeHeader(hin, pre, this.parms, this.headers); 746 | 747 | this.method = Method.lookup(pre.get("method")); 748 | if (this.method == null) { 749 | throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error."); 750 | } 751 | 752 | this.uri = pre.get("uri"); 753 | 754 | this.cookies = new CookieHandler(this.headers); 755 | 756 | String connection = this.headers.get("connection"); 757 | boolean keepAlive = protocolVersion.equals("HTTP/1.1") && (connection == null || !connection.matches("(?i).*close.*")); 758 | 759 | // Ok, now do the serve() 760 | r = serve(this); 761 | if (r == null) { 762 | throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); 763 | } else { 764 | String acceptEncoding = this.headers.get("accept-encoding"); 765 | this.cookies.unloadQueue(r); 766 | r.setRequestMethod(this.method); 767 | r.setGzipEncoding(useGzipWhenAccepted(r) && acceptEncoding != null && acceptEncoding.contains("gzip")); 768 | r.setKeepAlive(keepAlive); 769 | r.send(this.outputStream); 770 | } 771 | if (!keepAlive || "close".equalsIgnoreCase(r.getHeader("connection"))) { 772 | throw new SocketException("NanoHttpd Shutdown"); 773 | } 774 | } catch (SocketException e) { 775 | // throw it out to close socket object (finalAccept) 776 | throw e; 777 | } catch (SocketTimeoutException ste) { 778 | // treat socket timeouts the same way we treat socket exceptions 779 | // i.e. close the stream & finalAccept object by throwing the 780 | // exception up the call stack. 781 | throw ste; 782 | } catch (IOException ioe) { 783 | Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); 784 | resp.send(this.outputStream); 785 | safeClose(this.outputStream); 786 | } catch (ResponseException re) { 787 | Response resp = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); 788 | resp.send(this.outputStream); 789 | safeClose(this.outputStream); 790 | } finally { 791 | safeClose(r); 792 | this.tempFileManager.clear(); 793 | } 794 | } 795 | 796 | /** 797 | * Find byte index separating header from body. It must be the last byte 798 | * of the first two sequential new lines. 799 | */ 800 | private int findHeaderEnd(final byte[] buf, int rlen) { 801 | int splitbyte = 0; 802 | while (splitbyte + 3 < rlen) { 803 | if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') { 804 | return splitbyte + 4; 805 | } 806 | splitbyte++; 807 | } 808 | return 0; 809 | } 810 | 811 | /** 812 | * Find the byte positions where multipart boundaries start. This reads 813 | * a large block at a time and uses a temporary buffer to optimize 814 | * (memory mapped) file access. 815 | */ 816 | private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { 817 | int[] res = new int[0]; 818 | if (b.remaining() < boundary.length) { 819 | return res; 820 | } 821 | 822 | int search_window_pos = 0; 823 | byte[] search_window = new byte[4 * 1024 + boundary.length]; 824 | 825 | int first_fill = (b.remaining() < search_window.length) ? b.remaining() : search_window.length; 826 | b.get(search_window, 0, first_fill); 827 | int new_bytes = first_fill - boundary.length; 828 | 829 | do { 830 | // Search the search_window 831 | for (int j = 0; j < new_bytes; j++) { 832 | for (int i = 0; i < boundary.length; i++) { 833 | if (search_window[j + i] != boundary[i]) 834 | break; 835 | if (i == boundary.length - 1) { 836 | // Match found, add it to results 837 | int[] new_res = new int[res.length + 1]; 838 | System.arraycopy(res, 0, new_res, 0, res.length); 839 | new_res[res.length] = search_window_pos + j; 840 | res = new_res; 841 | } 842 | } 843 | } 844 | search_window_pos += new_bytes; 845 | 846 | // Copy the end of the buffer to the start 847 | System.arraycopy(search_window, search_window.length - boundary.length, search_window, 0, boundary.length); 848 | 849 | // Refill search_window 850 | new_bytes = search_window.length - boundary.length; 851 | new_bytes = (b.remaining() < new_bytes) ? b.remaining() : new_bytes; 852 | b.get(search_window, boundary.length, new_bytes); 853 | } while (new_bytes > 0); 854 | return res; 855 | } 856 | 857 | @Override 858 | public CookieHandler getCookies() { 859 | return this.cookies; 860 | } 861 | 862 | @Override 863 | public final Map getHeaders() { 864 | return this.headers; 865 | } 866 | 867 | @Override 868 | public final InputStream getInputStream() { 869 | return this.inputStream; 870 | } 871 | 872 | @Override 873 | public final Method getMethod() { 874 | return this.method; 875 | } 876 | 877 | @Override 878 | public final Map getParms() { 879 | return this.parms; 880 | } 881 | 882 | @Override 883 | public String getQueryParameterString() { 884 | return this.queryParameterString; 885 | } 886 | 887 | private RandomAccessFile getTmpBucket() { 888 | try { 889 | TempFile tempFile = this.tempFileManager.createTempFile(); 890 | return new RandomAccessFile(tempFile.getName(), "rw"); 891 | } catch (Exception e) { 892 | throw new Error(e); // we won't recover, so throw an error 893 | } 894 | } 895 | 896 | @Override 897 | public final String getUri() { 898 | return this.uri; 899 | } 900 | 901 | @Override 902 | public void parseBody(Map files) throws IOException, ResponseException { 903 | final int REQUEST_BUFFER_LEN = 512; 904 | final int MEMORY_STORE_LIMIT = 1024; 905 | RandomAccessFile randomAccessFile = null; 906 | try { 907 | long size; 908 | if (this.headers.containsKey("content-length")) { 909 | size = Integer.parseInt(this.headers.get("content-length")); 910 | } else if (this.splitbyte < this.rlen) { 911 | size = this.rlen - this.splitbyte; 912 | } else { 913 | size = 0; 914 | } 915 | 916 | ByteArrayOutputStream baos = null; 917 | DataOutput request_data_output = null; 918 | 919 | // Store the request in memory or a file, depending on size 920 | if (size < MEMORY_STORE_LIMIT) { 921 | baos = new ByteArrayOutputStream(); 922 | request_data_output = new DataOutputStream(baos); 923 | } else { 924 | randomAccessFile = getTmpBucket(); 925 | request_data_output = randomAccessFile; 926 | } 927 | 928 | // Read all the body and write it to request_data_output 929 | byte[] buf = new byte[REQUEST_BUFFER_LEN]; 930 | while (this.rlen >= 0 && size > 0) { 931 | this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN)); 932 | size -= this.rlen; 933 | if (this.rlen > 0) { 934 | request_data_output.write(buf, 0, this.rlen); 935 | } 936 | } 937 | 938 | ByteBuffer fbuf = null; 939 | if (baos != null) { 940 | fbuf = ByteBuffer.wrap(baos.toByteArray(), 0, baos.size()); 941 | } else { 942 | fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); 943 | randomAccessFile.seek(0); 944 | } 945 | 946 | // If the method is POST, there may be parameters 947 | // in data section, too, read it: 948 | if (Method.POST.equals(this.method)) { 949 | String contentType = ""; 950 | String contentTypeHeader = this.headers.get("content-type"); 951 | 952 | StringTokenizer st = null; 953 | if (contentTypeHeader != null) { 954 | st = new StringTokenizer(contentTypeHeader, ",; "); 955 | if (st.hasMoreTokens()) { 956 | contentType = st.nextToken(); 957 | } 958 | } 959 | 960 | if ("multipart/form-data".equalsIgnoreCase(contentType)) { 961 | // Handle multipart/form-data 962 | if (!st.hasMoreTokens()) { 963 | throw new ResponseException(Response.Status.BAD_REQUEST, 964 | "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); 965 | } 966 | 967 | String boundaryStartString = "boundary="; 968 | int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length(); 969 | String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length()); 970 | if (boundary.startsWith("\"") && boundary.endsWith("\"")) { 971 | boundary = boundary.substring(1, boundary.length() - 1); 972 | } 973 | 974 | decodeMultipartFormData(boundary, fbuf, this.parms, files); 975 | } else { 976 | byte[] postBytes = new byte[fbuf.remaining()]; 977 | fbuf.get(postBytes); 978 | String postLine = new String(postBytes).trim(); 979 | // Handle application/x-www-form-urlencoded 980 | if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) { 981 | decodeParms(postLine, this.parms); 982 | } else if (postLine.length() != 0) { 983 | // Special case for raw POST data => create a 984 | // special files entry "postData" with raw content 985 | // data 986 | files.put("postData", postLine); 987 | } 988 | } 989 | } else if (Method.PUT.equals(this.method)) { 990 | files.put("content", saveTmpFile(fbuf, 0, fbuf.limit())); 991 | } 992 | } finally { 993 | safeClose(randomAccessFile); 994 | } 995 | } 996 | 997 | /** 998 | * Retrieves the content of a sent file and saves it to a temporary 999 | * file. The full path to the saved file is returned. 1000 | */ 1001 | private String saveTmpFile(ByteBuffer b, int offset, int len) { 1002 | String path = ""; 1003 | if (len > 0) { 1004 | FileOutputStream fileOutputStream = null; 1005 | try { 1006 | TempFile tempFile = this.tempFileManager.createTempFile(); 1007 | ByteBuffer src = b.duplicate(); 1008 | fileOutputStream = new FileOutputStream(tempFile.getName()); 1009 | FileChannel dest = fileOutputStream.getChannel(); 1010 | src.position(offset).limit(offset + len); 1011 | dest.write(src.slice()); 1012 | path = tempFile.getName(); 1013 | } catch (Exception e) { // Catch exception if any 1014 | throw new Error(e); // we won't recover, so throw an error 1015 | } finally { 1016 | safeClose(fileOutputStream); 1017 | } 1018 | } 1019 | return path; 1020 | } 1021 | } 1022 | 1023 | /** 1024 | * Handles one session, i.e. parses the HTTP request and returns the 1025 | * response. 1026 | */ 1027 | public interface IHTTPSession { 1028 | 1029 | void execute() throws IOException; 1030 | 1031 | CookieHandler getCookies(); 1032 | 1033 | Map getHeaders(); 1034 | 1035 | InputStream getInputStream(); 1036 | 1037 | Method getMethod(); 1038 | 1039 | Map getParms(); 1040 | 1041 | String getQueryParameterString(); 1042 | 1043 | /** 1044 | * @return the path part of the URL. 1045 | */ 1046 | String getUri(); 1047 | 1048 | /** 1049 | * Adds the files in the request body to the files map. 1050 | * 1051 | * @param files 1052 | * map to modify 1053 | */ 1054 | void parseBody(Map files) throws IOException, ResponseException; 1055 | } 1056 | 1057 | /** 1058 | * HTTP Request methods, with the ability to decode a String 1059 | * back to its enum value. 1060 | */ 1061 | public enum Method { 1062 | GET, 1063 | PUT, 1064 | POST, 1065 | DELETE, 1066 | HEAD, 1067 | OPTIONS, 1068 | TRACE, 1069 | CONNECT, 1070 | PATCH; 1071 | 1072 | static Method lookup(String method) { 1073 | for (Method m : Method.values()) { 1074 | if (m.toString().equalsIgnoreCase(method)) { 1075 | return m; 1076 | } 1077 | } 1078 | return null; 1079 | } 1080 | } 1081 | 1082 | /** 1083 | * HTTP response. Return one of these from serve(). 1084 | */ 1085 | public static class Response implements Closeable { 1086 | 1087 | public interface IStatus { 1088 | 1089 | String getDescription(); 1090 | 1091 | int getRequestStatus(); 1092 | } 1093 | 1094 | /** 1095 | * Some HTTP response status codes 1096 | */ 1097 | public enum Status implements IStatus { 1098 | SWITCH_PROTOCOL(101, "Switching Protocols"), 1099 | OK(200, "OK"), 1100 | CREATED(201, "Created"), 1101 | ACCEPTED(202, "Accepted"), 1102 | NO_CONTENT(204, "No Content"), 1103 | PARTIAL_CONTENT(206, "Partial Content"), 1104 | REDIRECT(301, "Moved Permanently"), 1105 | NOT_MODIFIED(304, "Not Modified"), 1106 | BAD_REQUEST(400, "Bad Request"), 1107 | UNAUTHORIZED(401, "Unauthorized"), 1108 | FORBIDDEN(403, "Forbidden"), 1109 | NOT_FOUND(404, "Not Found"), 1110 | METHOD_NOT_ALLOWED(405, "Method Not Allowed"), 1111 | REQUEST_TIMEOUT(408, "Request Timeout"), 1112 | RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"), 1113 | INTERNAL_ERROR(500, "Internal Server Error"), 1114 | UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported"); 1115 | 1116 | private final int requestStatus; 1117 | 1118 | private final String description; 1119 | 1120 | Status(int requestStatus, String description) { 1121 | this.requestStatus = requestStatus; 1122 | this.description = description; 1123 | } 1124 | 1125 | @Override 1126 | public String getDescription() { 1127 | return "" + this.requestStatus + " " + this.description; 1128 | } 1129 | 1130 | @Override 1131 | public int getRequestStatus() { 1132 | return this.requestStatus; 1133 | } 1134 | 1135 | } 1136 | 1137 | /** 1138 | * Output stream that will automatically send every write to the wrapped 1139 | * OutputStream according to chunked transfer: 1140 | * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1 1141 | */ 1142 | private static class ChunkedOutputStream extends FilterOutputStream { 1143 | 1144 | public ChunkedOutputStream(OutputStream out) { 1145 | super(out); 1146 | } 1147 | 1148 | @Override 1149 | public void write(int b) throws IOException { 1150 | byte[] data = { 1151 | (byte) b 1152 | }; 1153 | write(data, 0, 1); 1154 | } 1155 | 1156 | @Override 1157 | public void write(byte[] b) throws IOException { 1158 | write(b, 0, b.length); 1159 | } 1160 | 1161 | @Override 1162 | public void write(byte[] b, int off, int len) throws IOException { 1163 | if (len == 0) 1164 | return; 1165 | out.write(String.format("%x\r\n", len).getBytes()); 1166 | out.write(b, off, len); 1167 | out.write("\r\n".getBytes()); 1168 | } 1169 | 1170 | public void finish() throws IOException { 1171 | out.write("0\r\n\r\n".getBytes()); 1172 | } 1173 | 1174 | } 1175 | 1176 | /** 1177 | * HTTP status code after processing, e.g. "200 OK", Status.OK 1178 | */ 1179 | private IStatus status; 1180 | 1181 | /** 1182 | * MIME type of content, e.g. "text/html" 1183 | */ 1184 | private String mimeType; 1185 | 1186 | /** 1187 | * Data of the response, may be null. 1188 | */ 1189 | private InputStream data; 1190 | 1191 | private long contentLength; 1192 | 1193 | /** 1194 | * Headers for the HTTP response. Use addHeader() to add lines. 1195 | */ 1196 | private final Map header = new HashMap(); 1197 | 1198 | /** 1199 | * The request method that spawned this response. 1200 | */ 1201 | private Method requestMethod; 1202 | 1203 | /** 1204 | * Use chunkedTransfer 1205 | */ 1206 | private boolean chunkedTransfer; 1207 | 1208 | private boolean encodeAsGzip; 1209 | 1210 | private boolean keepAlive; 1211 | 1212 | /** 1213 | * Creates a fixed length response if totalBytes>=0, otherwise chunked. 1214 | */ 1215 | protected Response(IStatus status, String mimeType, InputStream data, long totalBytes) { 1216 | this.status = status; 1217 | this.mimeType = mimeType; 1218 | if (data == null) { 1219 | this.data = new ByteArrayInputStream(new byte[0]); 1220 | this.contentLength = 0L; 1221 | } else { 1222 | this.data = data; 1223 | this.contentLength = totalBytes; 1224 | } 1225 | this.chunkedTransfer = this.contentLength < 0; 1226 | keepAlive = true; 1227 | } 1228 | 1229 | @Override 1230 | public void close() throws IOException { 1231 | if (this.data != null) { 1232 | this.data.close(); 1233 | } 1234 | } 1235 | 1236 | /** 1237 | * Adds given line to the header. 1238 | */ 1239 | public void addHeader(String name, String value) { 1240 | this.header.put(name, value); 1241 | } 1242 | 1243 | public InputStream getData() { 1244 | return this.data; 1245 | } 1246 | 1247 | public String getHeader(String name) { 1248 | for (String headerName : header.keySet()) { 1249 | if (headerName.equalsIgnoreCase(name)) { 1250 | return header.get(headerName); 1251 | } 1252 | } 1253 | return null; 1254 | } 1255 | 1256 | public String getMimeType() { 1257 | return this.mimeType; 1258 | } 1259 | 1260 | public Method getRequestMethod() { 1261 | return this.requestMethod; 1262 | } 1263 | 1264 | public IStatus getStatus() { 1265 | return this.status; 1266 | } 1267 | 1268 | public void setGzipEncoding(boolean encodeAsGzip) { 1269 | this.encodeAsGzip = encodeAsGzip; 1270 | } 1271 | 1272 | public void setKeepAlive(boolean useKeepAlive) { 1273 | this.keepAlive = useKeepAlive; 1274 | } 1275 | 1276 | private boolean headerAlreadySent(Map header, String name) { 1277 | boolean alreadySent = false; 1278 | for (String headerName : header.keySet()) { 1279 | alreadySent |= headerName.equalsIgnoreCase(name); 1280 | } 1281 | return alreadySent; 1282 | } 1283 | 1284 | /** 1285 | * Sends given response to the socket. 1286 | */ 1287 | protected void send(OutputStream outputStream) { 1288 | String mime = this.mimeType; 1289 | SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); 1290 | gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); 1291 | 1292 | try { 1293 | if (this.status == null) { 1294 | throw new Error("sendResponse(): Status can't be null."); 1295 | } 1296 | PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8")), false); 1297 | pw.print("HTTP/1.1 " + this.status.getDescription() + " \r\n"); 1298 | 1299 | if (mime != null) { 1300 | pw.print("Content-Type: " + mime + "\r\n"); 1301 | } 1302 | 1303 | if (this.header == null || this.header.get("Date") == null) { 1304 | pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n"); 1305 | } 1306 | 1307 | if (this.header != null) { 1308 | for (String key : this.header.keySet()) { 1309 | String value = this.header.get(key); 1310 | pw.print(key + ": " + value + "\r\n"); 1311 | } 1312 | } 1313 | 1314 | if (!headerAlreadySent(header, "connection")) { 1315 | pw.print("Connection: " + (this.keepAlive ? "keep-alive" : "close") + "\r\n"); 1316 | } 1317 | 1318 | if (headerAlreadySent(this.header, "content-length")) { 1319 | encodeAsGzip = false; 1320 | } 1321 | 1322 | if (encodeAsGzip) { 1323 | pw.print("Content-Encoding: gzip\r\n"); 1324 | setChunkedTransfer(true); 1325 | } 1326 | 1327 | long pending = this.data != null ? this.contentLength : 0; 1328 | if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { 1329 | pw.print("Transfer-Encoding: chunked\r\n"); 1330 | } else if (!encodeAsGzip) { 1331 | pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, this.header, pending); 1332 | } 1333 | pw.print("\r\n"); 1334 | pw.flush(); 1335 | sendBodyWithCorrectTransferAndEncoding(outputStream, pending); 1336 | outputStream.flush(); 1337 | safeClose(this.data); 1338 | } catch (IOException ioe) { 1339 | NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe); 1340 | } 1341 | } 1342 | 1343 | private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException { 1344 | if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { 1345 | ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream); 1346 | sendBodyWithCorrectEncoding(chunkedOutputStream, -1); 1347 | chunkedOutputStream.finish(); 1348 | } else { 1349 | sendBodyWithCorrectEncoding(outputStream, pending); 1350 | } 1351 | } 1352 | 1353 | private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException { 1354 | if (encodeAsGzip) { 1355 | GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream); 1356 | sendBody(gzipOutputStream, -1); 1357 | gzipOutputStream.finish(); 1358 | } else { 1359 | sendBody(outputStream, pending); 1360 | } 1361 | } 1362 | 1363 | /** 1364 | * Sends the body to the specified OutputStream. The pending parameter 1365 | * limits the maximum amounts of bytes sent unless it is -1, in which 1366 | * case everything is sent. 1367 | * 1368 | * @param outputStream 1369 | * the OutputStream to send data to 1370 | * @param pending 1371 | * -1 to send everything, otherwise sets a max limit to the 1372 | * number of bytes sent 1373 | * @throws IOException 1374 | * if something goes wrong while sending the data. 1375 | */ 1376 | private void sendBody(OutputStream outputStream, long pending) throws IOException { 1377 | long BUFFER_SIZE = 16 * 1024; 1378 | byte[] buff = new byte[(int) BUFFER_SIZE]; 1379 | boolean sendEverything = pending == -1; 1380 | while (pending > 0 || sendEverything) { 1381 | long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE); 1382 | int read = this.data.read(buff, 0, (int) bytesToRead); 1383 | if (read <= 0) { 1384 | break; 1385 | } 1386 | outputStream.write(buff, 0, read); 1387 | if (!sendEverything) { 1388 | pending -= read; 1389 | } 1390 | } 1391 | } 1392 | 1393 | protected long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map header, long size) { 1394 | for (String headerName : header.keySet()) { 1395 | if (headerName.equalsIgnoreCase("content-length")) { 1396 | try { 1397 | return Long.parseLong(header.get(headerName)); 1398 | } catch (NumberFormatException ex) { 1399 | return size; 1400 | } 1401 | } 1402 | } 1403 | 1404 | pw.print("Content-Length: " + size + "\r\n"); 1405 | return size; 1406 | } 1407 | 1408 | public void setChunkedTransfer(boolean chunkedTransfer) { 1409 | this.chunkedTransfer = chunkedTransfer; 1410 | } 1411 | 1412 | public void setData(InputStream data) { 1413 | this.data = data; 1414 | } 1415 | 1416 | public void setMimeType(String mimeType) { 1417 | this.mimeType = mimeType; 1418 | } 1419 | 1420 | public void setRequestMethod(Method requestMethod) { 1421 | this.requestMethod = requestMethod; 1422 | } 1423 | 1424 | public void setStatus(IStatus status) { 1425 | this.status = status; 1426 | } 1427 | } 1428 | 1429 | public static final class ResponseException extends Exception { 1430 | 1431 | private static final long serialVersionUID = 6569838532917408380L; 1432 | 1433 | private final Response.Status status; 1434 | 1435 | public ResponseException(Response.Status status, String message) { 1436 | super(message); 1437 | this.status = status; 1438 | } 1439 | 1440 | public ResponseException(Response.Status status, String message, Exception e) { 1441 | super(message, e); 1442 | this.status = status; 1443 | } 1444 | 1445 | public Response.Status getStatus() { 1446 | return this.status; 1447 | } 1448 | } 1449 | 1450 | /** 1451 | * The runnable that will be used for the main listening thread. 1452 | */ 1453 | public class ServerRunnable implements Runnable { 1454 | 1455 | private final int timeout; 1456 | 1457 | private IOException bindException; 1458 | 1459 | private boolean hasBinded = false; 1460 | 1461 | private ServerRunnable(int timeout) { 1462 | this.timeout = timeout; 1463 | } 1464 | 1465 | @Override 1466 | public void run() { 1467 | try { 1468 | myServerSocket.bind(hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort)); 1469 | hasBinded = true; 1470 | } catch (IOException e) { 1471 | this.bindException = e; 1472 | return; 1473 | } 1474 | do { 1475 | try { 1476 | final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept(); 1477 | if (this.timeout > 0) { 1478 | finalAccept.setSoTimeout(this.timeout); 1479 | } 1480 | final InputStream inputStream = finalAccept.getInputStream(); 1481 | NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream)); 1482 | } catch (IOException e) { 1483 | NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e); 1484 | } 1485 | } while (!NanoHTTPD.this.myServerSocket.isClosed()); 1486 | } 1487 | } 1488 | 1489 | /** 1490 | * A temp file. 1491 | *

1492 | *

1493 | * Temp files are responsible for managing the actual temporary storage and 1494 | * cleaning themselves up when no longer needed. 1495 | *

1496 | */ 1497 | public interface TempFile { 1498 | 1499 | void delete() throws Exception; 1500 | 1501 | String getName(); 1502 | 1503 | OutputStream open() throws Exception; 1504 | } 1505 | 1506 | /** 1507 | * Temp file manager. 1508 | *

1509 | *

1510 | * Temp file managers are created 1-to-1 with incoming requests, to create 1511 | * and cleanup temporary files created as a result of handling the request. 1512 | *

1513 | */ 1514 | public interface TempFileManager { 1515 | 1516 | void clear(); 1517 | 1518 | TempFile createTempFile() throws Exception; 1519 | } 1520 | 1521 | /** 1522 | * Factory to create temp file managers. 1523 | */ 1524 | public interface TempFileManagerFactory { 1525 | 1526 | TempFileManager create(); 1527 | } 1528 | 1529 | /** 1530 | * Maximum time to wait on Socket.getInputStream().read() (in milliseconds) 1531 | * This is required as the Keep-Alive HTTP connections would otherwise block 1532 | * the socket reading thread forever (or as long the browser is open). 1533 | */ 1534 | public static final int SOCKET_READ_TIMEOUT = 5000; 1535 | 1536 | /** 1537 | * Common MIME type for dynamic content: plain text 1538 | */ 1539 | public static final String MIME_PLAINTEXT = "text/plain"; 1540 | 1541 | /** 1542 | * Common MIME type for dynamic content: html 1543 | */ 1544 | public static final String MIME_HTML = "text/html"; 1545 | 1546 | /** 1547 | * Pseudo-Parameter to use to store the actual query string in the 1548 | * parameters map for later re-processing. 1549 | */ 1550 | private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; 1551 | 1552 | /** 1553 | * logger to log to. 1554 | */ 1555 | private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); 1556 | 1557 | /** 1558 | * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an 1559 | * array of loaded KeyManagers. These objects must properly 1560 | * loaded/initialized by the caller. 1561 | */ 1562 | public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException { 1563 | SSLServerSocketFactory res = null; 1564 | try { 1565 | TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1566 | trustManagerFactory.init(loadedKeyStore); 1567 | SSLContext ctx = SSLContext.getInstance("TLS"); 1568 | ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null); 1569 | res = ctx.getServerSocketFactory(); 1570 | } catch (Exception e) { 1571 | throw new IOException(e.getMessage()); 1572 | } 1573 | return res; 1574 | } 1575 | 1576 | /** 1577 | * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a 1578 | * loaded KeyManagerFactory. These objects must properly loaded/initialized 1579 | * by the caller. 1580 | */ 1581 | public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException { 1582 | SSLServerSocketFactory res = null; 1583 | try { 1584 | TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1585 | trustManagerFactory.init(loadedKeyStore); 1586 | SSLContext ctx = SSLContext.getInstance("TLS"); 1587 | ctx.init(loadedKeyFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); 1588 | res = ctx.getServerSocketFactory(); 1589 | } catch (Exception e) { 1590 | throw new IOException(e.getMessage()); 1591 | } 1592 | return res; 1593 | } 1594 | 1595 | /** 1596 | * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your 1597 | * certificate and passphrase 1598 | */ 1599 | public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException { 1600 | SSLServerSocketFactory res = null; 1601 | try { 1602 | KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); 1603 | InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); 1604 | keystore.load(keystoreStream, passphrase); 1605 | TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1606 | trustManagerFactory.init(keystore); 1607 | KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 1608 | keyManagerFactory.init(keystore, passphrase); 1609 | SSLContext ctx = SSLContext.getInstance("TLS"); 1610 | ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); 1611 | res = ctx.getServerSocketFactory(); 1612 | } catch (Exception e) { 1613 | throw new IOException(e.getMessage()); 1614 | } 1615 | return res; 1616 | } 1617 | 1618 | private static final void safeClose(Object closeable) { 1619 | try { 1620 | if (closeable != null) { 1621 | if (closeable instanceof Closeable) { 1622 | ((Closeable) closeable).close(); 1623 | } else if (closeable instanceof Socket) { 1624 | ((Socket) closeable).close(); 1625 | } else if (closeable instanceof ServerSocket) { 1626 | ((ServerSocket) closeable).close(); 1627 | } else { 1628 | throw new IllegalArgumentException("Unknown object to close"); 1629 | } 1630 | } 1631 | } catch (IOException e) { 1632 | NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e); 1633 | } 1634 | } 1635 | 1636 | private final String hostname; 1637 | 1638 | private final int myPort; 1639 | 1640 | private ServerSocket myServerSocket; 1641 | 1642 | private SSLServerSocketFactory sslServerSocketFactory; 1643 | 1644 | private Thread myThread; 1645 | 1646 | /** 1647 | * Pluggable strategy for asynchronously executing requests. 1648 | */ 1649 | protected AsyncRunner asyncRunner; 1650 | 1651 | /** 1652 | * Pluggable strategy for creating and cleaning up temporary files. 1653 | */ 1654 | private TempFileManagerFactory tempFileManagerFactory; 1655 | 1656 | /** 1657 | * Constructs an HTTP server on given port. 1658 | */ 1659 | public NanoHTTPD(int port) { 1660 | this(null, port); 1661 | } 1662 | 1663 | // ------------------------------------------------------------------------------- 1664 | // // 1665 | // 1666 | // Threading Strategy. 1667 | // 1668 | // ------------------------------------------------------------------------------- 1669 | // // 1670 | 1671 | /** 1672 | * Constructs an HTTP server on given hostname and port. 1673 | */ 1674 | public NanoHTTPD(String hostname, int port) { 1675 | this.hostname = hostname; 1676 | this.myPort = port; 1677 | setTempFileManagerFactory(new DefaultTempFileManagerFactory()); 1678 | setAsyncRunner(new DefaultAsyncRunner()); 1679 | } 1680 | 1681 | /** 1682 | * Forcibly closes all connections that are open. 1683 | */ 1684 | public synchronized void closeAllConnections() { 1685 | stop(); 1686 | } 1687 | 1688 | /** 1689 | * create a instance of the client handler, subclasses can return a subclass 1690 | * of the ClientHandler. 1691 | * 1692 | * @param finalAccept 1693 | * the socket the cleint is connected to 1694 | * @param inputStream 1695 | * the input stream 1696 | * @return the client handler 1697 | */ 1698 | protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) { 1699 | return new ClientHandler(inputStream, finalAccept); 1700 | } 1701 | 1702 | /** 1703 | * Instantiate the server runnable, can be overwritten by subclasses to 1704 | * provide a subclass of the ServerRunnable. 1705 | * 1706 | * @param timeout 1707 | * the socet timeout to use. 1708 | * @return the server runnable. 1709 | */ 1710 | protected ServerRunnable createServerRunnable(final int timeout) { 1711 | return new ServerRunnable(timeout); 1712 | } 1713 | 1714 | /** 1715 | * Decode parameters from a URL, handing the case where a single parameter 1716 | * name might have been supplied several times, by return lists of values. 1717 | * In general these lists will contain a single element. 1718 | * 1719 | * @param parms 1720 | * original NanoHTTPD parameters values, as passed to the 1721 | * serve() method. 1722 | * @return a map of String (parameter name) to 1723 | * List<String> (a list of the values supplied). 1724 | */ 1725 | protected Map> decodeParameters(Map parms) { 1726 | return this.decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER)); 1727 | } 1728 | 1729 | // ------------------------------------------------------------------------------- 1730 | // // 1731 | 1732 | /** 1733 | * Decode parameters from a URL, handing the case where a single parameter 1734 | * name might have been supplied several times, by return lists of values. 1735 | * In general these lists will contain a single element. 1736 | * 1737 | * @param queryString 1738 | * a query string pulled from the URL. 1739 | * @return a map of String (parameter name) to 1740 | * List<String> (a list of the values supplied). 1741 | */ 1742 | protected Map> decodeParameters(String queryString) { 1743 | Map> parms = new HashMap>(); 1744 | if (queryString != null) { 1745 | StringTokenizer st = new StringTokenizer(queryString, "&"); 1746 | while (st.hasMoreTokens()) { 1747 | String e = st.nextToken(); 1748 | int sep = e.indexOf('='); 1749 | String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); 1750 | if (!parms.containsKey(propertyName)) { 1751 | parms.put(propertyName, new ArrayList()); 1752 | } 1753 | String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null; 1754 | if (propertyValue != null) { 1755 | parms.get(propertyName).add(propertyValue); 1756 | } 1757 | } 1758 | } 1759 | return parms; 1760 | } 1761 | 1762 | /** 1763 | * Decode percent encoded String values. 1764 | * 1765 | * @param str 1766 | * the percent encoded String 1767 | * @return expanded form of the input, for example "foo%20bar" becomes 1768 | * "foo bar" 1769 | */ 1770 | protected String decodePercent(String str) { 1771 | String decoded = null; 1772 | try { 1773 | decoded = URLDecoder.decode(str, "UTF8"); 1774 | } catch (UnsupportedEncodingException ignored) { 1775 | NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored); 1776 | } 1777 | return decoded; 1778 | } 1779 | 1780 | /** 1781 | * @return true if the gzip compression should be used if the client 1782 | * accespts it. Default this option is on for text content and off 1783 | * for everything else. 1784 | */ 1785 | protected boolean useGzipWhenAccepted(Response r) { 1786 | return r.getMimeType() != null && r.getMimeType().toLowerCase().contains("text/"); 1787 | } 1788 | 1789 | public final int getListeningPort() { 1790 | return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort(); 1791 | } 1792 | 1793 | public final boolean isAlive() { 1794 | return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive(); 1795 | } 1796 | 1797 | /** 1798 | * Call before start() to serve over HTTPS instead of HTTP 1799 | */ 1800 | public void makeSecure(SSLServerSocketFactory sslServerSocketFactory) { 1801 | this.sslServerSocketFactory = sslServerSocketFactory; 1802 | } 1803 | 1804 | /** 1805 | * Create a response with unknown length (using HTTP 1.1 chunking). 1806 | */ 1807 | public Response newChunkedResponse(IStatus status, String mimeType, InputStream data) { 1808 | return new Response(status, mimeType, data, -1); 1809 | } 1810 | 1811 | /** 1812 | * Create a response with known length. 1813 | */ 1814 | public Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) { 1815 | return new Response(status, mimeType, data, totalBytes); 1816 | } 1817 | 1818 | /** 1819 | * Create a text response with known length. 1820 | */ 1821 | public Response newFixedLengthResponse(IStatus status, String mimeType, String txt) { 1822 | if (txt == null) { 1823 | return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0); 1824 | } else { 1825 | byte[] bytes; 1826 | try { 1827 | bytes = txt.getBytes("UTF-8"); 1828 | } catch (UnsupportedEncodingException e) { 1829 | NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e); 1830 | bytes = new byte[0]; 1831 | } 1832 | return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(bytes), bytes.length); 1833 | } 1834 | } 1835 | 1836 | /** 1837 | * Create a text response with known length. 1838 | */ 1839 | public Response newFixedLengthResponse(String msg) { 1840 | return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg); 1841 | } 1842 | 1843 | /** 1844 | * Override this to customize the server. 1845 | *

1846 | *

1847 | * (By default, this returns a 404 "Not Found" plain text error response.) 1848 | * 1849 | * @param session 1850 | * The HTTP session 1851 | * @return HTTP response, see class Response for details 1852 | */ 1853 | public Response serve(IHTTPSession session) { 1854 | Map files = new HashMap(); 1855 | Method method = session.getMethod(); 1856 | if (Method.PUT.equals(method) || Method.POST.equals(method)) { 1857 | try { 1858 | session.parseBody(files); 1859 | } catch (IOException ioe) { 1860 | return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); 1861 | } catch (ResponseException re) { 1862 | return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); 1863 | } 1864 | } 1865 | 1866 | Map parms = session.getParms(); 1867 | parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString()); 1868 | return serve(session.getUri(), method, session.getHeaders(), parms, files); 1869 | } 1870 | 1871 | /** 1872 | * Override this to customize the server. 1873 | *

1874 | *

1875 | * (By default, this returns a 404 "Not Found" plain text error response.) 1876 | * 1877 | * @param uri 1878 | * Percent-decoded URI without parameters, for example 1879 | * "/index.cgi" 1880 | * @param method 1881 | * "GET", "POST" etc. 1882 | * @param parms 1883 | * Parsed, percent decoded parameters from URI and, in case of 1884 | * POST, data. 1885 | * @param headers 1886 | * Header entries, percent decoded 1887 | * @return HTTP response, see class Response for details 1888 | */ 1889 | @Deprecated 1890 | public Response serve(String uri, Method method, Map headers, Map parms, Map files) { 1891 | return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found"); 1892 | } 1893 | 1894 | /** 1895 | * Pluggable strategy for asynchronously executing requests. 1896 | * 1897 | * @param asyncRunner 1898 | * new strategy for handling threads. 1899 | */ 1900 | public void setAsyncRunner(AsyncRunner asyncRunner) { 1901 | this.asyncRunner = asyncRunner; 1902 | } 1903 | 1904 | /** 1905 | * Pluggable strategy for creating and cleaning up temporary files. 1906 | * 1907 | * @param tempFileManagerFactory 1908 | * new strategy for handling temp files. 1909 | */ 1910 | public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { 1911 | this.tempFileManagerFactory = tempFileManagerFactory; 1912 | } 1913 | 1914 | /** 1915 | * Start the server. 1916 | * 1917 | * @throws IOException 1918 | * if the socket is in use. 1919 | */ 1920 | public void start() throws IOException { 1921 | start(NanoHTTPD.SOCKET_READ_TIMEOUT); 1922 | } 1923 | 1924 | /** 1925 | * Start the server. 1926 | * 1927 | * @param timeout 1928 | * timeout to use for socket connections. 1929 | * @throws IOException 1930 | * if the socket is in use. 1931 | */ 1932 | public void start(final int timeout) throws IOException { 1933 | if (this.sslServerSocketFactory != null) { 1934 | SSLServerSocket ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket(); 1935 | ss.setNeedClientAuth(false); 1936 | this.myServerSocket = ss; 1937 | } else { 1938 | this.myServerSocket = new ServerSocket(); 1939 | } 1940 | this.myServerSocket.setReuseAddress(true); 1941 | 1942 | ServerRunnable serverRunnable = createServerRunnable(timeout); 1943 | this.myThread = new Thread(serverRunnable); 1944 | this.myThread.setDaemon(true); 1945 | this.myThread.setName("NanoHttpd Main Listener"); 1946 | this.myThread.start(); 1947 | while (!serverRunnable.hasBinded && serverRunnable.bindException == null) { 1948 | try { 1949 | Thread.sleep(10L); 1950 | } catch (Throwable e) { 1951 | // on android this may not be allowed, that's why we 1952 | // catch throwable the wait should be very short because we are 1953 | // just waiting for the bind of the socket 1954 | } 1955 | } 1956 | if (serverRunnable.bindException != null) { 1957 | throw serverRunnable.bindException; 1958 | } 1959 | } 1960 | 1961 | /** 1962 | * Stop the server. 1963 | */ 1964 | public void stop() { 1965 | try { 1966 | safeClose(this.myServerSocket); 1967 | this.asyncRunner.closeAll(); 1968 | if (this.myThread != null) { 1969 | this.myThread.join(); 1970 | } 1971 | } catch (Exception e) { 1972 | NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e); 1973 | } 1974 | } 1975 | 1976 | public final boolean wasStarted() { 1977 | return this.myServerSocket != null && this.myThread != null; 1978 | } 1979 | } 1980 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 |

4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreivisan/AndroidHttpServer/8f8bd8592c013dd6e6c4f361869a0a7a63a3e00e/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreivisan/AndroidHttpServer/8f8bd8592c013dd6e6c4f361869a0a7a63a3e00e/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreivisan/AndroidHttpServer/8f8bd8592c013dd6e6c4f361869a0a7a63a3e00e/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreivisan/AndroidHttpServer/8f8bd8592c013dd6e6c4f361869a0a7a63a3e00e/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidHttpServer 3 | 4 | Hello world! 5 | Settings 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.2.3' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreivisan/AndroidHttpServer/8f8bd8592c013dd6e6c4f361869a0a7a63a3e00e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------