From 994c5751ca05cfec2740728bd07d67e67ec9efff Mon Sep 17 00:00:00 2001 From: Prajwal Goudar Date: Wed, 6 Mar 2019 02:53:09 +0530 Subject: [PATCH] added code to generate font fun captcha(type_1) added code to generate gif format captcha(type_2) added code to generate shadow text captcha(type_3) minor changes in Main.scala file to fix errors --- .idea/compiler.xml | 6 + .idea/hydra.xml | 9 + .../sbt__ar_com_hjg_pngj_2_1_0_jar.xml | 11 + .../sbt__com_adobe_xmp_xmpcore_5_1_2_jar.xml | 11 + ...rewnoakes_metadata_extractor_2_8_1_jar.xml | 11 + ...son_core_jackson_annotations_2_9_0_jar.xml | 11 + ...ml_jackson_core_jackson_core_2_9_6_jar.xml | 11 + ...ackson_core_jackson_databind_2_9_6_jar.xml | 11 + ..._scrimage_scrimage_core_2_12_2_1_8_jar.xml | 11 + ...rimage_scrimage_filters_2_12_2_1_8_jar.xml | 11 + ...image_scrimage_io_extra_2_12_2_1_8_jar.xml | 11 + ...oughtworks_paranamer_paranamer_2_8_jar.xml | 11 + ...emonkeys_common_common_image_3_2_1_jar.xml | 11 + ...elvemonkeys_common_common_io_3_2_1_jar.xml | 11 + ...vemonkeys_common_common_lang_3_2_1_jar.xml | 11 + ...emonkeys_imageio_imageio_bmp_3_2_1_jar.xml | 11 + ...monkeys_imageio_imageio_core_3_2_1_jar.xml | 11 + ...monkeys_imageio_imageio_icns_3_2_1_jar.xml | 11 + ...emonkeys_imageio_imageio_iff_3_2_1_jar.xml | 11 + ...monkeys_imageio_imageio_jpeg_3_2_1_jar.xml | 11 + ...eys_imageio_imageio_metadata_3_2_1_jar.xml | 11 + ...emonkeys_imageio_imageio_pcx_3_2_1_jar.xml | 11 + ...emonkeys_imageio_imageio_pdf_3_2_1_jar.xml | 9 + ...monkeys_imageio_imageio_pict_3_2_1_jar.xml | 11 + ...emonkeys_imageio_imageio_pnm_3_2_1_jar.xml | 11 + ...emonkeys_imageio_imageio_psd_3_2_1_jar.xml | 11 + ...emonkeys_imageio_imageio_sgi_3_2_1_jar.xml | 11 + ...emonkeys_imageio_imageio_tga_3_2_1_jar.xml | 11 + ...eys_imageio_imageio_thumbsdb_3_2_1_jar.xml | 11 + ...monkeys_imageio_imageio_tiff_3_2_1_jar.xml | 11 + .../sbt__commons_io_commons_io_2_4_jar.xml | 11 + ...__org_json4s_json4s_ast_2_12_3_6_1_jar.xml | 11 + ..._org_json4s_json4s_core_2_12_3_6_1_jar.xml | 11 + ...g_json4s_json4s_jackson_2_12_3_6_1_jar.xml | 11 + ...rg_json4s_json4s_scalap_2_12_3_6_1_jar.xml | 11 + ...cala_parser_combinators_2_12_1_0_4_jar.xml | 11 + ..._lang_modules_scala_xml_2_12_1_0_5_jar.xml | 11 + ...rg_scala_lang_scala_library_2_12_3_jar.xml | 20 + ...rg_scala_lang_scala_reflect_2_12_3_jar.xml | 11 + ...org_scalactic_scalactic_2_12_3_0_3_jar.xml | 11 + ...org_scalatest_scalatest_2_12_3_0_3_jar.xml | 11 + .../sbt__org_slf4j_slf4j_api_1_7_7_jar.xml | 11 + .idea/misc.xml | 6 + .idea/modules.xml | 9 + .idea/modules/root-build.iml | 167 + .idea/modules/root.iml | 75 + .idea/sbt.xml | 20 + .idea/scala_compiler.xml | 6 + .idea/vcs.xml | 6 + captcha.mv.db | Bin 0 -> 16384 bytes src/main/java/HTTPServer.java | 2 - src/main/java/lc/CaptchaTypes.java | 117 + src/main/java/{ => lc}/Challenge.java | 0 src/main/java/{ => lc}/ChallengeProvider.java | 1 - src/main/java/lc/GifSequenceWriter.java | 151 + src/main/java/lc/MainCaptcha.java | 23 + src/main/scala/lc/HTTPServer.java | 3107 +++++++++++++++++ src/main/scala/lc/Main.scala | 2 +- 58 files changed, 4150 insertions(+), 4 deletions(-) create mode 100644 .idea/compiler.xml create mode 100644 .idea/hydra.xml create mode 100644 .idea/libraries/sbt__ar_com_hjg_pngj_2_1_0_jar.xml create mode 100644 .idea/libraries/sbt__com_adobe_xmp_xmpcore_5_1_2_jar.xml create mode 100644 .idea/libraries/sbt__com_drewnoakes_metadata_extractor_2_8_1_jar.xml create mode 100644 .idea/libraries/sbt__com_fasterxml_jackson_core_jackson_annotations_2_9_0_jar.xml create mode 100644 .idea/libraries/sbt__com_fasterxml_jackson_core_jackson_core_2_9_6_jar.xml create mode 100644 .idea/libraries/sbt__com_fasterxml_jackson_core_jackson_databind_2_9_6_jar.xml create mode 100644 .idea/libraries/sbt__com_sksamuel_scrimage_scrimage_core_2_12_2_1_8_jar.xml create mode 100644 .idea/libraries/sbt__com_sksamuel_scrimage_scrimage_filters_2_12_2_1_8_jar.xml create mode 100644 .idea/libraries/sbt__com_sksamuel_scrimage_scrimage_io_extra_2_12_2_1_8_jar.xml create mode 100644 .idea/libraries/sbt__com_thoughtworks_paranamer_paranamer_2_8_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_common_common_image_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_common_common_io_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_common_common_lang_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_bmp_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_core_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_icns_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_iff_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_jpeg_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_metadata_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pcx_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pdf_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pict_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pnm_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_psd_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_sgi_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_tga_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_thumbsdb_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_tiff_3_2_1_jar.xml create mode 100644 .idea/libraries/sbt__commons_io_commons_io_2_4_jar.xml create mode 100644 .idea/libraries/sbt__org_json4s_json4s_ast_2_12_3_6_1_jar.xml create mode 100644 .idea/libraries/sbt__org_json4s_json4s_core_2_12_3_6_1_jar.xml create mode 100644 .idea/libraries/sbt__org_json4s_json4s_jackson_2_12_3_6_1_jar.xml create mode 100644 .idea/libraries/sbt__org_json4s_json4s_scalap_2_12_3_6_1_jar.xml create mode 100644 .idea/libraries/sbt__org_scala_lang_modules_scala_parser_combinators_2_12_1_0_4_jar.xml create mode 100644 .idea/libraries/sbt__org_scala_lang_modules_scala_xml_2_12_1_0_5_jar.xml create mode 100644 .idea/libraries/sbt__org_scala_lang_scala_library_2_12_3_jar.xml create mode 100644 .idea/libraries/sbt__org_scala_lang_scala_reflect_2_12_3_jar.xml create mode 100644 .idea/libraries/sbt__org_scalactic_scalactic_2_12_3_0_3_jar.xml create mode 100644 .idea/libraries/sbt__org_scalatest_scalatest_2_12_3_0_3_jar.xml create mode 100644 .idea/libraries/sbt__org_slf4j_slf4j_api_1_7_7_jar.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/modules/root-build.iml create mode 100644 .idea/modules/root.iml create mode 100644 .idea/sbt.xml create mode 100644 .idea/scala_compiler.xml create mode 100644 .idea/vcs.xml create mode 100644 captcha.mv.db create mode 100644 src/main/java/lc/CaptchaTypes.java rename src/main/java/{ => lc}/Challenge.java (100%) rename src/main/java/{ => lc}/ChallengeProvider.java (99%) create mode 100644 src/main/java/lc/GifSequenceWriter.java create mode 100644 src/main/java/lc/MainCaptcha.java create mode 100644 src/main/scala/lc/HTTPServer.java diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..ffb022f --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/hydra.xml b/.idea/hydra.xml new file mode 100644 index 0000000..66eeb9a --- /dev/null +++ b/.idea/hydra.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__ar_com_hjg_pngj_2_1_0_jar.xml b/.idea/libraries/sbt__ar_com_hjg_pngj_2_1_0_jar.xml new file mode 100644 index 0000000..7ac283f --- /dev/null +++ b/.idea/libraries/sbt__ar_com_hjg_pngj_2_1_0_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_adobe_xmp_xmpcore_5_1_2_jar.xml b/.idea/libraries/sbt__com_adobe_xmp_xmpcore_5_1_2_jar.xml new file mode 100644 index 0000000..53c0a33 --- /dev/null +++ b/.idea/libraries/sbt__com_adobe_xmp_xmpcore_5_1_2_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_drewnoakes_metadata_extractor_2_8_1_jar.xml b/.idea/libraries/sbt__com_drewnoakes_metadata_extractor_2_8_1_jar.xml new file mode 100644 index 0000000..00c3fd9 --- /dev/null +++ b/.idea/libraries/sbt__com_drewnoakes_metadata_extractor_2_8_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_fasterxml_jackson_core_jackson_annotations_2_9_0_jar.xml b/.idea/libraries/sbt__com_fasterxml_jackson_core_jackson_annotations_2_9_0_jar.xml new file mode 100644 index 0000000..c9151cc --- /dev/null +++ b/.idea/libraries/sbt__com_fasterxml_jackson_core_jackson_annotations_2_9_0_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_fasterxml_jackson_core_jackson_core_2_9_6_jar.xml b/.idea/libraries/sbt__com_fasterxml_jackson_core_jackson_core_2_9_6_jar.xml new file mode 100644 index 0000000..85e94b4 --- /dev/null +++ b/.idea/libraries/sbt__com_fasterxml_jackson_core_jackson_core_2_9_6_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_fasterxml_jackson_core_jackson_databind_2_9_6_jar.xml b/.idea/libraries/sbt__com_fasterxml_jackson_core_jackson_databind_2_9_6_jar.xml new file mode 100644 index 0000000..d632e3f --- /dev/null +++ b/.idea/libraries/sbt__com_fasterxml_jackson_core_jackson_databind_2_9_6_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_sksamuel_scrimage_scrimage_core_2_12_2_1_8_jar.xml b/.idea/libraries/sbt__com_sksamuel_scrimage_scrimage_core_2_12_2_1_8_jar.xml new file mode 100644 index 0000000..e27657e --- /dev/null +++ b/.idea/libraries/sbt__com_sksamuel_scrimage_scrimage_core_2_12_2_1_8_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_sksamuel_scrimage_scrimage_filters_2_12_2_1_8_jar.xml b/.idea/libraries/sbt__com_sksamuel_scrimage_scrimage_filters_2_12_2_1_8_jar.xml new file mode 100644 index 0000000..31db1e2 --- /dev/null +++ b/.idea/libraries/sbt__com_sksamuel_scrimage_scrimage_filters_2_12_2_1_8_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_sksamuel_scrimage_scrimage_io_extra_2_12_2_1_8_jar.xml b/.idea/libraries/sbt__com_sksamuel_scrimage_scrimage_io_extra_2_12_2_1_8_jar.xml new file mode 100644 index 0000000..f293ab5 --- /dev/null +++ b/.idea/libraries/sbt__com_sksamuel_scrimage_scrimage_io_extra_2_12_2_1_8_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_thoughtworks_paranamer_paranamer_2_8_jar.xml b/.idea/libraries/sbt__com_thoughtworks_paranamer_paranamer_2_8_jar.xml new file mode 100644 index 0000000..39d005f --- /dev/null +++ b/.idea/libraries/sbt__com_thoughtworks_paranamer_paranamer_2_8_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_common_common_image_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_common_common_image_3_2_1_jar.xml new file mode 100644 index 0000000..1e64a7f --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_common_common_image_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_common_common_io_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_common_common_io_3_2_1_jar.xml new file mode 100644 index 0000000..f60d6de --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_common_common_io_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_common_common_lang_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_common_common_lang_3_2_1_jar.xml new file mode 100644 index 0000000..913e5cd --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_common_common_lang_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_bmp_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_bmp_3_2_1_jar.xml new file mode 100644 index 0000000..b8b5c3d --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_bmp_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_core_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_core_3_2_1_jar.xml new file mode 100644 index 0000000..cdde8e1 --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_core_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_icns_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_icns_3_2_1_jar.xml new file mode 100644 index 0000000..275d444 --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_icns_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_iff_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_iff_3_2_1_jar.xml new file mode 100644 index 0000000..1ac8158 --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_iff_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_jpeg_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_jpeg_3_2_1_jar.xml new file mode 100644 index 0000000..8ecb608 --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_jpeg_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_metadata_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_metadata_3_2_1_jar.xml new file mode 100644 index 0000000..3fdac9e --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_metadata_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pcx_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pcx_3_2_1_jar.xml new file mode 100644 index 0000000..8172bee --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pcx_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pdf_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pdf_3_2_1_jar.xml new file mode 100644 index 0000000..4b68bc7 --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pdf_3_2_1_jar.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pict_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pict_3_2_1_jar.xml new file mode 100644 index 0000000..378506d --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pict_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pnm_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pnm_3_2_1_jar.xml new file mode 100644 index 0000000..0b91eda --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_pnm_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_psd_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_psd_3_2_1_jar.xml new file mode 100644 index 0000000..1318769 --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_psd_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_sgi_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_sgi_3_2_1_jar.xml new file mode 100644 index 0000000..46e487b --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_sgi_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_tga_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_tga_3_2_1_jar.xml new file mode 100644 index 0000000..dbc0f2a --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_tga_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_thumbsdb_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_thumbsdb_3_2_1_jar.xml new file mode 100644 index 0000000..4890b68 --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_thumbsdb_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_tiff_3_2_1_jar.xml b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_tiff_3_2_1_jar.xml new file mode 100644 index 0000000..5c80305 --- /dev/null +++ b/.idea/libraries/sbt__com_twelvemonkeys_imageio_imageio_tiff_3_2_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__commons_io_commons_io_2_4_jar.xml b/.idea/libraries/sbt__commons_io_commons_io_2_4_jar.xml new file mode 100644 index 0000000..cd9ec1e --- /dev/null +++ b/.idea/libraries/sbt__commons_io_commons_io_2_4_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__org_json4s_json4s_ast_2_12_3_6_1_jar.xml b/.idea/libraries/sbt__org_json4s_json4s_ast_2_12_3_6_1_jar.xml new file mode 100644 index 0000000..73e2c0e --- /dev/null +++ b/.idea/libraries/sbt__org_json4s_json4s_ast_2_12_3_6_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__org_json4s_json4s_core_2_12_3_6_1_jar.xml b/.idea/libraries/sbt__org_json4s_json4s_core_2_12_3_6_1_jar.xml new file mode 100644 index 0000000..0827f69 --- /dev/null +++ b/.idea/libraries/sbt__org_json4s_json4s_core_2_12_3_6_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__org_json4s_json4s_jackson_2_12_3_6_1_jar.xml b/.idea/libraries/sbt__org_json4s_json4s_jackson_2_12_3_6_1_jar.xml new file mode 100644 index 0000000..d15e047 --- /dev/null +++ b/.idea/libraries/sbt__org_json4s_json4s_jackson_2_12_3_6_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__org_json4s_json4s_scalap_2_12_3_6_1_jar.xml b/.idea/libraries/sbt__org_json4s_json4s_scalap_2_12_3_6_1_jar.xml new file mode 100644 index 0000000..a9c107b --- /dev/null +++ b/.idea/libraries/sbt__org_json4s_json4s_scalap_2_12_3_6_1_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__org_scala_lang_modules_scala_parser_combinators_2_12_1_0_4_jar.xml b/.idea/libraries/sbt__org_scala_lang_modules_scala_parser_combinators_2_12_1_0_4_jar.xml new file mode 100644 index 0000000..f9252bb --- /dev/null +++ b/.idea/libraries/sbt__org_scala_lang_modules_scala_parser_combinators_2_12_1_0_4_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__org_scala_lang_modules_scala_xml_2_12_1_0_5_jar.xml b/.idea/libraries/sbt__org_scala_lang_modules_scala_xml_2_12_1_0_5_jar.xml new file mode 100644 index 0000000..687bf61 --- /dev/null +++ b/.idea/libraries/sbt__org_scala_lang_modules_scala_xml_2_12_1_0_5_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__org_scala_lang_scala_library_2_12_3_jar.xml b/.idea/libraries/sbt__org_scala_lang_scala_library_2_12_3_jar.xml new file mode 100644 index 0000000..f732c1f --- /dev/null +++ b/.idea/libraries/sbt__org_scala_lang_scala_library_2_12_3_jar.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__org_scala_lang_scala_reflect_2_12_3_jar.xml b/.idea/libraries/sbt__org_scala_lang_scala_reflect_2_12_3_jar.xml new file mode 100644 index 0000000..d662638 --- /dev/null +++ b/.idea/libraries/sbt__org_scala_lang_scala_reflect_2_12_3_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__org_scalactic_scalactic_2_12_3_0_3_jar.xml b/.idea/libraries/sbt__org_scalactic_scalactic_2_12_3_0_3_jar.xml new file mode 100644 index 0000000..849d3c5 --- /dev/null +++ b/.idea/libraries/sbt__org_scalactic_scalactic_2_12_3_0_3_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__org_scalatest_scalatest_2_12_3_0_3_jar.xml b/.idea/libraries/sbt__org_scalatest_scalatest_2_12_3_0_3_jar.xml new file mode 100644 index 0000000..2919a66 --- /dev/null +++ b/.idea/libraries/sbt__org_scalatest_scalatest_2_12_3_0_3_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/sbt__org_slf4j_slf4j_api_1_7_7_jar.xml b/.idea/libraries/sbt__org_slf4j_slf4j_api_1_7_7_jar.xml new file mode 100644 index 0000000..c007a81 --- /dev/null +++ b/.idea/libraries/sbt__org_slf4j_slf4j_api_1_7_7_jar.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9594111 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..fb539fa --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/root-build.iml b/.idea/modules/root-build.iml new file mode 100644 index 0000000..c1a60c1 --- /dev/null +++ b/.idea/modules/root-build.iml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/root.iml b/.idea/modules/root.iml new file mode 100644 index 0000000..ba61499 --- /dev/null +++ b/.idea/modules/root.iml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/sbt.xml b/.idea/sbt.xml new file mode 100644 index 0000000..81dd689 --- /dev/null +++ b/.idea/sbt.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/.idea/scala_compiler.xml b/.idea/scala_compiler.xml new file mode 100644 index 0000000..ac38352 --- /dev/null +++ b/.idea/scala_compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/captcha.mv.db b/captcha.mv.db new file mode 100644 index 0000000000000000000000000000000000000000..f408e80d3602ac714648dbc91701457e862d3ea6 GIT binary patch literal 16384 zcmeI3&1%~~5XYq?{lIOYhW1eCVUs}Jf-qWru0$`gt;VKy94saDR@TxgF|jRUDf00e*l5C8%|00;m9 zAn?EmG(rDA@EL;ifB+Bx0zd!=00AHX1b_e#00KY&2&_&ZIzAnr@c;G29spho7_u?% zjp)R+xjlDvz9C@D&NEk6CiIBS_$AXc&0K@;6i`*$Fqo1JM$FYPQO?+OHb{9qTJ7Ll z3gi#+&-HZ)Yr0h4*g|hV2Y!g^J>LucFSUbKyMeG{mp8W2_U1P^_oIWr@1fR5wAT%h z|G5=}0SY|+Ylo=9OlsL`>`>A-4aZO`sObewRIv?aSSE8c-AW7`8GUNwzQt54>SL@D z+ayV1+SKkllu>H4Sc?cHahzZ$qRb%`eYJFYq$NqUH@HaPABYs^7qi8*W>=4B3!M;azF6-gN4h!{a*L5)!@&$9NF=9eN=0A zYo+&`9g==aFQi9&602BPO;}x6Ls(N-OIZ8)n2uO=nD!eqqt6zd4xL=Idn|b+{ON>^ z!zmrlXq54(p5>t#?GIU1z1q?9&gnQ#+v(Bc`I@O}`AHW;xi=2w76BsKEyfjTxIDi_ zO7k{PDZi-5`)Y(PTe2aWGM07OmNi)|<;ztw4Yv9#10w(dAOHk_01yBIKmZ5;0U!Vb z?nL0a{=U%Ampko=5v#G8SgVQZo1;tl{fln^xX8cM|3v72O)K=jv8?}z(EnP5)m;B$ zqLbvJ0-)V|w_AJX9~Hcoemq^5%4=J=ytb{C*LL(Au>4CtmR?FvMCOISy_L)rVwdE) zbTE$D`9iV&JLQ`TU^2Qs>pAt|;EZ*6%>@%xnYZ^v!DZ!Y%>B#lH}l$nTQDuYA30nU zgJkj-HC`&#kr}CXdVL@W2mk>f00e*l5C8%|00;m9An@N1xURn!`d_=$|BlJ^Ke;FU F{|C5z-e3R# literal 0 HcmV?d00001 diff --git a/src/main/java/HTTPServer.java b/src/main/java/HTTPServer.java index de185e3..9dc4ae5 100644 --- a/src/main/java/HTTPServer.java +++ b/src/main/java/HTTPServer.java @@ -19,8 +19,6 @@ * For additional info see http://www.freeutils.net/source/jlhttp/ */ -package httpserver; - import java.io.*; import java.lang.annotation.*; import java.lang.reflect.*; diff --git a/src/main/java/lc/CaptchaTypes.java b/src/main/java/lc/CaptchaTypes.java new file mode 100644 index 0000000..bd4b552 --- /dev/null +++ b/src/main/java/lc/CaptchaTypes.java @@ -0,0 +1,117 @@ +package lc; + +import javax.imageio.ImageIO; +import javax.imageio.stream.FileImageOutputStream; +import javax.imageio.stream.ImageOutputStream; +import java.awt.*; +import java.awt.font.TextLayout; +import java.awt.image.BufferedImage; +import java.awt.image.ConvolveOp; +import java.awt.image.Kernel; +import java.io.IOException; +import java.io.File; +import java.util.Random; +import java.io.ByteArrayOutputStream; + +public class CaptchaTypes{ + + private String secret = ""; + + private void setRenderingHints(Graphics2D g2d){ + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, + RenderingHints.VALUE_FRACTIONALMETRICS_ON); + } + + private BufferedImage charToImg(String text){ + BufferedImage img = new BufferedImage(250, 100, BufferedImage.TYPE_INT_RGB); + Font font = new Font("Bradley Hand", Font.ROMAN_BASELINE, 48); + Graphics2D graphics2D = img.createGraphics(); + graphics2D.setFont(font); + graphics2D.setColor(new Color((int)(Math.random() * 0x1000000))); + graphics2D.drawString( text , 45, 45); + graphics2D.dispose(); + return img; + } + + public byte[] type_1(String captchaText){ + String[] fonts = {"Captcha Code","Mom'sTypewriter","Annifont","SF Intoxicated Blues", + "BeachType","Batmos","Barbecue","Bad Seed","Aswell","Alien Marksman"}; + String[] colors = {"#f68787","#f8a978","#f1eb9a","#a4f6a5"}; + BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics2D = img.createGraphics(); + Random rand = new Random(); + for(int i=0; i< captchaText.length(); i++) { + Font font = new Font(fonts[rand.nextInt(10)], Font.ROMAN_BASELINE, 48); + graphics2D.setFont(font); + FontMetrics fontMetrics = graphics2D.getFontMetrics(); + setRenderingHints(graphics2D); + graphics2D.setColor(Color.decode(colors[rand.nextInt(4)])); + if(rand.nextBoolean()) { + graphics2D.drawString(String.valueOf(captchaText.toLowerCase().charAt(i)), (i * 48), fontMetrics.getAscent()); + } + else { + graphics2D.drawString(String.valueOf(captchaText.toUpperCase().charAt(i)), (i * 48), fontMetrics.getAscent()); + } + } + graphics2D.dispose(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try{ + ImageIO.write(img, "png",new File("Text.png")); + ImageIO.write(img,"png",baos); + } + catch (IOException ex){ + ex.printStackTrace(); + } + return baos.toByteArray(); + } + + public void type_2(String text){ + try { + ImageOutputStream output = new FileImageOutputStream(new File("captchaTest.gif")); + GifSequenceWriter writer = new GifSequenceWriter( output, 1,1000, true ); + for(int i=0; i< text.length(); i++){ + BufferedImage nextImage = charToImg(String.valueOf(text.charAt(i))); + writer.writeToSequence(nextImage); + } + writer.close(); + output.close(); + } catch (IOException e){ + e.printStackTrace(); + } + } + + public void type_3(String text){ + BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB); + Font font = new Font("Arial",Font.ROMAN_BASELINE ,48); + Graphics2D graphics2D = img.createGraphics(); + TextLayout textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext()); + setRenderingHints(graphics2D); + graphics2D.setPaint(Color.WHITE); + graphics2D.fillRect(0, 0, 350, 100); + graphics2D.setPaint(Color.BLACK); + textLayout.draw(graphics2D, 15, 50); + graphics2D.dispose(); + float[] kernel = { + 1f / 9f, 1f / 9f, 1f / 9f, + 1f / 9f, 1f / 9f, 1f / 9f, + 1f / 9f, 1f / 9f, 1f / 9f + }; + ConvolveOp op = new ConvolveOp(new Kernel(3, 3, kernel), + ConvolveOp.EDGE_NO_OP, null); + BufferedImage img2 = op.filter(img, null); + Graphics2D g2d = img2.createGraphics(); + setRenderingHints(g2d); + g2d.setPaint(Color.WHITE); + textLayout.draw(g2d, 13, 50); + g2d.dispose(); + try{ + ImageIO.write(img2, "png",new File("Te.png")); + } + catch (IOException ex){ + ex.printStackTrace(); + } + } +} + diff --git a/src/main/java/Challenge.java b/src/main/java/lc/Challenge.java similarity index 100% rename from src/main/java/Challenge.java rename to src/main/java/lc/Challenge.java diff --git a/src/main/java/ChallengeProvider.java b/src/main/java/lc/ChallengeProvider.java similarity index 99% rename from src/main/java/ChallengeProvider.java rename to src/main/java/lc/ChallengeProvider.java index 0ed04fc..f377408 100644 --- a/src/main/java/ChallengeProvider.java +++ b/src/main/java/lc/ChallengeProvider.java @@ -7,4 +7,3 @@ interface ChallengeProvider { //TODO: def configure(): Unit } - diff --git a/src/main/java/lc/GifSequenceWriter.java b/src/main/java/lc/GifSequenceWriter.java new file mode 100644 index 0000000..46c2f70 --- /dev/null +++ b/src/main/java/lc/GifSequenceWriter.java @@ -0,0 +1,151 @@ +package lc; +import javax.imageio.*; +import javax.imageio.metadata.*; +import javax.imageio.stream.*; +import java.awt.image.*; +import java.io.*; +import java.util.Iterator; + +public class GifSequenceWriter { + protected ImageWriter gifWriter; + protected ImageWriteParam imageWriteParam; + protected IIOMetadata imageMetaData; + + /** + * Creates a new GifSequenceWriter + * + * @param outputStream the ImageOutputStream to be written to + * @param imageType one of the imageTypes specified in BufferedImage + * @param timeBetweenFramesMS the time between frames in miliseconds + * @param loopContinuously wether the gif should loop repeatedly + * @throws IIOException if no gif ImageWriters are found + * + * @author Elliot Kroo (elliot[at]kroo[dot]net) + */ + public GifSequenceWriter( + ImageOutputStream outputStream, + int imageType, + int timeBetweenFramesMS, + boolean loopContinuously) throws IIOException, IOException { + // my method to create a writer + gifWriter = getWriter(); + imageWriteParam = gifWriter.getDefaultWriteParam(); + ImageTypeSpecifier imageTypeSpecifier = + ImageTypeSpecifier.createFromBufferedImageType(imageType); + + imageMetaData = + gifWriter.getDefaultImageMetadata(imageTypeSpecifier, + imageWriteParam); + + String metaFormatName = imageMetaData.getNativeMetadataFormatName(); + + IIOMetadataNode root = (IIOMetadataNode) + imageMetaData.getAsTree(metaFormatName); + + IIOMetadataNode graphicsControlExtensionNode = getNode( + root, + "GraphicControlExtension"); + + graphicsControlExtensionNode.setAttribute("disposalMethod", "none"); + graphicsControlExtensionNode.setAttribute("userInputFlag", "FALSE"); + graphicsControlExtensionNode.setAttribute( + "transparentColorFlag", + "FALSE"); + graphicsControlExtensionNode.setAttribute( + "delayTime", + Integer.toString(timeBetweenFramesMS / 10)); + graphicsControlExtensionNode.setAttribute( + "transparentColorIndex", + "0"); + + IIOMetadataNode commentsNode = getNode(root, "CommentExtensions"); + commentsNode.setAttribute("CommentExtension", "Created by MAH"); + + IIOMetadataNode appEntensionsNode = getNode( + root, + "ApplicationExtensions"); + + IIOMetadataNode child = new IIOMetadataNode("ApplicationExtension"); + + child.setAttribute("applicationID", "NETSCAPE"); + child.setAttribute("authenticationCode", "2.0"); + + int loop = loopContinuously ? 0 : 1; + + child.setUserObject(new byte[]{ 0x1, (byte) (loop & 0xFF), (byte) + ((loop >> 8) & 0xFF)}); + appEntensionsNode.appendChild(child); + + imageMetaData.setFromTree(metaFormatName, root); + + gifWriter.setOutput(outputStream); + + gifWriter.prepareWriteSequence(null); + } + + public void writeToSequence(RenderedImage img) throws IOException { + gifWriter.writeToSequence( + new IIOImage( + img, + null, + imageMetaData), + imageWriteParam); + } + + /** + * Close this GifSequenceWriter object. This does not close the underlying + * stream, just finishes off the GIF. + */ + public void close() throws IOException { + gifWriter.endWriteSequence(); + } + + /** + * Returns the first available GIF ImageWriter using + * ImageIO.getImageWritersBySuffix("gif"). + * + * @return a GIF ImageWriter object + * @throws IIOException if no GIF image writers are returned + */ + private static ImageWriter getWriter() throws IIOException { + Iterator iter = ImageIO.getImageWritersBySuffix("gif"); + if(!iter.hasNext()) { + throw new IIOException("No GIF Image Writers Exist"); + } else { + return iter.next(); + } + } + + /** + * Returns an existing child node, or creates and returns a new child node (if + * the requested node does not exist). + * + * @param rootNode the IIOMetadataNode to search for the child node. + * @param nodeName the name of the child node. + * + * @return the child node, if found or a new node created with the given name. + */ + private static IIOMetadataNode getNode( + IIOMetadataNode rootNode, + String nodeName) { + int nNodes = rootNode.getLength(); + for (int i = 0; i < nNodes; i++) { + if (rootNode.item(i).getNodeName().compareToIgnoreCase(nodeName) + == 0) { + return((IIOMetadataNode) rootNode.item(i)); + } + } + IIOMetadataNode node = new IIOMetadataNode(nodeName); + rootNode.appendChild(node); + return(node); + } + + /** + public GifSequenceWriter( + BufferedOutputStream outputStream, + int imageType, + int timeBetweenFramesMS, + boolean loopContinuously) { + + */ +} diff --git a/src/main/java/lc/MainCaptcha.java b/src/main/java/lc/MainCaptcha.java new file mode 100644 index 0000000..5d74135 --- /dev/null +++ b/src/main/java/lc/MainCaptcha.java @@ -0,0 +1,23 @@ +package lc; + +public class MainCaptcha implements ChallengeProvider{ + + public String getId(){ + return "SomeText"; + } + + public Challenge returnChallenge(){ + CaptchaTypes captchaTypes = new CaptchaTypes(); + return new Challenge(captchaTypes.type_1("Hello"),"png","qwert"); + } + + public boolean checkAnswer(String secret, String answer){ + return true; + } + + public static void main(String[] args){ + MainCaptcha mainCaptcha = new MainCaptcha(); + Challenge challenge = mainCaptcha.returnChallenge(); + System.out.println("Content type: " + challenge.contentType); + } +} diff --git a/src/main/scala/lc/HTTPServer.java b/src/main/scala/lc/HTTPServer.java new file mode 100644 index 0000000..d0976cb --- /dev/null +++ b/src/main/scala/lc/HTTPServer.java @@ -0,0 +1,3107 @@ +package lc;/* + * Copyright © 2005-2018 Amichai Rothman + * + * This file is part of JLHTTP - the Java Lightweight HTTP Server. + * + * JLHTTP is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * JLHTTP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with JLHTTP. If not, see . + * + * For additional info see http://www.freeutils.net/source/jlhttp/ + */ + +import javax.net.ServerSocketFactory; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocket; +import java.io.*; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.*; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.*; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.GZIPOutputStream; + +/** + * The {@code HTTPServer} class implements a light-weight HTTP server. + *

+ * This server implements all functionality required by RFC 2616 ("Hypertext + * Transfer Protocol -- HTTP/1.1"), as well as some of the optional + * functionality (this is termed "conditionally compliant" in the RFC). + * In fact, a couple of bugs in the RFC itself were discovered + * (and fixed) during the development of this server. + *

+ * Feature Overview + *

    + *
  • RFC compliant - correctness is not sacrificed for the sake of size
  • + *
  • Virtual hosts - multiple domains and subdomains per server
  • + *
  • File serving - built-in handler to serve files and folders from disk
  • + *
  • Mime type mappings - configurable via API or a standard mime.types file
  • + *
  • Directory index generation - enables browsing folder contents
  • + *
  • Welcome files - configurable default filename (e.g. index.html)
  • + *
  • All HTTP methods supported - GET/HEAD/OPTIONS/TRACE/POST/PUT/DELETE/custom
  • + *
  • Conditional statuses - ETags and If-* header support
  • + *
  • Chunked transfer encoding - for serving dynamically-generated data streams
  • + *
  • Gzip/deflate compression - reduces bandwidth and download time
  • + *
  • HTTPS - secures all server communications
  • + *
  • Partial content - download continuation (a.k.a. byte range serving)
  • + *
  • File upload - multipart/form-data handling as stream or iterator
  • + *
  • Multiple context handlers - a different handler method per URL path
  • + *
  • @Context annotations - auto-detection of context handler methods
  • + *
  • Parameter parsing - from query string or x-www-form-urlencoded body
  • + *
  • A single source file - super-easy to integrate into any application
  • + *
  • Standalone - no dependencies other than the Java runtime
  • + *
  • Small footprint - standard jar is ~50K, stripped jar is ~35K
  • + *
  • Extensible design - easy to override, add or remove functionality
  • + *
  • Reusable utility methods to simplify your custom code
  • + *
  • Extensive documentation of API and implementation (>40% of source lines)
  • + *
+ *

+ * Use Cases + *

+ * Being a lightweight, standalone, easily embeddable and tiny-footprint + * server, it is well-suited for + *

    + *
  • Resource-constrained environments such as embedded devices. + * For really extreme constraints, you can easily remove unneeded + * functionality to make it even smaller (and use the -Dstripped + * maven build option to strip away debug info, license, etc.)
  • + *
  • Unit and integration tests - fast setup/teardown times, small overhead + * and simple context handler setup make it a great web server for testing + * client components under various server response conditions.
  • + *
  • Embedding a web console into any headless application for + * administration, monitoring, or a full portable GUI.
  • + *
  • A full-fledged standalone web server serving static files, + * dynamically-generated content, REST APIs, pseudo-streaming, etc.
  • + *
  • A good reference for learning how HTTP works under the hood.
  • + *
+ *

+ * Implementation Notes + *

+ * The design and implementation of this server attempt to balance correctness, + * compliance, readability, size, features, extensibility and performance, + * and often prioritize them in this order, but some trade-offs must be made. + *

+ * This server is multithreaded in its support for multiple concurrent HTTP + * connections, however most of its constituent classes are not thread-safe and + * require external synchronization if accessed by multiple threads concurrently. + *

+ * Source Structure and Documentation + *

+ * This server is intentionally written as a single source file, in order to make + * it as easy as possible to integrate into any existing project - by simply adding + * this single file to the project sources. It does, however, aim to maintain a + * structured and flexible design. There are no external package dependencies. + *

+ * This file contains extensive documentation of its classes and methods, as + * well as implementation details and references to specific RFC sections + * which clarify the logic behind the code. It is recommended that anyone + * attempting to modify the protocol-level functionality become acquainted with + * the RFC, in order to make sure that protocol compliance is not broken. + *

+ * Getting Started + *

+ * For an example and a good starting point for learning how to use the API, + * see the {@link #main main} method at the bottom of the file, and follow + * the code into the API from there. Alternatively, you can just browse through + * the classes and utility methods and read their documentation and code. + * + * @author Amichai Rothman + * @since 2008-07-24 + */ +public class HTTPServer { + + /** + * The SimpleDateFormat-compatible formats of dates which must be supported. + * Note that all generated date fields must be in the RFC 1123 format only, + * while the others are supported by recipients for backwards-compatibility. + */ + public static final String[] DATE_PATTERNS = { + "EEE, dd MMM yyyy HH:mm:ss z", // RFC 822, updated by RFC 1123 + "EEEE, dd-MMM-yy HH:mm:ss z", // RFC 850, obsoleted by RFC 1036 + "EEE MMM d HH:mm:ss yyyy" // ANSI C's asctime() format + }; + + /** A GMT (UTC) timezone instance. */ + protected static final TimeZone GMT = TimeZone.getTimeZone("GMT"); + + /** Date format strings. */ + protected static final char[] + DAYS = "Sun Mon Tue Wed Thu Fri Sat".toCharArray(), + MONTHS = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".toCharArray(); + + /** A convenience array containing the carriage-return and line feed chars. */ + public static final byte[] CRLF = { 0x0d, 0x0a }; + + /** The HTTP status description strings. */ + protected static final String[] statuses = new String[600]; + + static { + // initialize status descriptions lookup table + Arrays.fill(statuses, "Unknown Status"); + statuses[100] = "Continue"; + statuses[200] = "OK"; + statuses[204] = "No Content"; + statuses[206] = "Partial Content"; + statuses[301] = "Moved Permanently"; + statuses[302] = "Found"; + statuses[304] = "Not Modified"; + statuses[307] = "Temporary Redirect"; + statuses[400] = "Bad Request"; + statuses[401] = "Unauthorized"; + statuses[403] = "Forbidden"; + statuses[404] = "Not Found"; + statuses[405] = "Method Not Allowed"; + statuses[408] = "Request Timeout"; + statuses[412] = "Precondition Failed"; + statuses[413] = "Request Entity Too Large"; + statuses[414] = "Request-URI Too Large"; + statuses[416] = "Requested Range Not Satisfiable"; + statuses[417] = "Expectation Failed"; + statuses[500] = "Internal Server Error"; + statuses[501] = "Not Implemented"; + statuses[502] = "Bad Gateway"; + statuses[503] = "Service Unavailable"; + statuses[504] = "Gateway Time-out"; + } + + /** + * A mapping of path suffixes (e.g. file extensions) to their + * corresponding MIME types. + */ + protected static final Map contentTypes = + new ConcurrentHashMap(); + + static { + // add some default common content types + // see http://www.iana.org/assignments/media-types/ for full list + addContentType("application/font-woff", "woff"); + addContentType("application/font-woff2", "woff2"); + addContentType("application/java-archive", "jar"); + addContentType("application/javascript", "js"); + addContentType("application/json", "json"); + addContentType("application/octet-stream", "exe"); + addContentType("application/pdf", "pdf"); + addContentType("application/x-7z-compressed", "7z"); + addContentType("application/x-compressed", "tgz"); + addContentType("application/x-gzip", "gz"); + addContentType("application/x-tar", "tar"); + addContentType("application/xhtml+xml", "xhtml"); + addContentType("application/zip", "zip"); + addContentType("audio/mpeg", "mp3"); + addContentType("image/gif", "gif"); + addContentType("image/jpeg", "jpg", "jpeg"); + addContentType("image/png", "png"); + addContentType("image/svg+xml", "svg"); + addContentType("image/x-icon", "ico"); + addContentType("text/css", "css"); + addContentType("text/csv", "csv"); + addContentType("text/html; charset=utf-8", "htm", "html"); + addContentType("text/plain", "txt", "text", "log"); + addContentType("text/xml", "xml"); + } + + /** The MIME types that can be compressed (prefix/suffix wildcards allowed). */ + protected static String[] compressibleContentTypes = + { "text/*", "*/javascript", "*icon", "*+xml", "*/json" }; + + /** + * The {@code LimitedInputStream} provides access to a limited number + * of consecutive bytes from the underlying InputStream, starting at its + * current position. If this limit is reached, it behaves as though the end + * of stream has been reached (although the underlying stream remains open + * and may contain additional data). + */ + public static class LimitedInputStream extends FilterInputStream { + + protected long limit; // decremented when read, until it reaches zero + protected boolean prematureEndException; + + /** + * Constructs a LimitedInputStream with the given underlying + * input stream and limit. + * + * @param in the underlying input stream + * @param limit the maximum number of bytes that may be consumed from + * the underlying stream before this stream ends. If zero or + * negative, this stream will be at its end from initialization. + * @param prematureEndException specifies the stream's behavior when + * the underlying stream end is reached before the limit is + * reached: if true, an exception is thrown, otherwise this + * stream reaches its end as well (i.e. read() returns -1) + * @throws NullPointerException if the given stream is null + */ + public LimitedInputStream(InputStream in, long limit, boolean prematureEndException) { + super(in); + if (in == null) + throw new NullPointerException("input stream is null"); + this.limit = limit < 0 ? 0 : limit; + this.prematureEndException = prematureEndException; + } + + @Override + public int read() throws IOException { + int res = limit == 0 ? -1 : in.read(); + if (res < 0 && limit > 0 && prematureEndException) + throw new IOException("unexpected end of stream"); + limit = res < 0 ? 0 : limit - 1; + return res; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int res = limit == 0 ? -1 : in.read(b, off, len > limit ? (int)limit : len); + if (res < 0 && limit > 0 && prematureEndException) + throw new IOException("unexpected end of stream"); + limit = res < 0 ? 0 : limit - res; + return res; + } + + @Override + public long skip(long len) throws IOException { + long res = in.skip(len > limit ? limit : len); + limit -= res; + return res; + } + + @Override + public int available() throws IOException { + int res = in.available(); + return res > limit ? (int)limit : res; + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public void close() { + limit = 0; // end this stream, but don't close the underlying stream + } + } + + /** + * The {@code ChunkedInputStream} decodes an InputStream whose data has the + * "chunked" transfer encoding applied to it, providing the underlying data. + */ + public static class ChunkedInputStream extends LimitedInputStream { + + protected Headers headers; + protected boolean initialized; + + /** + * Constructs a ChunkedInputStream with the given underlying stream, and + * a headers container to which the stream's trailing headers will be + * added. + * + * @param in the underlying "chunked"-encoded input stream + * @param headers the headers container to which the stream's trailing + * headers will be added, or null if they are to be discarded + * @throws NullPointerException if the given stream is null + */ + public ChunkedInputStream(InputStream in, Headers headers) { + super(in, 0, true); + this.headers = headers; + } + + @Override + public int read() throws IOException { + return limit <= 0 && initChunk() < 0 ? -1 : super.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return limit <= 0 && initChunk() < 0 ? -1 : super.read(b, off, len); + } + + /** + * Initializes the next chunk. If the previous chunk has not yet + * ended, or the end of stream has been reached, does nothing. + * + * @return the length of the chunk, or -1 if the end of stream + * has been reached + * @throws IOException if an IO error occurs or the stream is corrupt + */ + protected long initChunk() throws IOException { + if (limit == 0) { // finished previous chunk + // read chunk-terminating CRLF if it's not the first chunk + if (initialized && readLine(in).length() > 0) + throw new IOException("chunk data must end with CRLF"); + initialized = true; + limit = parseChunkSize(readLine(in)); // read next chunk size + if (limit == 0) { // last chunk has size 0 + limit = -1; // mark end of stream + // read trailing headers, if any + Headers trailingHeaders = readHeaders(in); + if (headers != null) + headers.addAll(trailingHeaders); + } + } + return limit; + } + + /** + * Parses a chunk-size line. + * + * @param line the chunk-size line to parse + * @return the chunk size + * @throws IllegalArgumentException if the chunk-size line is invalid + */ + protected static long parseChunkSize(String line) throws IllegalArgumentException { + int pos = line.indexOf(';'); + line = pos < 0 ? line : line.substring(0, pos); // ignore params, if any + try { + return parseULong(line, 16); // throws NFE + } catch (NumberFormatException nfe) { + throw new IllegalArgumentException( + "invalid chunk size line: \"" + line + "\""); + } + } + } + + /** + * The {@code ChunkedOutputStream} encodes an OutputStream with the + * "chunked" transfer encoding. It should be used only when the content + * length is not known in advance, and with the response Transfer-Encoding + * header set to "chunked". + *

+ * Data is written to the stream by calling the {@link #write(byte[], int, int)} + * method, which writes a new chunk per invocation. To end the stream, + * the {@link #writeTrailingChunk} method must be called or the stream closed. + */ + public static class ChunkedOutputStream extends FilterOutputStream { + + protected int state; // the current stream state + + /** + * Constructs a ChunkedOutputStream with the given underlying stream. + * + * @param out the underlying output stream to which the chunked stream + * is written + * @throws NullPointerException if the given stream is null + */ + public ChunkedOutputStream(OutputStream out) { + super(out); + if (out == null) + throw new NullPointerException("output stream is null"); + } + + /** + * Initializes a new chunk with the given size. + * + * @param size the chunk size (must be positive) + * @throws IllegalArgumentException if size is negative + * @throws IOException if an IO error occurs, or the stream has + * already been ended + */ + protected void initChunk(long size) throws IOException { + if (size < 0) + throw new IllegalArgumentException("invalid size: " + size); + if (state > 0) + out.write(CRLF); // end previous chunk + else if (state == 0) + state = 1; // start first chunk + else + throw new IOException("chunked stream has already ended"); + out.write(getBytes(Long.toHexString(size))); + out.write(CRLF); + } + + /** + * Writes the trailing chunk which marks the end of the stream. + * + * @param headers the (optional) trailing headers to write, or null + * @throws IOException if an error occurs + */ + public void writeTrailingChunk(Headers headers) throws IOException { + initChunk(0); // zero-sized chunk marks the end of the stream + if (headers == null) + out.write(CRLF); // empty header block + else + headers.writeTo(out); + state = -1; + } + + /** + * Writes a chunk containing the given byte. This method initializes + * a new chunk of size 1, and then writes the byte as the chunk data. + * + * @param b the byte to write as a chunk + * @throws IOException if an error occurs + */ + @Override + public void write(int b) throws IOException { + write(new byte[] { (byte)b }, 0, 1); + } + + /** + * Writes a chunk containing the given bytes. This method initializes + * a new chunk of the given size, and then writes the chunk data. + * + * @param b an array containing the bytes to write + * @param off the offset within the array where the data starts + * @param len the length of the data in bytes + * @throws IOException if an error occurs + * @throws IndexOutOfBoundsException if the given offset or length + * are outside the bounds of the given array + */ + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (len > 0) // zero-sized chunk is the trailing chunk + initChunk(len); + out.write(b, off, len); + } + + /** + * Writes the trailing chunk if necessary, and closes the underlying stream. + * + * @throws IOException if an error occurs + */ + @Override + public void close() throws IOException { + if (state > -1) + writeTrailingChunk(null); + super.close(); + } + } + + /** + * The {@code MultipartInputStream} decodes an InputStream whose data has + * a "multipart/*" content type (see RFC 2046), providing the underlying + * data of its various parts. + *

+ * The {@code InputStream} methods (e.g. {@link #read}) relate only to + * the current part, and the {@link #nextPart} method advances to the + * beginning of the next part. + */ + public static class MultipartInputStream extends FilterInputStream { + + protected final byte[] boundary; // including leading CRLF-- + protected final byte[] buf = new byte[4096]; + protected int head, tail; // indices of current part's data in buf + protected int end; // last index of input data read into buf + protected int len; // length of found boundary + protected int state; // initial, started data, start boundary, EOS, last boundary, epilogue + + /** + * Constructs a MultipartInputStream with the given underlying stream. + * + * @param in the underlying multipart stream + * @param boundary the multipart boundary + * @throws NullPointerException if the given stream or boundary is null + * @throws IllegalArgumentException if the given boundary's size is not + * between 1 and 70 + */ + protected MultipartInputStream(InputStream in, byte[] boundary) { + super(in); + int len = boundary.length; + if (len == 0 || len > 70) + throw new IllegalArgumentException("invalid boundary length"); + this.boundary = new byte[len + 4]; // CRLF--boundary + System.arraycopy(CRLF, 0, this.boundary, 0, 2); + this.boundary[2] = this.boundary[3] = '-'; + System.arraycopy(boundary, 0, this.boundary, 4, len); + } + + @Override + public int read() throws IOException { + if (!fill()) + return -1; + return buf[head++] & 0xFF; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (!fill()) + return -1; + len = Math.min(tail - head, len); + System.arraycopy(buf, head, b, off, len); // throws IOOBE as necessary + head += len; + return len; + } + + @Override + public long skip(long len) throws IOException { + if (len <= 0 || !fill()) + return 0; + len = Math.min(tail - head, len); + head += len; + return len; + } + + @Override + public int available() throws IOException { + return tail - head; + } + + @Override + public boolean markSupported() { + return false; + } + + /** + * Advances the stream position to the beginning of the next part. + * Data read before calling this method for the first time is the preamble, + * and data read after this method returns false is the epilogue. + * + * @return true if successful, or false if there are no more parts + * @throws IOException if an error occurs + */ + public boolean nextPart() throws IOException { + while (skip(buf.length) != 0); // skip current part (until boundary) + head = tail += len; // the next part starts right after boundary + state |= 1; // started data (after first boundary) + if (state >= 8) { // found last boundary + state |= 0x10; // now beyond last boundary (epilogue) + return false; + } + findBoundary(); // update indices + return true; + } + + /** + * Fills the buffer with more data from the underlying stream. + * + * @return true if there is available data for the current part, + * or false if the current part's end has been reached + * @throws IOException if an error occurs or the input format is invalid + */ + protected boolean fill() throws IOException { + // check if we already have more available data + if (head != tail) // remember that if we continue, head == tail below + return true; + // if there's no more room, shift extra unread data to beginning of buffer + if (tail > buf.length - 256) { // max boundary + whitespace supported size + System.arraycopy(buf, tail, buf, 0, end -= tail); + head = tail = 0; + } + // read more data and look for boundary (or potential partial boundary) + int read; + do { + read = super.read(buf, end, buf.length - end); + if (read < 0) + state |= 4; // end of stream (EOS) + else + end += read; + findBoundary(); // updates tail and length to next potential boundary + // if we found a partial boundary with no data before it, we must + // continue reading to determine if there is more data or not + } while (read > 0 && tail == head && len == 0); + // update and validate state + if (tail != 0) // anything but a boundary right at the beginning + state |= 1; // started data (preamble or after boundary) + if (state < 8 && len > 0) + state |= 2; // found start boundary + if ((state & 6) == 4 // EOS but no start boundary found + || len == 0 && ((state & 0xFC) == 4 // EOS but no last and no more boundaries + || read == 0 && tail == head)) // boundary longer than buffer + throw new IOException("missing boundary"); + if (state >= 0x10) // in epilogue + tail = end; // ignore boundaries, return everything + return tail > head; // available data in current part + } + + /** + * Finds the first (potential) boundary within the buffer's remaining data. + * Updates tail, length and state fields accordingly. + * + * @throws IOException if an error occurs or the input format is invalid + */ + protected void findBoundary() throws IOException { + // see RFC2046#5.1.1 for boundary syntax + len = 0; + int off = tail - ((state & 1) != 0 || buf[0] != '-' ? 0 : 2); // skip initial CRLF? + for (int end = this.end; tail < end; tail++, off = tail) { + int j = tail; // end of potential boundary + // try to match boundary value (leading CRLF is optional at first boundary) + while (j < end && j - off < boundary.length && buf[j] == boundary[j - off]) + j++; + // return potential partial boundary which is cut off at end of current data + if (j + 1 >= end) // at least two more chars needed for full boundary (CRLF or --) + return; + // if we found the boundary value, expand selection to include full line + if (j - off == boundary.length) { + // check if last boundary of entire multipart + if (buf[j] == '-' && buf[j + 1] == '-') { + j += 2; + state |= 8; // found last boundary that ends multipart + } + // allow linear whitespace after boundary + while (j < end && (buf[j] == ' ' || buf[j] == '\t')) + j++; + // check for CRLF (required, except in last boundary with no epilogue) + if (j + 1 < end && buf[j] == '\r' && buf[j + 1] == '\n') // found CRLF + len = j - tail + 2; // including optional whitespace and CRLF + else if (j + 1 < end || (state & 4) != 0 && j + 1 == end) // should have found or never will + throw new IOException("boundary must end with CRLF"); + else if ((state & 4) != 0) // last boundary with no CRLF at end of data is valid + len = j - tail; + return; + } + } + } + } + + /** + * The {@code MultipartIterator} iterates over the parts of a multipart/form-data request. + *

+ * For example, to support file upload from a web browser: + *

    + *
  1. Create an HTML form which includes an input field of type "file", attributes + * method="post" and enctype="multipart/form-data", and an action URL of your choice, + * for example action="/upload". This form can be served normally like any other + * resource, e.g. from an HTML file on disk. + *
  2. Add a context handler for the action path ("/upload" in this example), using either + * the explicit {@link VirtualHost#addContext} method or the {@link Context} annotation. + *
  3. In the context handler implementation, construct a {@code MultipartIterator} from + * the client {@code Request}. + *
  4. Iterate over the form {@link Part}s, processing each named field as appropriate - + * for the file input field, read the uploaded file using the body input stream. + *
+ */ + public static class MultipartIterator implements Iterator { + + /** + * The {@code Part} class encapsulates a single part of the multipart. + */ + public static class Part { + + public String name; + public String filename; + public Headers headers; + public InputStream body; + + /** + * Returns the part's name (form field name). + * + * @return the part's name + */ + public String getName() { return name; } + + /** + * Returns the part's filename (original filename entered in file form field). + * + * @return the part's filename, or null if there is none + */ + public String getFilename() { return filename; } + + /** + * Returns the part's headers. + * + * @return the part's headers + */ + public Headers getHeaders() { return headers; } + + /** + * Returns the part's body (form field value). + * + * @return the part's body + */ + public InputStream getBody() { return body; } + + /*** + * Returns the part's body as a string. If the part + * headers do not specify a charset, UTF-8 is used. + * + * @return the part's body as a string + * @throws IOException if an IO error occurs + */ + public String getString() throws IOException { + String charset = headers.getParams("Content-Type").get("charset"); + return readToken(body, -1, charset == null ? "UTF-8" : charset, 8192); + } + } + + protected final MultipartInputStream in; + protected boolean next; + + /** + * Creates a new MultipartIterator from the given request. + * + * @param req the multipart/form-data request + * @throws IOException if an IO error occurs + * @throws IllegalArgumentException if the given request's content type + * is not multipart/form-data, or is missing the boundary + */ + public MultipartIterator(Request req) throws IOException { + Map ct = req.getHeaders().getParams("Content-Type"); + if (!ct.containsKey("multipart/form-data")) + throw new IllegalArgumentException("Content-Type is not multipart/form-data"); + String boundary = ct.get("boundary"); // should be US-ASCII + if (boundary == null) + throw new IllegalArgumentException("Content-Type is missing boundary"); + in = new MultipartInputStream(req.getBody(), getBytes(boundary)); + } + + public boolean hasNext() { + try { + return next || (next = in.nextPart()); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + + public Part next() { + if (!hasNext()) + throw new NoSuchElementException(); + next = false; + Part p = new Part(); + try { + p.headers = readHeaders(in); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + Map cd = p.headers.getParams("Content-Disposition"); + p.name = cd.get("name"); + p.filename = cd.get("filename"); + p.body = in; + return p; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } + + /** + * The {@code VirtualHost} class represents a virtual host in the server. + */ + public static class VirtualHost { + + public static final String DEFAULT_HOST_NAME = "~DEFAULT~"; + + /** + * The {@code ContextInfo} class holds a single context's information. + */ + public class ContextInfo { + + protected final String path; + protected final Map handlers = + new ConcurrentHashMap(2); + + /** + * Constructs a ContextInfo with the given context path. + * + * @param path the context path (without trailing slash) + */ + public ContextInfo(String path) { + this.path = path; + } + + /** + * Returns the context path. + * + * @return the context path, or null if there is none + */ + public String getPath() { + return path; + } + + /** + * Returns the map of supported HTTP methods and their corresponding handlers. + * + * @return the map of supported HTTP methods and their corresponding handlers + */ + public Map getHandlers() { + return handlers; + } + + /** + * Adds (or replaces) a context handler for the given HTTP methods. + * + * @param handler the context handler + * @param methods the HTTP methods supported by the handler (default is "GET") + */ + public void addHandler(ContextHandler handler, String... methods) { + if (methods.length == 0) + methods = new String[] { "GET" }; + for (String method : methods) { + handlers.put(method, handler); + VirtualHost.this.methods.add(method); // it's now supported by server + } + } + } + + protected final String name; + protected final Set aliases = new CopyOnWriteArraySet(); + protected volatile String directoryIndex = "index.html"; + protected volatile boolean allowGeneratedIndex; + protected final Set methods = new CopyOnWriteArraySet(); + protected final ContextInfo emptyContext = new ContextInfo(null); + protected final ConcurrentMap contexts = + new ConcurrentHashMap(); + + /** + * Constructs a VirtualHost with the given name. + * + * @param name the host's name, or null if it is the default host + */ + public VirtualHost(String name) { + this.name = name; + contexts.put("*", new ContextInfo(null)); // for "OPTIONS *" + } + + /** + * Returns this host's name. + * + * @return this host's name, or null if it is the default host + */ + public String getName() { + return name; + } + + /** + * Adds an alias for this host. + * + * @param alias the alias + */ + public void addAlias(String alias) { + aliases.add(alias); + } + + /** + * Returns this host's aliases. + * + * @return the (unmodifiable) set of aliases (which may be empty) + */ + public Set getAliases() { + return Collections.unmodifiableSet(aliases); + } + + /** + * Sets the directory index file. For every request whose URI ends with + * a '/' (i.e. a directory), the index file is appended to the path, + * and the resulting resource is served if it exists. If it does not + * exist, an auto-generated index for the requested directory may be + * served, depending on whether {@link #setAllowGeneratedIndex + * a generated index is allowed}, otherwise an error is returned. + * The default directory index file is "index.html". + * + * @param directoryIndex the directory index file, or null if no + * index file should be used + */ + public void setDirectoryIndex(String directoryIndex) { + this.directoryIndex = directoryIndex; + } + + /** + * Gets this host's directory index file. + * + * @return the directory index file, or null + */ + public String getDirectoryIndex() { + return directoryIndex; + } + + /** + * Sets whether auto-generated indices are allowed. If false, and a + * directory resource is requested, an error will be returned instead. + * + * @param allowed specifies whether generated indices are allowed + */ + public void setAllowGeneratedIndex(boolean allowed) { + this.allowGeneratedIndex = allowed; + } + + /** + * Returns whether auto-generated indices are allowed. + * + * @return whether auto-generated indices are allowed + */ + public boolean isAllowGeneratedIndex() { + return allowGeneratedIndex; + } + + /** + * Returns all HTTP methods explicitly supported by at least one context + * (this may or may not include the methods with required or built-in support). + * + * @return all HTTP methods explicitly supported by at least one context + */ + public Set getMethods() { + return methods; + } + + /** + * Returns the context handler for the given path. + * + * If a context is not found for the given path, the search is repeated for + * its parent path, and so on until a base context is found. If neither the + * given path nor any of its parents has a context, an empty context is returned. + * + * @param path the context's path + * @return the context info for the given path, or an empty context if none exists + */ + public ContextInfo getContext(String path) { + // all context paths are without trailing slash + for (path = trimRight(path, '/'); path != null; path = getParentPath(path)) { + ContextInfo info = contexts.get(path); + if (info != null) + return info; + } + return emptyContext; + } + + /** + * Adds a context and its corresponding context handler to this server. + * Paths are normalized by removing trailing slashes (except the root). + * + * @param path the context's path (must start with '/') + * @param handler the context handler for the given path + * @param methods the HTTP methods supported by the context handler (default is "GET") + * @throws IllegalArgumentException if path is malformed + */ + public void addContext(String path, ContextHandler handler, String... methods) { + if (path == null || !path.startsWith("/") && !path.equals("*")) + throw new IllegalArgumentException("invalid path: " + path); + path = trimRight(path, '/'); // remove trailing slash + ContextInfo info = new ContextInfo(path); + ContextInfo existing = contexts.putIfAbsent(path, info); + info = existing != null ? existing : info; + info.addHandler(handler, methods); + } + + /** + * Adds contexts for all methods of the given object that + * are annotated with the {@link Context} annotation. + * + * @param o the object whose annotated methods are added + * @throws IllegalArgumentException if a Context-annotated + * method has an {@link Context invalid signature} + */ + public void addContexts(Object o) throws IllegalArgumentException { + for (Class c = o.getClass(); c != null; c = c.getSuperclass()) { + // add to contexts those with @Context annotation + for (Method m : c.getDeclaredMethods()) { + Context context = m.getAnnotation(Context.class); + if (context != null) { + m.setAccessible(true); // allow access to private method + ContextHandler handler = new MethodContextHandler(m, o); + addContext(context.value(), handler, context.methods()); + } + } + } + } + } + + /** + * The {@code Context} annotation decorates methods which are mapped + * to a context (path) within the server, and provide its contents. + *

+ * The annotated methods must have the same signature and contract + * as {@link ContextHandler#serve}, but can have arbitrary names. + * + * @see VirtualHost#addContexts(Object) + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface Context { + + /** + * The context (path) that this field maps to (must begin with '/'). + * + * @return the context (path) that this field maps to + */ + String value(); + + /** + * The HTTP methods supported by this context handler (default is "GET"). + * + * @return the HTTP methods supported by this context handler + */ + String[] methods() default "GET"; + } + + /** + * A {@code ContextHandler} serves the content of resources within a context. + * + * @see VirtualHost#addContext + */ + public interface ContextHandler { + + /** + * Serves the given request using the given response. + * + * @param req the request to be served + * @param resp the response to be filled + * @return an HTTP status code, which will be used in returning + * a default response appropriate for this status. If this + * method invocation already sent anything in the response + * (headers or content), it must return 0, and no further + * processing will be done + * @throws IOException if an IO error occurs + */ + int serve(Request req, Response resp) throws IOException; + } + + /** + * The {@code FileContextHandler} services a context by mapping it + * to a file or folder (recursively) on disk. + */ + public static class FileContextHandler implements ContextHandler { + + protected final File base; + + public FileContextHandler(File dir) throws IOException { + this.base = dir.getCanonicalFile(); + } + + public int serve(Request req, Response resp) throws IOException { + return serveFile(base, req.getContext().getPath(), req, resp); + } + } + + /** + * The {@code MethodContextHandler} services a context + * by invoking a handler method on a specified object. + *

+ * The method must have the same signature and contract as + * {@link ContextHandler#serve}, but can have an arbitrary name. + * + * @see VirtualHost#addContexts(Object) + */ + public static class MethodContextHandler implements ContextHandler { + + protected final Method m; + protected final Object obj; + + public MethodContextHandler(Method m, Object obj) throws IllegalArgumentException { + this.m = m; + this.obj = obj; + Class[] params = m.getParameterTypes(); + if (params.length != 2 + || !Request.class.isAssignableFrom(params[0]) + || !Response.class.isAssignableFrom(params[1]) + || !int.class.isAssignableFrom(m.getReturnType())) + throw new IllegalArgumentException("invalid method signature: " + m); + } + + public int serve(Request req, Response resp) throws IOException { + try { + return (Integer)m.invoke(obj, req, resp); + } catch (InvocationTargetException ite) { + throw new IOException("error: " + ite.getCause().getMessage()); + } catch (Exception e) { + throw new IOException("error: " + e); + } + } + } + + /** + * The {@code Header} class encapsulates a single HTTP header. + */ + public static class Header { + + protected final String name; + protected final String value; + + /** + * Constructs a header with the given name and value. + * Leading and trailing whitespace are trimmed. + * + * @param name the header name + * @param value the header value + * @throws NullPointerException if name or value is null + * @throws IllegalArgumentException if name is empty + */ + public Header(String name, String value) { + this.name = name.trim(); + this.value = value.trim(); + // RFC2616#14.23 - header can have an empty value (e.g. Host) + if (this.name.length() == 0) // but name cannot be empty + throw new IllegalArgumentException("name cannot be empty"); + } + + /** + * Returns this header's name. + * + * @return this header's name + */ + public String getName() { return name; } + + /** + * Returns this header's value. + * + * @return this header's value + */ + public String getValue() { return value; } + } + + /** + * The {@code Headers} class encapsulates a collection of HTTP headers. + * + * Header names are treated case-insensitively, although this class retains + * their original case. Header insertion order is maintained as well. + */ + public static class Headers implements Iterable

{ + + // due to the requirements of case-insensitive name comparisons, + // retaining the original case, and retaining header insertion order, + // and due to the fact that the number of headers is generally + // quite small (usually under 12 headers), we use a simple array with + // linear access times, which proves to be more efficient and + // straightforward than the alternatives + protected Header[] headers = new Header[12]; + protected int count; + + /** + * Returns the number of added headers. + * + * @return the number of added headers + */ + public int size() { + return count; + } + + /** + * Returns the value of the first header with the given name. + * + * @param name the header name (case insensitive) + * @return the header value, or null if none exists + */ + public String get(String name) { + for (int i = 0; i < count; i++) + if (headers[i].getName().equalsIgnoreCase(name)) + return headers[i].getValue(); + return null; + } + + /** + * Returns the Date value of the header with the given name. + * + * @param name the header name (case insensitive) + * @return the header value as a Date, or null if none exists + * or if the value is not in any supported date format + */ + public Date getDate(String name) { + try { + String header = get(name); + return header == null ? null : parseDate(header); + } catch (IllegalArgumentException iae) { + return null; + } + } + + /** + * Returns whether there exists a header with the given name. + * + * @param name the header name (case insensitive) + * @return whether there exists a header with the given name + */ + public boolean contains(String name) { + return get(name) != null; + } + + /** + * Adds a header with the given name and value to the end of this + * collection of headers. Leading and trailing whitespace are trimmed. + * + * @param name the header name (case insensitive) + * @param value the header value + */ + public void add(String name, String value) { + Header header = new Header(name, value); // also validates + // expand array if necessary + if (count == headers.length) { + Header[] expanded = new Header[2 * count]; + System.arraycopy(headers, 0, expanded, 0, count); + headers = expanded; + } + headers[count++] = header; // inlining header would cause a bug! + } + + /** + * Adds all given headers to the end of this collection of headers, + * in their original order. + * + * @param headers the headers to add + */ + public void addAll(Headers headers) { + for (Header header : headers) + add(header.getName(), header.getValue()); + } + + /** + * Adds a header with the given name and value, replacing the first + * existing header with the same name. If there is no existing header + * with the same name, it is added as in {@link #add}. + * + * @param name the header name (case insensitive) + * @param value the header value + * @return the replaced header, or null if none existed + */ + public Header replace(String name, String value) { + for (int i = 0; i < count; i++) { + if (headers[i].getName().equalsIgnoreCase(name)) { + Header prev = headers[i]; + headers[i] = new Header(name, value); + return prev; + } + } + add(name, value); + return null; + } + + /** + * Removes all headers with the given name (if any exist). + * + * @param name the header name (case insensitive) + */ + public void remove(String name) { + int j = 0; + for (int i = 0; i < count; i++) + if (!headers[i].getName().equalsIgnoreCase(name)) + headers[j++] = headers[i]; + while (count > j) + headers[--count] = null; + } + + /** + * Writes the headers to the given stream (including trailing CRLF). + * + * @param out the stream to write the headers to + * @throws IOException if an error occurs + */ + public void writeTo(OutputStream out) throws IOException { + for (int i = 0; i < count; i++) { + out.write(getBytes(headers[i].getName(), ": ", headers[i].getValue())); + out.write(CRLF); + } + out.write(CRLF); // ends header block + } + + /** + * Returns a header's parameters. Parameter order is maintained, + * and the first key (in iteration order) is the header's value + * without the parameters. + * + * @param name the header name (case insensitive) + * @return the header's parameter names and values + */ + public Map getParams(String name) { + Map params = new LinkedHashMap(); + for (String param : split(get(name), ";", -1)) { + String[] pair = split(param, "=", 2); + String val = pair.length == 1 ? "" : trimLeft(trimRight(pair[1], '"'), '"'); + params.put(pair[0], val); + } + return params; + } + + /** + * Returns an iterator over the headers, in their insertion order. + * If the headers collection is modified during iteration, the + * iteration result is undefined. The remove operation is unsupported. + * + * @return an Iterator over the headers + */ + public Iterator
iterator() { + // we use the built-in wrapper instead of a trivial custom implementation + // since even a tiny anonymous class here compiles to a 1.5K class file + return Arrays.asList(headers).subList(0, count).iterator(); + } + } + + /** + * The {@code Request} class encapsulates a single HTTP request. + */ + public class Request { + + protected String method; + protected URI uri; + protected URL baseURL; // cached value + protected String version; + protected Headers headers; + protected InputStream body; + protected Map params; // cached value + protected VirtualHost host; // cached value + protected VirtualHost.ContextInfo context; // cached value + + /** + * Constructs a Request from the data in the given input stream. + * + * @param in the input stream from which the request is read + * @throws IOException if an error occurs + */ + public Request(InputStream in) throws IOException { + readRequestLine(in); + headers = readHeaders(in); + // RFC2616#3.6 - if "chunked" is used, it must be the last one + // RFC2616#4.4 - if non-identity Transfer-Encoding is present, + // it must either include "chunked" or close the connection after + // the body, and in any case ignore Content-Length. + // if there is no such Transfer-Encoding, use Content-Length + // if neither header exists, there is no body + String header = headers.get("Transfer-Encoding"); + if (header != null && !header.toLowerCase(Locale.US).equals("identity")) { + if (Arrays.asList(splitElements(header, true)).contains("chunked")) + body = new ChunkedInputStream(in, headers); + else + body = in; // body ends when connection closes + } else { + header = headers.get("Content-Length"); + long len = header == null ? 0 : parseULong(header, 10); + body = new LimitedInputStream(in, len, false); + } + } + + /** + * Returns the request method. + * + * @return the request method + */ + public String getMethod() { return method; } + + /** + * Returns the request URI. + * + * @return the request URI + */ + public URI getURI() { return uri; } + + /** + * Returns the request version string. + * + * @return the request version string + */ + public String getVersion() { return version; } + + /** + * Returns the request headers. + * + * @return the request headers + */ + public Headers getHeaders() { return headers; } + + /** + * Returns the input stream containing the request body. + * + * @return the input stream containing the request body + */ + public InputStream getBody() { return body; } + + /** + * Returns the body in a String format + */ + public String getJson(){ + Scanner s = new Scanner(body).useDelimiter("\\A"); + return s.hasNext() ? s.next() : ""; + } + + /** + * Returns the path component of the request URI, after + * URL decoding has been applied (using the UTF-8 charset). + * + * @return the decoded path component of the request URI + */ + public String getPath() { + return uri.getPath(); + } + + /** + * Sets the path component of the request URI. This can be useful + * in URL rewriting, etc. + * + * @param path the path to set + * @throws IllegalArgumentException if the given path is malformed + */ + public void setPath(String path) { + try { + uri = new URI(uri.getScheme(), uri.getHost(), + trimDuplicates(path, '/'), uri.getFragment()); + context = null; // clear cached context so it will be recalculated + } catch (URISyntaxException use) { + throw new IllegalArgumentException("error setting path", use); + } + } + + /** + * Returns the base URL (scheme, host and port) of the request resource. + * The host name is taken from the request URI or the Host header or a + * default host (see RFC2616#5.2). + * + * @return the base URL of the requested resource, or null if it + * is malformed + */ + public URL getBaseURL() { + if (baseURL != null) + return baseURL; + // normalize host header + String host = uri.getHost(); + if (host == null) { + host = headers.get("Host"); + if (host == null) // missing in HTTP/1.0 + host = detectLocalHostName(); + } + int pos = host.indexOf(':'); + host = pos < 0 ? host : host.substring(0, pos); + try { + return baseURL = new URL(secure ? "https" : "http", host, port, ""); + } catch (MalformedURLException mue) { + return null; + } + } + + /** + * Returns the request parameters, which are parsed both from the query + * part of the request URI, and from the request body if its content + * type is "application/x-www-form-urlencoded" (i.e. a submitted form). + * UTF-8 encoding is assumed in both cases. + *

+ * The parameters are returned as a list of string arrays, each containing + * the parameter name as the first element and its corresponding value + * as the second element (or an empty string if there is no value). + *

+ * The list retains the original order of the parameters. + * + * @return the request parameters name-value pairs, + * or an empty list if there are none + * @throws IOException if an error occurs + * @see HTTPServer#parseParamsList(String) + */ + public List getParamsList() throws IOException { + List queryParams = parseParamsList(uri.getRawQuery()); + List bodyParams = Collections.emptyList(); + String ct = headers.get("Content-Type"); + if (ct != null && ct.toLowerCase(Locale.US).startsWith("application/x-www-form-urlencoded")) + bodyParams = parseParamsList(readToken(body, -1, "UTF-8", 2097152)); // 2MB limit + if (bodyParams.isEmpty()) + return queryParams; + if (queryParams.isEmpty()) + return bodyParams; + queryParams.addAll(bodyParams); + return queryParams; + } + + /** + * Returns the request parameters, which are parsed both from the query + * part of the request URI, and from the request body if its content + * type is "application/x-www-form-urlencoded" (i.e. a submitted form). + * UTF-8 encoding is assumed in both cases. + *

+ * For multivalued parameters (i.e. multiple parameters with the same + * name), only the first one is considered. For access to all values, + * use {@link #getParamsList()} instead. + *

+ * The map iteration retains the original order of the parameters. + * + * @return the request parameters name-value pairs, + * or an empty map if there are none + * @throws IOException if an error occurs + * @see #getParamsList() + */ + public Map getParams() throws IOException { + if (params == null) + params = toMap(getParamsList()); + return params; + } + + /** + * Returns the absolute (zero-based) content range value read + * from the Range header. If multiple ranges are requested, a single + * range containing all of them is returned. + * + * @param length the full length of the requested resource + * @return the requested range, or null if the Range header + * is missing or invalid + */ + public long[] getRange(long length) { + String header = headers.get("Range"); + return header == null || !header.startsWith("bytes=") + ? null : parseRange(header.substring(6), length); + } + + /** + * Reads the request line, parsing the method, URI and version string. + * + * @param in the input stream from which the request line is read + * @throws IOException if an error occurs or the request line is invalid + */ + protected void readRequestLine(InputStream in) throws IOException { + // RFC2616#4.1: should accept empty lines before request line + // RFC2616#19.3: tolerate additional whitespace between tokens + String line; + try { + do { line = readLine(in); } while (line.length() == 0); + } catch (IOException ioe) { // if EOF, timeout etc. + throw new IOException("missing request line"); // signal that the request did not begin + } + String[] tokens = split(line, " ", -1); + if (tokens.length != 3) + throw new IOException("invalid request line: \"" + line + "\""); + try { + method = tokens[0]; + // must remove '//' prefix which constructor parses as host name + uri = new URI(trimDuplicates(tokens[1], '/')); + version = tokens[2]; // RFC2616#2.1: allow implied LWS; RFC7230#3.1.1: disallow it + } catch (URISyntaxException use) { + throw new IOException("invalid URI: " + use.getMessage()); + } + } + + /** + * Returns the virtual host corresponding to the requested host name, + * or the default host if none exists. + * + * @return the virtual host corresponding to the requested host name, + * or the default virtual host + */ + public VirtualHost getVirtualHost() { + return host != null ? host + : (host = HTTPServer.this.getVirtualHost(getBaseURL().getHost())) != null ? host + : (host = HTTPServer.this.getVirtualHost(null)); + } + + /** + * Returns the info of the context handling this request. + * + * @return the info of the context handling this request, or an empty context + */ + public VirtualHost.ContextInfo getContext() { + return context != null ? context : (context = getVirtualHost().getContext(getPath())); + } + } + + /** + * The {@code Response} class encapsulates a single HTTP response. + */ + public class Response implements Closeable { + + protected OutputStream out; // the underlying output stream + protected OutputStream[] encoders = new OutputStream[4]; // chained encoder streams + protected Headers headers; + protected boolean discardBody; + protected int state; // nothing sent, headers sent, or closed + protected Request req; // request used in determining client capabilities + + /** + * Constructs a Response whose output is written to the given stream. + * + * @param out the stream to which the response is written + */ + public Response(OutputStream out) { + this.out = out; + this.headers = new Headers(); + } + + /** + * Sets whether this response's body is discarded or sent. + * + * @param discardBody specifies whether the body is discarded or not + */ + public void setDiscardBody(boolean discardBody) { + this.discardBody = discardBody; + } + + /** + * Sets the request which is used in determining the capabilities + * supported by the client (e.g. compression, encoding, etc.) + * + * @param req the request + */ + public void setClientCapabilities(Request req) { this.req = req; } + + /** + * Returns the request headers collection. + * + * @return the request headers collection + */ + public Headers getHeaders() { return headers; } + + /** + * Returns the underlying output stream to which the response is written. + * Except for special cases, you should use {@link #getBody()} instead. + * + * @return the underlying output stream to which the response is written + */ + public OutputStream getOutputStream() { return out; } + + /** + * Returns whether the response headers were already sent. + * + * @return whether the response headers were already sent + */ + public boolean headersSent() { return state == 1; } + + /** + * Returns an output stream into which the response body can be written. + * The stream applies encodings (e.g. compression) according to the sent headers. + * This method must be called after response headers have been sent + * that indicate there is a body. Normally, the content should be + * prepared (not sent) even before the headers are sent, so that any + * errors during processing can be caught and a proper error response returned - + * after the headers are sent, it's too late to change the status into an error. + * + * @return an output stream into which the response body can be written, + * or null if the body should not be written (e.g. it is discarded) + * @throws IOException if an error occurs + */ + public OutputStream getBody() throws IOException { + if (encoders[0] != null || discardBody) + return encoders[0]; // return the existing stream (or null) + // set up chain of encoding streams according to headers + List te = Arrays.asList(splitElements(headers.get("Transfer-Encoding"), true)); + List ce = Arrays.asList(splitElements(headers.get("Content-Encoding"), true)); + int i = encoders.length - 1; + encoders[i] = new FilterOutputStream(out) { + @Override + public void close() {} // keep underlying connection stream open for now + @Override // override the very inefficient default implementation + public void write(byte[] b, int off, int len) throws IOException { out.write(b, off, len); } + }; + if (te.contains("chunked")) + encoders[--i] = new ChunkedOutputStream(encoders[i + 1]); + if (ce.contains("gzip") || te.contains("gzip")) + encoders[--i] = new GZIPOutputStream(encoders[i + 1], 4096); + else if (ce.contains("deflate") || te.contains("deflate")) + encoders[--i] = new DeflaterOutputStream(encoders[i + 1]); + encoders[0] = encoders[i]; + encoders[i] = null; // prevent duplicate reference + return encoders[0]; // returned stream is always first + } + + /** + * Closes this response and flushes all output. + * + * @throws IOException if an error occurs + */ + public void close() throws IOException { + state = -1; // closed + if (encoders[0] != null) + encoders[0].close(); // close all chained streams (except the underlying one) + out.flush(); // always flush underlying stream (even if getBody was never called) + } + + /** + * Sends the response headers with the given response status. + * A Date header is added if it does not already exist. + * If the response has a body, the Content-Length/Transfer-Encoding + * and Content-Type headers must be set before sending the headers. + * + * @param status the response status + * @throws IOException if an error occurs or headers were already sent + * @see #sendHeaders(int, long, long, String, String, long[]) + */ + public void sendHeaders(int status) throws IOException { + if (headersSent()) + throw new IOException("headers were already sent"); + if (!headers.contains("Date")) + headers.add("Date", formatDate(System.currentTimeMillis())); + headers.add("Server", "JLHTTP/2.4"); + out.write(getBytes("HTTP/1.1 ", Integer.toString(status), " ", statuses[status])); + out.write(CRLF); + headers.writeTo(out); + state = 1; // headers sent + } + + /** + * Sends the response headers, including the given response status + * and description, and all response headers. If they do not already + * exist, the following headers are added as necessary: + * Content-Range, Content-Type, Transfer-Encoding, Content-Encoding, + * Content-Length, Last-Modified, ETag, Connection and Date. Ranges are + * properly calculated as well, with a 200 status changed to a 206 status. + * + * @param status the response status + * @param length the response body length, or zero if there is no body, + * or negative if there is a body but its length is not yet known + * @param lastModified the last modified date of the response resource, + * or non-positive if unknown. A time in the future will be + * replaced with the current system time. + * @param etag the ETag of the response resource, or null if unknown + * (see RFC2616#3.11) + * @param contentType the content type of the response resource, or null + * if unknown (in which case "application/octet-stream" will be sent) + * @param range the content range that will be sent, or null if the + * entire resource will be sent + * @throws IOException if an error occurs + */ + public void sendHeaders(int status, long length, long lastModified, + String etag, String contentType, long[] range) throws IOException { + if (range != null) { + headers.add("Content-Range", "bytes " + range[0] + "-" + + range[1] + "/" + (length >= 0 ? length : "*")); + length = range[1] - range[0] + 1; + if (status == 200) + status = 206; + } + String ct = headers.get("Content-Type"); + if (ct == null) { + ct = contentType != null ? contentType : "application/octet-stream"; + headers.add("Content-Type", ct); + } + if (!headers.contains("Content-Length") && !headers.contains("Transfer-Encoding")) { + // RFC2616#3.6: transfer encodings are case-insensitive and must not be sent to an HTTP/1.0 client + boolean modern = req != null && req.getVersion().endsWith("1.1"); + String accepted = req == null ? null : req.getHeaders().get("Accept-Encoding"); + List encodings = Arrays.asList(splitElements(accepted, true)); + String compression = encodings.contains("gzip") ? "gzip" : + encodings.contains("deflate") ? "deflate" : null; + if (compression != null && (length < 0 || length > 300) && isCompressible(ct) && modern) { + headers.add("Transfer-Encoding", "chunked"); // compressed data is always unknown length + headers.add("Content-Encoding", compression); + } else if (length < 0 && modern) { + headers.add("Transfer-Encoding", "chunked"); // unknown length + } else if (length >= 0) { + headers.add("Content-Length", Long.toString(length)); // known length + } + } + if (!headers.contains("Vary")) // RFC7231#7.1.4: Vary field should include headers + headers.add("Vary", "Accept-Encoding"); // that are used in selecting representation + if (lastModified > 0 && !headers.contains("Last-Modified")) // RFC2616#14.29 + headers.add("Last-Modified", formatDate(Math.min(lastModified, System.currentTimeMillis()))); + if (etag != null && !headers.contains("ETag")) + headers.add("ETag", etag); + if (req != null && "close".equalsIgnoreCase(req.getHeaders().get("Connection")) + && !headers.contains("Connection")) + headers.add("Connection", "close"); // #RFC7230#6.6: should reply to close with close + sendHeaders(status); + } + + /** + * Sends the full response with the given status, and the given string + * as the body. The text is sent in the UTF-8 charset. If a + * Content-Type header was not explicitly set, it will be set to + * text/html, and so the text must contain valid (and properly + * {@link HTTPServer#escapeHTML escaped}) HTML. + * + * @param status the response status + * @param text the text body (sent as text/html) + * @throws IOException if an error occurs + */ + public void send(int status, String text) throws IOException { + byte[] content = text.getBytes("UTF-8"); + sendHeaders(status, content.length, -1, + "W/\"" + Integer.toHexString(text.hashCode()) + "\"", + "text/html; charset=utf-8", null); + OutputStream out = getBody(); + if (out != null) + out.write(content); + } + + public void send(int status, byte[] text) throws IOException { + //byte[] content = text.getBytes("UTF-8"); + byte[] content = text; + sendHeaders(status, content.length, -1, + "W/\"" + Integer.toHexString(text.hashCode()) + "\"", + "text/html; charset=utf-8", null); + OutputStream out = getBody(); + if (out != null) + out.write(content); + } + + /** + * Sends an error response with the given status and detailed message. + * An HTML body is created containing the status and its description, + * as well as the message, which is escaped using the + * {@link HTTPServer#escapeHTML escape} method. + * + * @param status the response status + * @param text the text body (sent as text/html) + * @throws IOException if an error occurs + */ + public void sendError(int status, String text) throws IOException { + send(status, String.format( + "%n%n%d %s%n" + + "

%d %s

%n

%s

%n", + status, statuses[status], status, statuses[status], escapeHTML(text))); + } + + /** + * Sends an error response with the given status and default body. + * + * @param status the response status + * @throws IOException if an error occurs + */ + public void sendError(int status) throws IOException { + String text = status < 400 ? ":)" : "sorry it didn't work out :("; + sendError(status, text); + } + + /** + * Sends the response body. This method must be called only after the + * response headers have been sent (and indicate that there is a body). + * + * @param body a stream containing the response body + * @param length the full length of the response body + * @param range the sub-range within the response body that should be + * sent, or null if the entire body should be sent + * @throws IOException if an error occurs + */ + public void sendBody(InputStream body, long length, long[] range) throws IOException { + OutputStream out = getBody(); + if (out != null) { + if (range != null) { + long offset = range[0]; + length = range[1] - range[0] + 1; + while (offset > 0) { + long skip = body.skip(offset); + if (skip == 0) + throw new IOException("can't skip to " + range[0]); + offset -= skip; + } + } + transfer(body, out, length); + } + } + + /** + * Sends a 301 or 302 response, redirecting the client to the given URL. + * + * @param url the absolute URL to which the client is redirected + * @param permanent specifies whether a permanent (301) or + * temporary (302) redirect status is sent + * @throws IOException if an IO error occurs or url is malformed + */ + public void redirect(String url, boolean permanent) throws IOException { + try { + url = new URI(url).toASCIIString(); + } catch (URISyntaxException e) { + throw new IOException("malformed URL: " + url); + } + headers.add("Location", url); + // some user-agents expect a body, so we send it + if (permanent) + sendError(301, "Permanently moved to " + url); + else + sendError(302, "Temporarily moved to " + url); + } + } + + /** + * The {@code SocketHandlerThread} handles accepted sockets. + */ + protected class SocketHandlerThread extends Thread { + @Override + public void run() { + setName(getClass().getSimpleName() + "-" + port); + try { + ServerSocket serv = HTTPServer.this.serv; // keep local to avoid NPE when stopped + while (serv != null && !serv.isClosed()) { + final Socket sock = serv.accept(); + executor.execute(new Runnable() { + public void run() { + try { + try { + sock.setSoTimeout(socketTimeout); + sock.setTcpNoDelay(true); // we buffer anyway, so improve latency + handleConnection(sock.getInputStream(), sock.getOutputStream()); + } finally { + try { + // RFC7230#6.6 - close socket gracefully + // (except SSL socket which doesn't support half-closing) + if (!(sock instanceof SSLSocket)) { + sock.shutdownOutput(); // half-close socket (only output) + transfer(sock.getInputStream(), null, -1); // consume input + } + } finally { + sock.close(); // and finally close socket fully + } + } + } catch (IOException ignore) {} + } + }); + } + } catch (IOException ignore) {} + } + } + + protected volatile int port; + protected volatile int socketTimeout = 10000; + protected volatile ServerSocketFactory serverSocketFactory; + protected volatile boolean secure; + protected volatile Executor executor; + protected volatile ServerSocket serv; + protected final Map hosts = new ConcurrentHashMap(); + + /** + * Constructs an HTTPServer which can accept connections on the given port. + * Note: the {@link #start()} method must be called to start accepting + * connections. + * + * @param port the port on which this server will accept connections + */ + public HTTPServer(int port) { + setPort(port); + addVirtualHost(new VirtualHost(null)); // add default virtual host + } + + /** + * Constructs an HTTPServer which can accept connections on the default HTTP port 80. + * Note: the {@link #start()} method must be called to start accepting connections. + */ + public HTTPServer() { + this(80); + } + + /** + * Sets the port on which this server will accept connections. + * + * @param port the port on which this server will accept connections + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Sets the factory used to create the server socket. + * If null or not set, the default {@link ServerSocketFactory#getDefault()} is used. + * For secure sockets (HTTPS), use an SSLServerSocketFactory instance. + * The port should usually also be changed for HTTPS, e.g. port 443 instead of 80. + *

+ * If using the default SSLServerSocketFactory returned by + * {@link SSLServerSocketFactory#getDefault()}, the appropriate system properties + * must be set to configure the default JSSE provider, such as + * {@code javax.net.ssl.keyStore} and {@code javax.net.ssl.keyStorePassword}. + * + * @param factory the server socket factory to use + */ + public void setServerSocketFactory(ServerSocketFactory factory) { + this.serverSocketFactory = factory; + this.secure = factory instanceof SSLServerSocketFactory; + } + + /** + * Sets the socket timeout for established connections. + * + * @param timeout the socket timeout in milliseconds + */ + public void setSocketTimeout(int timeout) { this.socketTimeout = timeout; } + + /** + * Sets the executor used in servicing HTTP connections. + * If null, a default executor is used. The caller is responsible + * for shutting down the provided executor when necessary. + * + * @param executor the executor to use + */ + public void setExecutor(Executor executor) { + this.executor = executor; + } + + /** + * Returns the virtual host with the given name. + * + * @param name the name of the virtual host to return, or null for + * the default virtual host + * @return the virtual host with the given name, or null if it doesn't exist + */ + public VirtualHost getVirtualHost(String name) { + return hosts.get(name == null ? VirtualHost.DEFAULT_HOST_NAME : name); + } + + /** + * Returns all virtual hosts. + * + * @return all virtual hosts (as an unmodifiable set) + */ + public Set getVirtualHosts() { + return Collections.unmodifiableSet(new HashSet(hosts.values())); + } + + /** + * Adds the given virtual host to the server. + * If the host's name or aliases already exist, they are overwritten. + * + * @param host the virtual host to add + */ + public void addVirtualHost(VirtualHost host) { + String name = host.getName(); + hosts.put(name == null ? VirtualHost.DEFAULT_HOST_NAME : name, host); + } + + /** + * Creates the server socket used to accept connections, using the configured + * {@link #setServerSocketFactory ServerSocketFactory} and {@link #setPort port}. + *

+ * Cryptic errors seen here often mean the factory configuration details are wrong. + * + * @return the created server socket + * @throws IOException if the socket cannot be created + */ + protected ServerSocket createServerSocket() throws IOException { + ServerSocket serv = serverSocketFactory.createServerSocket(); + serv.setReuseAddress(true); + serv.bind(new InetSocketAddress(port)); + return serv; + } + + /** + * Starts this server. If it is already started, does nothing. + * Note: Once the server is started, configuration-altering methods + * of the server and its virtual hosts must not be used. To modify the + * configuration, the server must first be stopped. + * + * @throws IOException if the server cannot begin accepting connections + */ + public synchronized void start() throws IOException { + if (serv != null) + return; + if (serverSocketFactory == null) // assign default server socket factory if needed + serverSocketFactory = ServerSocketFactory.getDefault(); // plain sockets + serv = createServerSocket(); + if (executor == null) // assign default executor if needed + executor = Executors.newCachedThreadPool(); // consumes no resources when idle + // register all host aliases (which may have been modified) + for (VirtualHost host : getVirtualHosts()) + for (String alias : host.getAliases()) + hosts.put(alias, host); + // start handling incoming connections + new SocketHandlerThread().start(); + } + + /** + * Stops this server. If it is already stopped, does nothing. + * Note that if an {@link #setExecutor Executor} was set, it must be closed separately. + */ + public synchronized void stop() { + try { + if (serv != null) + serv.close(); + } catch (IOException ignore) {} + serv = null; + } + + /** + * Handles communications for a single connection over the given streams. + * Multiple subsequent transactions are handled on the connection, + * until the streams are closed, an error occurs, or the request + * contains a "Connection: close" header which explicitly requests + * the connection be closed after the transaction ends. + * + * @param in the stream from which the incoming requests are read + * @param out the stream into which the outgoing responses are written + * @throws IOException if an error occurs + */ + protected void handleConnection(InputStream in, OutputStream out) throws IOException { + in = new BufferedInputStream(in, 4096); + out = new BufferedOutputStream(out, 4096); + Request req; + Response resp; + do { + // create request and response and handle transaction + req = null; + resp = new Response(out); + try { + req = new Request(in); + handleTransaction(req, resp); + } catch (Throwable t) { // unhandled errors (not normal error responses like 404) + if (req == null) { // error reading request + if (t instanceof IOException && t.getMessage().contains("missing request line")) + break; // we're not in the middle of a transaction - so just disconnect + resp.getHeaders().add("Connection", "close"); // about to close connection + if (t instanceof InterruptedIOException) // e.g. SocketTimeoutException + resp.sendError(408, "Timeout waiting for client request"); + else + resp.sendError(400, "Invalid request: " + t.getMessage()); + } else if (!resp.headersSent()) { // if headers were not already sent, we can send an error response + resp = new Response(out); // ignore whatever headers may have already been set + resp.getHeaders().add("Connection", "close"); // about to close connection + resp.sendError(500, "Error processing request: " + t.getMessage()); + } // otherwise just abort the connection since we can't recover + break; // proceed to close connection + } finally { + resp.close(); // close response and flush output + } + // consume any leftover body data so next request can be processed + transfer(req.getBody(), null, -1); + // RFC7230#6.6: persist connection unless client or server close explicitly (or legacy client) + } while (!"close".equalsIgnoreCase(req.getHeaders().get("Connection")) + && !"close".equalsIgnoreCase(resp.getHeaders().get("Connection")) && req.getVersion().endsWith("1.1")); + } + + /** + * Handles a single transaction on a connection. + * + * Subclasses can override this method to perform filtering on the + * request or response, apply wrappers to them, or further customize + * the transaction processing in some other way. + * + * @param req the transaction request + * @param resp the transaction response (into which the response is written) + * @throws IOException if and error occurs + */ + protected void handleTransaction(Request req, Response resp) throws IOException { + resp.setClientCapabilities(req); + if (preprocessTransaction(req, resp)) + handleMethod(req, resp); + } + + /** + * Preprocesses a transaction, performing various validation checks + * and required special header handling, possibly returning an + * appropriate response. + * + * @param req the request + * @param resp the response + * @return whether further processing should be performed on the transaction + * @throws IOException if an error occurs + */ + protected boolean preprocessTransaction(Request req, Response resp) throws IOException { + Headers reqHeaders = req.getHeaders(); + // validate request + String version = req.getVersion(); + if (version.equals("HTTP/1.1")) { + if (!reqHeaders.contains("Host")) { + // RFC2616#14.23: missing Host header gets 400 + resp.sendError(400, "Missing required Host header"); + return false; + } + // return a continue response before reading body + String expect = reqHeaders.get("Expect"); + if (expect != null) { + if (expect.equalsIgnoreCase("100-continue")) { + Response tempResp = new Response(resp.getOutputStream()); + tempResp.sendHeaders(100); + resp.getOutputStream().flush(); + } else { + // RFC2616#14.20: if unknown expect, send 417 + resp.sendError(417); + return false; + } + } + } else if (version.equals("HTTP/1.0") || version.equals("HTTP/0.9")) { + // RFC2616#14.10 - remove connection headers from older versions + for (String token : splitElements(reqHeaders.get("Connection"), false)) + reqHeaders.remove(token); + } else { + resp.sendError(400, "Unknown version: " + version); + return false; + } + return true; + } + + /** + * Handles a transaction according to the request method. + * + * @param req the transaction request + * @param resp the transaction response (into which the response is written) + * @throws IOException if and error occurs + */ + protected void handleMethod(Request req, Response resp) throws IOException { + String method = req.getMethod(); + Map handlers = req.getContext().getHandlers(); + // RFC 2616#5.1.1 - GET and HEAD must be supported + if (method.equals("GET") || handlers.containsKey(method)) { + serve(req, resp); // method is handled by context handler (or 404) + } else if (method.equals("HEAD")) { // default HEAD handler + req.method = "GET"; // identical to a GET + resp.setDiscardBody(true); // process normally but discard body + serve(req, resp); + } else if (method.equals("TRACE")) { // default TRACE handler + handleTrace(req, resp); + } else { + Set methods = new LinkedHashSet(); + methods.addAll(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS")); // built-in methods + // "*" is a special server-wide (no-context) request supported by OPTIONS + boolean isServerOptions = req.getPath().equals("*") && method.equals("OPTIONS"); + methods.addAll(isServerOptions ? req.getVirtualHost().getMethods() : handlers.keySet()); + resp.getHeaders().add("Allow", join(", ", methods)); + if (method.equals("OPTIONS")) { // default OPTIONS handler + resp.getHeaders().add("Content-Length", "0"); // RFC2616#9.2 + resp.sendHeaders(200); + } else if (req.getVirtualHost().getMethods().contains(method)) { + resp.sendHeaders(405); // supported by server, but not this context (nor built-in) + } else { + resp.sendError(501); // unsupported method + } + } + } + + /** + * Handles a TRACE method request. + * + * @param req the request + * @param resp the response into which the content is written + * @throws IOException if an error occurs + */ + public void handleTrace(Request req, Response resp) throws IOException { + resp.sendHeaders(200, -1, -1, null, "message/http", null); + OutputStream out = resp.getBody(); + out.write(getBytes("TRACE ", req.getURI().toString(), " ", req.getVersion())); + out.write(CRLF); + req.getHeaders().writeTo(out); + transfer(req.getBody(), out, -1); + } + + /** + * Serves the content for a request by invoking the context + * handler for the requested context (path) and HTTP method. + * + * @param req the request + * @param resp the response into which the content is written + * @throws IOException if an error occurs + */ + protected void serve(Request req, Response resp) throws IOException { + // get context handler to handle request + ContextHandler handler = req.getContext().getHandlers().get(req.getMethod()); + if (handler == null) { + resp.sendError(404); + return; + } + // serve request + int status = 404; + // add directory index if necessary + String path = req.getPath(); + if (path.endsWith("/")) { + String index = req.getVirtualHost().getDirectoryIndex(); + if (index != null) { + req.setPath(path + index); + status = handler.serve(req, resp); + req.setPath(path); + } + } + if (status == 404) + status = handler.serve(req, resp); + if (status > 0) + resp.sendError(status); + } + + /** + * Adds a Content-Type mapping for the given path suffixes. + * If any of the path suffixes had a previous Content-Type associated + * with it, it is replaced with the given one. Path suffixes are + * considered case-insensitive, and contentType is converted to lowercase. + * + * @param contentType the content type (MIME type) to be associated with + * the given path suffixes + * @param suffixes the path suffixes which will be associated with + * the contentType, e.g. the file extensions of served files + * (excluding the '.' character) + */ + public static void addContentType(String contentType, String... suffixes) { + for (String suffix : suffixes) + contentTypes.put(suffix.toLowerCase(Locale.US), contentType.toLowerCase(Locale.US)); + } + + /** + * Adds Content-Type mappings from a standard mime.types file. + * + * @param mimeTypes a mime.types file + * @throws IOException if an error occurs + * @throws FileNotFoundException if the file is not found or cannot be read + */ + public static void addContentTypes(File mimeTypes) throws IOException { + InputStream in = new FileInputStream(mimeTypes); + try { + while (true) { + String line = readLine(in).trim(); // throws EOFException when done + if (line.length() > 0 && line.charAt(0) != '#') { + String[] tokens = split(line, " \t", -1); + for (int i = 1; i < tokens.length; i++) + addContentType(tokens[0], tokens[i]); + } + } + } catch (EOFException ignore) { // the end of file was reached - it's ok + } finally { + in.close(); + } + } + + /** + * Returns the content type for the given path, according to its suffix, + * or the given default content type if none can be determined. + * + * @param path the path whose content type is requested + * @param def a default content type which is returned if none can be + * determined + * @return the content type for the given path, or the given default + */ + public static String getContentType(String path, String def) { + int dot = path.lastIndexOf('.'); + String type = dot < 0 ? def : contentTypes.get(path.substring(dot + 1).toLowerCase(Locale.US)); + return type != null ? type : def; + } + + /** + * Checks whether data of the given content type (MIME type) is compressible. + * + * @param contentType the content type + * @return true if the data is compressible, false if not + */ + public static boolean isCompressible(String contentType) { + int pos = contentType.indexOf(';'); // exclude params + String ct = pos < 0 ? contentType : contentType.substring(0, pos); + for (String s : compressibleContentTypes) + if (s.equals(ct) || s.charAt(0) == '*' && ct.endsWith(s.substring(1)) + || s.charAt(s.length() - 1) == '*' && ct.startsWith(s.substring(0, s.length() - 1))) + return true; + return false; + } + + /** + * Returns the local host's auto-detected name. + * + * @return the local host name + */ + public static String detectLocalHostName() { + try { + return InetAddress.getLocalHost().getCanonicalHostName(); + } catch (UnknownHostException uhe) { + return "localhost"; + } + } + + /** + * Parses name-value pair parameters from the given "x-www-form-urlencoded" + * MIME-type string. This is the encoding used both for parameters passed + * as the query of an HTTP GET method, and as the content of HTML forms + * submitted using the HTTP POST method (as long as they use the default + * "application/x-www-form-urlencoded" encoding in their ENCTYPE attribute). + * UTF-8 encoding is assumed. + *

+ * The parameters are returned as a list of string arrays, each containing + * the parameter name as the first element and its corresponding value + * as the second element (or an empty string if there is no value). + *

+ * The list retains the original order of the parameters. + * + * @param s an "application/x-www-form-urlencoded" string + * @return the parameter name-value pairs parsed from the given string, + * or an empty list if there are none + */ + public static List parseParamsList(String s) { + if (s == null || s.length() == 0) + return Collections.emptyList(); + List params = new ArrayList(8); + for (String pair : split(s, "&", -1)) { + int pos = pair.indexOf('='); + String name = pos < 0 ? pair : pair.substring(0, pos); + String val = pos < 0 ? "" : pair.substring(pos + 1); + try { + name = URLDecoder.decode(name.trim(), "UTF-8"); + val = URLDecoder.decode(val.trim(), "UTF-8"); + if (name.length() > 0) + params.add(new String[] { name, val }); + } catch (UnsupportedEncodingException ignore) {} // never thrown + } + return params; + } + + /** + * Converts a collection of pairs of objects (arrays of size two, + * each representing a key and corresponding value) into a Map. + * Duplicate keys are ignored (only the first occurrence of each key is considered). + * The map retains the original collection's iteration order. + * + * @param pairs a collection of arrays, each containing a key and corresponding value + * @param the key type + * @param the value type + * @return a map containing the paired keys and values, or an empty map + */ + @SuppressWarnings("unchecked") + public static Map toMap(Collection pairs) { + if (pairs == null || pairs.isEmpty()) + return Collections.emptyMap(); + Map map = new LinkedHashMap(pairs.size()); + for (Object[] pair : pairs) + if (!map.containsKey(pair[0])) + map.put((K)pair[0], (V)pair[1]); + return map; + } + + /** + * Returns the absolute (zero-based) content range value specified + * by the given range string. If multiple ranges are requested, a single + * range containing all of them is returned. + * + * @param range the string containing the range description + * @param length the full length of the requested resource + * @return the requested range, or null if the range value is invalid + */ + public static long[] parseRange(String range, long length) { + long min = Long.MAX_VALUE; + long max = Long.MIN_VALUE; + try { + for (String token : splitElements(range, false)) { + long start, end; + int dash = token.indexOf('-'); + if (dash == 0) { // suffix range + start = length - parseULong(token.substring(1), 10); + end = length - 1; + } else if (dash == token.length() - 1) { // open range + start = parseULong(token.substring(0, dash), 10); + end = length - 1; + } else { // explicit range + start = parseULong(token.substring(0, dash), 10); + end = parseULong(token.substring(dash + 1), 10); + } + if (end < start) + throw new RuntimeException(); + if (start < min) + min = start; + if (end > max) + max = end; + } + if (max < 0) // no tokens + throw new RuntimeException(); + if (max >= length && min < length) + max = length - 1; + return new long[] { min, max }; // start might be >= length! + } catch (RuntimeException re) { // NFE, IOOBE or explicit RE + return null; // RFC2616#14.35.1 - ignore header if invalid + } + } + + /** + * Parses an unsigned long value. This method behaves the same as calling + * {@link Long#parseLong(String, int)}, but considers the string invalid + * if it starts with an ASCII minus sign ('-') or plus sign ('+'). + * + * @param s the String containing the long representation to be parsed + * @param radix the radix to be used while parsing s + * @return the long represented by s in the specified radix + * @throws NumberFormatException if the string does not contain a parsable + * long, or if it starts with an ASCII minus sign or plus sign + */ + public static long parseULong(String s, int radix) throws NumberFormatException { + long val = Long.parseLong(s, radix); // throws NumberFormatException + if (s.charAt(0) == '-' || s.charAt(0) == '+') + throw new NumberFormatException("invalid digit: " + s.charAt(0)); + return val; + } + + /** + * Parses a date string in one of the supported {@link #DATE_PATTERNS}. + * + * Received date header values must be in one of the following formats: + * Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 + * Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 + * Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format + * + * @param time a string representation of a time value + * @return the parsed date value + * @throws IllegalArgumentException if the given string does not contain + * a valid date format in any of the supported formats + */ + public static Date parseDate(String time) { + for (String pattern : DATE_PATTERNS) { + try { + SimpleDateFormat df = new SimpleDateFormat(pattern, Locale.US); + df.setLenient(false); + df.setTimeZone(GMT); + return df.parse(time); + } catch (ParseException ignore) {} + } + throw new IllegalArgumentException("invalid date format: " + time); + } + + /** + * Formats the given time value as a string in RFC 1123 format. + * + * @param time the time in milliseconds since January 1, 1970, 00:00:00 GMT + * @return the given time value as a string in RFC 1123 format + */ + public static String formatDate(long time) { + // this implementation performs far better than SimpleDateFormat instances, and even + // quite better than ThreadLocal SDFs - the server's CPU-bound benchmark gains over 20%! + if (time < -62167392000000L || time > 253402300799999L) + throw new IllegalArgumentException("year out of range (0001-9999): " + time); + char[] s = "DAY, 00 MON 0000 00:00:00 GMT".toCharArray(); // copy the format template + Calendar cal = new GregorianCalendar(GMT, Locale.US); + cal.setTimeInMillis(time); + System.arraycopy(DAYS, 4 * (cal.get(Calendar.DAY_OF_WEEK) - 1), s, 0, 3); + System.arraycopy(MONTHS, 4 * cal.get(Calendar.MONTH), s, 8, 3); + int n = cal.get(Calendar.DATE); s[5] += n / 10; s[6] += n % 10; + n = cal.get(Calendar.YEAR); s[12] += n / 1000; s[13] += n / 100 % 10; + s[14] += n / 10 % 10; s[15] += n % 10; + n = cal.get(Calendar.HOUR_OF_DAY); s[17] += n / 10; s[18] += n % 10; + n = cal.get(Calendar.MINUTE); s[20] += n / 10; s[21] += n % 10; + n = cal.get(Calendar.SECOND); s[23] += n / 10; s[24] += n % 10; + return new String(s); + } + + /** + * Splits the given element list string (comma-separated header value) + * into its constituent non-empty trimmed elements. + * (RFC2616#2.1: element lists are delimited by a comma and optional LWS, + * and empty elements are ignored). + * + * @param list the element list string + * @param lower specifies whether the list elements should be lower-cased + * @return the non-empty elements in the list, or an empty array + */ + public static String[] splitElements(String list, boolean lower) { + return split(lower && list != null ? list.toLowerCase(Locale.US) : list, ",", -1); + } + + /** + * Splits the given string into its constituent non-empty trimmed elements, + * which are delimited by any of the given delimiter characters. + * This is a more direct and efficient implementation than using a regex + * (e.g. String.split()), trimming the elements and removing empty ones. + * + * @param str the string to split + * @param delimiters the characters used as the delimiters between elements + * @param limit if positive, limits the returned array size (remaining of str in last element) + * @return the non-empty elements in the string, or an empty array + */ + public static String[] split(String str, String delimiters, int limit) { + if (str == null) + return new String[0]; + Collection elements = new ArrayList(); + int len = str.length(); + int start = 0; + int end; + while (start < len) { + for (end = --limit == 0 ? len : start; + end < len && delimiters.indexOf(str.charAt(end)) < 0; end++); + String element = str.substring(start, end).trim(); + if (element.length() > 0) + elements.add(element); + start = end + 1; + } + return elements.toArray(new String[elements.size()]); + } + + /** + * Returns a string constructed by joining the string representations of the + * iterated objects (in order), with the delimiter inserted between them. + * + * @param delim the delimiter that is inserted between the joined strings + * @param items the items whose string representations are joined + * @param the item type + * @return the joined string + */ + public static String join(String delim, Iterable items) { + StringBuilder sb = new StringBuilder(); + for (Iterator it = items.iterator(); it.hasNext(); ) + sb.append(it.next()).append(it.hasNext() ? delim : ""); + return sb.toString(); + } + + /** + * Returns the parent of the given path. + * + * @param path the path whose parent is returned (must start with '/') + * @return the parent of the given path (excluding trailing slash), + * or null if given path is the root path + */ + public static String getParentPath(String path) { + path = trimRight(path, '/'); // remove trailing slash + int slash = path.lastIndexOf('/'); + return slash < 0 ? null : path.substring(0, slash); + } + + /** + * Returns the given string with all occurrences of the given character + * removed from its right side. + * + * @param s the string to trim + * @param c the character to remove + * @return the trimmed string + */ + public static String trimRight(String s, char c) { + int len = s.length() - 1; + int end; + for (end = len; end >= 0 && s.charAt(end) == c; end--); + return end == len ? s : s.substring(0, end + 1); + } + + /** + * Returns the given string with all occurrences of the given character + * removed from its left side. + * + * @param s the string to trim + * @param c the character to remove + * @return the trimmed string + */ + public static String trimLeft(String s, char c) { + int len = s.length(); + int start; + for (start = 0; start < len && s.charAt(start) == c; start++); + return start == 0 ? s : s.substring(start); + } + + /** + * Trims duplicate consecutive occurrences of the given character within the + * given string, replacing them with a single instance of the character. + * + * @param s the string to trim + * @param c the character to trim + * @return the given string with duplicate consecutive occurrences of c + * replaced by a single instance of c + */ + public static String trimDuplicates(String s, char c) { + int start = 0; + while ((start = s.indexOf(c, start) + 1) > 0) { + int end; + for (end = start; end < s.length() && s.charAt(end) == c; end++); + if (end > start) + s = s.substring(0, start) + s.substring(end); + } + return s; + } + + /** + * Returns a human-friendly string approximating the given data size, + * e.g. "316", "1.8K", "324M", etc. + * + * @param size the size to display + * @return a human-friendly string approximating the given data size + */ + public static String toSizeApproxString(long size) { + final char[] units = { ' ', 'K', 'M', 'G', 'T', 'P', 'E' }; + int u; + double s; + for (u = 0, s = size; s >= 1000; u++, s /= 1024); + return String.format(s < 10 ? "%.1f%c" : "%.0f%c", s, units[u]); + } + + /** + * Returns an HTML-escaped version of the given string for safe display + * within a web page. The characters '&', '>' and '<' must always + * be escaped, and single and double quotes must be escaped within + * attribute values; this method escapes them always. This method can + * be used for generating both HTML and XHTML valid content. + * + * @param s the string to escape + * @return the escaped string + * @see The W3C FAQ + */ + public static String escapeHTML(String s) { + int len = s.length(); + StringBuilder sb = new StringBuilder(len + 30); + int start = 0; + for (int i = 0; i < len; i++) { + String ref = null; + switch (s.charAt(i)) { + case '&': ref = "&"; break; + case '>': ref = ">"; break; + case '<': ref = "<"; break; + case '"': ref = """; break; + case '\'': ref = "'"; break; + } + if (ref != null) { + sb.append(s.substring(start, i)).append(ref); + start = i + 1; + } + } + return start == 0 ? s : sb.append(s.substring(start)).toString(); + } + + /** + * Converts strings to bytes by casting the chars to bytes. + * This is a fast way to encode a string as ISO-8859-1/US-ASCII bytes. + * If multiple strings are provided, their bytes are concatenated. + * + * @param strings the strings to convert (containing only ISO-8859-1 chars) + * @return the byte array + */ + public static byte[] getBytes(String... strings) { + int n = 0; + for (String s : strings) + n += s.length(); + byte[] dest = new byte[n]; + n = 0; + for (String s : strings) + for (int i = 0, len = s.length(); i < len; i++) + dest[n++] = (byte)s.charAt(i); + return dest; + } + + /** + * Transfers data from an input stream to an output stream. + * + * @param in the input stream to transfer from + * @param out the output stream to transfer to (or null to discard output) + * @param len the number of bytes to transfer. If negative, the entire + * contents of the input stream are transferred. + * @throws IOException if an IO error occurs or the input stream ends + * before the requested number of bytes have been read + */ + public static void transfer(InputStream in, OutputStream out, long len) throws IOException { + if (len == 0 || out == null && len < 0 && in.read() < 0) + return; // small optimization - avoid buffer creation + byte[] buf = new byte[4096]; + while (len != 0) { + int count = len < 0 || buf.length < len ? buf.length : (int)len; + count = in.read(buf, 0, count); + if (count < 0) { + if (len > 0) + throw new IOException("unexpected end of stream"); + break; + } + if (out != null) + out.write(buf, 0, count); + len -= len > 0 ? count : 0; + } + } + + /** + * Reads the token starting at the current stream position and ending at + * the first occurrence of the given delimiter byte, in the given encoding. + * + * @param in the stream from which the token is read + * @param delim the byte value which marks the end of the token, + * or -1 if the token ends at the end of the stream + * @param enc a character-encoding name + * @param maxLength the maximum length (in bytes) to read + * @return the read token, excluding the delimiter + * @throws UnsupportedEncodingException if the encoding is not supported + * @throws EOFException if the stream end is reached before a delimiter is found + * @throws IOException if an IO error occurs, or the maximum length + * is reached before the token end is reached + */ + public static String readToken(InputStream in, int delim, + String enc, int maxLength) throws IOException { + // note: we avoid using a ByteArrayOutputStream here because it + // suffers the overhead of synchronization for each byte written + int buflen = maxLength < 512 ? maxLength : 512; // start with less + byte[] buf = new byte[buflen]; + int count = 0; + int c; + while ((c = in.read()) != -1 && c != delim) { + if (count == buflen) { // expand buffer + if (count == maxLength) + throw new IOException("token too large (" + count + ")"); + buflen = maxLength < 2 * buflen ? maxLength : 2 * buflen; + byte[] expanded = new byte[buflen]; + System.arraycopy(buf, 0, expanded, 0, count); + buf = expanded; + } + buf[count++] = (byte)c; + } + if (c < 0 && delim != -1) + throw new EOFException("unexpected end of stream"); + return new String(buf, 0, count, enc); + } + + /** + * Reads the ISO-8859-1 encoded string starting at the current stream + * position and ending at the first occurrence of the LF character. + * + * @param in the stream from which the line is read + * @return the read string, excluding the terminating LF character + * and (if exists) the CR character immediately preceding it + * @throws EOFException if the stream end is reached before an LF character is found + * @throws IOException if an IO error occurs, or the line is longer than 8192 bytes + * @see #readToken(InputStream, int, String, int) + */ + public static String readLine(InputStream in) throws IOException { + String s = readToken(in, '\n', "ISO8859_1", 8192); + return s.length() > 0 && s.charAt(s.length() - 1) == '\r' + ? s.substring(0, s.length() - 1) : s; + } + + /** + * Reads headers from the given stream. Headers are read according to the + * RFC, including folded headers, element lists, and multiple headers + * (which are concatenated into a single element list header). + * Leading and trailing whitespace is removed. + * + * @param in the stream from which the headers are read + * @return the read headers (possibly empty, if none exist) + * @throws IOException if an IO error occurs or the headers are malformed + * or there are more than 100 header lines + */ + public static Headers readHeaders(InputStream in) throws IOException { + Headers headers = new Headers(); + String line; + String prevLine = ""; + int count = 0; + while ((line = readLine(in)).length() > 0) { + int first; + for (first = 0; first < line.length() && + Character.isWhitespace(line.charAt(first)); first++); + if (first > 0) // unfold header continuation line + line = prevLine + ' ' + line.substring(first); + int separator = line.indexOf(':'); + if (separator < 0) + throw new IOException("invalid header: \"" + line + "\""); + String name = line.substring(0, separator); + String value = line.substring(separator + 1).trim(); // ignore LWS + Header replaced = headers.replace(name, value); + // concatenate repeated headers (distinguishing repeated from folded) + if (replaced != null && first == 0) { + value = replaced.getValue() + ", " + value; + line = name + ": " + value; + headers.replace(name, value); + } + prevLine = line; + if (++count > 100) + throw new IOException("too many header lines"); + } + return headers; + } + + /** + * Matches the given ETag value against the given ETags. A match is found + * if the given ETag is not null, and either the ETags contain a "*" value, + * or one of them is identical to the given ETag. If strong comparison is + * used, tags beginning with the weak ETag prefix "W/" never match. + * See RFC2616#3.11, RFC2616#13.3.3. + * + * @param strong if true, strong comparison is used, otherwise weak + * comparison is used + * @param etags the ETags to match against + * @param etag the ETag to match + * @return true if the ETag is matched, false otherwise + */ + public static boolean match(boolean strong, String[] etags, String etag) { + if (etag == null || strong && etag.startsWith("W/")) + return false; + for (String e : etags) + if (e.equals("*") || (e.equals(etag) && !(strong && (e.startsWith("W/"))))) + return true; + return false; + } + + /** + * Calculates the appropriate response status for the given request and + * its resource's last-modified time and ETag, based on the conditional + * headers present in the request. + * + * @param req the request + * @param lastModified the resource's last modified time + * @param etag the resource's ETag + * @return the appropriate response status for the request + */ + public static int getConditionalStatus(Request req, long lastModified, String etag) { + Headers headers = req.getHeaders(); + // If-Match + String header = headers.get("If-Match"); + if (header != null && !match(true, splitElements(header, false), etag)) + return 412; + // If-Unmodified-Since + Date date = headers.getDate("If-Unmodified-Since"); + if (date != null && lastModified > date.getTime()) + return 412; + // If-Modified-Since + int status = 200; + boolean force = false; + date = headers.getDate("If-Modified-Since"); + if (date != null && date.getTime() <= System.currentTimeMillis()) { + if (lastModified > date.getTime()) + force = true; + else + status = 304; + } + // If-None-Match + header = headers.get("If-None-Match"); + if (header != null) { + if (match(false, splitElements(header, false), etag)) // RFC7232#3.2: use weak matching + status = req.getMethod().equals("GET") + || req.getMethod().equals("HEAD") ? 304 : 412; + else + force = true; + } + return force ? 200 : status; + } + + /** + * Serves a context's contents from a file based resource. + * + * The file is located by stripping the given context prefix from + * the request's path, and appending the result to the given base directory. + * + * Missing, forbidden and otherwise invalid files return the appropriate + * error response. Directories are served as an HTML index page if the + * virtual host allows one, or a forbidden error otherwise. Files are + * sent with their corresponding content types, and handle conditional + * and partial retrievals according to the RFC. + * + * @param base the base directory to which the context is mapped + * @param context the context which is mapped to the base directory + * @param req the request + * @param resp the response into which the content is written + * @return the HTTP status code to return, or 0 if a response was sent + * @throws IOException if an error occurs + */ + public static int serveFile(File base, String context, + Request req, Response resp) throws IOException { + String relativePath = req.getPath().substring(context.length()); + File file = new File(base, relativePath).getCanonicalFile(); + if (!file.exists() || file.isHidden() || file.getName().startsWith(".")) { + return 404; + } else if (!file.canRead() || !file.getPath().startsWith(base.getPath())) { // validate + return 403; + } else if (file.isDirectory()) { + if (relativePath.endsWith("/")) { + if (!req.getVirtualHost().isAllowGeneratedIndex()) + return 403; + resp.send(200, createIndex(file, req.getPath())); + } else { // redirect to the normalized directory URL ending with '/' + resp.redirect(req.getBaseURL() + req.getPath() + "/", true); + } + } else if (relativePath.endsWith("/")) { + return 404; // non-directory ending with slash (File constructor removed it) + } else { + serveFileContent(file, req, resp); + } + return 0; + } + + /** + * Serves the contents of a file, with its corresponding content type, + * last modification time, etc. conditional and partial retrievals are + * handled according to the RFC. + * + * @param file the existing and readable file whose contents are served + * @param req the request + * @param resp the response into which the content is written + * @throws IOException if an error occurs + */ + public static void serveFileContent(File file, Request req, Response resp) throws IOException { + long len = file.length(); + long lastModified = file.lastModified(); + String etag = "W/\"" + lastModified + "\""; // a weak tag based on date + int status = 200; + // handle range or conditional request + long[] range = req.getRange(len); + if (range == null || len == 0) { + status = getConditionalStatus(req, lastModified, etag); + } else { + String ifRange = req.getHeaders().get("If-Range"); + if (ifRange == null) { + if (range[0] >= len) + status = 416; // unsatisfiable range + else + status = getConditionalStatus(req, lastModified, etag); + } else if (range[0] >= len) { + // RFC2616#14.16, 10.4.17: invalid If-Range gets everything + range = null; + } else { // send either range or everything + if (!ifRange.startsWith("\"") && !ifRange.startsWith("W/")) { + Date date = req.getHeaders().getDate("If-Range"); + if (date != null && lastModified > date.getTime()) + range = null; // modified - send everything + } else if (!ifRange.equals(etag)) { + range = null; // modified - send everything + } + } + } + // send the response + Headers respHeaders = resp.getHeaders(); + switch (status) { + case 304: // no other headers or body allowed + respHeaders.add("ETag", etag); + respHeaders.add("Vary", "Accept-Encoding"); + respHeaders.add("Last-Modified", formatDate(lastModified)); + resp.sendHeaders(304); + break; + case 412: + resp.sendHeaders(412); + break; + case 416: + respHeaders.add("Content-Range", "bytes */" + len); + resp.sendHeaders(416); + break; + case 200: + // send OK response + resp.sendHeaders(200, len, lastModified, etag, + getContentType(file.getName(), "application/octet-stream"), range); + // send body + InputStream in = new FileInputStream(file); + try { + resp.sendBody(in, len, range); + } finally { + in.close(); + } + break; + default: + resp.sendHeaders(500); // should never happen + break; + } + } + + /** + * Serves the contents of a directory as an HTML file index. + * + * @param dir the existing and readable directory whose contents are served + * @param path the displayed base path corresponding to dir + * @return an HTML string containing the file index for the directory + */ + public static String createIndex(File dir, String path) { + if (!path.endsWith("/")) + path += "/"; + // calculate name column width + int w = 21; // minimum width + for (String name : dir.list()) + if (name.length() > w) + w = name.length(); + w += 2; // with room for added slash and space + // note: we use apache's format, for consistent user experience + Formatter f = new Formatter(Locale.US); + f.format("%n" + + "Index of %s%n" + + "

Index of %s

%n" + + "
 Name%" + (w - 5) + "s Last modified      Size
", + path, path, ""); + if (path.length() > 1) // add parent link if not root path + f.format(" Parent Directory%" + + (w + 5) + "s-%n", getParentPath(path), ""); + for (File file : dir.listFiles()) { + try { + String name = file.getName() + (file.isDirectory() ? "/" : ""); + String size = file.isDirectory() ? "- " : toSizeApproxString(file.length()); + // properly url-encode the link + String link = new URI(null, path + name, null).toASCIIString(); + if (!file.isHidden() && !name.startsWith(".")) + f.format(" %s%-" + (w - name.length()) + + "s‎%td-%"); + return f.toString(); + } + + /** + * Starts a stand-alone HTTP server, serving files from disk. + * + * @param args the command line arguments + */ + // public static void main(String[] args) { + // try { + // if (args.length == 0) { + // System.err.printf("Usage: java [-options] %s [port]%n" + + // "To enable SSL: specify options -Djavax.net.ssl.keyStore, " + + // "-Djavax.net.ssl.keyStorePassword, etc.%n", HTTPServer.class.getName()); + // return; + // } + // File dir = new File(args[0]); + // if (!dir.canRead()) + // throw new FileNotFoundException(dir.getAbsolutePath()); + // int port = args.length < 2 ? 80 : Integer.parseInt(args[1]); + // // set up server + // for (File f : Arrays.asList(new File("/etc/mime.types"), new File(dir, ".mime.types"))) + // if (f.exists()) + // addContentTypes(f); + // HTTPServer server = new HTTPServer(port); + // if (System.getProperty("javax.net.ssl.keyStore") != null) // enable SSL if configured + // server.setServerSocketFactory(SSLServerSocketFactory.getDefault()); + // VirtualHost host = server.getVirtualHost(null); // default host + // host.setAllowGeneratedIndex(true); // with directory index pages + // host.addContext("/", new FileContextHandler(dir)); + // host.addContext("/api/time", new ContextHandler() { + // public int serve(Request req, Response resp) throws IOException { + // long now = System.currentTimeMillis(); + // resp.getHeaders().add("Content-Type", "text/plain"); + // resp.send(200, String.format("%tF %