├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── AWebDB-Sample.iml ├── README.md ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── lusfold │ │ └── awebdb_sample │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── lusfold │ │ └── awebdb_sample │ │ └── MainActivity.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 ├── awebdb ├── .gitignore ├── awebdb.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── lusfold │ │ └── awebdb │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── lusfold │ │ └── awebdb │ │ ├── AWebDB.java │ │ ├── db │ │ └── DBUtil.java │ │ ├── mapper │ │ ├── HtmlMapper.java │ │ └── Mapper.java │ │ ├── net │ │ └── nanohttpd │ │ │ ├── HtmlResponse.java │ │ │ ├── NanoHTTPD.java │ │ │ └── XNanoHTTPD.java │ │ └── utils │ │ ├── LogUtil.java │ │ ├── NetUtils.java │ │ └── StringUtils.java │ └── res │ └── values │ └── strings.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 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | AWebDB-Sample -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 1.8 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AWebDB-Sample.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWebDB 2 | 3 | *** 4 | 5 | [![build passing](https://img.shields.io/badge/build-passing-green.svg?style=flat)](https://bintray.com/lusfold/maven/AWebDB/view) 6 | [![AWebDB Maven](https://img.shields.io/badge/AWebDB-Maven-green.svg?style=flat)](https://bintray.com/lusfold/maven/AWebDB/view) 7 | 8 | I found it was pretty hard to check database when you develop apps, cause you had to export th db file then... 9 | 10 | Finally,i found nanohttpd, a android base mini web server. 11 | 12 | As you see,i made AWebDB (Android web database),help you to look up you sqlite databse from web browser. 13 | 14 | 15 | #Quickstart 16 | 17 | ####step 1 18 | 19 | add dependency 20 | 21 | ```gradle 22 | dependencies { 23 | compile 'com.lusfold.awebdb:awebdb:0.1.0' 24 | } 25 | ``` 26 | 27 | ####step 2 28 | 29 | hook it 30 | 31 | ```java 32 | AWebDB.hook(mContext, dbName,port) 33 | String ip=AWebDB.getLocalIPAddress(); 34 | int port = AWebDB.getLocalServerPort(); 35 | ``` 36 | 37 | ####step 3 38 | 39 | in your browser 40 | http://ip:port/?sql 41 | 42 | like this: 43 | 44 | http://192.168.1.141:8080/?SELECT * FROM sqlite_master WHERE type = 'table'; 45 | 46 | *** 47 | 48 | #LICENSE 49 | 50 | Copyright 2014-2015 Lusfold(https://github.com/lusfold) 51 | 52 | Licensed under the Apache License, Version 2.0 (the "License");you may not use this file except in compliance with the License. 53 | 54 | You may obtain a copy of the License at 55 | 56 | http://www.apache.org/licenses/LICENSE-2.0 57 | 58 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 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 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "23.0.0 rc3" 6 | 7 | defaultConfig { 8 | applicationId "com.lusfold.awebdb_sample" 9 | minSdkVersion 14 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 | compile project(':awebdb') 26 | } 27 | -------------------------------------------------------------------------------- /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/bzh/tools/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/com/lusfold/awebdb_sample/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.lusfold.awebdb_sample; 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 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/lusfold/awebdb_sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.lusfold.awebdb_sample; 2 | 3 | import android.database.sqlite.SQLiteDatabase; 4 | import android.database.sqlite.SQLiteDatabaseCorruptException; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.os.Bundle; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | import android.widget.TextView; 10 | import android.widget.Toast; 11 | 12 | import com.lusfold.awebdb.AWebDB; 13 | 14 | public class MainActivity extends AppCompatActivity { 15 | private static final String DBName = "awebdb"; 16 | private TextView tvIPPort; 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_main); 21 | initDb(); 22 | AWebDB.hook(this,DBName,8080); 23 | String ip=AWebDB.getLocalIPAddress(); 24 | int port=AWebDB.getLocalServerPort(); 25 | tvIPPort = (TextView) findViewById(R.id.tv_ip_port); 26 | tvIPPort.setText("IP:"+ip+" Port:"+port); 27 | } 28 | 29 | private void initDb(){ 30 | String createTB = "CREATE TABLE IF NOT EXISTS USER(ID INT PRIMARY KEY ,NAME varchar(20))"; 31 | SQLiteDatabase db = openOrCreateDatabase(DBName,MODE_PRIVATE, null); 32 | db.execSQL(createTB); 33 | } 34 | @Override 35 | public boolean onCreateOptionsMenu(Menu menu) { 36 | // Inflate the menu; this adds items to the action bar if it is present. 37 | getMenuInflater().inflate(R.menu.menu_main, menu); 38 | return true; 39 | } 40 | 41 | @Override 42 | public boolean onOptionsItemSelected(MenuItem item) { 43 | // Handle action bar item clicks here. The action bar will 44 | // automatically handle clicks on the Home/Up button, so long 45 | // as you specify a parent activity in AndroidManifest.xml. 46 | int id = item.getItemId(); 47 | 48 | //noinspection SimplifiableIfStatement 49 | if (id == R.id.action_settings) { 50 | return true; 51 | } 52 | 53 | return super.onOptionsItemSelected(item); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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/lusfold/AWebDB-Sample/1d82512bcd9280cee5e1383e6e3189d7fd2bd396/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lusfold/AWebDB-Sample/1d82512bcd9280cee5e1383e6e3189d7fd2bd396/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lusfold/AWebDB-Sample/1d82512bcd9280cee5e1383e6e3189d7fd2bd396/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lusfold/AWebDB-Sample/1d82512bcd9280cee5e1383e6e3189d7fd2bd396/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 | AWebDB-Sample 3 | 4 | Hello world! 5 | Settings 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /awebdb/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /awebdb/awebdb.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 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 | 96 | -------------------------------------------------------------------------------- /awebdb/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | apply plugin: 'com.jfrog.bintray' 4 | // This is the library version used when deploying the artifact 5 | version = "0.1.0" 6 | android { 7 | compileSdkVersion 21 8 | buildToolsVersion "21.1.2" 9 | resourcePrefix "AWebDB" //这个随便填 10 | defaultConfig { 11 | minSdkVersion 9 12 | targetSdkVersion 21 13 | versionCode 1 14 | versionName version 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | } 23 | dependencies { 24 | compile fileTree(dir: 'libs', include: ['*.jar']) 25 | } 26 | def siteUrl = 'https://github.com/lusfold/AWebDB-Sample' // 项目的主页 27 | def gitUrl = 'https://github.com/lusfold/AWebDB-Sample.git' // Git仓库的url 28 | group = "com.lusfold.awebdb" // Maven Group ID for the artifact,一般填你唯一的包名 29 | install { 30 | repositories.mavenInstaller { 31 | // This generates POM.xml with proper parameters 32 | pom { 33 | project { 34 | packaging 'aar' 35 | // Add your description here 36 | name 'Android Web Database' //项目描述 37 | url siteUrl 38 | // Set your license 39 | licenses { 40 | license { 41 | name 'The Apache Software License, Version 2.0' 42 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 43 | } 44 | } 45 | developers { 46 | developer { 47 | id 'lusfold' //填写的一些基本信息 48 | name 'lusfold' 49 | email 'lusfold@gmail.com' 50 | } 51 | } 52 | scm { 53 | connection gitUrl 54 | developerConnection gitUrl 55 | url siteUrl 56 | } 57 | } 58 | } 59 | } 60 | } 61 | task sourcesJar(type: Jar) { 62 | from android.sourceSets.main.java.srcDirs 63 | classifier = 'sources' 64 | } 65 | task javadoc(type: Javadoc) { 66 | source = android.sourceSets.main.java.srcDirs 67 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 68 | } 69 | task javadocJar(type: Jar, dependsOn: javadoc) { 70 | classifier = 'javadoc' 71 | from javadoc.destinationDir 72 | } 73 | artifacts { 74 | archives javadocJar 75 | archives sourcesJar 76 | } 77 | Properties properties = new Properties() 78 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 79 | bintray { 80 | user = properties.getProperty("bintray.user") 81 | key = properties.getProperty("bintray.apikey") 82 | configurations = ['archives'] 83 | pkg { 84 | repo = "maven" 85 | name = "AWebDB" //发布到JCenter上的项目名字 86 | websiteUrl = siteUrl 87 | vcsUrl = gitUrl 88 | licenses = ["Apache-2.0"] 89 | publish = true 90 | } 91 | } -------------------------------------------------------------------------------- /awebdb/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/bzh/tools/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 | -------------------------------------------------------------------------------- /awebdb/src/androidTest/java/com/lusfold/awebdb/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.lusfold.awebdb; 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 | } -------------------------------------------------------------------------------- /awebdb/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /awebdb/src/main/java/com/lusfold/awebdb/AWebDB.java: -------------------------------------------------------------------------------- 1 | package com.lusfold.awebdb; 2 | 3 | import android.content.Context; 4 | 5 | import com.lusfold.awebdb.db.DBUtil; 6 | import com.lusfold.awebdb.net.nanohttpd.XNanoHTTPD; 7 | import com.lusfold.awebdb.utils.LogUtil; 8 | import com.lusfold.awebdb.utils.NetUtils; 9 | 10 | import java.io.IOException; 11 | /** 12 | * @author Lusfold 13 | */ 14 | public class AWebDB { 15 | private static XNanoHTTPD mHttpd; 16 | 17 | /** 18 | * @param context 19 | * @param dbName 20 | * @param port 21 | */ 22 | public static void hook(Context context, String dbName, int port) { 23 | 24 | DBUtil.init(context, dbName); 25 | port = NetUtils.getAGoodPort(port); 26 | mHttpd = new XNanoHTTPD(port); 27 | try { 28 | mHttpd.start(); 29 | } catch (IOException e) { 30 | e.printStackTrace(); 31 | } 32 | } 33 | 34 | /** 35 | * @return 36 | */ 37 | public static String getLocalIPAddress() { 38 | return NetUtils.getLocalIpAddress(); 39 | } 40 | 41 | /** 42 | * @return 43 | */ 44 | public static int getLocalServerPort() { 45 | return mHttpd == null ? -1 : mHttpd.getListeningPort(); 46 | } 47 | 48 | /** 49 | * @param status 50 | */ 51 | public static void enableLog(boolean status) { 52 | LogUtil.DEBUG = status; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /awebdb/src/main/java/com/lusfold/awebdb/db/DBUtil.java: -------------------------------------------------------------------------------- 1 | package com.lusfold.awebdb.db; 2 | 3 | import android.content.Context; 4 | import android.database.Cursor; 5 | import android.database.sqlite.SQLiteDatabase; 6 | 7 | import com.lusfold.awebdb.mapper.HtmlMapper; 8 | import com.lusfold.awebdb.utils.StringUtils; 9 | 10 | /** 11 | * @author Lusfold 12 | */ 13 | public class DBUtil { 14 | private static SQLiteDatabase db; 15 | private static String dbName; 16 | private static Context mContext; 17 | 18 | public static void init(Context context, String name) { 19 | if (db != null || context == null || StringUtils.isBlank(name)) 20 | return; 21 | dbName = name; 22 | mContext = context; 23 | db = mContext.openOrCreateDatabase(dbName, Context.MODE_PRIVATE, null); 24 | } 25 | 26 | public static void inflateMapper(String query, HtmlMapper htmlMapper) { 27 | 28 | if (StringUtils.isBlank(query) || htmlMapper == null) 29 | return; 30 | htmlMapper.addHeadInFo("QueryString: " + query); 31 | htmlMapper.addHeadInFo("DbVersion: " + db.getVersion()); 32 | Cursor cursor = null; 33 | boolean status =true; 34 | try { 35 | cursor = db.rawQuery(query, null); 36 | 37 | int columnCount = cursor.getColumnCount(); 38 | htmlMapper.addData(cursor.getColumnNames()); 39 | while (cursor.moveToNext()) { 40 | String[] data = new String[columnCount]; 41 | for (int i = 0; i < columnCount; i++) { 42 | data[i] = cursor.getString(i); 43 | } 44 | htmlMapper.addData(data); 45 | } 46 | } catch (Exception e) { 47 | status=false; 48 | htmlMapper.addHeadInFo("Info: " + e.toString()); 49 | } finally { 50 | if (cursor != null) { 51 | cursor.close(); 52 | htmlMapper.addHeadInFo("Status: " + status); 53 | } 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /awebdb/src/main/java/com/lusfold/awebdb/mapper/HtmlMapper.java: -------------------------------------------------------------------------------- 1 | package com.lusfold.awebdb.mapper; 2 | 3 | import java.util.ArrayList; 4 | 5 | /** 6 | * @author Lusfold 7 | */ 8 | public class HtmlMapper implements Mapper { 9 | private ArrayList mHead; 10 | private ArrayList mData; 11 | private StringBuilder mStringBuilder; 12 | 13 | public String map() { 14 | 15 | if ((mHead == null || mHead.size() == 0) && (mData == null || mData.size() == 0)) 16 | return ""; 17 | 18 | mStringBuilder = new StringBuilder(); 19 | 20 | mStringBuilder.append(HTMLString.HTMLPrefix); 21 | 22 | if (mHead != null) { 23 | mStringBuilder.append(HTMLString.HeaderPrefix); 24 | int headSize = mHead.size(); 25 | for (int i = 0; i < headSize; i++) { 26 | mStringBuilder.append(HTMLString.HeaderPrefix); 27 | mStringBuilder.append(mHead.get(i)); 28 | mStringBuilder.append(HTMLString.HeaderSuffix); 29 | } 30 | mStringBuilder.append(HTMLString.HeaderSuffix); 31 | } 32 | 33 | if (mHead != null) { 34 | mStringBuilder.append(HTMLString.TablePrefix); 35 | int dataSize = mData.size(); 36 | int columnSize = mData.get(0).length; 37 | for (int i = 0; i < dataSize; i++) { 38 | mStringBuilder.append(HTMLString.RowPrefix); 39 | for (int j = 0;j(); 57 | mHead.add(head); 58 | return this; 59 | } 60 | 61 | public HtmlMapper addData(String[] data) { 62 | if (mData == null) 63 | mData = new ArrayList<>(); 64 | mData.add(data); 65 | return this; 66 | } 67 | 68 | public static class HTMLString { 69 | public static final String HTMLPrefix = ""; 70 | public static final String HTMLSuffix = ""; 71 | public static final String HeaderPrefix = "

"; 72 | public static final String HeaderSuffix = "

"; 73 | public static final String TablePrefix = ""; 74 | public static final String TableSuffix = "
"; 75 | public static final String RowPrefix= ""; 76 | public static final String RowSuffix = ""; 77 | public static final String CellPrefix= ""; 78 | public static final String CellSuffix = ""; 79 | 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /awebdb/src/main/java/com/lusfold/awebdb/mapper/Mapper.java: -------------------------------------------------------------------------------- 1 | package com.lusfold.awebdb.mapper; 2 | 3 | /** 4 | * @author Lusfold 5 | */ 6 | public interface Mapper { 7 | String map(); 8 | } 9 | -------------------------------------------------------------------------------- /awebdb/src/main/java/com/lusfold/awebdb/net/nanohttpd/HtmlResponse.java: -------------------------------------------------------------------------------- 1 | package com.lusfold.awebdb.net.nanohttpd; 2 | 3 | import com.lusfold.awebdb.mapper.Mapper; 4 | 5 | import java.io.ByteArrayInputStream; 6 | 7 | /** 8 | * @author Lusfold 9 | 10 | */ 11 | public class HtmlResponse extends NanoHTTPD.Response{ 12 | private static final String DefaultMimeType = "text/HTML"; 13 | private static final NanoHTTPD.Response.Status DefaultStatus = NanoHTTPD.Response.Status.OK; 14 | 15 | /** 16 | * @param mapper 17 | */ 18 | protected HtmlResponse(Mapper mapper) { 19 | super(DefaultStatus, DefaultMimeType, new ByteArrayInputStream(mapper.map().getBytes()), mapper.map().getBytes().length); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /awebdb/src/main/java/com/lusfold/awebdb/net/nanohttpd/NanoHTTPD.java: -------------------------------------------------------------------------------- 1 | package com.lusfold.awebdb.net.nanohttpd; 2 | 3 | /* 4 | * #%L 5 | * NanoHttpd-Core 6 | * %% 7 | * Copyright (C) 2012 - 2015 nanohttpd 8 | * %% 9 | * Redistribution and use in source and binary forms, with or without modification, 10 | * are permitted provided that the following conditions are met: 11 | * 12 | * 1. Redistributions of source code must retain the above copyright notice, this 13 | * list of conditions and the following disclaimer. 14 | * 15 | * 2. Redistributions in binary form must reproduce the above copyright notice, 16 | * this list of conditions and the following disclaimer in the documentation 17 | * and/or other materials provided with the distribution. 18 | * 19 | * 3. Neither the name of the nanohttpd nor the names of its contributors 20 | * may be used to endorse or promote products derived from this software without 21 | * specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 26 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 27 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 30 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 31 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 32 | * OF THE POSSIBILITY OF SUCH DAMAGE. 33 | * #L% 34 | */ 35 | 36 | import java.io.*; 37 | import java.net.InetAddress; 38 | import java.net.InetSocketAddress; 39 | import java.net.ServerSocket; 40 | import java.net.Socket; 41 | import java.net.SocketException; 42 | import java.net.SocketTimeoutException; 43 | import java.net.URLDecoder; 44 | import java.nio.ByteBuffer; 45 | import java.nio.channels.FileChannel; 46 | import java.nio.charset.Charset; 47 | import java.security.KeyStore; 48 | import java.text.SimpleDateFormat; 49 | import java.util.ArrayList; 50 | import java.util.Calendar; 51 | import java.util.Collections; 52 | import java.util.Date; 53 | import java.util.HashMap; 54 | import java.util.Iterator; 55 | import java.util.List; 56 | import java.util.Locale; 57 | import java.util.Map; 58 | import java.util.StringTokenizer; 59 | import java.util.TimeZone; 60 | import java.util.logging.Level; 61 | import java.util.logging.Logger; 62 | import java.util.regex.Matcher; 63 | import java.util.regex.Pattern; 64 | import java.util.zip.GZIPOutputStream; 65 | 66 | import javax.net.ssl.KeyManager; 67 | import javax.net.ssl.KeyManagerFactory; 68 | import javax.net.ssl.SSLContext; 69 | import javax.net.ssl.SSLServerSocket; 70 | import javax.net.ssl.SSLServerSocketFactory; 71 | import javax.net.ssl.TrustManagerFactory; 72 | 73 | 74 | /** 75 | * A simple, tiny, nicely embeddable HTTP server in Java 76 | * NanoHTTPD 77 | * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 78 | * 2010 by Konstantinos Togias 79 | * Features + limitations: 80 | *
    81 | 82 | *
  • Only one Java file
  • 83 | *
  • Java 5 compatible
  • 84 | *
  • Released as open source, Modified BSD licence
  • 85 | *
  • No fixed config files, logging, authorization etc. (Implement yourself if 86 | * you need them.)
  • 87 | *
  • Supports parameter parsing of GET and POST methods (+ rudimentary PUT 88 | * support in 1.25)
  • 89 | *
  • Supports both dynamic content and file serving
  • 90 | *
  • Supports file upload (since version 1.2, 2010)
  • 91 | *
  • Supports partial content (streaming)
  • 92 | *
  • Supports ETags
  • 93 | *
  • Never caches anything
  • 94 | *
  • Doesn't limit bandwidth, request time or simultaneous connections
  • 95 | *
  • Default code serves files and shows all HTTP parameters and headers
  • 96 | *
  • File server supports directory listing, index.html and index.htm
  • 97 | *
  • File server supports partial content (streaming)
  • 98 | *
  • File server supports ETags
  • 99 | *
  • File server does the 301 redirection trick for directories without '/'
  • 100 | *
  • File server supports simple skipping for files (continue download)
  • 101 | *
  • File server serves also very long files without memory overhead
  • 102 | *
  • Contains a built-in list of most common MIME types
  • 103 | *
  • All header names are converted to lower case so they don't vary between 104 | * browsers/clients
  • 105 | 106 | *
107 | 108 | 109 | * How to use: 110 | *
    111 | 112 | *
  • Subclass and implement serve() and embed to your own program
  • 113 | *
114 | * See the separate "LICENSE.md" file for the distribution license (Modified BSD 115 | * licence) 116 | */ 117 | public abstract class NanoHTTPD { 118 | 119 | /** 120 | * Pluggable strategy for asynchronously executing requests. 121 | */ 122 | public interface AsyncRunner { 123 | 124 | void closeAll(); 125 | 126 | void closed(ClientHandler clientHandler); 127 | 128 | void exec(ClientHandler code); 129 | } 130 | 131 | /** 132 | * The runnable that will be used for every new client connection. 133 | */ 134 | public class ClientHandler implements Runnable { 135 | 136 | private final InputStream inputStream; 137 | 138 | private final Socket acceptSocket; 139 | 140 | private ClientHandler(InputStream inputStream, Socket acceptSocket) { 141 | this.inputStream = inputStream; 142 | this.acceptSocket = acceptSocket; 143 | } 144 | 145 | public void close() { 146 | safeClose(this.inputStream); 147 | safeClose(this.acceptSocket); 148 | } 149 | 150 | @Override 151 | public void run() { 152 | OutputStream outputStream = null; 153 | try { 154 | outputStream = this.acceptSocket.getOutputStream(); 155 | TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create(); 156 | HTTPSession session = new HTTPSession(tempFileManager, this.inputStream, outputStream, this.acceptSocket.getInetAddress()); 157 | while (!this.acceptSocket.isClosed()) { 158 | session.execute(); 159 | } 160 | } catch (Exception e) { 161 | // When the socket is closed by the client, 162 | // we throw our own SocketException 163 | // to break the "keep alive" loop above. If 164 | // the exception was anything other 165 | // than the expected SocketException OR a 166 | // SocketTimeoutException, print the 167 | // stacktrace 168 | if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) { 169 | NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e); 170 | } 171 | } finally { 172 | safeClose(outputStream); 173 | safeClose(this.inputStream); 174 | safeClose(this.acceptSocket); 175 | NanoHTTPD.this.asyncRunner.closed(this); 176 | } 177 | } 178 | } 179 | 180 | public static class Cookie { 181 | 182 | public static String getHTTPTime(int days) { 183 | Calendar calendar = Calendar.getInstance(); 184 | SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); 185 | dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); 186 | calendar.add(Calendar.DAY_OF_MONTH, days); 187 | return dateFormat.format(calendar.getTime()); 188 | } 189 | 190 | private final String n, v, e; 191 | 192 | public Cookie(String name, String value) { 193 | this(name, value, 30); 194 | } 195 | 196 | public Cookie(String name, String value, int numDays) { 197 | this.n = name; 198 | this.v = value; 199 | this.e = getHTTPTime(numDays); 200 | } 201 | 202 | public Cookie(String name, String value, String expires) { 203 | this.n = name; 204 | this.v = value; 205 | this.e = expires; 206 | } 207 | 208 | public String getHTTPHeader() { 209 | String fmt = "%s=%s; expires=%s"; 210 | return String.format(fmt, this.n, this.v, this.e); 211 | } 212 | } 213 | 214 | /** 215 | * Provides rudimentary support for cookies. Doesn't support 'path', 216 | * 'secure' nor 'httpOnly'. Feel free to improve it and/or add unsupported 217 | * features. 218 | * 219 | * @author LordFokas 220 | */ 221 | public class CookieHandler implements Iterable { 222 | 223 | private final HashMap cookies = new HashMap(); 224 | 225 | private final ArrayList queue = new ArrayList(); 226 | 227 | public CookieHandler(Map httpHeaders) { 228 | String raw = httpHeaders.get("cookie"); 229 | if (raw != null) { 230 | String[] tokens = raw.split(";"); 231 | for (String token : tokens) { 232 | String[] data = token.trim().split("="); 233 | if (data.length == 2) { 234 | this.cookies.put(data[0], data[1]); 235 | } 236 | } 237 | } 238 | } 239 | 240 | /** 241 | * Set a cookie with an expiration date from a month ago, effectively 242 | * deleting it on the client side. 243 | * 244 | * @param name 245 | * The cookie name. 246 | */ 247 | public void delete(String name) { 248 | set(name, "-delete-", -30); 249 | } 250 | 251 | @Override 252 | public Iterator iterator() { 253 | return this.cookies.keySet().iterator(); 254 | } 255 | 256 | /** 257 | * Read a cookie from the HTTP Headers. 258 | * 259 | * @param name 260 | * The cookie's name. 261 | * @return The cookie's value if it exists, null otherwise. 262 | */ 263 | public String read(String name) { 264 | return this.cookies.get(name); 265 | } 266 | 267 | public void set(Cookie cookie) { 268 | this.queue.add(cookie); 269 | } 270 | 271 | /** 272 | * Sets a cookie. 273 | * 274 | * @param name 275 | * The cookie's name. 276 | * @param value 277 | * The cookie's value. 278 | * @param expires 279 | * How many days until the cookie expires. 280 | */ 281 | public void set(String name, String value, int expires) { 282 | this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); 283 | } 284 | 285 | /** 286 | * Internally used by the webserver to add all queued cookies into the 287 | * Response's HTTP Headers. 288 | * 289 | * @param response 290 | * The Response object to which headers the queued cookies 291 | * will be added. 292 | */ 293 | public void unloadQueue(Response response) { 294 | for (Cookie cookie : this.queue) { 295 | response.addHeader("Set-Cookie", cookie.getHTTPHeader()); 296 | } 297 | } 298 | } 299 | 300 | /** 301 | * Default threading strategy for NanoHTTPD. 302 | 303 | *

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

308 | */ 309 | public static class DefaultAsyncRunner implements AsyncRunner { 310 | 311 | private long requestCount; 312 | 313 | private final List running = Collections.synchronizedList(new ArrayList()); 314 | 315 | /** 316 | * @return a list with currently running clients. 317 | */ 318 | public List getRunning() { 319 | return running; 320 | } 321 | 322 | @Override 323 | public void closeAll() { 324 | // copy of the list for concurrency 325 | for (ClientHandler clientHandler : new ArrayList(this.running)) { 326 | clientHandler.close(); 327 | } 328 | } 329 | 330 | @Override 331 | public void closed(ClientHandler clientHandler) { 332 | this.running.remove(clientHandler); 333 | } 334 | 335 | @Override 336 | public void exec(ClientHandler clientHandler) { 337 | ++this.requestCount; 338 | Thread t = new Thread(clientHandler); 339 | t.setDaemon(true); 340 | t.setName("NanoHttpd Request Processor (#" + this.requestCount + ")"); 341 | this.running.add(clientHandler); 342 | t.start(); 343 | } 344 | } 345 | 346 | /** 347 | * Default strategy for creating and cleaning up temporary files. 348 | 349 | *

350 | * By default, files are created by File.createTempFile() in 351 | * the directory specified. 352 | *

353 | */ 354 | public static class DefaultTempFile implements TempFile { 355 | 356 | private final File file; 357 | 358 | private final OutputStream fstream; 359 | 360 | public DefaultTempFile(String tempdir) throws IOException { 361 | this.file = File.createTempFile("NanoHTTPD-", "", new File(tempdir)); 362 | this.fstream = new FileOutputStream(this.file); 363 | } 364 | 365 | @Override 366 | public void delete() throws Exception { 367 | safeClose(this.fstream); 368 | if (!this.file.delete()) { 369 | throw new Exception("could not delete temporary file"); 370 | } 371 | } 372 | 373 | @Override 374 | public String getName() { 375 | return this.file.getAbsolutePath(); 376 | } 377 | 378 | @Override 379 | public OutputStream open() throws Exception { 380 | return this.fstream; 381 | } 382 | } 383 | 384 | /** 385 | * Default strategy for creating and cleaning up temporary files. 386 | 387 | *

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

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

1462 | * Temp files are responsible for managing the actual temporary storage and 1463 | * cleaning themselves up when no longer needed. 1464 | *

1465 | */ 1466 | public interface TempFile { 1467 | 1468 | void delete() throws Exception; 1469 | 1470 | String getName(); 1471 | 1472 | OutputStream open() throws Exception; 1473 | } 1474 | 1475 | /** 1476 | * Temp file manager. 1477 | 1478 | *

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

1482 | */ 1483 | public interface TempFileManager { 1484 | 1485 | void clear(); 1486 | 1487 | TempFile createTempFile() throws Exception; 1488 | } 1489 | 1490 | /** 1491 | * Factory to create temp file managers. 1492 | */ 1493 | public interface TempFileManagerFactory { 1494 | 1495 | TempFileManager create(); 1496 | } 1497 | 1498 | /** 1499 | * Maximum time to wait on Socket.getInputStream().read() (in milliseconds) 1500 | * This is required as the Keep-Alive HTTP connections would otherwise block 1501 | * the socket reading thread forever (or as long the browser is open). 1502 | */ 1503 | public static final int SOCKET_READ_TIMEOUT = 5000; 1504 | 1505 | /** 1506 | * Common MIME type for dynamic content: plain text 1507 | */ 1508 | public static final String MIME_PLAINTEXT = "text/plain"; 1509 | 1510 | /** 1511 | * Common MIME type for dynamic content: html 1512 | */ 1513 | public static final String MIME_HTML = "text/html"; 1514 | 1515 | /** 1516 | * Pseudo-Parameter to use to store the actual query string in the 1517 | * parameters map for later re-processing. 1518 | */ 1519 | private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; 1520 | 1521 | /** 1522 | * logger to log to. 1523 | */ 1524 | private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); 1525 | 1526 | /** 1527 | * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an 1528 | * array of loaded KeyManagers. These objects must properly 1529 | * loaded/initialized by the caller. 1530 | */ 1531 | public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException { 1532 | SSLServerSocketFactory res = null; 1533 | try { 1534 | TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1535 | trustManagerFactory.init(loadedKeyStore); 1536 | SSLContext ctx = SSLContext.getInstance("TLS"); 1537 | ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null); 1538 | res = ctx.getServerSocketFactory(); 1539 | } catch (Exception e) { 1540 | throw new IOException(e.getMessage()); 1541 | } 1542 | return res; 1543 | } 1544 | 1545 | /** 1546 | * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a 1547 | * loaded KeyManagerFactory. These objects must properly loaded/initialized 1548 | * by the caller. 1549 | */ 1550 | public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException { 1551 | SSLServerSocketFactory res = null; 1552 | try { 1553 | TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1554 | trustManagerFactory.init(loadedKeyStore); 1555 | SSLContext ctx = SSLContext.getInstance("TLS"); 1556 | ctx.init(loadedKeyFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); 1557 | res = ctx.getServerSocketFactory(); 1558 | } catch (Exception e) { 1559 | throw new IOException(e.getMessage()); 1560 | } 1561 | return res; 1562 | } 1563 | 1564 | /** 1565 | * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your 1566 | * certificate and passphrase 1567 | */ 1568 | public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException { 1569 | SSLServerSocketFactory res = null; 1570 | try { 1571 | KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); 1572 | InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); 1573 | keystore.load(keystoreStream, passphrase); 1574 | TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 1575 | trustManagerFactory.init(keystore); 1576 | KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 1577 | keyManagerFactory.init(keystore, passphrase); 1578 | SSLContext ctx = SSLContext.getInstance("TLS"); 1579 | ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); 1580 | res = ctx.getServerSocketFactory(); 1581 | } catch (Exception e) { 1582 | throw new IOException(e.getMessage()); 1583 | } 1584 | return res; 1585 | } 1586 | 1587 | private static final void safeClose(Object closeable) { 1588 | try { 1589 | if (closeable != null) { 1590 | if (closeable instanceof Closeable) { 1591 | ((Closeable) closeable).close(); 1592 | } else if (closeable instanceof Socket) { 1593 | ((Socket) closeable).close(); 1594 | } else if (closeable instanceof ServerSocket) { 1595 | ((ServerSocket) closeable).close(); 1596 | } else { 1597 | throw new IllegalArgumentException("Unknown object to close"); 1598 | } 1599 | } 1600 | } catch (IOException e) { 1601 | NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e); 1602 | } 1603 | } 1604 | 1605 | private final String hostname; 1606 | 1607 | private final int myPort; 1608 | 1609 | private ServerSocket myServerSocket; 1610 | 1611 | private SSLServerSocketFactory sslServerSocketFactory; 1612 | 1613 | private Thread myThread; 1614 | 1615 | /** 1616 | * Pluggable strategy for asynchronously executing requests. 1617 | */ 1618 | protected AsyncRunner asyncRunner; 1619 | 1620 | /** 1621 | * Pluggable strategy for creating and cleaning up temporary files. 1622 | */ 1623 | private TempFileManagerFactory tempFileManagerFactory; 1624 | 1625 | /** 1626 | * Constructs an HTTP server on given port. 1627 | */ 1628 | public NanoHTTPD(int port) { 1629 | this(null, port); 1630 | } 1631 | 1632 | // ------------------------------------------------------------------------------- 1633 | // // 1634 | // 1635 | // Threading Strategy. 1636 | // 1637 | // ------------------------------------------------------------------------------- 1638 | // // 1639 | 1640 | /** 1641 | * Constructs an HTTP server on given hostname and port. 1642 | */ 1643 | public NanoHTTPD(String hostname, int port) { 1644 | this.hostname = hostname; 1645 | this.myPort = port; 1646 | setTempFileManagerFactory(new DefaultTempFileManagerFactory()); 1647 | setAsyncRunner(new DefaultAsyncRunner()); 1648 | } 1649 | 1650 | /** 1651 | * Forcibly closes all connections that are open. 1652 | */ 1653 | public synchronized void closeAllConnections() { 1654 | stop(); 1655 | } 1656 | 1657 | /** 1658 | * create a instance of the client handler, subclasses can return a subclass 1659 | * of the ClientHandler. 1660 | * 1661 | * @param finalAccept 1662 | * the socket the cleint is connected to 1663 | * @param inputStream 1664 | * the input stream 1665 | * @return the client handler 1666 | */ 1667 | protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) { 1668 | return new ClientHandler(inputStream, finalAccept); 1669 | } 1670 | 1671 | /** 1672 | * Instantiate the server runnable, can be overwritten by subclasses to 1673 | * provide a subclass of the ServerRunnable. 1674 | * 1675 | * @param timeout 1676 | * the socet timeout to use. 1677 | * @return the server runnable. 1678 | */ 1679 | protected ServerRunnable createServerRunnable(final int timeout) { 1680 | return new ServerRunnable(timeout); 1681 | } 1682 | 1683 | /** 1684 | * Decode parameters from a URL, handing the case where a single parameter 1685 | * name might have been supplied several times, by return lists of values. 1686 | * In general these lists will contain a single element. 1687 | * 1688 | * @param parms 1689 | * original NanoHTTPD parameters values, as passed to the 1690 | * serve() method. 1691 | * @return a map of String (parameter name) to 1692 | * List<String> (a list of the values supplied). 1693 | */ 1694 | protected Map> decodeParameters(Map parms) { 1695 | return this.decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER)); 1696 | } 1697 | 1698 | // ------------------------------------------------------------------------------- 1699 | // // 1700 | 1701 | /** 1702 | * Decode parameters from a URL, handing the case where a single parameter 1703 | * name might have been supplied several times, by return lists of values. 1704 | * In general these lists will contain a single element. 1705 | * 1706 | * @param queryString 1707 | * a query string pulled from the URL. 1708 | * @return a map of String (parameter name) to 1709 | * List<String> (a list of the values supplied). 1710 | */ 1711 | protected Map> decodeParameters(String queryString) { 1712 | Map> parms = new HashMap>(); 1713 | if (queryString != null) { 1714 | StringTokenizer st = new StringTokenizer(queryString, "&"); 1715 | while (st.hasMoreTokens()) { 1716 | String e = st.nextToken(); 1717 | int sep = e.indexOf('='); 1718 | String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); 1719 | if (!parms.containsKey(propertyName)) { 1720 | parms.put(propertyName, new ArrayList()); 1721 | } 1722 | String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null; 1723 | if (propertyValue != null) { 1724 | parms.get(propertyName).add(propertyValue); 1725 | } 1726 | } 1727 | } 1728 | return parms; 1729 | } 1730 | 1731 | /** 1732 | * Decode percent encoded String values. 1733 | * 1734 | * @param str 1735 | * the percent encoded String 1736 | * @return expanded form of the input, for example "foo%20bar" becomes 1737 | * "foo bar" 1738 | */ 1739 | protected String decodePercent(String str) { 1740 | String decoded = null; 1741 | try { 1742 | decoded = URLDecoder.decode(str, "UTF8"); 1743 | } catch (UnsupportedEncodingException ignored) { 1744 | NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored); 1745 | } 1746 | return decoded; 1747 | } 1748 | 1749 | /** 1750 | * @return true if the gzip compression should be used if the client 1751 | * accespts it. Default this option is on for text content and off 1752 | * for everything else. 1753 | */ 1754 | protected boolean useGzipWhenAccepted(Response r) { 1755 | return r.getMimeType() != null && r.getMimeType().toLowerCase().contains("text/"); 1756 | } 1757 | 1758 | public final int getListeningPort() { 1759 | return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort(); 1760 | } 1761 | 1762 | public final boolean isAlive() { 1763 | return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive(); 1764 | } 1765 | 1766 | /** 1767 | * Call before start() to serve over HTTPS instead of HTTP 1768 | */ 1769 | public void makeSecure(SSLServerSocketFactory sslServerSocketFactory) { 1770 | this.sslServerSocketFactory = sslServerSocketFactory; 1771 | } 1772 | 1773 | /** 1774 | * Create a response with unknown length (using HTTP 1.1 chunking). 1775 | */ 1776 | public Response newChunkedResponse(Response.IStatus status, String mimeType, InputStream data) { 1777 | return new Response(status, mimeType, data, -1); 1778 | } 1779 | 1780 | /** 1781 | * Create a response with known length. 1782 | */ 1783 | public Response newFixedLengthResponse(Response.IStatus status, String mimeType, InputStream data, long totalBytes) { 1784 | return new Response(status, mimeType, data, totalBytes); 1785 | } 1786 | 1787 | /** 1788 | * Create a text response with known length. 1789 | */ 1790 | public Response newFixedLengthResponse(Response.IStatus status, String mimeType, String txt) { 1791 | if (txt == null) { 1792 | return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0); 1793 | } else { 1794 | byte[] bytes; 1795 | try { 1796 | bytes = txt.getBytes("UTF-8"); 1797 | } catch (UnsupportedEncodingException e) { 1798 | NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e); 1799 | bytes = new byte[0]; 1800 | } 1801 | return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(bytes), bytes.length); 1802 | } 1803 | } 1804 | 1805 | /** 1806 | * Create a text response with known length. 1807 | */ 1808 | public Response newFixedLengthResponse(String msg) { 1809 | return newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_HTML, msg); 1810 | } 1811 | 1812 | /** 1813 | * Override this to customize the server. 1814 | * (By default, this returns a 404 "Not Found" plain text error response.) 1815 | * 1816 | * @param session 1817 | * The HTTP session 1818 | * @return HTTP response, see class Response for details 1819 | */ 1820 | public Response serve(IHTTPSession session) { 1821 | Map files = new HashMap(); 1822 | Method method = session.getMethod(); 1823 | if (Method.PUT.equals(method) || Method.POST.equals(method)) { 1824 | try { 1825 | session.parseBody(files); 1826 | } catch (IOException ioe) { 1827 | return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); 1828 | } catch (ResponseException re) { 1829 | return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); 1830 | } 1831 | } 1832 | 1833 | Map parms = session.getParms(); 1834 | parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString()); 1835 | return serve(session.getUri(), method, session.getHeaders(), parms, files); 1836 | } 1837 | 1838 | /** 1839 | * Override this to customize the server. 1840 | 1841 | 1842 | * (By default, this returns a 404 "Not Found" plain text error response.) 1843 | * 1844 | * @param uri 1845 | * Percent-decoded URI without parameters, for example 1846 | * "/index.cgi" 1847 | * @param method 1848 | * "GET", "POST" etc. 1849 | * @param parms 1850 | * Parsed, percent decoded parameters from URI and, in case of 1851 | * POST, data. 1852 | * @param headers 1853 | * Header entries, percent decoded 1854 | * @return HTTP response, see class Response for details 1855 | */ 1856 | @Deprecated 1857 | public Response serve(String uri, Method method, Map headers, Map parms, Map files) { 1858 | return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found"); 1859 | } 1860 | 1861 | /** 1862 | * Pluggable strategy for asynchronously executing requests. 1863 | * 1864 | * @param asyncRunner 1865 | * new strategy for handling threads. 1866 | */ 1867 | public void setAsyncRunner(AsyncRunner asyncRunner) { 1868 | this.asyncRunner = asyncRunner; 1869 | } 1870 | 1871 | /** 1872 | * Pluggable strategy for creating and cleaning up temporary files. 1873 | * 1874 | * @param tempFileManagerFactory 1875 | * new strategy for handling temp files. 1876 | */ 1877 | public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { 1878 | this.tempFileManagerFactory = tempFileManagerFactory; 1879 | } 1880 | 1881 | /** 1882 | * Start the server. 1883 | * 1884 | * @throws IOException 1885 | * if the socket is in use. 1886 | */ 1887 | public void start() throws IOException { 1888 | start(NanoHTTPD.SOCKET_READ_TIMEOUT); 1889 | } 1890 | 1891 | /** 1892 | * Start the server. 1893 | * 1894 | * @param timeout 1895 | * timeout to use for socket connections. 1896 | * @throws IOException 1897 | * if the socket is in use. 1898 | */ 1899 | public void start(final int timeout) throws IOException { 1900 | if (this.sslServerSocketFactory != null) { 1901 | SSLServerSocket ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket(); 1902 | ss.setNeedClientAuth(false); 1903 | this.myServerSocket = ss; 1904 | } else { 1905 | this.myServerSocket = new ServerSocket(); 1906 | } 1907 | this.myServerSocket.setReuseAddress(true); 1908 | 1909 | ServerRunnable serverRunnable = createServerRunnable(timeout); 1910 | this.myThread = new Thread(serverRunnable); 1911 | this.myThread.setDaemon(true); 1912 | this.myThread.setName("NanoHttpd Main Listener"); 1913 | this.myThread.start(); 1914 | while (!serverRunnable.hasBinded && serverRunnable.bindException == null) { 1915 | try { 1916 | Thread.sleep(10L); 1917 | } catch (Throwable e) { 1918 | // on android this may not be allowed, that's why we 1919 | // catch throwable the wait should be very short because we are 1920 | // just waiting for the bind of the socket 1921 | } 1922 | } 1923 | if (serverRunnable.bindException != null) { 1924 | throw serverRunnable.bindException; 1925 | } 1926 | } 1927 | 1928 | /** 1929 | * Stop the server. 1930 | */ 1931 | public void stop() { 1932 | try { 1933 | safeClose(this.myServerSocket); 1934 | this.asyncRunner.closeAll(); 1935 | if (this.myThread != null) { 1936 | this.myThread.join(); 1937 | } 1938 | } catch (Exception e) { 1939 | NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e); 1940 | } 1941 | } 1942 | 1943 | public final boolean wasStarted() { 1944 | return this.myServerSocket != null && this.myThread != null; 1945 | } 1946 | } -------------------------------------------------------------------------------- /awebdb/src/main/java/com/lusfold/awebdb/net/nanohttpd/XNanoHTTPD.java: -------------------------------------------------------------------------------- 1 | package com.lusfold.awebdb.net.nanohttpd; 2 | 3 | import com.lusfold.awebdb.utils.LogUtil; 4 | import com.lusfold.awebdb.db.DBUtil; 5 | import com.lusfold.awebdb.mapper.HtmlMapper; 6 | 7 | import java.net.URLDecoder; 8 | 9 | /** 10 | * @author Lusfold 11 | */ 12 | public class XNanoHTTPD extends NanoHTTPD { 13 | 14 | /** 15 | * @param port 16 | */ 17 | public XNanoHTTPD(int port) { 18 | super(port); 19 | } 20 | 21 | /** 22 | * @param hostname 23 | * @param port 24 | */ 25 | public XNanoHTTPD(String hostname, int port) { 26 | super(hostname, port); 27 | } 28 | 29 | /** 30 | * @param session The HTTP session 31 | * @return 32 | */ 33 | public Response serve(IHTTPSession session) { 34 | Method method = session.getMethod(); 35 | LogUtil.d(method.toString()); 36 | if (NanoHTTPD.Method.GET.equals(method)) { 37 | //get method 38 | String queryParams = session.getQueryParameterString(); 39 | queryParams = URLDecoder.decode(queryParams); 40 | HtmlMapper htmlMapper = new HtmlMapper(); 41 | DBUtil.inflateMapper(queryParams, htmlMapper); 42 | LogUtil.d(htmlMapper.map()); 43 | return new HtmlResponse(htmlMapper); 44 | } else if (NanoHTTPD.Method.POST.equals(method)) { 45 | //post method 46 | HtmlMapper htmlMapper = new HtmlMapper(); 47 | htmlMapper.addHeadInFo("Don't support post method"); 48 | LogUtil.d(htmlMapper.map()); 49 | return new HtmlResponse(htmlMapper); 50 | } 51 | return super.serve(session); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /awebdb/src/main/java/com/lusfold/awebdb/utils/LogUtil.java: -------------------------------------------------------------------------------- 1 | package com.lusfold.awebdb.utils; 2 | 3 | import android.util.Log; 4 | 5 | import java.text.SimpleDateFormat; 6 | import java.util.Date; 7 | 8 | public class LogUtil { 9 | public static boolean DEBUG = true; 10 | 11 | public static void d(String TAG, String method, String msg) { 12 | Log.d(TAG, "[" + method + "]" + msg); 13 | } 14 | 15 | public static void d(String TAG, String msg){ 16 | if (DEBUG) { 17 | Log.d(TAG, "[" + getFileLineMethod() + "]" + msg); 18 | } 19 | } 20 | 21 | public static void d(String msg){ 22 | if (DEBUG) { 23 | Log.d(_FILE_(), "[" + getLineMethod() + "]" + msg); 24 | } 25 | } 26 | 27 | public static void e(String msg){ 28 | if (DEBUG) { 29 | Log.e(_FILE_(), getLineMethod() + msg); 30 | } 31 | } 32 | 33 | public static void e(String TAG, String msg){ 34 | if (DEBUG) { 35 | Log.e(TAG, getLineMethod() + msg); 36 | } 37 | } 38 | 39 | public static String getFileLineMethod() { 40 | StackTraceElement traceElement = ((new Exception()).getStackTrace())[2]; 41 | StringBuffer toStringBuffer = new StringBuffer("[") 42 | .append(traceElement.getFileName()).append(" | ") 43 | .append(traceElement.getLineNumber()).append(" | ") 44 | .append(traceElement.getMethodName()).append("]"); 45 | return toStringBuffer.toString(); 46 | } 47 | 48 | public static String getLineMethod() { 49 | StackTraceElement traceElement = ((new Exception()).getStackTrace())[2]; 50 | StringBuffer toStringBuffer = new StringBuffer("[") 51 | .append(traceElement.getLineNumber()).append(" | ") 52 | .append(traceElement.getMethodName()).append("]"); 53 | return toStringBuffer.toString(); 54 | } 55 | 56 | public static String _FILE_() { 57 | StackTraceElement traceElement = ((new Exception()).getStackTrace())[2]; 58 | return traceElement.getFileName(); 59 | } 60 | 61 | public static String _FUNC_() { 62 | StackTraceElement traceElement = ((new Exception()).getStackTrace())[1]; 63 | return traceElement.getMethodName(); 64 | } 65 | 66 | public static int _LINE_() { 67 | StackTraceElement traceElement = ((new Exception()).getStackTrace())[1]; 68 | return traceElement.getLineNumber(); 69 | } 70 | 71 | public static String _TIME_() { 72 | Date now = new Date(); 73 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 74 | return sdf.format(now); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /awebdb/src/main/java/com/lusfold/awebdb/utils/NetUtils.java: -------------------------------------------------------------------------------- 1 | package com.lusfold.awebdb.utils; 2 | 3 | import org.apache.http.conn.util.InetAddressUtils; 4 | 5 | import java.net.InetAddress; 6 | import java.net.NetworkInterface; 7 | import java.net.SocketException; 8 | import java.util.Enumeration; 9 | 10 | /** 11 | * @author Lusfold 12 | 13 | */ 14 | public class NetUtils { 15 | public static final int DefaultPort = 8080; 16 | 17 | /** 18 | * @param port 19 | * @return a legally port. 20 | */ 21 | public static int getAGoodPort(int port) { 22 | return port < 1023 || port > 65535 ? DefaultPort : port; 23 | } 24 | 25 | /** 26 | * @return local ip address 27 | */ 28 | public static String getLocalIpAddress() { 29 | try { 30 | Enumeration infos = NetworkInterface 31 | .getNetworkInterfaces(); 32 | while (infos.hasMoreElements()) { 33 | NetworkInterface niFace = infos.nextElement(); 34 | Enumeration enumIpAddr = niFace.getInetAddresses(); 35 | while (enumIpAddr.hasMoreElements()) { 36 | InetAddress mInetAddress = enumIpAddr.nextElement(); 37 | if (!mInetAddress.isLoopbackAddress() 38 | && InetAddressUtils.isIPv4Address(mInetAddress 39 | .getHostAddress())) { 40 | return mInetAddress.getHostAddress().toString(); 41 | } 42 | } 43 | } 44 | } catch (SocketException e) { 45 | 46 | } 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /awebdb/src/main/java/com/lusfold/awebdb/utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | package com.lusfold.awebdb.utils; 2 | 3 | /** 4 | * @author Lusfold 5 | */ 6 | public class StringUtils { 7 | /** 8 | * @param str 9 | * @return if string is null or it's length equals 0 except space,return true,else return false. 10 | */ 11 | public static boolean isBlank(String str) { 12 | return (str == null || str.trim().length() == 0); 13 | } 14 | 15 | /** 16 | * @param str 17 | * @return if string is not null and it's length do not equals 0 except space,return true,else return false. 18 | */ 19 | public static boolean isNotBlank(String str) { 20 | return !(str == null || str.trim().length() == 0); 21 | } 22 | 23 | /** 24 | * @param str 25 | * @return if string is null or it's length equals 0,return true,else return false. 26 | */ 27 | public static boolean isEmpty(CharSequence str) { 28 | return (str == null || str.length() == 0); 29 | } 30 | 31 | /** 32 | * @param str 33 | * @return if string is not null and it's length do not equals 0,return true,else return false. 34 | */ 35 | public static boolean isNotEmpty(CharSequence str) { 36 | return !(str == null || str.length() == 0); 37 | } 38 | 39 | /** 40 | * @param str 41 | * @return if sql string is not empty and starts with select. 42 | */ 43 | public static boolean isSelectStr(String str) { 44 | return (isNotEmpty(str) && str.toUpperCase().startsWith("SELECT")); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /awebdb/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Method_Unsupported 3 | Method_Unsupported 4 | 5 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:1.2.3' 8 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' 9 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.0' 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | allprojects { 15 | repositories { 16 | jcenter() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /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/lusfold/AWebDB-Sample/1d82512bcd9280cee5e1383e6e3189d7fd2bd396/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jul 13 22:31:23 CST 2015 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.4-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', ':awebdb' 2 | --------------------------------------------------------------------------------