├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── SERVER_README.md ├── build.sbt ├── lib └── test-docroot.jar ├── project ├── CiPublishPlugin.scala ├── build.properties └── plugins.sbt └── src ├── main └── scala │ └── io │ └── shaka │ └── http │ ├── ClientHttpHandler.scala │ ├── ContentType.scala │ ├── Cookie.scala │ ├── Entity.scala │ ├── FormParameter.scala │ ├── FormParameters.scala │ ├── Handlers.scala │ ├── Headers.scala │ ├── Http.scala │ ├── HttpHeader.scala │ ├── HttpServer.scala │ ├── Https.scala │ ├── IO.scala │ ├── Method.scala │ ├── QueryParameters.scala │ ├── Request.scala │ ├── RequestMatching.scala │ ├── Response.scala │ ├── StaticResponse.scala │ ├── Status.scala │ ├── SunHttpHandlerAdapter.scala │ ├── TrustAllSslCertificates.scala │ ├── Zero.scala │ └── proxy.scala └── test ├── resources ├── certs │ ├── client-certificate-unrecognised-by-server.jks │ ├── client-truststore.jks │ ├── keystore-testing-client.jks │ ├── keystore-testing.jks │ ├── naive-testing-client.crt │ └── server-truststore.jks └── web │ ├── clocks.png │ ├── specification.pdf │ ├── test.css │ ├── test.html │ └── testdir │ ├── test.csv │ ├── test.txt │ └── testsubdir │ └── test.txt └── scala └── io └── shaka └── http ├── FormParameterSpec.scala ├── FormParametersSpec.scala ├── HeadersSpec.scala ├── HttpServerSpec.scala ├── HttpServerSpecSupport.scala ├── HttpSpec.scala ├── HttpsSpec.scala ├── MutualSslAuthSpec.scala ├── QueryParametersSpec.scala ├── RequestAssertions.scala ├── ServingStaticResourcesSpec.scala ├── SslAuthSpec.scala ├── TestCerts.scala ├── TestHttpServer.scala └── pdf-sample.pdf /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: ["*"] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - uses: actions/checkout@v2.3.4 10 | with: 11 | fetch-depth: 0 12 | - uses: olafurpg/setup-scala@v10 13 | - run: sbt clean test 14 | - uses: blended-zio/setup-gpg@v3 15 | - run: sbt ci-publish 16 | env: 17 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 18 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 19 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 20 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .idea_modules 3 | target 4 | todo.txt 5 | publish/releases 6 | publish/gpg 7 | bin 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | naive-http 2 | ========== 3 | 4 | [![Build Status][badge-build]][link-build] 5 | [![Release Artifacts][badge-release]][link-release] 6 | 7 | [comment]: <> ([![Maven Central][badge-maven]][link-maven]) 8 | 9 | A really simple http library implemented in scala with no dependencies 10 | 11 | Requirements 12 | ------------ 13 | 14 | * [scala](http://www.scala-lang.org) 2.13.3 15 | * [scala](http://www.scala-lang.org) 2.12.1 16 | 17 | Client Usage 18 | ------------ 19 | Add the following lines to your build.sbt 20 | 21 | [comment]: <> ( resolvers += "Tim Tennant's repo" at "http://dl.bintray.com/timt/repo/") 22 | 23 | libraryDependencies += "io.shaka" %% "naive-http" % "126" 24 | 25 | Start hacking 26 | 27 | import io.shaka.http.Http.http 28 | import io.shaka.http.Request.{GET, POST} 29 | ... 30 | val response = http(GET("http://www.google.com")) 31 | ... 32 | //Add header 33 | import io.shaka.http.HttpHeader.USER_AGENT 34 | val response = http(GET("http://www.google.com").header(USER_AGENT,"my agent")) 35 | ... 36 | //Post JSON 37 | import io.shaka.http.ContentType.APPLICATION_JSON 38 | val response = http(POST("http://some/json/server").contentType(APPLICATION_JSON).entity("""{"foo":"bar"}""")) 39 | ... 40 | //Post form parameters 41 | val response = http(POST("http://some/json/server").entity("""{"foo":"bar"}""").formParameters(FormParameter("name","value"))) 42 | ... 43 | //Specify a timeout in milli seconds (same value used for both connect and read) 44 | implicit val timeout = Timeout(1000) 45 | val response = http(GET("http://www.google.com")) 46 | ... 47 | //Specify a proxy 48 | implicit val proxy = io.shaka.http.proxy("my.proxy.server", 8080) 49 | val response = http(GET("http://www.google.com")) 50 | implicit val proxy = io.shaka.http.proxy("my.proxy.server", 8080, "proxyUser", "proxyPassword") 51 | val response = http(GET("http://www.google.com")) 52 | ... 53 | //Trust all SSL certificates (globally) 54 | import io.shaka.http.Https.TrustAllSslCertificates 55 | TrustAllSslCertificates 56 | ... 57 | //Trust all SSL certificates (non-globally) 58 | import io.shaka.http.Https.HttpsConfig 59 | implicit val https: Option[HttpsConfig] = Some(HttpsConfig(TrustAnyServer)) 60 | val response = http(GET("https://someurl")) 61 | ... 62 | //Use a trust store containing all the certificates that the client trusts 63 | import io.shaka.http.Https.TrustServersByTrustStore 64 | implicit val https = HttpsConfig(TrustServersByTrustStore("src/test/resources/certs/server-truststore.jks", "password")) 65 | val response = http(GET("https://someurl")) 66 | ... 67 | //Use a key store containing the client certificate for connecting to http servers using SSL mutual-auth 68 | import io.shaka.http.Https.UseKeyStore 69 | implicit val mutualSslAuth: Option[HttpsConfig] = Some(HttpsConfig( 70 | TrustServersByTrustStore("src/test/resources/certs/server-truststore.jks", "password"), 71 | UseKeyStore("src/test/resources/certs/keystore-testing-client.jks", "password") 72 | )) 73 | val response = http(GET("https://someurl")) 74 | ... 75 | //Basic auth 76 | val response = http(GET("https://someurl").basicAuth("me","myPassword")) 77 | 78 | 79 | For more examples see 80 | 81 | * [HttpSpec.scala](https://github.com/timt/naive-http/blob/master/src/test/scala/io/shaka/http/HttpSpec.scala) 82 | * [HttpsSpec.scala](https://github.com/timt/naive-http/blob/master/src/test/scala/io/shaka/http/HttpsSpec.scala) 83 | * [SslAuthSpec.scala](https://github.com/timt/naive-http/blob/master/src/test/scala/io/shaka/http/SslAuthSpec.scala) 84 | * [MutualSslAuthSpec.scala](https://github.com/timt/naive-http/blob/master/src/test/scala/io/shaka/http/MutualSslAuthSpec.scala) 85 | 86 | Server Usage 87 | ------------ 88 | see [SERVER_README.md](SERVER_README.md) 89 | 90 | 91 | Code license 92 | ------------ 93 | Apache License 2.0 94 | 95 | [badge-build]: https://github.com/timt/naive-http/actions/workflows/build.yml/badge.svg 96 | [link-build]: https://github.com/timt/naive-http/actions/ 97 | 98 | [badge-release]: https://img.shields.io/nexus/r/https/oss.sonatype.org/io.shaka/naive-http_2.12.svg "Sonatype Releases" 99 | [link-release]: https://oss.sonatype.org/content/repositories/releases/io/shaka/naive-http_2.12/ "Sonatype Releases" 100 | 101 | [comment]: <> ([badge-maven]: https://maven-badges.herokuapp.com/maven-central/io.shaka/naive-http_2.12/badge.svg) 102 | 103 | [comment]: <> ([link-maven]: https://maven-badges.herokuapp.com/maven-central/io.shaka/naive-http_2.12) 104 | -------------------------------------------------------------------------------- /SERVER_README.md: -------------------------------------------------------------------------------- 1 | naive-http-server 2 | ================= 3 | A really simple http server library implemented in scala with no dependencies 4 | 5 | Server Usage 6 | ------------ 7 | Starting a server 8 | 9 | import io.shaka.http.HttpServer 10 | import io.shaka.http.Response.respond 11 | import io.shaka.http.Response.seeOther 12 | val httpServer = HttpServer(request => respond("Hello World!")).start() 13 | ... 14 | val httpServer = HttpServer(8080).handler(request => respond("Hello World!")).start() 15 | 16 | Handling requests 17 | 18 | import io.shaka.http.RequestMatching._ 19 | httpServer.handler{ 20 | case GET("/hello") => respond("Hello world") 21 | case GET(echoUrl) => respond(echoUrl) 22 | case request@POST("/some/restful/thing") => respond(...) 23 | case req@GET(_) if req.accepts(APPLICATION_JSON) => respond("""{"hello":"world"}""").contentType(APPLICATION_JSON) 24 | case GET(url"/tickets/$ticketId?messageContains=$messageContains") => respond(s"Ticket $ticketId, messageContains $messageContains").contentType(TEXT_PLAIN) 25 | case GET("/nothingToSeeHere") => seeOther("http://moveAlong") 26 | case _ => respond("doh!").status(NOT_FOUND) 27 | } 28 | 29 | Serving static content 30 | 31 | import io.shaka.http.StaticResponse.static 32 | import io.shaka.http.StaticResponse.classpathDocRoot 33 | httpServer.handler{ 34 | case GET(path) => static("/home/timt/docRoot", path) 35 | case GET(path) => static(classpathDocRoot("web"), path) 36 | } 37 | 38 | 39 | Stopping the server 40 | 41 | httpServer.stop() 42 | 43 | Start an SSL server 44 | 45 | import io.shaka.http.HttpServer 46 | import io.shaka.http.PathAndPassword 47 | import io.shaka.http.Response.respond 48 | val server = HttpServer.https( 49 | keyStoreConfig = PathAndPassword("src/test/resources/certs/keystore-testing.jks", "password") 50 | ).handler(_ => respond("Hello world")).start() 51 | 52 | Start an SSL with mutual SSL auth 53 | 54 | import io.shaka.http.HttpServer 55 | import io.shaka.http.PathAndPassword 56 | import io.shaka.http.Response.respond 57 | val server = HttpServer.httpsMutualAuth( 58 | keyStoreConfig = PathAndPassword("src/test/resources/certs/keystore-testing.jks", "password"), 59 | trustStoreConfig = PathAndPassword("src/test/resources/certs/server-truststore.jks", "password") 60 | ).handler(_ => respond("Hello world")).start() 61 | 62 | 63 | For more examples see 64 | 65 | * [HttpServerSpec.scala](https://github.com/timt/naive-http/blob/master/src/test/scala/io/shaka/http/HttpServerSpec.scala) 66 | * [ServingStaticResourcesSpec.scala](https://github.com/timt/naive-http/blob/master/src/test/scala/io/shaka/http/ServingStaticResourcesSpec.scala) 67 | * [SslAuthSpec.scala](https://github.com/timt/naive-http/blob/master/src/test/scala/io/shaka/http/SslAuthSpec.scala) 68 | * [MutualSslAuthSpec.scala](https://github.com/timt/naive-http/blob/master/src/test/scala/io/shaka/http/MutualSslAuthSpec.scala) 69 | 70 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import scala.util.Try 2 | 3 | name := "naive-http" 4 | 5 | organization := "io.shaka" 6 | 7 | version := (Try(sys.env("GITHUB_RUN_NUMBER")).getOrElse("1").toInt + 114).toString 8 | 9 | scalaVersion := "2.13.5" 10 | 11 | crossScalaVersions := Seq("2.12.13", "2.13.5") 12 | 13 | homepage := Some(url("https://github.com/timt/naive-http")) 14 | 15 | licenses +=("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0.html")) 16 | 17 | libraryDependencies ++= Seq( 18 | "org.eclipse.jetty.orbit" % "javax.servlet" % "2.5.0.v201103041518" % "test", 19 | "org.scalatest" %% "scalatest" % "3.1.2" % "test" 20 | ) 21 | 22 | developers := List( 23 | Developer("timt", "Tim Tennant", "", url("https://github.com/timt")) 24 | ) 25 | 26 | usePgpKeyHex("timt-ci bot") 27 | 28 | publishArtifact in Test := false 29 | -------------------------------------------------------------------------------- /lib/test-docroot.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timt/naive-http/2cfd29a59715e22afca213760e6d0b52233af000/lib/test-docroot.jar -------------------------------------------------------------------------------- /project/CiPublishPlugin.scala: -------------------------------------------------------------------------------- 1 | import com.jsuereth.sbtpgp.SbtPgp 2 | import com.typesafe.sbt.GitPlugin 3 | import sbt.Keys._ 4 | import sbt.{Def, _} 5 | import sbt.plugins.JvmPlugin 6 | import xerial.sbt.Sonatype 7 | import xerial.sbt.Sonatype.autoImport._ 8 | 9 | import scala.sys.process._ 10 | import scala.util.control.NonFatal 11 | 12 | object CiPublishPlugin extends AutoPlugin { 13 | 14 | override def trigger: PluginTrigger = allRequirements 15 | override def requires: Plugins = JvmPlugin && SbtPgp && GitPlugin && Sonatype 16 | 17 | def isSecure: Boolean = 18 | System.getenv("PGP_SECRET") != null 19 | 20 | def setupGpg(): Unit = { 21 | val versionLine = List("gpg", "--version").!!.linesIterator.toList.head 22 | 23 | println(versionLine) 24 | 25 | val TaggedVersion = """(\d{1,14})([\.\d{1,14}]*)((?:-\w+)*)""".r 26 | 27 | val gpgVersion: Long = versionLine.split(" ").last match { 28 | case TaggedVersion(m, _, _) ⇒ m.toLong 29 | case _ ⇒ 0L 30 | } 31 | 32 | // https://dev.gnupg.org/T2313 33 | val importCommand = if (gpgVersion < 2L) "--import" else "--batch --import" 34 | 35 | val secret = sys.env("PGP_SECRET") 36 | 37 | (s"echo $secret" #| "base64 --decode" #| s"gpg $importCommand").! 38 | } 39 | 40 | private def gitHubScmInfo(user: String, repo: String) = 41 | ScmInfo( 42 | url(s"https://github.com/$user/$repo"), 43 | s"scm:git:https://github.com/$user/$repo.git", 44 | Some(s"scm:git:git@github.com:$user/$repo.git") 45 | ) 46 | 47 | override lazy val buildSettings: Seq[Def.Setting[_]] = List( 48 | scmInfo ~= { 49 | case Some(info) ⇒ Some(info) 50 | case None ⇒ { 51 | import scala.sys.process._ 52 | val identifier = """([^\/]+?)""" 53 | val GitHubHttps = s"https://github.com/$identifier/$identifier(?:\\.git)?".r 54 | val GitHubGit = s"git://github.com:$identifier/$identifier(?:\\.git)?".r 55 | val GitHubSsh = s"git@github.com:$identifier/$identifier(?:\\.git)?".r 56 | try { 57 | val remote = List("git", "ls-remote", "--get-url", "origin").!!.trim() 58 | remote match { 59 | case GitHubHttps(user, repo) ⇒ Some(gitHubScmInfo(user, repo)) 60 | case GitHubGit(user, repo) ⇒ Some(gitHubScmInfo(user, repo)) 61 | case GitHubSsh(user, repo) ⇒ Some(gitHubScmInfo(user, repo)) 62 | case _ ⇒ None 63 | } 64 | } catch { 65 | case NonFatal(_) ⇒ None 66 | } 67 | } 68 | } 69 | ) 70 | 71 | override lazy val globalSettings: Seq[Def.Setting[_]] = List( 72 | publishArtifact.in(Test) := false, 73 | publishMavenStyle := true, 74 | commands += Command.command("ci-publish")(currentState ⇒ { 75 | if (!isSecure) { 76 | println("No access to secret variables, skipping publish") 77 | currentState 78 | } else { 79 | println("Running ci-publish") 80 | setupGpg() 81 | // https://github.com/olafurpg/sbt-ci-release/issues/64 82 | 83 | "set pgpSecretRing := pgpSecretRing.value" :: 84 | "set pgpPublicRing := pgpPublicRing.value" :: 85 | "+publishSigned" :: 86 | "sonatypeBundleRelease" :: 87 | currentState 88 | } 89 | }) 90 | ) 91 | 92 | override lazy val projectSettings: Seq[Def.Setting[_]] = List( 93 | publishConfiguration := publishConfiguration.value.withOverwrite(true), 94 | publishLocalConfiguration := publishLocalConfiguration.value.withOverwrite(true), 95 | publishTo := sonatypePublishToBundle.value 96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.2 2 | 3 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") 2 | addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0") 3 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.7") 4 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/ClientHttpHandler.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import java.net.{HttpURLConnection, URL} 4 | import javax.net.ssl.HttpsURLConnection 5 | 6 | import io.shaka.http.Headers.toHeaders 7 | import io.shaka.http.Http._ 8 | import io.shaka.http.Https.{HttpsConfig, sslFactory, hostNameVerifier} 9 | import io.shaka.http.IO.inputStreamToByteArray 10 | import io.shaka.http.Status._ 11 | import io.shaka.http.proxy.{Proxy, noProxy} 12 | 13 | class ClientHttpHandler(proxy: Proxy = noProxy, httpsConfig: Option[HttpsConfig] = None, timeout: Timeout = tenSecondTimeout) extends HttpHandler { 14 | 15 | override def apply(request: Request): Response = { 16 | val connection = createConnection(request.url, proxy) 17 | connection.setRequestMethod(request.method.name) 18 | request.headers.foreach { 19 | header => 20 | connection.setRequestProperty(header._1.name, header._2) 21 | } 22 | request.entity.foreach { 23 | entity => 24 | connection.setDoOutput(true) 25 | connection.getOutputStream.write(entity.content) 26 | } 27 | val response = buildResponse(connection) 28 | connection.disconnect() 29 | response 30 | } 31 | 32 | def buildResponse(connection: HttpURLConnection): Response = { 33 | val s = status(connection.getResponseCode, connection.getResponseMessage) 34 | val headers: Headers = toHeaders(connection.getHeaderFields) 35 | val entity: Option[Entity] = for { 36 | is <- Option(if(s.code >=400) connection.getErrorStream else connection.getInputStream) 37 | content <- Zero.toOption(inputStreamToByteArray(is)) 38 | entity <- Some(Entity(content)) 39 | } yield entity 40 | 41 | Response( 42 | status = s, 43 | headers = headers, 44 | entity = entity 45 | ) 46 | } 47 | 48 | protected def createConnection(url: Url, proxy: Proxy): HttpURLConnection = { 49 | val connection = new URL(url).openConnection(proxy()).asInstanceOf[HttpURLConnection] 50 | (connection, httpsConfig) match { 51 | case (c: HttpsURLConnection, Some(config)) => 52 | c.setSSLSocketFactory(sslFactory(config)) 53 | c.setHostnameVerifier(hostNameVerifier(config.trustStoreConfig)) 54 | case _ => 55 | } 56 | connection.setUseCaches(false) 57 | connection.setConnectTimeout(timeout.millis) 58 | connection.setReadTimeout(timeout.millis) 59 | connection.setInstanceFollowRedirects(false) 60 | connection 61 | } 62 | } 63 | 64 | trait SslConnection { 65 | ClientHttpHandler => { 66 | 67 | } 68 | 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/ContentType.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import io.shaka.http.HttpHeader.CONTENT_TYPE 4 | 5 | object ContentType { 6 | case object WILDCARD extends ContentType {val value = "*/*"} 7 | case object APPLICATION_XML extends ContentType {val value = "application/xml"} 8 | case object APPLICATION_ATOM_XML extends ContentType {val value = "application/atom+xml"} 9 | case object APPLICATION_XHTML_XML extends ContentType {val value = "application/xhtml+xml"} 10 | case object APPLICATION_SVG_XML extends ContentType {val value = "application/svg+xml"} 11 | case object APPLICATION_JAVASCRIPT extends ContentType {val value = "application/javascript"} 12 | case object APPLICATION_JSON extends ContentType {val value = "application/json"} 13 | case object APPLICATION_PDF extends ContentType {val value = "application/pdf"} 14 | case object APPLICATION_FORM_URLENCODED extends ContentType {val value = "application/x-www-form-urlencoded"} 15 | case object APPLICATION_OCTET_STREAM extends ContentType {val value = "application/octet-stream"} 16 | case object APPLICATION_MS_EXCEL extends ContentType {val value = "application/vnd.ms-excel"} 17 | case object MULTIPART_FORM_DATA extends ContentType {val value = "multipart/form-data"} 18 | case object TEXT_PLAIN extends ContentType {val value = "text/plain"} 19 | case object TEXT_CSV extends ContentType {val value = "text/csv"} 20 | case object TEXT_XML extends ContentType {val value = "text/xml"} 21 | case object TEXT_HTML extends ContentType {val value = "text/html"} 22 | case object TEXT_CSS extends ContentType {val value = "text/css"} 23 | case object TEXT_JAVASCRIPT extends ContentType {val value = "text/javascript"} 24 | case object TEXT_CACHE_MANIFEST extends ContentType {val value = "text/cache-manifest"} 25 | case object IMAGE_PNG extends ContentType {val value = "image/png"} 26 | case object IMAGE_GIF extends ContentType {val value = "image/gif"} 27 | case object IMAGE_X_ICON extends ContentType {val value = "image/x-icon"} 28 | case object IMAGE_JPEG extends ContentType {val value = "image/jpeg"} 29 | case object IMAGE_SVG extends ContentType {val value = "image/svg+xml"} 30 | 31 | case class unknownContentType(value: String) extends ContentType 32 | 33 | val values = List( 34 | 35 | WILDCARD, 36 | APPLICATION_XML, 37 | APPLICATION_ATOM_XML, 38 | APPLICATION_XHTML_XML, 39 | APPLICATION_SVG_XML, 40 | APPLICATION_JAVASCRIPT, 41 | APPLICATION_JSON, 42 | APPLICATION_PDF, 43 | APPLICATION_FORM_URLENCODED, 44 | APPLICATION_OCTET_STREAM, 45 | APPLICATION_MS_EXCEL, 46 | MULTIPART_FORM_DATA, 47 | TEXT_PLAIN, 48 | TEXT_CSV, 49 | TEXT_XML, 50 | TEXT_HTML, 51 | TEXT_CSS, 52 | TEXT_JAVASCRIPT, 53 | TEXT_CACHE_MANIFEST, 54 | IMAGE_PNG, 55 | IMAGE_GIF, 56 | IMAGE_X_ICON, 57 | IMAGE_JPEG, 58 | IMAGE_SVG 59 | ) 60 | 61 | def contentType(value: String) = values.find(h => h.value equalsIgnoreCase value).getOrElse(unknownContentType(value)) 62 | 63 | def unapply(request: Request): Option[ContentType] = if (request.headers.contains(CONTENT_TYPE)) request.headers(CONTENT_TYPE).headOption.map(contentType) else None 64 | 65 | } 66 | 67 | trait ContentType { 68 | def value: String 69 | def unapply(request: Request): Boolean = request.headers(HttpHeader.ACCEPT).exists(accept => 70 | accept.contains(value) || accept.contains(ContentType.WILDCARD.value)) 71 | } 72 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/Cookie.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | case class Cookie(name: String, value: String){ 4 | lazy val toSetCookie = s"$name=$value" 5 | } -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/Entity.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | object Entity { 4 | def apply(entity: String): Entity = Entity(entity.getBytes) 5 | } 6 | 7 | case class Entity(content: Array[Byte]) { 8 | override def toString: String = new String(content) 9 | override def equals(other: scala.Any): Boolean = Option(other).exists(o => o.isInstanceOf[Entity] && o.toString.equals(this.toString)) 10 | override def hashCode(): Int = toString.hashCode 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/FormParameter.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import java.net.{URLDecoder, URLEncoder} 4 | import java.nio.charset.Charset 5 | 6 | case class FormParameter(name: String, value: Option[String]) 7 | 8 | object FormParameter { 9 | def apply(name: String): FormParameter = FormParameter(name, None) 10 | 11 | def apply(name: String, value: String): FormParameter = FormParameter(name, Some(value)) 12 | 13 | def serialize(formParameter: FormParameter): String = encode(formParameter.name) + (formParameter.value match { 14 | case Some(value) => s"=${encode(value)}" 15 | case _ => "" 16 | }) 17 | 18 | private def encode(s: String) = URLEncoder.encode(s, Charset.forName("UTF-8").toString) 19 | 20 | def deserialize(serializedFormParameter: String): FormParameter = if (serializedFormParameter.contains("=")) { 21 | val values = serializedFormParameter.split("=") 22 | FormParameter(decode(values(0)), Some(if (values.length < 2) "" else decode(values(1)))) 23 | } else { 24 | FormParameter(decode(serializedFormParameter)) 25 | } 26 | 27 | private def decode(s: String) = URLDecoder.decode(s, Charset.forName("UTF-8").toString) 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/FormParameters.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import FormParameter.{deserialize, serialize} 4 | 5 | object FormParameters { 6 | type FormParameters = List[FormParameter] 7 | def toEntity(formParameters: FormParameters):Entity = Entity(formParameters.map(serialize).mkString("&")) 8 | def fromEntity(entity: Entity):FormParameters = entity.toString.split("&").map(deserialize).toList 9 | } -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/Handlers.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import io.shaka.http.Http.HttpHandler 4 | import io.shaka.http.HttpHeader.CONTENT_LENGTH 5 | import io.shaka.http.Method.{GET, HEAD} 6 | import io.shaka.http.Status.INTERNAL_SERVER_ERROR 7 | 8 | object Handlers { 9 | 10 | object HEADRequestHandler { 11 | def ~>(handler: HttpHandler): HttpHandler = (request) => { 12 | def foldHeadRequest[T](original: T)(doWhenHead: T => T): T = { 13 | if(request.method == HEAD) doWhenHead(original) else original 14 | } 15 | val response = handler(foldHeadRequest(request)(_.copy(method = GET))) 16 | foldHeadRequest(response)(_.header(CONTENT_LENGTH, response.entity.fold("0")(_.content.length.toString))) 17 | 18 | 19 | } 20 | } 21 | 22 | object SafeRequestHandler { 23 | def ~>(handler: HttpHandler): HttpHandler = (request) => try { 24 | handler(request) 25 | } catch { 26 | case e: Throwable => Response().entity(s"Server error: ${e.getMessage}").status(INTERNAL_SERVER_ERROR) 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/Headers.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import io.shaka.http.Http.Header 4 | import io.shaka.http.HttpHeader._ 5 | 6 | case class Headers(headers: List[Header]) { 7 | def contains(header: HttpHeader): Boolean = headers.exists(_._1 == header) 8 | 9 | def contains(header: Header): Boolean = headers.contains(header) 10 | 11 | def apply(header: HttpHeader): List[String] = filter(_._1 == header).map(_._2) 12 | 13 | def foreach[A](f: Header => A): Unit = headers.foreach(f) 14 | 15 | def ::(header: Header): Headers = Headers(header :: headers) 16 | 17 | def map[A](f: Header => A) = headers.map(f) 18 | 19 | def find(p: Header => Boolean): Option[Header] = headers.find(p) 20 | 21 | def filter(p: Header => Boolean): List[Header] = headers.filter(p) 22 | } 23 | 24 | object Headers { 25 | val Empty = Headers(List()) 26 | import scala.collection.JavaConverters._ 27 | def toHeaders(rawHeaders: java.util.Map[String, java.util.List[String]]):Headers = Headers(rawHeaders.asScala.toList.flatMap(pair => pair._2.asScala.map((httpHeader(pair._1), _)))) 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/Http.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import io.shaka.http.proxy._ 4 | 5 | object Http { 6 | type HttpHandler = (Request) => (Response) 7 | type Url = String 8 | type Header = (HttpHeader, String) 9 | val tenSecondTimeout = Timeout(10000) 10 | 11 | def http(request: Request)(implicit proxy: Proxy = noProxy, httpsConfig: Option[io.shaka.http.Https.HttpsConfig] = None, timeout: Timeout = tenSecondTimeout ): Response = new ClientHttpHandler(proxy, httpsConfig, timeout: Timeout).apply(request) 12 | 13 | case class Timeout(millis: Int) 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/HttpHeader.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | trait HttpHeader {def name: String} 4 | object HttpHeader { 5 | case object ACCEPT extends HttpHeader {val name = "Accept"} 6 | case object ACCEPT_CHARSET extends HttpHeader {val name = "Accept-Charset"} 7 | case object ACCEPT_ENCODING extends HttpHeader {val name = "Accept-Encoding"} 8 | case object ACCEPT_LANGUAGE extends HttpHeader {val name = "Accept-Language"} 9 | case object ACCESS_CONTROL_ALLOW_HEADERS extends HttpHeader { val name = "Access-Control-Allow-Headers" } 10 | case object ACCESS_CONTROL_ALLOW_METHODS extends HttpHeader { val name = "Access-Control-Allow-Methods" } 11 | case object ACCESS_CONTROL_ALLOW_ORIGIN extends HttpHeader {val name = "Access-Control-Allow-Origin"} 12 | case object AUTHORIZATION extends HttpHeader {val name = "Authorization"} 13 | case object CACHE_CONTROL extends HttpHeader {val name = "Cache-Control"} 14 | case object COOKIE extends HttpHeader {val name = "Cookie"} 15 | case object CONTENT_ENCODING extends HttpHeader {val name = "Content-Encoding"} 16 | case object CONTENT_DISPOSITION extends HttpHeader {val name = "Content-Disposition"} 17 | case object CONTENT_LANGUAGE extends HttpHeader {val name = "Content-Language"} 18 | case object CONTENT_LENGTH extends HttpHeader {val name = "Content-Length"} 19 | case object CONTENT_LOCATION extends HttpHeader {val name = "Content-Location"} 20 | case object CONTENT_SECURITY_POLICY extends HttpHeader {val name = "Content-Security-Policy"} 21 | case object CONTENT_TYPE extends HttpHeader {val name = "Content-Type"} 22 | case object Content_MD5 extends HttpHeader {val name = "Content-MD5"} 23 | case object DATE extends HttpHeader {val name = "Date"} 24 | case object ETAG extends HttpHeader {val name = "ETag"} 25 | case object EXPIRES extends HttpHeader {val name = "Expires"} 26 | case object FEATURE_POLICY extends HttpHeader {val name = "Feature-Policy"} 27 | case object HOST extends HttpHeader {val name = "Host"} 28 | case object IF_MATCH extends HttpHeader {val name = "If-Match"} 29 | case object IF_MODIFIED_SINCE extends HttpHeader {val name = "If-Modified-Since"} 30 | case object IF_NONE_MATCH extends HttpHeader {val name = "If-None-Match"} 31 | case object IF_UNMODIFIED_SINCE extends HttpHeader {val name = "If-Unmodified-Since"} 32 | case object LAST_MODIFIED extends HttpHeader {val name = "Last-Modified"} 33 | case object LOCATION extends HttpHeader {val name = "Location"} 34 | case object ORIGIN extends HttpHeader {val name = "Origin"} 35 | case object PRAGMA extends HttpHeader {val name = "Pragma"} 36 | case object SERVER extends HttpHeader {val name = "Server"} 37 | case object SET_COOKIE extends HttpHeader {val name = "Set-Cookie"} 38 | case object STRICT_TRANSPORT_SECURITY extends HttpHeader {val name = "Strict-Transport-Security"} 39 | case object TRANSFER_ENCODING extends HttpHeader {val name = "Transfer-Encoding"} 40 | case object USER_AGENT extends HttpHeader {val name = "User-Agent"} 41 | case object VARY extends HttpHeader {val name = "Vary"} 42 | case object WWW_AUTHENTICATE extends HttpHeader {val name = "WWW-Authenticate"} 43 | case object X_VIA extends HttpHeader {val name = "X-Via"} 44 | case object X_FORWARDED_FOR extends HttpHeader {val name = "X-Forwarded-For"} 45 | case object X_FORWARDED_PROTO extends HttpHeader {val name = "X-Forwarded-Proto"} 46 | case object X_CORRELATION_ID extends HttpHeader {val name = "X-CorrelationID"} 47 | 48 | 49 | 50 | case class unknownHeader(name: String) extends HttpHeader 51 | 52 | val values = List( 53 | ACCEPT, 54 | ACCEPT_CHARSET, 55 | ACCEPT_ENCODING, 56 | ACCEPT_LANGUAGE, 57 | ACCESS_CONTROL_ALLOW_HEADERS, 58 | ACCESS_CONTROL_ALLOW_METHODS, 59 | ACCESS_CONTROL_ALLOW_ORIGIN, 60 | AUTHORIZATION, 61 | CACHE_CONTROL, 62 | CONTENT_DISPOSITION, 63 | CONTENT_ENCODING, 64 | CONTENT_LANGUAGE, 65 | CONTENT_LENGTH, 66 | CONTENT_LOCATION, 67 | CONTENT_SECURITY_POLICY, 68 | CONTENT_TYPE, 69 | Content_MD5, 70 | DATE, 71 | ETAG, 72 | EXPIRES, 73 | FEATURE_POLICY, 74 | HOST, 75 | IF_MATCH, 76 | IF_MODIFIED_SINCE, 77 | IF_NONE_MATCH, 78 | IF_UNMODIFIED_SINCE, 79 | LAST_MODIFIED, 80 | LOCATION, 81 | ORIGIN, 82 | PRAGMA, 83 | USER_AGENT, 84 | VARY, 85 | WWW_AUTHENTICATE, 86 | COOKIE, 87 | SET_COOKIE, 88 | STRICT_TRANSPORT_SECURITY, 89 | X_FORWARDED_FOR, 90 | X_FORWARDED_PROTO, 91 | X_CORRELATION_ID, 92 | X_VIA, 93 | TRANSFER_ENCODING, 94 | SERVER 95 | ) 96 | 97 | def httpHeader(name: String) = values.find(h => h.name equalsIgnoreCase name).getOrElse(unknownHeader(name)) 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/HttpServer.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import java.io.FileInputStream 4 | import java.net.InetSocketAddress 5 | import java.security.{SecureRandom, KeyStore} 6 | import javax.net.ssl._ 7 | 8 | import com.sun.net.httpserver.spi.HttpServerProvider 9 | import com.sun.net.httpserver.{HttpServer => SunHttpServer, HttpsParameters, HttpsConfigurator, HttpsServer} 10 | import io.shaka.http.Http.HttpHandler 11 | import io.shaka.http.HttpServer.ToLog 12 | import io.shaka.http.Response.respond 13 | import io.shaka.http.Status.NOT_FOUND 14 | 15 | class HttpServer(private val usePort: Int = 0, otherLog: ToLog, sslConfig: HttpServerSslConfig = NoSslConfig) { 16 | import HttpServer.createServer 17 | 18 | val server = createServer(usePort, sslConfig) 19 | server.setExecutor(null) 20 | server.createContext("/", new SunHttpHandlerAdapter((req) => respond("No handler defined!").status(NOT_FOUND))) 21 | 22 | def start() = { 23 | val startedAt = System.nanoTime() 24 | server.start() 25 | val elapsedTime = BigDecimal((System.nanoTime() - startedAt) / 1000000.0).formatted("%.2f") 26 | otherLog(s"naive-http-server started on port $port in $elapsedTime milli seconds") 27 | this 28 | } 29 | 30 | def stop() { 31 | val delayInSeconds = 0 32 | server.stop(delayInSeconds) 33 | } 34 | 35 | def port = server.getAddress.getPort 36 | 37 | def handler(handler: HttpHandler) = { 38 | server.removeContext("/") 39 | server.createContext("/", new SunHttpHandlerAdapter(handler)) 40 | this 41 | } 42 | } 43 | 44 | object HttpServer { 45 | type ToLog = String => Unit 46 | private val printlnLog: ToLog = (s) => println(s) 47 | 48 | def apply(): HttpServer = apply(0) 49 | def apply(port: Int): HttpServer = new HttpServer(port, printlnLog) 50 | def apply(handler: HttpHandler, port: Int = 0, log: ToLog = printlnLog): HttpServer = new HttpServer(port, log).handler(handler) 51 | 52 | def https(keyStoreConfig: PathAndPassword, maybeTrustStoreConfig: Option[PathAndPassword] = None, port: Int = 0) = 53 | new HttpServer(port, printlnLog, SslConfig(keyStoreConfig, maybeTrustStoreConfig)) 54 | def httpsMutualAuth(keyStoreConfig: PathAndPassword, trustStoreConfig: PathAndPassword, port: Int = 0) = 55 | https(keyStoreConfig, Some(trustStoreConfig), port) 56 | 57 | private def createServer(requestedPort: Int, sslConfig: HttpServerSslConfig): SunHttpServer = { 58 | val address: InetSocketAddress = new InetSocketAddress(requestedPort) 59 | val httpServerProvider: HttpServerProvider = HttpServerProvider.provider() 60 | 61 | sslConfig match { 62 | case NoSslConfig => httpServerProvider.createHttpServer(address, 0) 63 | case SslConfig(ksConfig, maybeTsConfig) => 64 | 65 | val ks: KeyStore = KeyStore.getInstance("JKS") 66 | ks.load(new FileInputStream(ksConfig.path), ksConfig.password.toCharArray) 67 | val kmf = KeyManagerFactory.getInstance("SunX509") 68 | kmf.init(ks, ksConfig.password.toCharArray) 69 | 70 | val trustManagers = maybeTsConfig.fold[Array[TrustManager]](null){ tsConfig => 71 | val ts: KeyStore = KeyStore.getInstance("JKS") 72 | ts.load(new FileInputStream(tsConfig.path), tsConfig.password.toCharArray) 73 | val tmf: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509") 74 | tmf.init(ts) 75 | 76 | tmf.getTrustManagers 77 | } 78 | 79 | val sslContext = SSLContext.getInstance("TLS") 80 | sslContext.init(kmf.getKeyManagers, trustManagers, new SecureRandom()) 81 | 82 | val server: HttpsServer = httpServerProvider.createHttpsServer(address, 0) 83 | 84 | server.setHttpsConfigurator(new HttpsConfigurator(sslContext){ 85 | override def configure(httpsParameters: HttpsParameters) = { 86 | val sslParameters: SSLParameters = getSSLContext.getDefaultSSLParameters 87 | sslParameters.setNeedClientAuth(maybeTsConfig.isDefined) 88 | httpsParameters.setSSLParameters(sslParameters) 89 | } 90 | }) 91 | server 92 | } 93 | } 94 | } 95 | 96 | case class PathAndPassword(path: String, password: String) 97 | 98 | sealed trait HttpServerSslConfig 99 | case object NoSslConfig extends HttpServerSslConfig 100 | case class SslConfig(keyStoreConfig: PathAndPassword, trustStoreConfig: Option[PathAndPassword]) extends HttpServerSslConfig 101 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/Https.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import java.io.FileInputStream 4 | import java.nio.file.{Files, Paths} 5 | import java.security.{KeyStore => JKeyStore} 6 | import java.security.cert.X509Certificate 7 | import javax.net.ssl._ 8 | 9 | object Https { 10 | private val defaultProtocol = "TLS" 11 | 12 | trait TrustStoreConfig 13 | case class TrustServersByTrustStore(path: String, password: String) extends TrustStoreConfig 14 | case object TrustAnyServer extends TrustStoreConfig 15 | 16 | trait KeyStoreConfig 17 | case class UseKeyStore(path: String, password: String) extends KeyStoreConfig 18 | case object DoNotUseKeyStore extends KeyStoreConfig 19 | 20 | case class HttpsConfig(trustStoreConfig: TrustStoreConfig, keyStoreConfig: KeyStoreConfig = DoNotUseKeyStore, protocol: String = defaultProtocol) 21 | 22 | def sslFactory(httpsConfig: HttpsConfig): SSLSocketFactory = { 23 | val sslContext = SSLContext.getInstance(httpsConfig.protocol) 24 | sslContext.init(keyManagers(httpsConfig.keyStoreConfig), trustManagers(httpsConfig.trustStoreConfig), new java.security.SecureRandom) 25 | sslContext.getSocketFactory 26 | } 27 | 28 | def hostNameVerifier(trustStoreConfig: TrustStoreConfig): HostnameVerifier = trustStoreConfig match { 29 | case TrustServersByTrustStore(_, _) ⇒ HttpsURLConnection.getDefaultHostnameVerifier 30 | case TrustAnyServer ⇒ TrustAllSslCertificates.allHostsValid 31 | } 32 | 33 | private def trustManagers(trustStoreConfig: TrustStoreConfig): Array[TrustManager] = trustStoreConfig match { 34 | case TrustServersByTrustStore(path, password) ⇒ 35 | val inputStream = new FileInputStream(path) 36 | val trustStore: JKeyStore = JKeyStore.getInstance(JKeyStore.getDefaultType) 37 | trustStore.load(inputStream, password.toCharArray) 38 | 39 | val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) 40 | trustManagerFactory.init(trustStore) 41 | trustManagerFactory.getTrustManagers 42 | case TrustAnyServer ⇒ TrustAllSslCertificates.trustAllCerts 43 | } 44 | 45 | private def keyManagers(keyStoreConfig: KeyStoreConfig): Array[KeyManager] = keyStoreConfig match { 46 | case UseKeyStore(path, password) ⇒ 47 | val inputStream = if (Files.exists(Paths.get(path))) new FileInputStream(path) 48 | else getClass.getResourceAsStream(path) 49 | val keyStore: JKeyStore = JKeyStore.getInstance(JKeyStore.getDefaultType) 50 | keyStore.load(inputStream, password.toCharArray) 51 | 52 | val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) 53 | keyManagerFactory.init(keyStore, password.toCharArray) 54 | keyManagerFactory.getKeyManagers 55 | 56 | case DoNotUseKeyStore ⇒ null 57 | } 58 | 59 | object TrustAllSslCertificates { 60 | val trustAllCerts = Array[TrustManager](new X509TrustManager { 61 | def getAcceptedIssuers: Array[X509Certificate] = { null } 62 | def checkClientTrusted(certs: Array[X509Certificate], authType: String) {} 63 | def checkServerTrusted(certs: Array[X509Certificate], authType: String) {} 64 | }) 65 | 66 | val sc = SSLContext.getInstance(defaultProtocol) 67 | sc.init(null, trustAllCerts, new java.security.SecureRandom) 68 | 69 | val allHostsValid = new HostnameVerifier { 70 | def verify(hostname: String, session: SSLSession) = { true } 71 | } 72 | 73 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory) 74 | HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid) 75 | } 76 | } -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/IO.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import java.io.InputStream 4 | 5 | import scala.collection.mutable.ArrayBuffer 6 | 7 | object IO { 8 | def inputStreamToByteArray(is: InputStream): Array[Byte] = { 9 | val buffer = new Array[Byte](1024 * 8) 10 | val output = new ArrayBuffer[Byte]() 11 | Stream.continually(is.read(buffer)).takeWhile(_ != -1).foreach(output ++= buffer.slice(0, _)) 12 | output.toArray 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/Method.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | object Method { 4 | case object OPTIONS extends Method {val name = "OPTIONS"} 5 | case object GET extends Method {val name = "GET"} 6 | case object HEAD extends Method {val name = "HEAD"} 7 | case object POST extends Method {val name = "POST"} 8 | case object PUT extends Method {val name = "PUT"} 9 | case object DELETE extends Method {val name = "DELETE"} 10 | case object TRACE extends Method {val name = "TRACE"} 11 | case object CONNECT extends Method {val name = "CONNECT"} 12 | case object PATCH extends Method {val name = "PATCH"} 13 | case class unknownMethod(name: String) extends Method 14 | 15 | val values = List( 16 | OPTIONS, 17 | GET, 18 | HEAD, 19 | POST, 20 | PUT, 21 | DELETE, 22 | TRACE, 23 | CONNECT, 24 | PATCH 25 | ) 26 | 27 | def method(name: String) = values.find(h => h.name == name).getOrElse(unknownMethod(name)) 28 | } 29 | 30 | sealed trait Method {def name: String} -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/QueryParameters.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import io.shaka.http.RequestMatching.URLMatcher 4 | import java.net.URLDecoder.decode 5 | import scala.collection.{mutable ⇒ M} 6 | 7 | object QueryParameters { 8 | def unapply(queryParams: String): Option[QueryParameters] = { 9 | val mutableResult: M.Map[String, M.ListBuffer[String]] = 10 | M.Map.empty[String, M.ListBuffer[String]].withDefault(_ => M.ListBuffer[String]()) 11 | 12 | val pairs = queryParams.split("&") 13 | pairs.map(decode(_, "UTF-8")).foreach { 14 | case url"$key=$value" => mutableResult += ((key, mutableResult(key) += value)) 15 | case url"$key=" => mutableResult += ((key, mutableResult(key))) 16 | case key if key.length > 0 => mutableResult += ((key, mutableResult(key))) 17 | case _ => 18 | } 19 | 20 | val queryParamsMultiMap: Map[String, List[String]] = mutableResult.map { 21 | case (key, valuesBuffer) ⇒ (key, valuesBuffer.toList) 22 | }.toMap 23 | 24 | Some(QueryParameters(queryParamsMultiMap)) 25 | } 26 | } 27 | 28 | case class QueryParameters(values: Map[String, List[String]]){ 29 | def get(key: String): Option[String] = values.get(key).flatMap(_.headOption) 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/Request.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import io.shaka.http.ContentType.APPLICATION_FORM_URLENCODED 4 | import io.shaka.http.FormParameters.{fromEntity, toEntity} 5 | import io.shaka.http.Http._ 6 | import io.shaka.http.HttpHeader.{ACCEPT, AUTHORIZATION, CONTENT_TYPE, COOKIE} 7 | 8 | import java.util.Base64 9 | 10 | 11 | object Request { 12 | 13 | object GET { 14 | def apply(url: Url): Request = Request(Method.GET, url) 15 | 16 | def unapply(req: Request): Option[String] = if (req.method == Method.GET) Some(req.url) else None 17 | } 18 | 19 | object POST { 20 | def apply(url: Url): Request = Request(Method.POST, url) 21 | 22 | def unapply(req: Request): Option[String] = if (req.method == Method.POST) Some(req.url) else None 23 | } 24 | 25 | object PUT { 26 | def apply(url: Url): Request = Request(Method.PUT, url) 27 | 28 | def unapply(req: Request): Option[String] = if (req.method == Method.PUT) Some(req.url) else None 29 | } 30 | 31 | object HEAD { 32 | def apply(url: Url): Request = Request(Method.HEAD, url) 33 | 34 | def unapply(req: Request): Option[String] = if (req.method == Method.HEAD) Some(req.url) else None 35 | } 36 | 37 | object DELETE { 38 | def apply(url: Url): Request = Request(Method.DELETE, url) 39 | 40 | def unapply(req: Request): Option[String] = if (req.method == Method.DELETE) Some(req.url) else None 41 | } 42 | 43 | object OPTIONS { 44 | def apply(url: Url): Request = Request(Method.OPTIONS, url) 45 | 46 | def unapply(req: Request): Option[String] = if (req.method == Method.OPTIONS) Some(req.url) else None 47 | } 48 | 49 | object TRACE { 50 | def apply(url: Url): Request = Request(Method.TRACE, url) 51 | 52 | def unapply(req: Request): Option[String] = if (req.method == Method.TRACE) Some(req.url) else None 53 | } 54 | 55 | object CONNECT { 56 | def apply(url: Url): Request = Request(Method.CONNECT, url) 57 | 58 | def unapply(req: Request): Option[String] = if (req.method == Method.CONNECT) Some(req.url) else None 59 | } 60 | 61 | object PATCH { 62 | def apply(url: Url): Request = Request(Method.PATCH, url) 63 | 64 | def unapply(req: Request): Option[String] = if (req.method == Method.PATCH) Some(req.url) else None 65 | } 66 | } 67 | 68 | case class Request(method: Method, url: Url, headers: Headers = Headers.Empty, entity: Option[Entity] = None) { 69 | 70 | def formParameters(parameters: FormParameter*): Request = { 71 | val existingFormParameters = entity.fold(List[FormParameter]())(fromEntity) 72 | copy( 73 | entity = Some(toEntity(existingFormParameters ++ parameters)), 74 | headers = (CONTENT_TYPE -> APPLICATION_FORM_URLENCODED.value) :: headers 75 | ) 76 | } 77 | def header(header: HttpHeader, value: String): Request = copy(headers = (header, value) :: headers) 78 | def contentType(value: String): Request = header(CONTENT_TYPE, value) 79 | def contentType(value: ContentType): Request = header(CONTENT_TYPE, value.value) 80 | def accept(value: ContentType): Request = header(ACCEPT, value.value) 81 | def entity(content: String): Request = copy(entity = Some(Entity(content))) 82 | def entity(content: Array[Byte]): Request = copy(entity = Some(Entity(content))) 83 | def entityAsString: String = entity match { 84 | case Some(value) => value.toString 85 | case _ => throw new RuntimeException("There is no entity in this request! Consider using request.entity:Option[Entity] instead.") 86 | } 87 | def basicAuth(user: String, password: String): Request = { 88 | header(AUTHORIZATION, "Basic " + Base64.getEncoder.encodeToString(s"$user:$password".getBytes)) 89 | } 90 | def cookie(cookie: Cookie): Request = header(COOKIE, cookie.toSetCookie) 91 | 92 | def cookies: Set[Cookie] = headers.filter(_._1 == COOKIE).flatMap(_._2.split(";")) 93 | .map(_.trim.split("=")) 94 | .map(cookie => Cookie(cookie.head, cookie.last)) 95 | .toSet 96 | } 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/RequestMatching.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import io.shaka.http.HttpHeader.{ACCEPT, CONTENT_TYPE} 4 | 5 | object RequestMatching { 6 | object && { 7 | def unapply[A](a: A) = Some((a, a)) 8 | } 9 | 10 | implicit class URLMatcher(val sc: StringContext) extends AnyVal { 11 | def url = sc.parts.mkString("(.+)") 12 | .replaceAllLiterally("?", "\\?") 13 | .r 14 | } 15 | 16 | object Accept { 17 | @deprecated("Only works for exactly 1 accept header value. Instead import RequestOps and use guard condition 'if req.accepts(someContentType)'", "2015-02-04") 18 | def unapply(request: Request) = if (request.headers.contains(ACCEPT)) request.headers(ACCEPT).headOption.map(ContentType.contentType) else None 19 | } 20 | 21 | object HasContentType{ 22 | def unapply(request: Request) = if (request.headers.contains(CONTENT_TYPE)) request.headers(CONTENT_TYPE).headOption.map(ContentType.contentType) else None 23 | } 24 | 25 | object Path { 26 | def unapply(path: String): Option[List[String]] = Some(path.split("/").toList match { 27 | case "" :: xs => xs 28 | case x => x 29 | }) 30 | } 31 | 32 | implicit class RequestOps(request: Request) { 33 | def accepts(contentType: ContentType): Boolean = 34 | request.headers(ACCEPT).exists(_.contains(contentType.value)) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/Response.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import io.shaka.http.Status.{NOT_FOUND, OK, SEE_OTHER} 4 | import io.shaka.http.HttpHeader.{CONTENT_TYPE, LOCATION, SET_COOKIE} 5 | 6 | object Response { 7 | def respond(content: String): Response = Response().entity(content) 8 | def respond(content: Array[Byte]): Response = Response().entity(content) 9 | def seeOther(url: String): Response = respond("").status(SEE_OTHER).header(LOCATION, url) 10 | val ok: Response = Response().status(OK) 11 | val notFound: Response = Response().status(NOT_FOUND) 12 | } 13 | 14 | case class Response(status: Status = OK, headers: Headers = Headers.Empty, entity: Option[Entity] = None) { 15 | def status(newStatus: Status): Response = copy(status = newStatus) 16 | def header(header: HttpHeader, value: String): Response = copy(headers = (header, value) :: headers) 17 | def header(header: HttpHeader): Option[String] = headers.find(_._1 == header).map(_._2) 18 | def contentType(value: String): Response = header(CONTENT_TYPE, value) 19 | def contentType(value: ContentType): Response = header(CONTENT_TYPE, value.value) 20 | def contentType: Option[ContentType with Product with Serializable] = header(CONTENT_TYPE).map(ContentType.contentType) 21 | def setCookie(cookie: Cookie): Response = header(SET_COOKIE, cookie.toSetCookie) 22 | def entity(content: String): Response = entity(Entity(content)) 23 | def entity(content: Array[Byte]): Response = entity(Entity(content)) 24 | def entity(entity: Entity): Response = copy(entity = Some(entity)) 25 | def entityAsString: String = entity match { 26 | case Some(value) => value.toString 27 | case _ => throw new RuntimeException("There is no entity in this response! Consider using response.entity:Option[Entity] instead.") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/StaticResponse.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import java.io.File 4 | import java.net.URL 5 | import java.nio.file.Files.readAllBytes 6 | 7 | import io.shaka.http.ContentType._ 8 | import io.shaka.http.Response._ 9 | import io.shaka.http.Status.NOT_FOUND 10 | 11 | import scala.util.Try 12 | 13 | object StaticResponse { 14 | type ClasspathDocRoot = Unit => String 15 | 16 | private def fileNotFound(path: String) = respond(s"file not found").status(NOT_FOUND) 17 | 18 | def static(docRoot: String, path: String): Response = new File(s"$docRoot$path") match { 19 | case dir if dir.isDirectory => respond(dir.listFiles().map(_.getName).mkString("\n")).contentType(TEXT_PLAIN) 20 | case file if file.exists() => respond(readAllBytes(file.toPath)).contentType(toContentType(path)) 21 | case _ => fileNotFound(path) 22 | } 23 | 24 | def static(docRoot: ClasspathDocRoot, path: String): Response = { 25 | staticFromClasspath(this.getClass.getClassLoader.getResource(docRoot()), path) 26 | } 27 | 28 | def classpathDocRoot(docRoot: String): ClasspathDocRoot = unit => docRoot 29 | 30 | private def staticFromClasspath(docRoot: URL, path: String): Response = 31 | urlToBytes(s"$docRoot$path").fold(fileNotFound(path))(respond(_).contentType(toContentType(path))) 32 | 33 | def urlToBytes(url: String): Option[Array[Byte]] = { 34 | val is = Try(new URL(url).openStream()).toOption 35 | is.map{is => 36 | val bytes = Iterator.continually(is.read()).takeWhile(_ != -1).map(_.toByte).toArray 37 | is.close() 38 | bytes 39 | } 40 | } 41 | 42 | private val fileExtensionToContentType = Map( 43 | "css" -> TEXT_CSS, 44 | "csv" -> TEXT_CSV, 45 | "htm" -> TEXT_HTML, 46 | "html" -> TEXT_HTML, 47 | "xml" -> TEXT_XML, 48 | "md" -> TEXT_PLAIN, 49 | "txt" -> TEXT_PLAIN, 50 | "asc" -> TEXT_PLAIN, 51 | "gif" -> IMAGE_GIF, 52 | "jpg" -> IMAGE_JPEG, 53 | "jpeg" -> IMAGE_JPEG, 54 | "png" -> IMAGE_PNG, 55 | "js" -> APPLICATION_JAVASCRIPT, 56 | "pdf" -> APPLICATION_PDF, 57 | "exe" -> APPLICATION_OCTET_STREAM, 58 | "json" -> APPLICATION_JSON 59 | ) 60 | 61 | private def toContentType(path: String) = fileExtensionToContentType.find { case (key, _) => path.endsWith("." + key)}.map(_._2).getOrElse(APPLICATION_OCTET_STREAM) 62 | } 63 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/Status.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | object Status { 4 | 5 | //INFORMATIONAL = set(100, 199} 6 | case object CONTINUE extends Status {val code = 100; val description = "Continue"} 7 | case object SWITCHING_PROTOCOLS extends Status {val code = 101; val description = "Switching Protocols"} 8 | 9 | //SUCCESSFUL = set(200, 299} 10 | case object OK extends Status {val code = 200; val description = "OK"} 11 | case object CREATED extends Status {val code = 201; val description = "Created"} 12 | case object ACCEPTED extends Status {val code = 202; val description = "Accepted"} 13 | case object NON_AUTHORITATIVE_INFORMATION extends Status {val code = 203; val description = "Non-Authoritative Information"} 14 | case object NO_CONTENT extends Status {val code = 204; val description = "No Content"} 15 | case object RESET_CONTENT extends Status {val code = 205; val description = "Reset Content"} 16 | case object PARTIAL_CONTENT extends Status {val code = 206; val description = "Partial Content"} 17 | 18 | //REDIRECTION = set(300, 399} 19 | case object MULTIPLE_CHOICES extends Status {val code = 300; val description = "Multiple Choices"} 20 | case object MOVED_PERMANENTLY extends Status {val code = 301; val description = "Moved Permanently"} 21 | case object FOUND extends Status {val code = 302; val description = "Found"} 22 | case object SEE_OTHER extends Status {val code = 303; val description = "See Other"} 23 | case object NOT_MODIFIED extends Status {val code = 304; val description = "Not Modified"} 24 | case object USE_PROXY extends Status {val code = 305; val description = "Use Proxy"} 25 | case object TEMPORARY_REDIRECT extends Status {val code = 307; val description = "Temporary Redirect"} 26 | 27 | //CLIENT_ERROR = set(400, 499} 28 | case object BAD_REQUEST extends Status {val code = 400; val description = "Bad Request"} 29 | case object UNAUTHORIZED extends Status {val code = 401; val description = "Unauthorized"} 30 | case object PAYMENT_REQUIRED extends Status {val code = 402; val description = "Payment Required"} 31 | case object FORBIDDEN extends Status {val code = 403; val description = "Forbidden"} 32 | case object NOT_FOUND extends Status {val code = 404; val description = "Not Found"} 33 | case object METHOD_NOT_ALLOWED extends Status {val code = 405; val description = "Method Not Allowed"} 34 | case object NOT_ACCEPTABLE extends Status {val code = 406; val description = "Not Acceptable"} 35 | case object PROXY_AUTHENTICATION_REQUIRED extends Status {val code = 407; val description = "Proxy Authentication Required"} 36 | case object REQUEST_TIMEOUT extends Status {val code = 408; val description = "Request Timeout"} 37 | case object CONFLICT extends Status {val code = 409; val description = "Conflict"} 38 | case object GONE extends Status {val code = 410; val description = "Gone"} 39 | case object LENGTH_REQUIRED extends Status {val code = 411; val description = "Length Required"} 40 | case object PRECONDITION_FAILED extends Status {val code = 412; val description = "Precondition Failed"} 41 | case object REQUEST_ENTITY_TOO_LARGE extends Status {val code = 413; val description = "Request Entity Too Large"} 42 | case object REQUEST_URI_TOO_LONG extends Status {val code = 414; val description = "Request-URI Too Long"} 43 | case object UNSUPPORTED_MEDIA_TYPE extends Status {val code = 415; val description = "Unsupported Media Type"} 44 | case object REQUESTED_RANGE_NOT_SATISFIABLE extends Status {val code = 416; val description = "Requested Range Not Satisfiable"} 45 | case object EXPECTATION_FAILED extends Status {val code = 417; val description = "Expectation Failed"} 46 | case object I_AM_A_TEAPOT extends Status {val code = 418; val description = "I'm a teapot"} 47 | case object UNPROCESSABLE_ENTITY extends Status {val code = 422; val description = "Unprocessable Entity"} 48 | 49 | //SERVER_ERROR = set(500, 599} 50 | case object INTERNAL_SERVER_ERROR extends Status { val code = 500; val description = "Internal Server Error"} 51 | case object NOT_IMPLEMENTED extends Status { val code = 501; val description = "Not Implemented"} 52 | case object BAD_GATEWAY extends Status { val code = 502; val description = "Bad Gateway"} 53 | case object SERVICE_UNAVAILABLE extends Status { val code = 503; val description = "Service Unavailable"} 54 | case object GATEWAY_TIMEOUT extends Status { val code = 504; val description = "Gateway Timeout"} 55 | case object HTTP_VERSION_NOT_SUPPORTED extends Status { val code = 505; val description = "HTTP Version Not Supported"} 56 | 57 | case class unknownStatus(code: Int, description: String) extends Status 58 | val values = List( 59 | CONTINUE, 60 | SWITCHING_PROTOCOLS, 61 | OK, 62 | CREATED, 63 | ACCEPTED, 64 | NON_AUTHORITATIVE_INFORMATION, 65 | NO_CONTENT, 66 | RESET_CONTENT, 67 | PARTIAL_CONTENT, 68 | MULTIPLE_CHOICES, 69 | MOVED_PERMANENTLY, 70 | FOUND, 71 | SEE_OTHER, 72 | NOT_MODIFIED, 73 | USE_PROXY, 74 | TEMPORARY_REDIRECT, 75 | BAD_REQUEST, 76 | UNAUTHORIZED, 77 | PAYMENT_REQUIRED, 78 | FORBIDDEN, 79 | NOT_FOUND, 80 | METHOD_NOT_ALLOWED, 81 | NOT_ACCEPTABLE, 82 | PROXY_AUTHENTICATION_REQUIRED, 83 | REQUEST_TIMEOUT, 84 | CONFLICT, 85 | GONE, 86 | LENGTH_REQUIRED, 87 | PRECONDITION_FAILED, 88 | REQUEST_ENTITY_TOO_LARGE, 89 | REQUEST_URI_TOO_LONG, 90 | UNSUPPORTED_MEDIA_TYPE, 91 | REQUESTED_RANGE_NOT_SATISFIABLE, 92 | EXPECTATION_FAILED, 93 | I_AM_A_TEAPOT, 94 | UNPROCESSABLE_ENTITY, 95 | INTERNAL_SERVER_ERROR, 96 | NOT_IMPLEMENTED, 97 | BAD_GATEWAY, 98 | SERVICE_UNAVAILABLE, 99 | GATEWAY_TIMEOUT, 100 | HTTP_VERSION_NOT_SUPPORTED 101 | ) 102 | 103 | def status(code: Int, description: String) = values.find(s => s.code == code).getOrElse(unknownStatus(code,description)) 104 | } 105 | 106 | sealed trait Status { 107 | def code: Int 108 | def description: String 109 | } 110 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/SunHttpHandlerAdapter.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import java.io.InputStream 4 | 5 | import com.sun.net.httpserver.{HttpExchange => SunHttpExchange, HttpHandler => SunHttpHandler} 6 | import io.shaka.http.Handlers.{HEADRequestHandler, SafeRequestHandler} 7 | import io.shaka.http.Headers._ 8 | import io.shaka.http.Http.HttpHandler 9 | import io.shaka.http.Method._ 10 | import scala.language.postfixOps 11 | 12 | 13 | class SunHttpHandlerAdapter(handler: HttpHandler) extends SunHttpHandler { 14 | override def handle(exchange: SunHttpExchange): Unit = { 15 | respond(exchange, (SafeRequestHandler ~> (HEADRequestHandler ~> handler))(request(exchange))) 16 | exchange.close() 17 | } 18 | 19 | private def request(exchange: SunHttpExchange) = Request( 20 | method(exchange.getRequestMethod), 21 | exchange.getRequestURI.toString, 22 | toHeaders(exchange.getRequestHeaders), 23 | Some(Entity(toBytes(exchange.getRequestBody))) 24 | ) 25 | 26 | def toBytes(inputStream: InputStream):Array[Byte] = { 27 | Stream.continually(inputStream.read).takeWhile(-1 !=).map(_.toByte).toArray 28 | } 29 | 30 | private def respond(exchange: SunHttpExchange, response: Response) { 31 | response.headers.foreach { 32 | header => 33 | exchange.getResponseHeaders.add(header._1.name, header._2) 34 | } 35 | exchange.sendResponseHeaders(response.status.code, response.entity.map(_.content.length).getOrElse(0).toLong) 36 | response.entity.foreach { 37 | entity => 38 | val os = exchange.getResponseBody 39 | os.write(entity.content) 40 | os.close() 41 | } 42 | } 43 | 44 | } 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/TrustAllSslCertificates.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import javax.net.ssl._ 4 | import java.security.cert.X509Certificate 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/Zero.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | object Zero { 4 | def toOption(value: Array[Byte]): Option[Array[Byte]] = if (value.length == 0) None else Some(value) 5 | } -------------------------------------------------------------------------------- /src/main/scala/io/shaka/http/proxy.scala: -------------------------------------------------------------------------------- 1 | package io.shaka.http 2 | 3 | import java.net.Proxy.Type._ 4 | import java.net.{Authenticator, InetSocketAddress, PasswordAuthentication, Proxy => JavaProxy} 5 | 6 | object proxy { 7 | type Proxy = () => JavaProxy 8 | type Credentials = (String, String) 9 | 10 | val noProxy: Proxy = () => JavaProxy.NO_PROXY 11 | 12 | def credentials(username: String, password: String): Credentials = (username, password) 13 | 14 | def apply(host: String, port: Int, credentials: Option[Credentials] = None): Proxy = () => { 15 | credentials.foreach{ case (username, password) => 16 | Authenticator.setDefault(new Authenticator() { 17 | override def getPasswordAuthentication = new PasswordAuthentication(username, password.toCharArray) 18 | }) 19 | } 20 | new JavaProxy(HTTP, new InetSocketAddress(host, port)) 21 | } 22 | 23 | def apply(host: String, port: Int, username: String, password: String): Proxy = { 24 | this(host, port, Some(credentials(username,password))) 25 | } 26 | 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/test/resources/certs/client-certificate-unrecognised-by-server.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timt/naive-http/2cfd29a59715e22afca213760e6d0b52233af000/src/test/resources/certs/client-certificate-unrecognised-by-server.jks -------------------------------------------------------------------------------- /src/test/resources/certs/client-truststore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timt/naive-http/2cfd29a59715e22afca213760e6d0b52233af000/src/test/resources/certs/client-truststore.jks -------------------------------------------------------------------------------- /src/test/resources/certs/keystore-testing-client.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timt/naive-http/2cfd29a59715e22afca213760e6d0b52233af000/src/test/resources/certs/keystore-testing-client.jks -------------------------------------------------------------------------------- /src/test/resources/certs/keystore-testing.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timt/naive-http/2cfd29a59715e22afca213760e6d0b52233af000/src/test/resources/certs/keystore-testing.jks -------------------------------------------------------------------------------- /src/test/resources/certs/naive-testing-client.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timt/naive-http/2cfd29a59715e22afca213760e6d0b52233af000/src/test/resources/certs/naive-testing-client.crt -------------------------------------------------------------------------------- /src/test/resources/certs/server-truststore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timt/naive-http/2cfd29a59715e22afca213760e6d0b52233af000/src/test/resources/certs/server-truststore.jks -------------------------------------------------------------------------------- /src/test/resources/web/clocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timt/naive-http/2cfd29a59715e22afca213760e6d0b52233af000/src/test/resources/web/clocks.png -------------------------------------------------------------------------------- /src/test/resources/web/specification.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timt/naive-http/2cfd29a59715e22afca213760e6d0b52233af000/src/test/resources/web/specification.pdf -------------------------------------------------------------------------------- /src/test/resources/web/test.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 13px; 3 | color: #404040; 4 | background-color: #f1f1f1; 5 | margin: 0; 6 | } -------------------------------------------------------------------------------- /src/test/resources/web/test.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |