├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── build.sbt
├── js
└── src
│ └── main
│ └── scala
│ └── fr
│ └── hmil
│ └── roshttp
│ ├── BrowserDriver.scala
│ ├── ByteBufferChopper.scala
│ ├── Converters.scala
│ ├── CrossPlatformUtils.scala
│ ├── HttpDriver.scala
│ ├── JsEnvUtils.scala
│ ├── NodeDriver.scala
│ └── node
│ ├── Global.scala
│ ├── Helpers.scala
│ ├── LICENSE.txt
│ ├── Module.scala
│ ├── Modules.scala
│ ├── buffer
│ └── Buffer.scala
│ ├── events
│ └── EventEmitter.scala
│ ├── http
│ ├── Agent.scala
│ ├── AgentOptions.scala
│ ├── ClientRequest.scala
│ ├── Http.scala
│ ├── Https.scala
│ ├── IncomingMessage.scala
│ └── RequestOptions.scala
│ └── net
│ └── SocketOptions.scala
├── jvm
└── src
│ └── main
│ └── scala
│ └── fr
│ └── hmil
│ └── roshttp
│ ├── CrossPlatformUtils.scala
│ ├── HttpDriver.scala
│ └── JsEnvUtils.scala
├── project
├── InBrowserTesting.scala
├── build.properties
└── plugins.sbt
├── release.sh
├── scalastyle-config.xml
├── shared
└── src
│ ├── main
│ ├── scala-2.12
│ │ └── fr
│ │ │ └── hmil
│ │ │ └── roshttp
│ │ │ └── util
│ │ │ └── HeaderMap.scala
│ └── scala
│ │ └── fr
│ │ └── hmil
│ │ └── roshttp
│ │ ├── BackendConfig.scala
│ │ ├── ByteBufferQueue.scala
│ │ ├── DriverTrait.scala
│ │ ├── HttpRequest.scala
│ │ ├── Method.scala
│ │ ├── Protocol.scala
│ │ ├── body
│ │ ├── BodyPart.scala
│ │ ├── BulkBodyPart.scala
│ │ ├── ByteBufferBody.scala
│ │ ├── Implicits.scala
│ │ ├── JSONBody.scala
│ │ ├── MultiPartBody.scala
│ │ ├── PlainTextBody.scala
│ │ ├── StreamBody.scala
│ │ └── URLEncodedBody.scala
│ │ ├── exceptions
│ │ ├── HttpException.scala
│ │ ├── RequestException.scala
│ │ ├── ResponseException.scala
│ │ ├── TimeoutException.scala
│ │ └── UploadStreamException.scala
│ │ ├── response
│ │ ├── HttpResponse.scala
│ │ ├── HttpResponseFactory.scala
│ │ ├── HttpResponseHeader.scala
│ │ ├── SimpleHttpResponse.scala
│ │ └── StreamHttpResponse.scala
│ │ ├── tools
│ │ └── io
│ │ │ └── IO.scala
│ │ └── util
│ │ ├── HeaderMap.scala
│ │ └── Utils.scala
│ └── test
│ └── scala
│ └── fr
│ └── hmil
│ ├── roshttp
│ ├── HttpRequestSpec.scala
│ └── StreamingPressureTest.scala
│ └── roshttp_test
│ └── ReadmeSanityCheck.scala
└── test
├── resources
├── README
└── icon.png
├── server
├── .gitignore
├── README
├── index.js
├── npm-shrinkwrap.json
└── package.json
└── update-readme-sanity-check.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | project/target/
3 | project/project/
4 | .idea/
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: scala
2 |
3 | jdk:
4 | - openjdk8
5 |
6 | scala:
7 | - 2.12.11
8 | - 2.13.2
9 |
10 | install:
11 | - nvm list
12 | - nvm install 5.10.0
13 | - nvm use 5.10.0
14 | #services:
15 | #- xvfb
16 | before_script:
17 | #- export DISPLAY=:99.0
18 | # Start test server
19 | - cd test/server && npm install && node index.js &
20 |
21 | script:
22 | - sbt ++$TRAVIS_SCALA_VERSION scalastyle
23 | - test/update-readme-sanity-check.sh --fatalDiff
24 | - sbt ++$TRAVIS_SCALA_VERSION scalaHttpJS/test
25 | - sbt ++$TRAVIS_SCALA_VERSION scalaHttpJVM/test
26 | # - sbt ++$TRAVIS_SCALA_VERSION firefox:test
27 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 |
4 | This repository is far from underwhelmed by requests so feel free to file an issue
5 | for any of the cases mentioned below.
6 |
7 | **Want to help?** Issues marked as [open](https://github.com/hmil/RosHTTP/issues?q=is%3Aissue+is%3Aopen+label%3A%22status%3A+open%22) are open to contributions.
8 | Post a comment stating that you would like to work on the issue and do feel free
9 | to ask for more details and discuss possible implementations at any time.
10 |
11 | ## Branches
12 |
13 | **Always base your pull requests on the latest release branch (ie. v2.x.x)**.
14 | master always reflects the latest published version of the library and therefore is
15 | not a suitable target for pull requests.
16 |
17 | ## Reporting bugs
18 |
19 | If you think you found a bug, please file an issue and try to provide a piece of
20 | code that triggers the bug. Indicate in which environment this bug happens and give
21 | some details on each affected environments (java version, jdk used, node version,
22 | browser + version).
23 |
24 | For bug fixes, file an issue as instructed above and create a pull request referencing
25 | the issue. Always try to provide a test case with your bug fix. If for some reason you
26 | can really not test your bug, let me know in your pull request comment.
27 |
28 | ## Feature requests
29 |
30 | Feel free to file an issue to request a feature, or even just to start a conversation
31 | related to the project. It is advised that you discuss any feature you would like
32 | to implement before starting working on it.
33 |
34 | New features **must be tested**.
35 |
36 | ## Development
37 |
38 | This project is built with sbt. While any IDE with decent sbt support would work,
39 | I recommend using _idea_ with the scala plugin.
40 |
41 | The sbt project contains two subprojects: `scalaHttpJS` and `scalaHttpJVM`.
42 | Run one of `sbt scalaHttpJS/console` or `sbt scalaHttpJVM/console` to start an interactive
43 | console with library code in the classpath.
44 |
45 | You can run tests in 4 different environment using the following commands:
46 | ```
47 | sbt scalaHttpJVM/test # Runs on your current JVM (default environment for Scala)
48 | sbt scalaHttpJS/test # Runs in Node.js
49 | sbt chrome:test # Run in chrome*
50 | sbt firefox:test # Runs in firefox*
51 | ```
52 | *Browser testing may require additional software on your computer. See
53 | [https://github.com/scala-js/scala-js-env-selenium#scalajs-env-selenium] for more details.
54 |
55 | The testing server needs to run on your machine while running the test commands.
56 |
57 | ## Testing
58 |
59 | All features are tested in [HttpRequestSpec](https://github.com/hmil/RosHTTP/blob/master/shared/src/test/scala/fr/hmil/roshttp/client/HttpRequestSpec.scala).
60 | In order to test this library, the testing server needs to run on the testing machine.
61 | This server enables us to run tests in a reproducible environment and to test edge cases.
62 |
63 | To start the testing server, go to `test/server` and install the dependencies with
64 | `npm install`. This command must be run when there has been some updates in the
65 | test server dependencies.
66 | Run the test server with `node index.js`.
67 |
68 | If Node.js is not installed on your machine, do the following:
69 | - For Windows and Mac, download the latest LTS release from the [official website](https://nodejs.org).
70 | - For Linux users, I advise downloading Node.js through [NVM](https://github.com/creationix/nvm)
71 |
72 | ## Code style
73 |
74 | The code in this repository should follow Scala.js
75 | [coding style guide](https://github.com/scala-js/scala-js/blob/master/CODINGSTYLE.md)
76 | as much as possible.
77 |
78 | ## Other things you need to know
79 |
80 | Environment-specific code lives in jvm/ and js/. The `DriverTrait` is the
81 | low-level interface between shared and specific code. Shared code is linked to the
82 | correct `HttpDriver` class by the Scala.js compiler.
83 |
84 | All code that gets merged in this code base falls under the terms of the MIT license.
85 | By submitting a pull request to this repository, you consent that your code may be
86 | used, copied, and distributed by anyone as per the terms of the MIT license and
87 | that you are not responsible for the usage other parties may make of your code.
88 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Hadrien Milano
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RösHTTP
2 | [](https://travis-ci.org/hmil/RosHTTP)
3 | [](https://jcenter.bintray.com/fr/hmil/roshttp_2.12/)
4 | [](https://www.scala-js.org)
5 |
6 |
7 | A human-readable scala http client API compatible with:
8 |
9 | - vanilla jvm **scala**
10 | - most **browsers** (_via_ [scala-js](https://github.com/scala-js/scala-js))
11 | - **node.js** (_via_ [scala-js](https://github.com/scala-js/scala-js))
12 |
13 | ## THIS PACKAGE IS NO LONGER MAINTAINED
14 |
15 | I moved on to different ventures and I can no longer afford the time to maintain this package. Feel free to use it as-is, or drop a comment in [#58](https://github.com/hmil/RosHTTP/issues/58) if you would like me to endorse your fork.
16 |
17 |
18 | # Installation
19 |
20 | Add a dependency in your build.sbt:
21 |
22 | ```
23 | resolvers += "hmil.fr" at "https://files.hmil.fr/maven/"
24 |
25 | libraryDependencies += "fr.hmil" %%% "roshttp" % "3.0.0"
26 | ```
27 |
28 | # Usage
29 |
30 | The following is a simplified usage guide. You may find useful information in
31 | the [API doc](http://hmil.github.io/RosHTTP/docs/index.html) too.
32 |
33 | ## Basic usage
34 |
35 | ```scala
36 | import fr.hmil.roshttp.HttpRequest
37 | import monix.execution.Scheduler.Implicits.global
38 | import scala.util.{Failure, Success}
39 | import fr.hmil.roshttp.response.SimpleHttpResponse
40 |
41 | // Runs consistently on the jvm, in node.js and in the browser!
42 | val request = HttpRequest("https://schema.org/WebPage")
43 |
44 | request.send().onComplete({
45 | case res:Success[SimpleHttpResponse] => println(res.get.body)
46 | case e: Failure[SimpleHttpResponse] => println("Houston, we got a problem!")
47 | })
48 | ```
49 |
50 | ## Configuring requests
51 |
52 | [HttpRequests](http://hmil.github.io/RosHTTP/docs/index.html#fr.hmil.roshttp.HttpRequest)
53 | are immutable objects. They expose methods named `.withXXX` which can be used to
54 | create more complex requests.
55 |
56 | ### URI
57 |
58 | The URI can be passed as argument of the request constructor or `.withURI`.
59 | The URI can be built using `.withProtocol`, `.withHost`, `.withPort`,
60 | `.withPath`, and `.withQuery...`. The latter is a bit more complex and
61 | is detailed below.
62 |
63 | ```scala
64 | import fr.hmil.roshttp.Protocol.HTTP
65 |
66 | HttpRequest()
67 | .withProtocol(HTTP)
68 | .withHost("localhost")
69 | .withPort(3000)
70 | .withPath("/weather")
71 | .withQueryParameter("city", "London")
72 | .send() // GET http://localhost:3000/weather?city=London
73 | ```
74 |
75 | #### `.withQueryString`
76 | The whole querystring can be set to a custom value like this:
77 |
78 | ```scala
79 | // Sets the query string such that the target url ends in "?hello%20world"
80 | request.withQueryString("hello world")
81 | ```
82 |
83 | `.withQueryString(string)` urlencodes string and replaces the whole query string
84 | with the result.
85 | To bypass encoding, use `.withQueryStringRaw(rawString)`.
86 |
87 | #### `.withQueryParameter`
88 | Most of the time, the query string is used to pass key/value pairs in the
89 | `application/x-www-form-urlencoded` format.
90 | [HttpRequest](http://hmil.github.io/RosHTTP/docs/index.html#fr.hmil.roshttp.HttpRequest)
91 | offers an API to add, update and delete keys in the query string.
92 |
93 | ```scala
94 | request
95 | .withQueryParameter("foo", "bar")
96 | .withQuerySeqParameter("table", Seq("a", "b", "c"))
97 | .withQueryObjectParameter("map", Seq(
98 | "d" -> "dval",
99 | "e" -> "e value"
100 | ))
101 | .withQueryParameters(
102 | "license" -> "MIT",
103 | "copy" -> "© 2016"
104 | )
105 | /* Query is now:
106 | foo=bar&table=a&table=b&table=c&map[d]=dval&map[e]=e%20value&license=MIT©=%C2%A9%202016
107 | */
108 | ```
109 |
110 | ### HTTP Method
111 |
112 | ```scala
113 | import fr.hmil.roshttp.Method.PUT
114 |
115 | request.withMethod(PUT).send()
116 | ```
117 |
118 | ### Headers
119 |
120 | Set individual headers using `.withHeader`
121 | ```scala
122 | request.withHeader("Accept", "text/html")
123 | ```
124 | Or multiple headers at once using `.withHeaders`
125 | ```scala
126 | request.withHeaders(
127 | "Accept" -> "text/html",
128 | "User-Agent" -> "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"
129 | )
130 | ```
131 |
132 | ### Backend configuration
133 |
134 | Some low-level configuration settings are available in [BackendConfig](http://hmil.github.io/RosHTTP/docs/index.html#fr.hmil.roshttp.BackendConfig).
135 | Each request can use a specific backend configuration using `.withBackendConfig`.
136 |
137 | example:
138 | ```scala
139 | import fr.hmil.roshttp.BackendConfig
140 |
141 | HttpRequest("long.source.of/data")
142 | .withBackendConfig(BackendConfig(
143 | // Uses stream chunks of at most 1024 bytes
144 | maxChunkSize = 1024
145 | ))
146 | .stream()
147 | ```
148 |
149 | ### Cross-domain authorization information
150 |
151 | For security reasons, cross-domain requests are not sent with authorization headers or cookies. If
152 | despite security concerns, this feature is needed, it can be enabled using `withCrossDomainCookies`,
153 | which internally uses the
154 | [`XMLHttpRequest.withCredentials`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials)
155 | method, but has no effect in non-browser environments. Also for same-site requests, setting it to
156 | `true` has no effect either.
157 | ```scala
158 | request.withCrossDomainCookies(true)
159 | ```
160 |
161 | ## Response headers
162 |
163 | A map of response headers is available on the `HttpResponse` object:
164 | ```scala
165 | request.send().map({res =>
166 | println(res.headers("Set-Cookie"))
167 | })
168 | ```
169 |
170 | ## Sending data
171 |
172 | An HTTP request can send data wrapped in an implementation of `BodyPart`. The most common
173 | formats are already provided but you can create your own as well.
174 | A set of implicit conversions is provided in `body.Implicits` for convenience.
175 |
176 | You can `post` or `put` some data with your favorite encoding.
177 | ```scala
178 | import fr.hmil.roshttp.body.Implicits._
179 | import fr.hmil.roshttp.body.URLEncodedBody
180 |
181 | val urlEncodedData = URLEncodedBody(
182 | "answer" -> "42",
183 | "platform" -> "jvm"
184 | )
185 | request.post(urlEncodedData)
186 | // or
187 | request.put(urlEncodedData)
188 | ```
189 |
190 | Create JSON requests easily using implicit conversions.
191 | ```scala
192 | import fr.hmil.roshttp.body.Implicits._
193 | import fr.hmil.roshttp.body.JSONBody._
194 |
195 | val jsonData = JSONObject(
196 | "answer" -> 42,
197 | "platform" -> "node"
198 | )
199 | request.post(jsonData)
200 | ```
201 |
202 | ### File upload
203 |
204 | To send file data you must turn a file into a ByteBuffer and then send it in a
205 | ByteBufferBody. For instance, on the jvm you could do:
206 | ```scala
207 | import java.nio.ByteBuffer
208 | import fr.hmil.roshttp.body.ByteBufferBody
209 |
210 | val buffer = ByteBuffer.wrap(
211 | List[Byte](0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x0a)
212 | .toArray)
213 | request.post(ByteBufferBody(buffer))
214 | ```
215 | Note that the codec argument is important to read the file as-is and avoid side-effects
216 | due to character interpretation.
217 |
218 | ### Multipart
219 |
220 | Use the `MultiPartBody` to compose request bodies arbitrarily. It allows for instance
221 | to send binary data with some textual data.
222 |
223 | The following example illustrates how you could send a form to update a user profile
224 | made of a variety of data types.
225 | ```scala
226 | import fr.hmil.roshttp.body.Implicits._
227 | import fr.hmil.roshttp.body.JSONBody._
228 | import fr.hmil.roshttp.body._
229 |
230 | request.post(MultiPartBody(
231 | // The name part is sent as plain text
232 | "name" -> PlainTextBody("John"),
233 | // The skills part is a complex nested structure sent as JSON
234 | "skills" -> JSONObject(
235 | "programming" -> JSONObject(
236 | "C" -> 3,
237 | "PHP" -> 1,
238 | "Scala" -> 5
239 | ),
240 | "design" -> true
241 | ),
242 | "hobbies" -> JSONArray(
243 | "programming",
244 | "stargazing"
245 | ),
246 | // The picture is sent using a ByteBufferBody, assuming buffer is a ByteBuffer
247 | // containing the image data
248 | "picture" -> ByteBufferBody(buffer, "image/jpeg")
249 | ))
250 | ```
251 |
252 | ## Streaming
253 |
254 | **Warning:** Even though the streaming API works flawlessly on the JVM, it is an
255 | experimental feature as the JS implementation may leak memory or buffer things
256 | in the background.
257 |
258 | ### Download streams
259 |
260 | Streaming a response is as simple as calling `.stream()` instead of `.send()`.
261 | `HttpRequest#stream()` returns a Future of `StreamHttpResponse`. A `StreamHttpResponse`
262 | is just like a `SimpleHttpResponse` except that its `body` property is an
263 | [Observable](https://monix.io/api/2.0/#monix.reactive.Observable).
264 | The observable will spit out a stream of `ByteBuffer`s as shown in this example:
265 |
266 | ```scala
267 | import fr.hmil.roshttp.util.Utils._
268 |
269 | request
270 | .stream()
271 | .map({ r =>
272 | r.body.foreach(buffer => println(getStringFromBuffer(buffer, "UTF-8")))
273 | })
274 | ```
275 | _Note that special care should be taken when converting chunks into strings because
276 | multibyte characters may span multiple chunks._
277 | _In general streaming is used for binary data and any reasonable quantity
278 | of text can safely be handled by the non-streaming API._
279 |
280 | #### HTTP methods
281 |
282 | There is no shortcut method such as `.post` to get a streaming response. You can
283 | still achieve that by using the constructor methods as shown below:
284 | ```scala
285 | import fr.hmil.roshttp.Method.POST
286 |
287 | request
288 | .withMethod(POST)
289 | .withBody(PlainTextBody("My upload data"))
290 | .stream()
291 | // The response will be streamed
292 | ```
293 |
294 | ### Upload Streams
295 |
296 | There are cases where you want to upload some very large data with minimal memory
297 | consumption. We've got you covered! The [StreamBody](http://hmil.github.io/RosHTTP/docs/index.html#fr.hmil.roshttp.body.StreamBody) takes an
298 | [Observable](https://monix.io/api/2.0/#monix.reactive.Observable)[ByteBuffer]
299 | and streams its contents to the server. You can also pass an InputStream directly
300 | using RösHTTP's implicit converters:
301 |
306 | ```scala
307 | import fr.hmil.roshttp.body.Implicits._
308 |
309 | // On the JVM:
310 | // val inputStream = new java.io.FileInputStream("video.avi")
311 | request
312 | .post(inputStream)
313 | .onComplete({
314 | case _:Success[SimpleHttpResponse] => println("Data successfully uploaded")
315 | case _:Failure[SimpleHttpResponse] => println("Error: Could not upload stream")
316 | })
317 | ```
318 |
319 | ## Error handling
320 |
321 | Have you ever been frustrated when an application fails silently or gives you a
322 | vague and insignificant error message? RösHTTP comes with a powerful error handling
323 | API which allows you to deal with exceptions at the granularity level of your choice!
324 |
325 | ### Quick and easy error handling
326 |
327 | Most applications only need to distinguish two failure cases: Application-level failures
328 | and lower-level failures.
329 |
330 | Application-level errors occur when a bad status code is received. For instance:
331 | - The request contained invalid data (400)
332 | - The requested resource does not exist (404)
333 | - The server encountered an error (500)
334 | - _etc..._
335 |
336 | Lower-level errors include timeouts, tcp and dns failures. They are beyond the
337 | scope of most applications and should be treated separately from application-level
338 | errors, especially in code tied to user interfaces.
339 |
340 |
341 | ```scala
342 | import fr.hmil.roshttp.exceptions.HttpException
343 | import java.io.IOException
344 | request.send()
345 | .recover {
346 | case HttpException(e: SimpleHttpResponse) =>
347 | // Here we may have some detailed application-level insight about the error
348 | println("There was an issue with your request." +
349 | " Here is what the application server says: " + e.body)
350 | case e: IOException =>
351 | // By handling transport issues separately, you get a chance to apply
352 | // your own recovery strategy. Should you report to the user? Log the error?
353 | // Retry the request? Send an alert to your ops team?
354 | println("There was a network issue, please try again")
355 | }
356 | ```
357 |
358 | note that `HttpException` is a case class which either contains a `SimpleHttpResponse`
359 | or a `StreamHttpResponse` depending on what you expect your response to be (see
360 | [Streaming](https://github.com/hmil/RosHTTP#streaming)).
361 |
362 | ### Fine-grain error handling
363 |
364 | If you ever need very specific error details, here is the list of exceptions
365 | which can occur in the Future.
366 |
367 | - IOException All RösHTTP exceptions inherit from `java.io.IOException`
368 | - TimeoutException Receiving the response took longer than the configured response timeout threshold.
369 | Note that in this case the headers were already received and you can access them if needed (mainly for debugging purposes).
370 | - RequestException A transport error occurred while sending the request (eg. DNS resolution failure).
371 | - ResponseException A transport error occurred while receiving the response (for buffered responses).
372 | Note that in this case the headers were already received and you can access them if needed (mainly for debugging purposes).
373 | - UploadStreamException The stream used as a data source for the request body failed.
374 | - HttpException Application-level errors (ie. status codes >= 400)
375 |
376 | ---
377 |
378 | Watch the [issues](https://github.com/hmil/RosHTTP/issues)
379 | for upcoming features. Feedback is very welcome so feel free to file an issue if you
380 | see something that is missing.
381 |
382 | # Known limitations
383 |
384 | - Streaming is emulated in the browser, meaning that streaming large request or
385 | response payloads in the browser will consume large amounts of memory and might fail.
386 | This [problem has a solution](https://github.com/hmil/RosHTTP/issues/46)
387 | - Some headers cannot be set in the browser ([list](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name)).
388 | - There is no way to avoid redirects in the browser. This is a W3C spec.
389 | - Chrome does not allow userspace handling of a 407 status code. It is treated
390 | like a network error. See [chromium issue](https://bugs.chromium.org/p/chromium/issues/detail?id=372136).
391 | - The `TRACE` HTTP method does not work in browsers and `PATCH` does not work in the JVM.
392 |
393 | # Contributing
394 |
395 | Please read the [contributing guide](https://github.com/hmil/RosHTTP/blob/master/CONTRIBUTING.md).
396 |
397 | # Changelog
398 |
399 | **v3.0.0**
400 |
401 | - Move to Scala.js 1.0
402 |
403 | **v2.2.4**
404 |
405 | - Update to monix v2.3.3
406 |
407 | **v2.2.0**
408 |
409 | - Add withCrossDomainCookies (by @nondeterministic)
410 |
411 | **v2.1.0**
412 |
413 | - Fix edge cases with `require` in JS environments
414 | - Add missing boolean and array JSON types
415 |
416 | **v2.0.2**
417 |
418 | - Update to monix v2.3.0
419 | - Update to scala v2.11.11
420 |
421 | **v2.0.1**
422 |
423 | - Update to monix v2.1.2
424 |
425 | **v2.0.0**
426 |
427 | - Renamed withQueryArrayParameter to withQuerySeqParameter
428 | - Timeout errors on body
429 | - Rename *Error classes to *Exception
430 | - Add streaming API
431 | - Add implicit Scheduler parameter
432 | - Add implicit execution context parameter
433 |
434 | **v1.1.0**
435 | - Fix bug on responses without Content-Type header
436 | - Detect key-value pairs during query string escapement
437 |
438 | **v1.0.1**
439 | - Fix NPE when reading empty error response
440 |
441 | **v1.0.0**
442 | - Using [semantic versioning](http://semver.org/) from now on
443 | - Renamed RösHTTP
444 | - Add .withBody()
445 |
446 | **v0.3.0**
447 | - Remove general purpose StringBody
448 | - Add missing patch method
449 | - Make Method constructor public
450 | - Disambiguate `withQueryArrayParameter` and `withQueryObjectParameter`
451 | - Remove map parameters from `.withQueryParameter(s)` and `.withHeaders`
452 |
453 | **v0.2.0**
454 | - Support request body with `post()`, `put()` and `options()`
455 | - Add `withHttpMethod()`
456 | - Support HTTPS
457 |
458 | **v0.1.0**
459 | - First release
460 |
461 | # License
462 |
463 | MIT
464 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | name := "RösHTTP root project"
2 |
3 | crossScalaVersions := Seq("2.12.11", "2.13.2")
4 |
5 | lazy val root = project.in(file("."))
6 | .aggregate(scalaHttpJS, scalaHttpJVM)
7 |
8 | lazy val scalaHttp = crossProject(JSPlatform, JVMPlatform).in(file("."))
9 | // .configureCross(InBrowserTesting.cross)
10 | .settings(
11 | name := "roshttp",
12 | version := "3.0.0",
13 | scalaVersion := "2.13.2",
14 | organization := "fr.hmil",
15 | licenses += ("MIT", url("http://opensource.org/licenses/MIT")),
16 | homepage := Some(url("http://github.com/hmil/RosHTTP")),
17 |
18 | pomExtra := (
19 |
20 | git@github.com:hmil/RosHTTP.git
21 | scm:git:git@github.com:hmil/RosHTTP.git
22 |
23 |
24 |
25 | hmil
26 | Hadrien Milano
27 | https://github.com/hmil/
28 |
29 |
30 | ),
31 | pomIncludeRepository := { _ => false },
32 |
33 | libraryDependencies += "org.scala-lang.modules" %% "scala-collection-compat" % "2.1.6",
34 | libraryDependencies += "com.lihaoyi" %%% "utest" % "0.7.4" % Test,
35 | libraryDependencies += "io.monix" %%% "monix" % "3.2.1",
36 |
37 | testFrameworks += new TestFramework("utest.runner.Framework")
38 | )
39 | .jvmSettings(
40 | // jvm-specific settings
41 | )
42 | .jsSettings(
43 | // js-specific settings
44 | libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "1.0.0",
45 |
46 | jsEnv := new org.scalajs.jsenv.nodejs.NodeJSEnv()
47 | )
48 |
49 | lazy val scalaHttpJVM = scalaHttp.jvm
50 | lazy val scalaHttpJS = scalaHttp.js
51 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/BrowserDriver.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import fr.hmil.roshttp.exceptions.{HttpException, RequestException, UploadStreamException}
6 | import fr.hmil.roshttp.response.{HttpResponse, HttpResponseFactory, HttpResponseHeader}
7 | import fr.hmil.roshttp.util.HeaderMap
8 | import monix.execution.Ack.Continue
9 | import monix.execution.{Ack, Scheduler}
10 | import monix.reactive.{Observable, Observer}
11 | import org.scalajs.dom
12 | import org.scalajs.dom.ext.Ajax
13 | import org.scalajs.dom.raw.ErrorEvent
14 |
15 | import scala.collection.mutable
16 | import scala.concurrent.{Future, Promise}
17 | import scala.scalajs.js.JavaScriptException
18 | import scala.scalajs.js.typedarray.ArrayBuffer
19 | import scala.util.{Failure, Success}
20 |
21 | private object BrowserDriver extends DriverTrait {
22 |
23 | override def send[T <: HttpResponse](req: HttpRequest, factory: HttpResponseFactory[T])
24 | (implicit scheduler: Scheduler): Future[T] = {
25 | val p: Promise[T] = Promise[T]()
26 |
27 | val xhr = new dom.XMLHttpRequest()
28 | xhr.open(req.method.toString, req.url)
29 | xhr.withCredentials = req.crossDomainCookies
30 | xhr.responseType = "arraybuffer"
31 | req.headers.foreach(t => xhr.setRequestHeader(t._1, t._2))
32 |
33 | xhr.onerror = { (e: ErrorEvent) =>
34 | p.failure(RequestException(JavaScriptException(e)))
35 | }
36 |
37 | val bufferQueue = new ByteBufferQueue(req.backendConfig.internalBufferLength)
38 |
39 | xhr.onreadystatechange = { (e: dom.Event) =>
40 | if (xhr.readyState == dom.XMLHttpRequest.HEADERS_RECEIVED) {
41 | val headers = xhr.getAllResponseHeaders() match {
42 | case null => Map[String, String]()
43 | case s: String => s.split("\r\n").map({ s =>
44 | val split = s.split(": ")
45 | (split.head, split.tail.mkString.trim)
46 | }).toMap[String, String]
47 | }
48 |
49 | p.completeWith(
50 | factory(
51 | new HttpResponseHeader(xhr.status, HeaderMap(headers)),
52 | bufferQueue.observable,
53 | req.backendConfig)
54 | .map({response =>
55 | if (xhr.status >= 400) {
56 | throw HttpException.badStatus(response)
57 | } else {
58 | response
59 | }
60 | })
61 | )
62 | } else if (xhr.readyState == dom.XMLHttpRequest.DONE) {
63 | chopChunk().foreach(bufferQueue.push)
64 | bufferQueue.end()
65 | }
66 | }
67 |
68 | def chopChunk(): Seq[ByteBuffer] = {
69 | val buffer = xhr.response.asInstanceOf[ArrayBuffer]
70 | val buffers = ByteBufferChopper.chop(
71 | Converters.arrayBufferToByteBuffer(buffer),
72 | req.backendConfig.maxChunkSize)
73 | buffers
74 | }
75 |
76 | if (req.body.isEmpty) {
77 | xhr.send()
78 | } else {
79 | bufferBody(req.body.get.content).andThen({
80 | case buffer: Success[ByteBuffer] => xhr.send(Ajax.InputData.byteBuffer2ajax(buffer.value))
81 | case f: Failure[ByteBuffer] => p.failure(f.exception)
82 | })
83 | }
84 |
85 | p.future
86 | }
87 |
88 | private def bufferBody(bodyStream: Observable[ByteBuffer])(implicit scheduler: Scheduler): Future[ByteBuffer] = {
89 | val bufferQueue = mutable.Queue[ByteBuffer]()
90 | var bytes = 0
91 | val p = Promise[ByteBuffer]()
92 |
93 | bodyStream.subscribe(new Observer[ByteBuffer] {
94 | override def onError(ex: Throwable): Unit = p.failure(UploadStreamException(ex))
95 |
96 | override def onComplete(): Unit = p.success(recomposeBody(bufferQueue, bytes))
97 |
98 | override def onNext(elem: ByteBuffer): Future[Ack] = {
99 | bytes += elem.limit()
100 | bufferQueue.enqueue(elem)
101 | Future.successful(Continue)
102 | }
103 | })
104 |
105 | p.future
106 | }
107 |
108 | private def recomposeBody(seq: mutable.Queue[ByteBuffer], bytes: Int): ByteBuffer = {
109 | // Allocate maximum expected body length
110 | val buffer = ByteBuffer.allocate(bytes)
111 | seq.foreach(chunk => buffer.put(chunk))
112 | buffer.rewind()
113 | buffer
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/ByteBufferChopper.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | import java.nio.ByteBuffer
4 |
5 |
6 | object ByteBufferChopper {
7 |
8 | def chop(buffer: ByteBuffer, maxChunkSize: Int): Seq[ByteBuffer] = {
9 | val nb_buffers = (buffer.limit() + maxChunkSize - 1) / maxChunkSize
10 | val buffers = new Array[ByteBuffer](nb_buffers)
11 | var i = 0
12 | while (i < nb_buffers) {
13 | val length = Math.min(maxChunkSize, buffer.remaining)
14 | buffers(i) = buffer.slice()
15 | buffers(i).limit(length)
16 | buffer.position(buffer.position() + length)
17 | i = i + 1
18 | }
19 | buffers
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/Converters.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import fr.hmil.roshttp.node.buffer.Buffer
6 |
7 | import scala.scalajs.js
8 | import scala.scalajs.js.typedarray.{ArrayBuffer, Int8Array, TypedArrayBuffer, Uint8Array}
9 | import scala.scalajs.js.JSConverters._
10 | import js.typedarray.TypedArrayBufferOps._
11 |
12 | private object Converters {
13 | def byteArrayToUint8Array(arr: Array[Byte]): Uint8Array = {
14 | js.Dynamic.newInstance(js.Dynamic.global.Uint8Array)(arr.toJSArray).asInstanceOf[Uint8Array]
15 | }
16 |
17 | def byteBufferToNodeBuffer(buffer: ByteBuffer): Buffer = {
18 | if (buffer.isDirect) {
19 | js.Dynamic.newInstance(js.Dynamic.global.Buffer)(buffer.arrayBuffer).asInstanceOf[Buffer]
20 | } else if (buffer.hasArray) {
21 | js.Dynamic.newInstance(js.Dynamic.global.Buffer)(byteArrayToUint8Array(buffer.array())).asInstanceOf[Buffer]
22 | } else {
23 | val arr = new Int8Array(buffer.limit())
24 | var i = 0
25 | while (i < arr.length) {
26 | arr(i) = buffer.get(i)
27 | i += 1
28 | }
29 | js.Dynamic.newInstance(js.Dynamic.global.Buffer)(arr).asInstanceOf[Buffer]
30 | }
31 | }
32 |
33 | def nodeBufferToByteBuffer(buffer: Buffer): ByteBuffer = {
34 | TypedArrayBuffer.wrap(buffer.asInstanceOf[ArrayBuffer])
35 | }
36 |
37 | def arrayBufferToByteBuffer(buffer: ArrayBuffer): ByteBuffer = {
38 | TypedArrayBuffer.wrap(buffer)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/CrossPlatformUtils.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | import scala.scalajs.js
4 |
5 |
6 | private object CrossPlatformUtils {
7 |
8 | def encodeURIComponent(query: String): String =
9 | js.URIUtils.encodeURIComponent(query)
10 |
11 | def decodeURIComponent(query: String): String =
12 | js.URIUtils.decodeURIComponent(query)
13 | }
14 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/HttpDriver.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | import fr.hmil.roshttp.node.Modules.{HttpModule, HttpsModule}
4 | import fr.hmil.roshttp.response.{HttpResponse, HttpResponseFactory}
5 | import monix.execution.Scheduler
6 |
7 | import scala.concurrent.Future
8 |
9 | private object HttpDriver extends DriverTrait {
10 |
11 | private var _driver: Option[DriverTrait] = None
12 |
13 | def send[T <: HttpResponse](req: HttpRequest, factory: HttpResponseFactory[T])(implicit scheduler: Scheduler):
14 | Future[T] = {
15 | _driver.getOrElse(chooseBackend()).send(req, factory)
16 | }
17 |
18 | private def chooseBackend(): DriverTrait = {
19 | if (HttpModule.isAvailable && HttpsModule.isAvailable) {
20 | _driver = Some(NodeDriver)
21 | } else {
22 | _driver = Some(BrowserDriver)
23 | }
24 | _driver.get
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/JsEnvUtils.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | /*
4 | Taken from http://japgolly.blogspot.fr/2016/03/scalajs-firefox-chrome-sbt.html
5 | */
6 |
7 | import scala.scalajs.js.Dynamic.global
8 | import scala.util.Try
9 |
10 | private object JsEnvUtils {
11 |
12 | /** Sample (real) values are:
13 | * - Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
14 | * - Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0
15 | * - Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.75 Safari/537.36
16 | */
17 | val userAgent: String =
18 | Try(global.navigator.userAgent.asInstanceOf[String]) getOrElse "Unknown"
19 |
20 | // Check each browser
21 | val isFirefox = userAgent contains "Firefox"
22 | val isChrome = userAgent contains "Chrome"
23 |
24 | val isRealBrowser = isFirefox || isChrome
25 |
26 | // Or you can even just check if running in X
27 | val isRunningInX = userAgent contains "X11"
28 | }
29 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/NodeDriver.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | import java.io.IOException
4 | import java.nio.ByteBuffer
5 |
6 | import fr.hmil.roshttp.ByteBufferQueue.Feeder
7 | import fr.hmil.roshttp.exceptions.{HttpException, RequestException, UploadStreamException}
8 | import fr.hmil.roshttp.node.Modules.{http, https}
9 | import fr.hmil.roshttp.node.buffer.Buffer
10 | import fr.hmil.roshttp.node.http.{IncomingMessage, RequestOptions}
11 | import fr.hmil.roshttp.response.{HttpResponse, HttpResponseFactory, HttpResponseHeader}
12 | import fr.hmil.roshttp.util.HeaderMap
13 | import monix.execution.Ack.Continue
14 | import monix.execution.{Ack, Scheduler}
15 | import monix.reactive.Observer
16 |
17 | import scala.concurrent.{Future, Promise}
18 | import scala.scalajs.js
19 | import scala.scalajs.js.JSConverters._
20 | import scala.scalajs.js.JavaScriptException
21 |
22 | private object NodeDriver extends DriverTrait {
23 |
24 | def makeRequest[T <: HttpResponse](req: HttpRequest, factory: HttpResponseFactory[T], p: Promise[T])
25 | (implicit scheduler: Scheduler): Unit = {
26 | val module = {
27 | if (req.protocol == Protocol.HTTP)
28 | http
29 | else
30 | https
31 | }
32 | var headers = req.headers
33 | if (!req.backendConfig.allowChunkedRequestBody) {
34 | headers += "Transfer-Encoding" -> ""
35 | }
36 | val nodeRequest = module.request(RequestOptions(
37 | hostname = req.host,
38 | port = req.port.orUndefined,
39 | method = req.method.toString,
40 | headers = js.Dictionary(headers.toSeq: _*),
41 | path = req.longPath
42 | ), handleResponse(req, factory, p)_)
43 | nodeRequest.on("error", { (s: js.Dynamic) =>
44 | p.tryFailure(RequestException(new IOException(s.toString)))
45 | ()
46 | })
47 | if (req.body.isDefined) {
48 | req.body.foreach({ part =>
49 | part.content.subscribe(new Observer[ByteBuffer] {
50 | override def onError(ex: Throwable): Unit = {
51 | p.tryFailure(UploadStreamException(ex))
52 | nodeRequest.abort()
53 | }
54 |
55 | override def onComplete(): Unit = {
56 | nodeRequest.end()
57 | }
58 |
59 | override def onNext(elem: ByteBuffer): Future[Ack] = {
60 | nodeRequest.write(Converters.byteBufferToNodeBuffer(elem))
61 | Continue
62 | }
63 | })
64 | })
65 | } else {
66 | nodeRequest.end()
67 | }
68 | }
69 |
70 | def handleResponse[T <: HttpResponse](req:HttpRequest, factory: HttpResponseFactory[T], p: Promise[T])
71 | (message: IncomingMessage)(implicit scheduler: Scheduler): Unit = {
72 | if (message.statusCode >= 300 && message.statusCode < 400 && message.headers.contains("location")) {
73 | makeRequest(req.withURL(message.headers("location")), factory, p)
74 | } else {
75 | val headers = message.headers.toMap[String, String]
76 | val bufferQueue = new ByteBufferQueue(req.backendConfig.internalBufferLength,
77 | new Feeder {
78 | override def onFlush(): Unit = message.resume()
79 | override def onFull(): Unit = message.pause()
80 | })
81 |
82 | message.on("data", { (nodeBuffer: js.Dynamic) =>
83 | convertAndChopBuffer(nodeBuffer, req.backendConfig.maxChunkSize).foreach(bufferQueue.push)
84 | })
85 | message.on("end", { (s: js.Dynamic) =>
86 | bufferQueue.end()
87 | })
88 | message.on("error", { (s: js.Dynamic) =>
89 | bufferQueue.pushError(JavaScriptException(s))
90 | })
91 |
92 | p.completeWith(
93 | factory(new HttpResponseHeader(message.statusCode, HeaderMap(headers)),
94 | bufferQueue.observable,
95 | req.backendConfig)
96 | .map({ response =>
97 | if (message.statusCode < 400) {
98 | response
99 | } else {
100 | throw HttpException.badStatus(response)
101 | }
102 | }))
103 | }
104 | ()
105 | }
106 |
107 | def send[T <: HttpResponse](req: HttpRequest, factory: HttpResponseFactory[T])(implicit scheduler: Scheduler):
108 | Future[T] = {
109 | val p: Promise[T] = Promise[T]()
110 | makeRequest(req, factory, p)
111 | p.future
112 | }
113 |
114 | private def convertAndChopBuffer(nodeBuffer: js.Any, maxChunkSize: Int): Seq[ByteBuffer] = {
115 | val buffer = Converters.nodeBufferToByteBuffer(nodeBuffer.asInstanceOf[Buffer])
116 | ByteBufferChopper.chop(buffer, maxChunkSize)
117 | }
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/node/Global.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.node
2 |
3 | import scala.scalajs.js
4 | import js.annotation._
5 |
6 | /**
7 | * Facade for objects accessible from node's global scope
8 | */
9 | @js.native
10 | @JSGlobalScope
11 | private[roshttp] object Global extends js.Object {
12 | def require[T](name: String): js.UndefOr[T] = js.native
13 | }
14 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/node/Helpers.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.node
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.JavaScriptException
5 |
6 | /**
7 | * collection of helper functions for nodejs related stuff
8 | */
9 | private[roshttp] object Helpers {
10 |
11 | /**
12 | * Tests whether the current environment is commonjs-like
13 | *
14 | * @return true if the function "require" is available on the global scope
15 | */
16 | def isRequireAvailable: Boolean = js.typeOf(js.Dynamic.global.selectDynamic("require")) != "undefined"
17 |
18 | /**
19 | * Gets javascript module using require()
20 | *
21 | * @param moduleName Module name
22 | * @return The requested module as a scala facade
23 | */
24 | def require[T](moduleName: String): Option[T] = {
25 | if (isRequireAvailable) {
26 | try {
27 | Global.require[T](moduleName).toOption
28 | } catch {
29 | case _: JavaScriptException => None
30 | }
31 | } else {
32 | None
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/node/Module.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.node
2 |
3 | private[roshttp] abstract class Module[T](val name: String) {
4 | def isAvailable: Boolean = require.isDefined
5 |
6 | def require(): Option[T]
7 |
8 | lazy val api = require.getOrElse(throw new ModuleNotFoundException(name))
9 | }
10 |
11 | private[roshttp] class ModuleNotFoundException(name: String) extends RuntimeException("Module " + name + " not found")
12 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/node/Modules.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.node
2 |
3 | import fr.hmil.roshttp.node.http.{Http, Https}
4 |
5 | import scala.scalajs.js
6 |
7 |
8 | /**
9 | * This object allows to access nodejs builtin modules without explicitely calling require.
10 | *
11 | * If a browser shim is used and is accessible in the global context, it will be returned
12 | * and no call to require() will take place
13 | */
14 | private[roshttp] object Modules {
15 |
16 | object HttpModule extends Module[Http]("http") {
17 | override def require: Option[Http] = {
18 | if (js.typeOf(js.Dynamic.global.Http) != "undefined")
19 | Some(js.Dynamic.global.Http.asInstanceOf[Http])
20 | else
21 | Helpers.require("http")
22 | }
23 | }
24 |
25 | object HttpsModule extends Module[Https]("https") {
26 | override def require(): Option[Https] = {
27 | if (js.typeOf(js.Dynamic.global.Https) != "undefined")
28 | Some(js.Dynamic.global.Https.asInstanceOf[Https])
29 | else
30 | Helpers.require("https")
31 | }
32 | }
33 |
34 | lazy val http: Http = HttpModule.api
35 | lazy val https: Https = HttpsModule.api
36 | }
37 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/node/buffer/Buffer.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.node.buffer
2 |
3 | import scala.scalajs.js
4 | import js.annotation._
5 |
6 |
7 | /**
8 | * Nodejs Buffer API
9 | *
10 | * @see https://nodejs.org/api/buffer.html
11 | */
12 | @js.native
13 | @JSGlobal
14 | private[roshttp] class Buffer extends js.Object {
15 | // new Buffer(array)
16 | // new Buffer(buffer)
17 | // new Buffer(arrayBuffer)
18 | // new Buffer(size)
19 | // new Buffer(str[, encoding])
20 | // Class Method: Buffer.byteLength(string[, encoding])
21 | // Class Method: Buffer.compare(buf1, buf2)
22 | // Class Method: Buffer.concat(list[, totalLength])
23 | // Class Method: Buffer.isBuffer(obj)
24 | // Class Method: Buffer.isEncoding(encoding)
25 |
26 | @JSBracketAccess
27 | def apply(index: Int): Byte = js.native
28 | @JSBracketAccess
29 | def update(index: Int, v: Byte): Unit = js.native
30 |
31 | def compare(otherBuffer: Buffer): Int = js.native
32 |
33 | def copy(targetBuffer: Buffer): Int = js.native
34 | def copy(targetBuffer: Buffer, targetStart: Int): Int = js.native
35 | def copy(targetBuffer: Buffer, targetStart: Int, sourceStart: Int): Int = js.native
36 | def copy(targetBuffer: Buffer, targetStart: Int, sourceStart: Int, sourceEnd: Int): Int = js.native
37 |
38 | // buf.entries()
39 |
40 | // override def equals(otherBuffer: Buffer): Boolean = js.native
41 |
42 | def fill(value: String): Buffer = js.native
43 | def fill(value: Buffer): Buffer = js.native
44 | def fill(value: Int): Buffer = js.native
45 | def fill(value: String, encoding: String): Buffer = js.native
46 | def fill(value: Buffer, encoding: String): Buffer = js.native
47 | def fill(value: Int, encoding: String): Buffer = js.native
48 | def fill(value: String, offset: Int): Buffer = js.native
49 | def fill(value: Buffer, offset: Int): Buffer = js.native
50 | def fill(value: Int, offset: Int): Buffer = js.native
51 | def fill(value: String, offset: Int, encoding: String): Buffer = js.native
52 | def fill(value: Buffer, offset: Int, encoding: String): Buffer = js.native
53 | def fill(value: Int, offset: Int, encoding: String): Buffer = js.native
54 | def fill(value: String, offset: Int, end: Int): Buffer = js.native
55 | def fill(value: Buffer, offset: Int, end: Int): Buffer = js.native
56 | def fill(value: Int, offset: Int, end: Int): Buffer = js.native
57 | def fill(value: String, offset: Int, end: Int, encoding: String): Buffer = js.native
58 | def fill(value: Buffer, offset: Int, end: Int, encoding: String): Buffer = js.native
59 | def fill(value: Int, offset: Int, end: Int, encoding: String): Buffer = js.native
60 |
61 | def indexOf(value: String): Int = js.native
62 | def indexOf(value: Buffer): Int = js.native
63 | def indexOf(value: Int): Int = js.native
64 | def indexOf(value: String, byteOffset: Int): Int = js.native
65 | def indexOf(value: Buffer, byteOffset: Int): Int = js.native
66 | def indexOf(value: Int, byteOffset: Int): Int = js.native
67 | def indexOf(value: String, byteOffset: Int, encoding: String): Int = js.native
68 | def indexOf(value: Buffer, byteOffset: Int, encoding: String): Int = js.native
69 | def indexOf(value: Int, byteOffset: Int, encoding: String): Int = js.native
70 |
71 | def includes(value: String): Boolean = js.native
72 | def includes(value: Buffer): Boolean = js.native
73 | def includes(value: Int): Boolean = js.native
74 | def includes(value: String, byteOffset: Int): Boolean = js.native
75 | def includes(value: Buffer, byteOffset: Int): Boolean = js.native
76 | def includes(value: Int, byteOffset: Int): Boolean = js.native
77 | def includes(value: String, byteOffset: Int, encoding: String): Boolean = js.native
78 | def includes(value: Buffer, byteOffset: Int, encoding: String): Boolean = js.native
79 | def includes(value: Int, byteOffset: Int, encoding: String): Boolean = js.native
80 |
81 | // buf.keys()
82 |
83 | val length: Int = js.native
84 |
85 | def readDoubleBE(offset: Double): Int = js.native
86 | def readDoubleBE(offset: Double, noAssert: Boolean): Int = js.native
87 |
88 | def readDoubleLE(offset: Double): Int = js.native
89 | def readDoubleLE(offset: Double, noAssert: Boolean): Int = js.native
90 |
91 | def readFloatBE(offset: Float): Int = js.native
92 | def readFloatBE(offset: Float, noAssert: Boolean): Int = js.native
93 |
94 | def readFloatLE(offset: Float): Int = js.native
95 | def readFloatLE(offset: Float, noAssert: Boolean): Int = js.native
96 |
97 | def readInt8(offset: Int): Int = js.native
98 | def readInt8(offset: Int, noAssert: Boolean): Int = js.native
99 |
100 | def readInt16BE(offset: Int): Int = js.native
101 | def readInt16BE(offset: Int, noAssert: Boolean): Int = js.native
102 |
103 | def readInt16LE(offset: Int): Int = js.native
104 | def readInt16LE(offset: Int, noAssert: Boolean): Int = js.native
105 |
106 | def readInt32BE(offset: Int): Int = js.native
107 | def readInt32BE(offset: Int, noAssert: Boolean): Int = js.native
108 |
109 | def readInt32LE(offset: Int): Int = js.native
110 | def readInt32LE(offset: Int, noAssert: Boolean): Int = js.native
111 |
112 | def readIntBE(offset: Int): Int = js.native
113 | def readIntBE(offset: Int, noAssert: Boolean): Int = js.native
114 |
115 | def readIntLE(offset: Int): Int = js.native
116 | def readIntLE(offset: Int, noAssert: Boolean): Int = js.native
117 |
118 | def readUInt8(offset: Int): Int = js.native
119 | def readUInt8(offset: Int, noAssert: Boolean): Int = js.native
120 |
121 | def readUInt16BE(offset: Int): Int = js.native
122 | def readUInt16BE(offset: Int, noAssert: Boolean): Int = js.native
123 |
124 | def readUInt16LE(offset: Int): Int = js.native
125 | def readUInt16LE(offset: Int, noAssert: Boolean): Int = js.native
126 |
127 | def readUInt32BE(offset: Int): Int = js.native
128 | def readUInt32BE(offset: Int, noAssert: Boolean): Int = js.native
129 |
130 | def readUInt32LE(offset: Int): Int = js.native
131 | def readUInt32LE(offset: Int, noAssert: Boolean): Int = js.native
132 |
133 | def readUIntBE(offset: Int): Int = js.native
134 | def readUIntBE(offset: Int, noAssert: Boolean): Int = js.native
135 |
136 | def readUIntLE(offset: Int): Int = js.native
137 | def readUIntLE(offset: Int, noAssert: Boolean): Int = js.native
138 |
139 | def slice(): Buffer = js.native
140 | def slice(start: Int): Buffer = js.native
141 | def slice(start: Int, end: Int): Buffer = js.native
142 |
143 | override def toString(): String = js.native
144 | def toString(encoding: String): String = js.native
145 | def toString(encoding: String, start: Int): String = js.native
146 | def toString(encoding: String, start: Int, end: Int): String = js.native
147 |
148 | def toJSON(): js.Object = js.native
149 |
150 | // buf.values()
151 |
152 | def write(string: String): Int = js.native
153 | def write(string: String, encoding: String): Int = js.native
154 | def write(string: String, offset: Int): Int = js.native
155 | def write(string: String, offset: Int, encoding: String): Int = js.native
156 | def write(string: String, offset: Int, length: Int): Int = js.native
157 | def write(string: String, offset: Int, length: Int, encoding: String): Int = js.native
158 |
159 | def writeDoubleBE(value: Double, offset: Int): Int = js.native
160 | def writeDoubleBE(value: Double, offset: Int, noAssert: Boolean): Int = js.native
161 |
162 | def writeDoubleLE(value: Double, offset: Int): Int = js.native
163 | def writeDoubleLE(value: Double, offset: Int, noAssert: Boolean): Int = js.native
164 |
165 | def writeFloatBE(value: Float, offset: Int): Int = js.native
166 | def writeFloatBE(value: Float, offset: Int, noAssert: Boolean): Int = js.native
167 |
168 | def writeFloatLE(value: Float, offset: Int): Int = js.native
169 | def writeFloatLE(value: Float, offset: Int, noAssert: Boolean): Int = js.native
170 |
171 | def writeInt8(value: Int, offset: Int): Int = js.native
172 | def writeInt8(value: Int, offset: Int, noAssert: Boolean): Int = js.native
173 |
174 | def writeInt16BE(value: Int, offset: Int): Int = js.native
175 | def writeInt16BE(value: Int, offset: Int, noAssert: Boolean): Int = js.native
176 |
177 | def writeInt16LE(value: Int, offset: Int): Int = js.native
178 | def writeInt16LE(value: Int, offset: Int, noAssert: Boolean): Int = js.native
179 |
180 | def writeInt32BE(value: Int, offset: Int): Int = js.native
181 | def writeInt32BE(value: Int, offset: Int, noAssert: Boolean): Int = js.native
182 |
183 | def writeInt32LE(value: Int, offset: Int): Int = js.native
184 | def writeInt32LE(value: Int, offset: Int, noAssert: Boolean): Int = js.native
185 |
186 | def writeIntBE(value: Int, offset: Int): Int = js.native
187 | def writeIntBE(value: Int, offset: Int, noAssert: Boolean): Int = js.native
188 |
189 | def writeIntLE(value: Int, offset: Int): Int = js.native
190 | def writeIntLE(value: Int, offset: Int, noAssert: Boolean): Int = js.native
191 |
192 | def writeUInt8(value: Int, offset: Int): Int = js.native
193 | def writeUInt8(value: Int, offset: Int, noAssert: Boolean): Int = js.native
194 |
195 | def writeUInt16BE(value: Int, offset: Int): Int = js.native
196 | def writeUInt16BE(value: Int, offset: Int, noAssert: Boolean): Int = js.native
197 |
198 | def writeUInt16LE(value: Int, offset: Int): Int = js.native
199 | def writeUInt16LE(value: Int, offset: Int, noAssert: Boolean): Int = js.native
200 |
201 | def writeUInt32BE(value: Int, offset: Int): Int = js.native
202 | def writeUInt32BE(value: Int, offset: Int, noAssert: Boolean): Int = js.native
203 |
204 | def writeUInt32LE(value: Int, offset: Int): Int = js.native
205 | def writeUInt32LE(value: Int, offset: Int, noAssert: Boolean): Int = js.native
206 |
207 | def writeUIntBE(value: Int, offset: Int): Int = js.native
208 | def writeUIntBE(value: Int, offset: Int, noAssert: Boolean): Int = js.native
209 |
210 | def writeUIntLE(value: Int, offset: Int): Int = js.native
211 | def writeUIntLE(value: Int, offset: Int, noAssert: Boolean): Int = js.native
212 | }
213 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/node/events/EventEmitter.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.node.events
2 |
3 | import scala.scalajs.js
4 |
5 | @js.native
6 | private[roshttp] trait EventEmitter extends js.Object {
7 | def on(event: String, cb: js.Function1[js.Dynamic, Unit]): Unit = js.native
8 | }
9 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/node/http/Agent.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.node.http
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation.JSGlobal
5 |
6 |
7 | /**
8 | * node http agent API.
9 | *
10 | * This facade is not complete!
11 | */
12 | @js.native
13 | @JSGlobal
14 | private[roshttp] class Agent extends js.Object {
15 |
16 | def this(options: AgentOptions) {
17 | this()
18 | }
19 |
20 | // def createConnection(options: net.SocketOptions): net.Socket = js.native -- Not implemented here
21 | // def createConnection(options: net.SocketOptions, js.Function): net.Socket = js.native -- Not implemented here
22 |
23 | /**
24 | * Destroy any sockets that are currently in use by the agent.
25 | *
26 | * It is usually not necessary to do this. However, if you are using an agent with
27 | * KeepAlive enabled, then it is best to explicitly shut down the agent when you
28 | * know that it will no longer be used. Otherwise, sockets may hang open for quite
29 | * a long time before the server terminates them.
30 | */
31 | def destroy(): Unit = js.native
32 |
33 | // val freeSockets:
34 |
35 | /**
36 | * Get a unique name for a set of request options, to determine whether a connection
37 | * can be reused. In the http agent, this returns host:port:localAddress.
38 | * In the https agent, the name includes the CA, cert, ciphers, and other
39 | * HTTPS/TLS-specific options that determine socket reusability.
40 | */
41 | def getName(options: RequestOptions): String = js.native
42 |
43 |
44 | /**
45 | * By default set to 256. For Agents supporting HTTP KeepAlive, this sets the
46 | * maximum number of sockets that will be left open in the free state.
47 | */
48 | var maxFreeSockets: Integer = js.native
49 |
50 | /**
51 | * By default set to Infinity. Determines how many concurrent sockets the agent
52 | * can have open per origin. Origin is either a 'host:port' or
53 | * 'host:port:localAddress' combination.
54 | */
55 | var maxSockets: Integer = js.native
56 |
57 | /**
58 | * An object which contains queues of requests that have not yet been assigned
59 | * to sockets. Do not modify.
60 | */
61 | // val requests
62 |
63 | /**
64 | * An object which contains arrays of sockets currently in use by the Agent.
65 | * Do not modify.
66 | */
67 | // val sockets: Seq[Socket] = js.native
68 | }
69 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/node/http/AgentOptions.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.node.http
2 |
3 | import scala.scalajs.js
4 |
5 | private[roshttp] trait AgentOptions extends js.Object {
6 | val keepAlive: Boolean
7 | val keepAliveMsecs: Integer
8 | val maxSockets: Integer
9 | val maxFreeSockets: Integer
10 | }
11 |
12 |
13 | private[roshttp] object AgentOptions {
14 |
15 | /**
16 | *
17 | * @param keepAlive Keep sockets around in a pool to be used by other requests in the future. Default = false
18 | * @param keepAliveMsecs When using HTTP KeepAlive, how often to send TCP KeepAlive packets over
19 | * sockets being kept alive. Default = 1000. Only relevant if keepAlive is set to true.
20 | * @param maxSockets Maximum number of sockets to allow per host. Default = Infinity.
21 | * @param maxFreeSockets Maximum number of sockets to leave open in a free state. Only relevant
22 | * if keepAlive is set to true. Default = 256.
23 | * @return An AgentOption instance
24 | */
25 | def apply(
26 | keepAlive: js.UndefOr[Boolean] = js.undefined,
27 | keepAliveMsecs: js.UndefOr[Integer] = js.undefined,
28 | maxSockets: js.UndefOr[Integer] = js.undefined,
29 | maxFreeSockets: js.UndefOr[Integer] = js.undefined
30 |
31 | ): AgentOptions = {
32 | val r = js.Dynamic.literal()
33 |
34 | keepAlive.foreach(r.keepAlive = _)
35 | keepAliveMsecs.foreach(r.keepAliveMsecs = _)
36 | maxSockets.foreach(r.maxSockets = _)
37 | maxFreeSockets.foreach(r.maxFreeSockets = _)
38 |
39 | r.asInstanceOf[AgentOptions]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/node/http/ClientRequest.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.node.http
2 |
3 | import fr.hmil.roshttp.node.events.EventEmitter
4 | import fr.hmil.roshttp.node.buffer.Buffer
5 |
6 | import scala.scalajs.js
7 | import js.annotation._
8 |
9 | /**
10 | * Complete nodejs http ClientRequest API facade
11 | */
12 | @js.native
13 | @JSGlobal
14 | private[roshttp] class ClientRequest extends EventEmitter {
15 |
16 | /**
17 | * Marks the request as aborting. Calling this will cause remaining data in
18 | * the response to be dropped and the socket to be destroyed.
19 | */
20 | def abort(): Unit = js.native
21 |
22 | /**
23 | * Finishes sending the request. If any parts of the body are unsent, it will
24 | * flush them to the stream. If the request is chunked, this will send the
25 | * terminating '0\r\n\r\n'.
26 | *
27 | * If data is specified, it is equivalent to calling response.write(data, encoding)
28 | * followed by request.end(callback).
29 | *
30 | * If callback is specified, it will be called when the request stream is finished.
31 | */
32 | def end(): Unit = js.native
33 | def end(data: Buffer): Unit = js.native
34 | def end(data: Buffer, callback: js.Function0[Unit]): Unit = js.native
35 | def end(data: String): Unit = js.native
36 | def end(data: String, encoding: String): Unit = js.native
37 | def end(data: String, callback: js.Function0[Unit]): Unit = js.native
38 | def end(data: String, encoding: String, callback: js.Function0[Unit]): Unit = js.native
39 | def end(callback: js.Function0[Unit]): Unit = js.native
40 |
41 |
42 | /**
43 | * Flush the request headers.
44 | *
45 | * For efficiency reasons, Node.js normally buffers the request headers until
46 | * you call request.end() or write the first chunk of request data. It then tries
47 | * hard to pack the request headers and data into a single TCP packet.
48 | *
49 | * That's usually what you want (it saves a TCP round-trip) but not when the
50 | * first data isn't sent until possibly much later. request.flushHeaders() lets
51 | * you bypass the optimization and kickstart the request.
52 | */
53 | def flushHeaders(): Unit = js.native
54 |
55 | /**
56 | * Once a socket is assigned to this request and is connected socket.setNoDelay()
57 | * will be called.
58 | */
59 | def setNoDelay(noDelay: Boolean): Unit = js.native
60 | def setNoDelay(): Unit = js.native
61 |
62 |
63 | /**
64 | * Once a socket is assigned to this request and is connected socket.setKeepAlive() will be called.
65 | */
66 | def setSocketKeepAlive(enable: Boolean, initialDelay: Int): Unit = js.native
67 | def setSocketKeepAlive(enable: Boolean): Unit = js.native
68 | def setSocketKeepAlive(initialDelay: Int): Unit = js.native
69 |
70 | /**
71 | * Once a socket is assigned to this request and is connected socket.setTimeout() will be called.
72 | *
73 | * @param timeout Milliseconds before a request is considered to be timed out.
74 | * @param callback Optional function to be called when a timeout occurs. Same as binding to the timeout event.
75 | */
76 | def setTimeout(timeout: Int, callback: js.Function0[Unit]): Unit = js.native
77 | def setTimeout(timeout: Int): Unit = js.native
78 |
79 |
80 | /**
81 | * Sends a chunk of the body. By calling this method many times, the user can stream
82 | * a request body to aserver--in that case it is suggested to use the ['Transfer-Encoding', 'chunked']
83 | * header line when creating the request.
84 | *
85 | * @param chunk should be a Buffer or a string.
86 | * @param encoding optional and only applies when chunk is a string. Defaults to 'utf8'.
87 | * @param callback optional and will be called when this chunk of data is flushed.
88 | * @return
89 | */
90 | def write(chunk: String, encoding: String, callback: js.Function0[Unit]): ClientRequest = js.native
91 | def write(chunk: String): ClientRequest = js.native
92 | def write(chunk: String, encoding: String): ClientRequest = js.native
93 | def write(chunk: String, callback: js.Function0[Unit]): ClientRequest = js.native
94 | def write(chunk: Buffer): ClientRequest = js.native
95 | def write(chunk: Buffer, callback: js.Function0[Unit]): ClientRequest = js.native
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/node/http/Http.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.node.http
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation.{JSGlobal, JSGlobalScope, JSName}
5 |
6 | /**
7 | * The node http API.
8 | *
9 | * Server-related stuff not included.
10 | * createClient not included because it is deprecated.
11 | *
12 | * @see https://nodejs.org/api/http.html
13 | */
14 | @js.native
15 | private[roshttp] trait Http extends js.Object{
16 |
17 | /**
18 | * A list of the HTTP methods that are supported by the parser.
19 | */
20 | val METHODS: Seq[String] = js.native
21 |
22 | /**
23 | * A collection of all the standard HTTP response status codes, and the short
24 | * description of each. For example, http.STATUS_CODES[404] === 'Not Found'.
25 | */
26 | val STATUS_CODES: Map[Number, String] = js.native
27 |
28 | /**
29 | * Global instance of Agent which is used as the default for all http client requests.
30 | */
31 | val globalAgent: Agent = js.native
32 |
33 | // http.createServer([requestListener]) -- server-side stuff, not needed in this project
34 | // http.createClient([port][, host]) -- deprecated API, not implemented
35 |
36 | /**
37 | * Since most requests are GET requests without bodies, Node.js provides this convenience
38 | * method. The only difference between this method and http.request() is that it sets the
39 | * method to GET and calls req.end() automatically.
40 | */
41 | def get(url: String): ClientRequest = js.native
42 | def get(url: String, cb: js.Function1[IncomingMessage, Unit]): ClientRequest = js.native
43 | def get(options: RequestOptions): ClientRequest = js.native
44 | def get(options: RequestOptions, cb: js.Function1[IncomingMessage, Unit]): ClientRequest = js.native
45 |
46 | /**
47 | * Node.js maintains several connections per server to make HTTP requests. This function
48 | * allows one to transparently issue requests.
49 | * options can be an object or a string. If options is a string, it is automatically
50 | * parsed with url.parse().
51 | */
52 | def request(url: String): ClientRequest = js.native
53 | def request(url: String, cb: js.Function1[IncomingMessage, Unit]): ClientRequest = js.native
54 | def request(options: RequestOptions): ClientRequest = js.native
55 | def request(options: RequestOptions, cb: js.Function1[IncomingMessage, Unit]): ClientRequest = js.native
56 | }
57 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/node/http/Https.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.node.http
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation._
5 |
6 | /**
7 | * For our purposes, we can just pretend https has the same interface as http
8 | */
9 | @js.native
10 | private[roshttp] trait Https extends Http
11 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/node/http/IncomingMessage.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.node.http
2 |
3 | import fr.hmil.roshttp.node.events.EventEmitter
4 |
5 | import scala.scalajs.js
6 | import scala.scalajs.js.annotation.JSGlobal
7 |
8 | @js.native
9 | @JSGlobal
10 | private[roshttp] class IncomingMessage extends EventEmitter {
11 | val headers: js.Dictionary[String] = js.native
12 | val httpVersion: String = js.native
13 | val method: String = js.native
14 | val rawHeaders: js.Dictionary[String] = js.native
15 | val rawTrailers: js.Dictionary[String] = js.native
16 | def setTimeout(msecs: Int, callback: js.Function): IncomingMessage = js.native
17 | val statusCode: Int = js.native
18 | val statusMessage: String = js.native
19 | // message.socket -- not facaded here
20 | val trailers: js.Dictionary[String] = js.native
21 | val url: String = js.native
22 |
23 | def pause(): Unit = js.native
24 | def resume(): Unit = js.native
25 | }
26 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/node/http/RequestOptions.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.node.http
2 |
3 | import scala.scalajs.js
4 |
5 |
6 |
7 | private[roshttp] trait RequestOptions extends js.Object {
8 | val protocol: String
9 | val host: String
10 | val hostname: String
11 | val family: Int
12 | val port: Int
13 | val localAddress: String
14 | val socketPath: String
15 | val method: String
16 | val path: String
17 | val headers: Map[String, String]
18 | val auth: String
19 | val agent: Agent
20 | // val createConnection
21 | }
22 |
23 | private[roshttp] object RequestOptions {
24 |
25 | /**
26 | * @param protocol Protocol to use. Defaults to 'http:'.
27 | * @param host A domain name or IP address of the server to issue the request to. Defaults to 'localhost'.
28 | * @param hostname Alias for host. To support url.parse() hostname is preferred over host.
29 | * @param family IP address family to use when resolving host and hostname. Valid values are 4 or 6.
30 | * When unspecified, both IP v4 and v6 will be used.
31 | * @param port Port of remote server. Defaults to 80.
32 | * @param localAddress Local interface to bind for network connections.
33 | * @param socketPath Unix Domain Socket (use one of host:port or socketPath).
34 | * @param method A string specifying the HTTP request method. Defaults to 'GET'.
35 | * @param path Request path. Defaults to '/'. Should include query string if any. E.G. '/index.html?page=12'.
36 | * An exception is thrown when the request path contains illegal characters. Currently, only
37 | * spaces are rejected but that may change in the future.
38 | * @param headers An object containing request headers.
39 | * @param auth Basic authentication i.e. 'user:password' to compute an Authorization header.
40 | * @param agent Controls Agent behavior. When an Agent is used request will default to Connection: keep-alive.
41 | * @return
42 | */
43 | def apply(
44 | protocol: js.UndefOr[String] = js.undefined,
45 | host: js.UndefOr[String] = js.undefined,
46 | hostname: js.UndefOr[String] = js.undefined,
47 | family: js.UndefOr[Int] = js.undefined,
48 | port: js.UndefOr[Int] = js.undefined,
49 | localAddress: js.UndefOr[String] = js.undefined,
50 | socketPath: js.UndefOr[String] = js.undefined,
51 | method: js.UndefOr[String] = js.undefined,
52 | path: js.UndefOr[String] = js.undefined,
53 | headers: js.UndefOr[js.Dictionary[String]] = js.undefined,
54 | auth: js.UndefOr[String] = js.undefined,
55 | agent: js.UndefOr[Agent] = js.undefined
56 |
57 | ): RequestOptions = {
58 | val r = js.Dynamic.literal()
59 |
60 | protocol.foreach(r.protocol = _)
61 | host.foreach(r.host = _)
62 | hostname.foreach(r.hostname = _)
63 | family.foreach(r.family = _)
64 | port.foreach(r.port = _)
65 | localAddress.foreach(r.localAddress = _)
66 | socketPath.foreach(r.socketPath = _)
67 | method.foreach(r.method = _)
68 | path.foreach(r.path = _)
69 | headers.foreach(r.headers = _)
70 | auth.foreach(r.auth = _)
71 | agent.foreach(r.agent = _)
72 |
73 | r.asInstanceOf[RequestOptions]
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/js/src/main/scala/fr/hmil/roshttp/node/net/SocketOptions.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.node.net
2 |
3 | import scala.scalajs.js
4 |
5 | private[roshttp] trait SocketOptions extends js.Object {
6 | // val fd: FileDescriptor - not implemented here
7 | val allowHalfOpen: Boolean
8 | val readable: Boolean
9 | val writable: Boolean
10 | }
11 |
12 | private[roshttp] object SocketOptions {
13 | def apply(
14 | allowHalfOpen: js.UndefOr[Boolean],
15 | readable: js.UndefOr[Boolean],
16 | writable: js.UndefOr[Boolean]
17 | ): SocketOptions = {
18 | val r = js.Dynamic.literal()
19 | allowHalfOpen.foreach(r.allowHalfOpen = _)
20 | readable.foreach(r.readable = _)
21 | writable.foreach(r.writable = _)
22 | r.asInstanceOf[SocketOptions]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/jvm/src/main/scala/fr/hmil/roshttp/CrossPlatformUtils.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | import java.net.{URLDecoder, URLEncoder}
4 |
5 | private object CrossPlatformUtils {
6 |
7 | def encodeURIComponent(query: String): String = {
8 | URLEncoder.encode(query, "UTF-8").replace("+", "%20")
9 | }
10 |
11 | def decodeURIComponent(query: String): String =
12 | URLDecoder.decode(query, "UTF-8")
13 | }
14 |
--------------------------------------------------------------------------------
/jvm/src/main/scala/fr/hmil/roshttp/HttpDriver.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | import java.net.{HttpURLConnection, URL}
4 | import java.nio.ByteBuffer
5 |
6 | import fr.hmil.roshttp.exceptions._
7 | import fr.hmil.roshttp.response.{HttpResponse, HttpResponseFactory, HttpResponseHeader}
8 | import fr.hmil.roshttp.util.HeaderMap
9 | import monix.execution.Ack.Continue
10 | import monix.execution.{Ack, Scheduler}
11 | import monix.reactive.{Observable, Observer}
12 | import monix.eval.Task
13 |
14 | import scala.concurrent.{Future, Promise, blocking}
15 |
16 |
17 | private object HttpDriver extends DriverTrait {
18 |
19 | def send[T <: HttpResponse]
20 | (req: HttpRequest, responseFactory: HttpResponseFactory[T])
21 | (implicit scheduler: Scheduler): Future[T] = {
22 |
23 | sendRequest(req).flatMap({ connection =>
24 | readResponse(connection, responseFactory, req.backendConfig)})
25 | }
26 |
27 | private def sendRequest(req: HttpRequest)(implicit scheduler: Scheduler): Future[HttpURLConnection] = {
28 | val p = Promise[HttpURLConnection]()
29 | val connection = new URL(req.url).openConnection().asInstanceOf[HttpURLConnection]
30 | req.headers.foreach(t => connection.addRequestProperty(t._1, t._2))
31 | connection.setRequestMethod(req.method.toString)
32 | if (req.body.isDefined) {
33 | req.body.foreach({ part =>
34 | connection.setDoOutput(true)
35 | if (req.backendConfig.allowChunkedRequestBody) {
36 | req.headers.get("Content-Length") match {
37 | case Some(lengthStr) =>
38 | try {
39 | val length = lengthStr.toInt
40 | connection.setFixedLengthStreamingMode(length)
41 | } catch {
42 | case e:NumberFormatException =>
43 | p.tryFailure(e)
44 | }
45 | case None => connection.setChunkedStreamingMode(req.backendConfig.maxChunkSize)
46 | }
47 | }
48 |
49 | val os = connection.getOutputStream
50 | part.content.subscribe(new Observer[ByteBuffer] {
51 | override def onError(ex: Throwable): Unit = {
52 | os.close()
53 | p.tryFailure(UploadStreamException(ex))
54 | }
55 | override def onComplete(): Unit = {
56 | os.close()
57 | p.trySuccess(connection)
58 | }
59 | override def onNext(buffer: ByteBuffer): Future[Ack] = {
60 | if (buffer.hasArray) {
61 | os.write(buffer.array().view.slice(0, buffer.limit()).toArray)
62 | } else {
63 | val tmp = new Array[Byte](buffer.limit)
64 | buffer.get(tmp)
65 | os.write(tmp)
66 | }
67 | Continue
68 | }
69 | })
70 | })
71 | } else {
72 | p.trySuccess(connection)
73 | }
74 | p.future
75 | }
76 |
77 | private def readResponse[T <: HttpResponse](
78 | connection: HttpURLConnection, responseFactory: HttpResponseFactory[T], config: BackendConfig)
79 | (implicit scheduler: Scheduler): Future[T] = {
80 | val code = connection.getResponseCode
81 | val headerMap = HeaderMap(Iterator.from(0)
82 | .map(i => (i, connection.getHeaderField(i)))
83 | .takeWhile(_._2 != null)
84 | .flatMap({ t =>
85 | connection.getHeaderFieldKey(t._1) match {
86 | case null => None
87 | case key => Some((key, t._2.mkString.trim))
88 | }
89 | }).toMap[String, String])
90 |
91 | val header = new HttpResponseHeader(code, headerMap)
92 |
93 | Future {
94 | blocking {
95 | if (code < 400) {
96 | responseFactory(
97 | header,
98 | inputStreamToObservable(connection.getInputStream, config.maxChunkSize),
99 | config
100 | )
101 | } else {
102 | responseFactory(
103 | header,
104 | Option(connection.getErrorStream)
105 | .map(is => inputStreamToObservable(is, config.maxChunkSize))
106 | .getOrElse(Observable.eval(ByteBuffer.allocate(0))),
107 | config
108 | ).map(response => throw HttpException.badStatus(response))
109 | }
110 | }
111 | }.flatMap(f => f)
112 | }
113 |
114 | private def inputStreamToObservable(in: java.io.InputStream, chunkSize: Int): Observable[ByteBuffer] = {
115 | val iterator = new Iterator[ByteBuffer] {
116 | private var buffer: Array[Byte] = _
117 | private var lastCount = 0
118 |
119 | def hasNext: Boolean =
120 | lastCount match {
121 | case 0 =>
122 | buffer = new Array[Byte](chunkSize)
123 | lastCount = in.read(buffer)
124 | lastCount >= 0
125 | case nr =>
126 | nr >= 0
127 | }
128 |
129 | def next(): ByteBuffer = {
130 | if (lastCount < 0)
131 | throw new NoSuchElementException
132 | else {
133 | val result = ByteBuffer.wrap(buffer, 0, lastCount)
134 | lastCount = 0
135 | result
136 | }
137 | }
138 | }
139 |
140 | Observable.fromIterator(Task(iterator))
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/jvm/src/main/scala/fr/hmil/roshttp/JsEnvUtils.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | private object JsEnvUtils {
4 | val userAgent: String = "jvm"
5 |
6 | // Check each browser
7 | val isFirefox = false
8 | val isChrome = false
9 |
10 | val isRealBrowser = false
11 |
12 | // Or you can even just check if running in X
13 | val isRunningInX = false
14 | }
15 |
--------------------------------------------------------------------------------
/project/InBrowserTesting.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 | import sbt.Keys._
3 | import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
4 | import org.scalajs.jsdependencies.sbtplugin.JSDependenciesPlugin.autoImport._
5 |
6 | import org.scalajs.jsenv.selenium._
7 |
8 | object InBrowserTesting {
9 |
10 | lazy val testAll = TaskKey[Unit]("test-all", "Run tests in all test platforms.")
11 |
12 | val ConfigFirefox = config("firefox")
13 | val ConfigChrome = config("chrome")
14 |
15 | private def browserConfig(cfg: Configuration, env: SeleniumJSEnv): Project => Project =
16 | _.settings(
17 | inConfig(cfg)(
18 | Defaults.testSettings ++
19 | // scalaJSTestSettings ++
20 | Seq(
21 |
22 | // Scala.JS public settings
23 | scalaJSLinkerConfig := (scalaJSLinkerConfig in Test).value,
24 | fastOptJS := (fastOptJS in Test).value,
25 | fullOptJS := (fullOptJS in Test).value,
26 | jsDependencies := (jsDependencies in Test).value,
27 | jsDependencyFilter := (jsDependencyFilter in Test).value,
28 | jsDependencyManifest := (jsDependencyManifest in Test).value,
29 | jsDependencyManifests := (jsDependencyManifests in Test).value,
30 | jsManifestFilter := (jsManifestFilter in Test).value,
31 | // loadedJSEnv := (loadedJSEnv in Test).value,
32 | packageJSDependencies := (packageJSDependencies in Test).value,
33 | packageMinifiedJSDependencies := (packageMinifiedJSDependencies in Test).value,
34 | resolvedJSDependencies := (resolvedJSDependencies in Test).value,
35 | // resolvedJSEnv := (resolvedJSEnv in Test).value,
36 | // scalaJSConsole := (scalaJSConsole in Test).value,
37 | scalaJSIR := (scalaJSIR in Test).value,
38 | scalaJSLinkedFile := (scalaJSLinkedFile in Test).value,
39 | scalaJSNativeLibraries := (scalaJSNativeLibraries in Test).value,
40 | scalajsp := (scalajsp in Test).evaluated,
41 | scalaJSStage := (scalaJSStage in Test).value,
42 |
43 | // Scala.JS internal settings
44 | scalaJSLinker := (scalaJSLinker in Test).value,
45 |
46 | // SBT test settings
47 | definedTestNames := (definedTestNames in Test).value,
48 | definedTests := (definedTests in Test).value,
49 | // executeTests := (executeTests in Test).value,
50 | // loadedTestFrameworks := (loadedTestFrameworks in Test).value,
51 | // testExecution := (testExecution in Test).value,
52 | // testFilter := (testFilter in Test).value,
53 | testForkedParallel := (testForkedParallel in Test).value,
54 | // testFrameworks := (testFrameworks in Test).value,
55 | testGrouping := (testGrouping in Test).value,
56 | // testListeners := (testListeners in Test).value,
57 | // testLoader := (testLoader in Test).value,
58 | // testOnly := (testOnly in Test).value,
59 | testOptions := (testOptions in Test).value,
60 | // testQuick := (testQuick in Test).value,
61 | testResultLogger := (testResultLogger in Test).value,
62 | // test := (test in Test).value,
63 |
64 | // In-browser settings
65 | jsEnv := env)))
66 |
67 | def js: Project => Project = {
68 | //val materializer = new CustomFileMaterializer("test/server/runtime", "http://localhost:3000/runtime")
69 | _.configure(
70 | browserConfig(ConfigFirefox, new SeleniumJSEnv(org.openqa.selenium.remote.DesiredCapabilities.firefox())),
71 | browserConfig(ConfigChrome, new SeleniumJSEnv(org.openqa.selenium.remote.DesiredCapabilities.chrome())))
72 | .settings(
73 | testAll := {
74 | (test in Test).value
75 | (test in ConfigFirefox).value
76 | (test in ConfigChrome).value
77 | })
78 | }
79 |
80 | def jvm: Project => Project =
81 | _.settings(
82 | testAll := (test in Test).value)
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version = 1.3.10
2 |
3 | set logLevel in Global := Level.Debug
4 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.0.0"
2 |
3 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.1")
4 |
5 | addSbtPlugin("org.scalastyle" % "scalastyle-sbt-plugin" % "1.0.0")
6 |
7 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.1")
8 |
9 | addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.5.0")
10 |
11 | addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4")
12 |
13 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0")
14 |
15 | addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.0")
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmil/RosHTTP/7711894cebb11a85e44ea040deb69b0bce66c9ad/release.sh
--------------------------------------------------------------------------------
/scalastyle-config.xml:
--------------------------------------------------------------------------------
1 |
2 | Scalastyle configuration for Scala.js
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | COLON, COMMA, RPAREN
69 |
70 |
71 |
72 |
73 | LPAREN
74 |
75 |
76 |
77 |
78 | IF, FOR, WHILE, DO, TRY
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/shared/src/main/scala-2.12/fr/hmil/roshttp/util/HeaderMap.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.util
2 |
3 | import fr.hmil.roshttp.util.HeaderMap.CaseInsensitiveString
4 |
5 |
6 | /** A set of HTTP headers identified by case insensitive keys
7 | *
8 | * A Map[CaseInsensitiveString, String] would conform to the strict Map specification
9 | * but it would make the API ugly, forcing the explicit usage of CaseInsensitiveString
10 | * instead of string.
11 | *
12 | * That's why we have the HeaderMap class to represent HTTP headers in a map like
13 | * interface which is nice to use. It is however not *exactly* a map because
14 | * different keys can map to the same value if they are case-insensitive equivalent.
15 | *
16 | * @tparam B Required for MapLike implementation. Should always be set to String.
17 | */
18 | class HeaderMap[B >: String] private(map: Map[CaseInsensitiveString, B] = Map())
19 | // extends WithDefault[String, B](new HeaderMap[B](), Map()) {
20 | // extends MapOps[String, B, Map[String, B], HeaderMap[B]] {
21 | extends Map[String, B] {
22 |
23 | override def empty: HeaderMap[B] = new HeaderMap(Map())
24 |
25 | override def get(key: String): Option[B] = {
26 | map.get(new CaseInsensitiveString(key))
27 | }
28 |
29 | override def iterator: Iterator[(String, B)] = {
30 | map.map({ t => (t._1.value, t._2)}).iterator
31 | }
32 |
33 | override def +[B1 >: B](kv: (String, B1)): HeaderMap[B1] = {
34 | val key = new CaseInsensitiveString(kv._1)
35 | new HeaderMap[B1](map - key + (key -> kv._2))
36 | }
37 |
38 | override def -(key: String): HeaderMap[B] = {
39 | new HeaderMap[B](map - new CaseInsensitiveString(key))
40 | }
41 |
42 | override def updated[B1 >: B](key: String, value: B1): HeaderMap[B1] = {
43 | new HeaderMap[B1](map.updated(new CaseInsensitiveString(key), value))
44 | }
45 |
46 | override def toString: String = {
47 | map.map({ t => t._1 + ": " + t._2}).mkString("\n")
48 | }
49 | }
50 |
51 | object HeaderMap {
52 |
53 | /** Creates a HeaderMap from a map of string to string. */
54 | def apply(map: Map[String, String]): HeaderMap[String] = new HeaderMap(
55 | map.map(t => (new CaseInsensitiveString(t._1), t._2))
56 | )
57 |
58 | /** Creates an empty HeaderMap. */
59 | def apply(): HeaderMap[String] = HeaderMap(Map())
60 |
61 | /** A string whose equals and hashCode methods are case insensitive. */
62 | class CaseInsensitiveString(val value: String) {
63 |
64 | override def equals(other: Any): Boolean = other match {
65 | case s:CaseInsensitiveString => s.value.equalsIgnoreCase(value)
66 | case _ => false
67 | }
68 |
69 | override def hashCode(): Int = value.toLowerCase.hashCode
70 |
71 | override def toString: String = value
72 | }
73 | }
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/BackendConfig.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | /** Low-level configuration for the HTTP client backend
4 | *
5 | * @param maxChunkSize Maximum size of each data chunk in streamed requests/responses.
6 | * @param internalBufferLength Maximum number of chunks of response data to buffer when the network is faster than what
7 | * the stream consumer can handle.
8 | * @param allowChunkedRequestBody If set to false, HTTP chunked encoding will be disabled (i.e. the request payload
9 | * cannot be streamed).
10 | */
11 | class BackendConfig private(
12 | val maxChunkSize: Int,
13 | val internalBufferLength: Int,
14 | val allowChunkedRequestBody: Boolean
15 | )
16 |
17 | object BackendConfig {
18 | def apply(
19 | maxChunkSize: Int = 8192,
20 | internalBufferLength: Int = 128,
21 | allowChunkedRequestBody: Boolean = true
22 | ): BackendConfig = new BackendConfig(
23 | maxChunkSize = maxChunkSize,
24 | internalBufferLength = internalBufferLength,
25 | allowChunkedRequestBody = allowChunkedRequestBody
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/ByteBufferQueue.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import fr.hmil.roshttp.ByteBufferQueue.Feeder
6 | import monix.execution.{Ack, Cancelable}
7 | import monix.execution.Ack.{Continue, Stop}
8 | import monix.reactive.Observable
9 | import monix.reactive.observers.Subscriber
10 |
11 | import scala.collection.mutable
12 | import scala.concurrent.ExecutionContext
13 | import scala.util.{Failure, Success, Try}
14 |
15 | /**
16 | * Mutable queue of byteBuffers which acts as a buffer between a source and a
17 | * sink. This queue guarantees that all operations (data and errors) exit in
18 | * the same order as they entered.
19 | */
20 | private[roshttp] class ByteBufferQueue(
21 | private val capacity: Int,
22 | private val feeder: Feeder = ByteBufferQueue.noopFeeder)
23 | (implicit ec: ExecutionContext) {
24 |
25 | private var subscriber: Option[Subscriber[ByteBuffer]] = None
26 | private val bufferQueue = mutable.Queue[ByteBuffer]()
27 | private var hasEnd = false
28 | private var isWaitingForAck = false
29 | private var error: Throwable = _
30 |
31 | private val cancelable = new Cancelable {
32 | override def cancel(): Unit = stop()
33 | }
34 |
35 | def propagate(): Unit = subscriber.foreach({ subscriber =>
36 | if (!isWaitingForAck) {
37 | if (bufferQueue.nonEmpty) {
38 | isWaitingForAck = true
39 | val wasFull = isFull
40 | subscriber.onNext(bufferQueue.dequeue()).onComplete(handleAck)
41 | if (wasFull) {
42 | feeder.onFlush()
43 | }
44 | } else if (hasEnd) {
45 | if (error != null) {
46 | subscriber.onError(error)
47 | }
48 | stop()
49 | }
50 | }
51 | })
52 |
53 | def handleAck(ack: Try[Ack]): Unit = {
54 | isWaitingForAck = false
55 | ack match {
56 | case Success(Stop) =>
57 | subscriber = None
58 | case Success(Continue) =>
59 | if (bufferQueue.nonEmpty) {
60 | propagate()
61 | } else if (hasEnd) {
62 | stop()
63 | }
64 | case Failure(ex) =>
65 | subscriber = None
66 | subscriber.foreach(_.onError(ex))
67 | }
68 | }
69 |
70 | def push(buffer: ByteBuffer): Unit = {
71 | if (hasEnd) throw new IllegalStateException("Trying to push new data to an ended buffer queue")
72 | if (isFull) throw new IllegalStateException("Buffer queue is full")
73 | bufferQueue.enqueue(buffer)
74 | if (isFull) {
75 | feeder.onFull()
76 | }
77 | if (bufferQueue.nonEmpty) {
78 | propagate()
79 | }
80 | }
81 |
82 | def end(): Unit = {
83 | hasEnd = true
84 | if (bufferQueue.isEmpty) {
85 | stop()
86 | }
87 | }
88 |
89 | def isFull: Boolean = {
90 | bufferQueue.length == capacity
91 | }
92 |
93 | def pushError(error: Throwable): Unit = {
94 | this.error = error
95 | this.hasEnd = true
96 | propagate()
97 | }
98 |
99 | val observable = new Observable[ByteBuffer]() {
100 | override def unsafeSubscribeFn(sub: Subscriber[ByteBuffer]): Cancelable = {
101 | if (subscriber.isDefined) {
102 | throw new IllegalStateException("A subscriber is already defined")
103 | }
104 | subscriber = Some(sub)
105 | if (bufferQueue.nonEmpty) {
106 | propagate()
107 | } else if (hasEnd) {
108 | stop()
109 | }
110 | cancelable
111 | }
112 | }
113 |
114 | private def stop(): Unit = {
115 | subscriber.foreach(_.onComplete())
116 | }
117 |
118 | def length: Int = {
119 | bufferQueue.length
120 | }
121 | }
122 |
123 | object ByteBufferQueue {
124 | trait Feeder {
125 | def onFull(): Unit
126 | def onFlush(): Unit
127 | }
128 |
129 | private val noopFeeder = new Feeder {
130 | override def onFlush(): Unit = ()
131 | override def onFull(): Unit = ()
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/DriverTrait.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | import fr.hmil.roshttp.response.{HttpResponse, HttpResponseFactory}
4 | import monix.execution.Scheduler
5 |
6 | import scala.concurrent.Future
7 |
8 | private trait DriverTrait {
9 | def send[T <: HttpResponse](req: HttpRequest, factory: HttpResponseFactory[T])(implicit scheduler: Scheduler):
10 | Future[T]
11 | }
12 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/HttpRequest.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | import java.net.URI
4 | import java.util.concurrent.TimeUnit
5 |
6 | import fr.hmil.roshttp.body.BodyPart
7 | import fr.hmil.roshttp.exceptions.TimeoutException
8 | import fr.hmil.roshttp.response.{HttpResponse, HttpResponseFactory, SimpleHttpResponse, StreamHttpResponse}
9 | import fr.hmil.roshttp.util.{HeaderMap, Utils}
10 | import monix.execution.Scheduler
11 |
12 | import scala.concurrent.{Future, Promise}
13 | import scala.concurrent.duration.{FiniteDuration, TimeUnit}
14 | import scala.util.{Success, Failure}
15 |
16 | /** Builds an HTTP request.
17 | *
18 | * The request is sent using [[send]]. A request can be sent multiple times.
19 | */
20 | final class HttpRequest private (
21 | val method: Method,
22 | val host: String,
23 | val path: String,
24 | val port: Option[Int],
25 | val protocol: Protocol,
26 | val queryString: Option[String],
27 | val crossDomainCookies: Boolean,
28 | val headers: HeaderMap[String],
29 | val body: Option[BodyPart],
30 | val backendConfig: BackendConfig,
31 | val timeout: FiniteDuration) {
32 |
33 | /** The path with the query string or just the path if there is no query string */
34 | val longPath = path + queryString.map(q => s"?$q").getOrElse("")
35 |
36 | /** The target url for this request */
37 | val url: String = s"$protocol://$host${port.fold("")(":" + _)}$longPath"
38 |
39 | /** Sets the HTTP method. Defaults to GET.
40 | *
41 | * Beware of browser limitations when using exotic methods.
42 | *
43 | * @param method The new method
44 | * @return A copy of this [[HttpRequest]] with an updated method
45 | */
46 | def withMethod(method: Method): HttpRequest =
47 | copy(method = method)
48 |
49 | /** Sets the host used in the request URI.
50 | *
51 | * @param host The new host
52 | * @return A copy of this [[HttpRequest]] with an updated host
53 | */
54 | def withHost(host: String): HttpRequest =
55 | copy(host = host)
56 |
57 | /** Sets the path used in the request URI.
58 | *
59 | * The path is the part that lies between the host (or port if present)
60 | * and the query string or the end of the request. Note that the query
61 | * string is not part of the path.
62 | *
63 | * @param path The new path, including the leading '/' and excluding
64 | * any '?' or '#' and subsequent characters
65 | * @return A copy of this [[HttpRequest]] with an updated path
66 | */
67 | def withPath(path: String): HttpRequest =
68 | copy(path = path)
69 |
70 | /** Sets the port used in the request URI.
71 | *
72 | * @param port The new port
73 | * @return A copy of this [[HttpRequest]] with an updated port
74 | */
75 | def withPort(port: Int): HttpRequest =
76 | copy(port = Some(port))
77 |
78 | /** Discards changes introduced by any call to [[withPort]]
79 | *
80 | * @return A copy of this [[HttpRequest]] with no explicit port.
81 | */
82 | def withDefaultPort(): HttpRequest =
83 | copy(port = None)
84 |
85 | /** Sets the protocol used in the request URL.
86 | *
87 | * Setting the protocol also sets the port accordingly (80 for HTTP, 443 for HTTPS).
88 | *
89 | * @param protocol The HTTP or HTTPS protocol
90 | * @return A copy of this [[HttpRequest]] with an updated protocol and port
91 | */
92 | def withProtocol(protocol: Protocol): HttpRequest = {
93 | copy(protocol = protocol)
94 | }
95 |
96 | /** Sets the query string.
97 | *
98 | * The argument is escaped by this method. If you want to bypass the escaping, use [[withQueryStringRaw]].
99 | *
100 | * Escaping also means that the queryString property will generally not be equal to what you passed as
101 | * argument to [[withQueryString]].
102 | *
103 | * For instance: `request.withQueryString("äéuô").queryString.get != "%C3%A4%C3%A9u%C3%B4"`
104 | *
105 | * @param queryString The unescaped query string.
106 | * @return A copy of this [[HttpRequest]] with an updated queryString
107 | */
108 | def withQueryString(queryString: String): HttpRequest =
109 | copy(queryString = Some(Utils.encodeQueryString(queryString)))
110 |
111 | /** Sets the query string without escaping.
112 | *
113 | * Raw query strings must only contain legal characters as per rfc3986. Adding
114 | * special characters yields undefined behaviour.
115 | *
116 | * In most cases, [[withQueryString]] should be preferred.
117 | *
118 | * @param queryString The raw, escaped query string
119 | * @return A copy of this [[HttpRequest]] with an updated queryString
120 | */
121 | def withQueryStringRaw(queryString: String): HttpRequest =
122 | copy(queryString = Some(queryString))
123 |
124 | /** Removes the query string.
125 | *
126 | * @return A copy of this [[HttpRequest]] without query string
127 | */
128 | def withoutQueryString(): HttpRequest =
129 | copy(queryString = None)
130 |
131 | /** Adds a query parameter key/value pair.
132 | *
133 | * Query parameters end up in the query string as `key=value` pairs separated by ampersands.
134 | * Both the key and parameter are escaped to ensure proper query string format.
135 | *
136 | * @param key The unescaped parameter key
137 | * @param value The unescaped parameter value
138 | * @return A copy of this [[HttpRequest]] with an updated query string.
139 | */
140 | def withQueryParameter(key: String, value: String): HttpRequest =
141 | copy(queryString = Some(
142 | queryString.map(q => q + '&').getOrElse("") +
143 | CrossPlatformUtils.encodeURIComponent(key) +
144 | "=" +
145 | CrossPlatformUtils.encodeURIComponent(value)))
146 |
147 | /** Adds a query array parameter.
148 | *
149 | * Although this is not part of a spec, most servers recognize bracket indices
150 | * in a query string as array indices or object keys.
151 | *
152 | * example: ?list[0]=foo&list[1]=bar
153 | *
154 | * This method formats array values according to the above example.
155 | *
156 | * @param key The unescaped parameter key
157 | * @param values The unescaped parameter array values
158 | * @return A copy of this [[HttpRequest]] with an updated query string.
159 | */
160 | def withQuerySeqParameter(key: String, values: Seq[String]): HttpRequest =
161 | values.foldLeft(this)((acc, value) => acc.withQueryParameter(key, value))
162 |
163 | /** Adds an object made of key/value pairs to the query string.
164 | *
165 | * Although this is not part of a spec, most servers recognize bracket indices
166 | * in a query string as object keys. The same key can appear multiple times as
167 | * some server will interpret this as multiple elements in an array for that key.
168 | *
169 | * example: ?obj[foo]=bar&obj[baz]=42
170 | *
171 | * @param key The unescaped parameter key
172 | * @param values The unescaped parameter map values
173 | * @return A copy of this [[HttpRequest]] with an updated query string.
174 | */
175 | def withQueryObjectParameter(key: String, values: Seq[(String, String)]): HttpRequest =
176 | withQueryParameters(values.map(p => (s"$key[${p._1}]", p._2)): _*)
177 |
178 | /** Adds multiple query parameters.
179 | *
180 | * @param parameters A sequence of new, unescaped parameters.
181 | * @return A copy of this [[HttpRequest]] with an updated query string.
182 | */
183 | def withQueryParameters(parameters: (String, String)*): HttpRequest =
184 | parameters.foldLeft(this)((acc, entry) => acc.withQueryParameter(entry._1, entry._2))
185 |
186 | /** Adds or updates a header to the current set of headers.
187 | *
188 | * @param key The header key (case insensitive)
189 | * @param value The header value
190 | * @return A copy of this [[HttpRequest]] with an updated header set.
191 | */
192 | def withHeader(key: String, value: String): HttpRequest =
193 | copy(headers = headers + (key -> value))
194 |
195 | /** Adds or updates multiple headers to the current set of headers.
196 | *
197 | * @param newHeaders The headers to add.
198 | * @return A copy of this [[HttpRequest]] with an updated header set.
199 | */
200 | def withHeaders(newHeaders: (String, String)*): HttpRequest =
201 | copy(headers = HeaderMap(headers ++ newHeaders))
202 |
203 | /** Allows browser to add a cookie to a cross-domain request, if one was
204 | * received prior from the server.
205 | *
206 | * @param toggle If true, browser is allowed to add cookie to cross-domain request.
207 | * @return A copy of this [[HttpRequest]] with an updated header set.
208 | */
209 | def withCrossDomainCookies(toggle: Boolean): HttpRequest =
210 | copy(crossDomainCookies = toggle)
211 |
212 | /** Specifies the request timeout.
213 | *
214 | * When a request takes longer than timeout to complete, the future is
215 | * rejected with a [[fr.hmil.roshttp.exceptions.TimeoutException]].
216 | *
217 | * @param timeout The duration to wait before throwing a timeout exception.
218 | * @return A copu of this [[HttpRequest]] with an updated timeout setting.
219 | */
220 | def withTimeout(timeout: FiniteDuration): HttpRequest =
221 | copy(timeout = timeout)
222 |
223 | /** Updates request protocol, host, port, path and queryString according to a url.
224 | *
225 | * @param url A valid HTTP url
226 | * @return A copy of this [[HttpRequest]] with updated URL-related attributes.
227 | */
228 | def withURL(url: String): HttpRequest = {
229 | val parser = new URI(url)
230 | copy(
231 | protocol = if (parser.getScheme != null) Protocol.fromString(parser.getScheme) else protocol,
232 | host = if (parser.getHost != null) parser.getHost else host,
233 | port = if (parser.getPort != -1) Some(parser.getPort) else port,
234 | path = if (parser.getPath != null) parser.getPath else path,
235 | queryString = {
236 | if (parser.getQuery != null)
237 | Some(Utils.encodeQueryString(parser.getQuery))
238 | else
239 | queryString
240 | }
241 | )
242 | }
243 |
244 | /**
245 | * Use the provided backend configuration when executing the request
246 | */
247 | def withBackendConfig(backendConfig: BackendConfig): HttpRequest = {
248 | copy(backendConfig = backendConfig)
249 | }
250 |
251 | /** Attaches a body to this request and sets the Content-Type header.
252 | *
253 | * The body will be sent with the request regardless of other parameters once
254 | * [[send()]] is invoked. Any subsequent call to [[withBody()]], [[send(BodyPart)]],
255 | * [[post(BodyPart)]], [[put(BodyPart)]] or similar methods will override the request body.
256 | *
257 | * The Content-Type header is set to this body's content-type. It can still be manually
258 | * overridden using a method of the [[withHeader()]] family.
259 | *
260 | * Note that the HTTP spec forbids sending data with some methods. In case you need to deal with a broken backend,
261 | * this library allows you to do so anyway. **Beware that in this case, the JVM can still enforce a compliant HTTP
262 | * method**.
263 | */
264 | def withBody(body: BodyPart): HttpRequest = {
265 | withHeader("Content-Type", body.contentType).copy(body = Some(body))
266 | }
267 |
268 |
269 | private def _send[T <: HttpResponse](factory: HttpResponseFactory[T])(implicit scheduler: Scheduler): Future[T] = {
270 | val promise = Promise[T]
271 |
272 | val timeoutTask = scheduler.scheduleOnce(timeout.length, timeout.unit,
273 | new Runnable {
274 | override def run(): Unit = {
275 | promise.tryFailure(new TimeoutException)
276 | }
277 | })
278 |
279 | val backendFuture: Future[T] = HttpDriver.send(this, factory)
280 | backendFuture.onComplete({response =>
281 | timeoutTask.cancel()
282 | promise.tryComplete(response)
283 | })
284 |
285 | promise.future
286 | }
287 |
288 | def stream()(implicit scheduler: Scheduler): Future[StreamHttpResponse] =
289 | _send(StreamHttpResponse)
290 |
291 | /** Sends this request.
292 | *
293 | * A request can be sent multiple times. When a request is sent, it returns a Future[HttpResponse]
294 | * which either succeeds with an [[HttpResponse]] or fails.]]
295 | */
296 | def send()(implicit scheduler: Scheduler): Future[SimpleHttpResponse] =
297 | _send(SimpleHttpResponse)
298 |
299 | /** Sends this request with the GET method.
300 | *
301 | * @see [[send]]
302 | */
303 | def get()(implicit scheduler: Scheduler): Future[SimpleHttpResponse] =
304 | withMethod(Method.GET).send()
305 |
306 | /** Sends this request with the POST method and a body
307 | *
308 | * @see [[send]]
309 | * @param body The body to send with the request
310 | */
311 | def post(body: BodyPart)(implicit scheduler: Scheduler): Future[SimpleHttpResponse] =
312 | withMethod(Method.POST).send(body)
313 |
314 | /** Sends this request with the PUT method and a body
315 | *
316 | * @see [[post]]
317 | * @param body The body to send with the request
318 | */
319 | def put(body: BodyPart)(implicit scheduler: Scheduler): Future[SimpleHttpResponse] =
320 | withMethod(Method.PUT).send(body)
321 |
322 | /** Sends this request with the OPTIONS method and a body
323 | *
324 | * @see [[post]]
325 | * @param body The body to send with the request
326 | */
327 | def options(body: BodyPart)(implicit scheduler: Scheduler): Future[SimpleHttpResponse] =
328 | withMethod(Method.OPTIONS).send(body)
329 |
330 | /** Sends this request with a body.
331 | *
332 | * This method should not be used directly. If you want to [[post]] or [[put]]
333 | * some data, you should use the appropriate methods. If you do not want to send
334 | * data with the request, you should use [[post]] without arguments.
335 | *
336 | * @param body The body to send.
337 | */
338 | def send(body: BodyPart)(implicit scheduler: Scheduler): Future[SimpleHttpResponse] =
339 | withBody(body).send()
340 |
341 | /** Internal method to back public facing .withXXX methods. */
342 | private def copy(
343 | method: Method = this.method,
344 | host: String = this.host,
345 | path: String = this.path,
346 | port: Option[Int] = this.port,
347 | protocol: Protocol = this.protocol,
348 | queryString: Option[String] = this.queryString,
349 | headers: HeaderMap[String] = this.headers,
350 | crossDomainCookies: Boolean = this.crossDomainCookies,
351 | body: Option[BodyPart] = this.body,
352 | backendConfig: BackendConfig = this.backendConfig,
353 | timeout: FiniteDuration = this.timeout
354 | ): HttpRequest = {
355 | new HttpRequest(
356 | method = method,
357 | host = host,
358 | path = path,
359 | port = port,
360 | protocol = protocol,
361 | queryString = queryString,
362 | crossDomainCookies = crossDomainCookies,
363 | headers = headers,
364 | body = body,
365 | backendConfig = backendConfig,
366 | timeout = timeout)
367 | }
368 |
369 | }
370 |
371 | object HttpRequest {
372 |
373 | private def default = new HttpRequest(
374 | method = Method.GET,
375 | host = null,
376 | path = null,
377 | port = None,
378 | protocol = Protocol.HTTP,
379 | queryString = None,
380 | headers = HeaderMap(),
381 | crossDomainCookies = false,
382 | body = None,
383 | backendConfig = BackendConfig(),
384 | timeout = FiniteDuration(30, TimeUnit.SECONDS)
385 | )
386 |
387 | /** Creates a blank HTTP request.
388 | *
389 | * It is most of the time more convenient to use apply(String) but this
390 | * constructor allows you to programatically construct a request from scratch.
391 | *
392 | * @return [[HttpRequest.default]]
393 | */
394 | def apply(): HttpRequest = default
395 |
396 | /** Creates an [[HttpRequest]] with the provided target url.
397 | *
398 | * Same thing as calling `HttpRequest().withURL(url)`.
399 | *
400 | * @param url The target url.
401 | * @return An [[HttpRequest]] ready to GET the target url.
402 | */
403 | def apply(url: String): HttpRequest = this().withURL(url)
404 | }
405 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/Method.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | /** Wraps HTTP method strings. */
4 | final class Method private(private val name: String) {
5 |
6 | override def toString: String = name.toUpperCase
7 |
8 | override def equals(o: Any): Boolean = o match {
9 | case that: Method => that.name.equalsIgnoreCase(this.name)
10 | case _ => false
11 | }
12 |
13 | override def hashCode: Int = name.toUpperCase.hashCode
14 | }
15 |
16 | /** Exposes available methods as object as well as an implicit conversion
17 | * from string to Method objects.
18 | *
19 | * Because all backends do not support all methods, this library imposes a subset
20 | * of all available HTTP Methods. Should you find a use case for this library
21 | * with other HTTP methods, please submit an issue with your motivation.
22 | */
23 | object Method {
24 | val GET = Method("GET")
25 | val POST = Method("POST")
26 | val HEAD = Method("HEAD")
27 | val OPTIONS = Method("OPTIONS")
28 | val PUT = Method("PUT")
29 | val DELETE = Method("DELETE")
30 | /** The PATCH HTTP method does not work on the JVM */
31 | val PATCH = Method("PATCH")
32 | /** The TRACE HTTP method does not work in the browser */
33 | val TRACE = Method("TRACE")
34 |
35 | /** Creates a custom http method.
36 | *
37 | * Support for custom methods depends on the backend so use at your own risk!
38 | *
39 | * @param name method name
40 | */
41 | def apply(name: String): Method = new Method(name)
42 | }
43 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/Protocol.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | /** Defines the protocol used.
4 | *
5 | * When setting a protocol from a string, we want to preserve the initial case such as
6 | * not to alter the url.
7 | */
8 | final case class Protocol private(private val name: String) {
9 |
10 | override implicit def toString: String = name
11 |
12 | override def equals(o: Any): Boolean = o match {
13 | case that: Protocol => that.name.equalsIgnoreCase(this.name)
14 | case _ => false
15 | }
16 |
17 | override def hashCode: Int = name.hashCode
18 | }
19 |
20 | object Protocol {
21 | val HTTP = fromString("http")
22 | val HTTPS = fromString("https")
23 |
24 | def fromString(name: String): Protocol = name.toUpperCase match {
25 | case "HTTP" => Protocol(name)
26 | case "HTTPS" => Protocol(name)
27 | case _ => throw new IllegalArgumentException(s"Invalid protocol: $name")
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/body/BodyPart.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.body
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import monix.reactive.Observable
6 |
7 | trait BodyPart {
8 | def contentType: String
9 | def content: Observable[ByteBuffer]
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/body/BulkBodyPart.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.body
2 | import java.nio.ByteBuffer
3 |
4 | import monix.reactive.Observable
5 |
6 | abstract class BulkBodyPart extends BodyPart {
7 | override def content: Observable[ByteBuffer] = Observable.eval(contentData)
8 | def contentData: ByteBuffer
9 | }
10 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/body/ByteBufferBody.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.body
2 |
3 | import java.nio.ByteBuffer
4 |
5 | /** A body containing raw binary data
6 | *
7 | * Usage: Stream bodies are used to send arbitrary binary data such as
8 | * audio, video or any other file attachment.
9 | * The content-type can be overridden to something more specific like `image/jpeg`
10 | * or `audio/wav` for instance.
11 | * It is common to embed a stream body in a [[MultiPartBody]] to send additional information
12 | * with the binary file.
13 | * When possible, send a `Content-Length` header along with an octet-stream body. It may allow the receiver
14 | * end to better handle the loading.
15 | *
16 | * A stream body is sent with the content-type application/octet-stream.
17 | *
18 | * @param data The bytes to send
19 | * @param contentType
20 | */
21 | class ByteBufferBody private(
22 | data: ByteBuffer,
23 | override val contentType: String
24 | ) extends BulkBodyPart {
25 | override def contentData: ByteBuffer = data
26 | }
27 |
28 | object ByteBufferBody {
29 | def apply(data: ByteBuffer, contentType: String = "application/octet-stream"): ByteBufferBody =
30 | new ByteBufferBody(data, contentType)
31 | }
32 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/body/Implicits.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.body
2 |
3 | import java.io.InputStream
4 | import java.nio.ByteBuffer
5 |
6 | import fr.hmil.roshttp.body.JSONBody._
7 | import monix.reactive.Observable
8 | import monix.eval.Task
9 |
10 |
11 | object Implicits {
12 | implicit def stringToJSONString(value: String): JSONString = new JSONString(value)
13 | implicit def intToJSONNumber(value: Int): JSONNumber = new JSONNumber(value)
14 | implicit def floatToJSONNumber(value: Float): JSONNumber = new JSONNumber(value)
15 | implicit def doubleToJSONNumber(value: Double): JSONNumber = new JSONNumber(value)
16 | implicit def booleanToJSONBoolean(value: Boolean):JSONBoolean = new JSONBoolean(value)
17 | implicit def JSONObjectToJSONBody(obj: JSONObject): JSONBody = JSONBody(obj)
18 | implicit def JSONArrayToJSONBody(arr: JSONArray): JSONBody = JSONBody(arr)
19 |
20 | implicit def byteBufferToByteBufferBody(buffer: ByteBuffer): BodyPart = ByteBufferBody(buffer)
21 | implicit def observableToStreamBody(is: InputStream): BodyPart =
22 | StreamBody(Observable.fromInputStream(Task(is)).map(ByteBuffer.wrap))
23 | }
24 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/body/JSONBody.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.body
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import fr.hmil.roshttp.body.JSONBody.JSONValue
6 |
7 | /** Allows to send arbitrarily complex JSON data.
8 | *
9 | * @param value The JSON value to send.
10 | */
11 | class JSONBody private(value: JSONValue) extends BulkBodyPart {
12 | override def contentType: String = s"application/json; charset=utf-8"
13 |
14 | override def contentData: ByteBuffer = ByteBuffer.wrap(value.toString.getBytes("utf-8"))
15 | }
16 |
17 | object JSONBody {
18 | trait JSONValue {
19 | def toString: String
20 | }
21 |
22 | class JSONNumber(value: Number) extends JSONValue {
23 | override def toString: String = value.toString
24 | }
25 |
26 | class JSONBoolean(value:Boolean) extends JSONValue {
27 | override def toString: String = value.toString
28 | }
29 |
30 | class JSONString(value: String) extends JSONValue {
31 | override def toString: String = "\"" + escapeJS(value) + "\""
32 | }
33 |
34 | class JSONObject(values: Map[String, JSONValue]) extends JSONValue {
35 | override def toString: String = {
36 | "{" +
37 | values.map({case (name, part) =>
38 | "\"" + escapeJS(name) + "\"" +
39 | ":" + part
40 | }).mkString(",") +
41 | "}"
42 | }
43 | }
44 |
45 | object JSONObject {
46 | def apply(values: (String, JSONValue)*): JSONObject = new JSONObject(Map(values: _*))
47 | }
48 |
49 | class JSONArray(values: Seq[JSONValue]) extends JSONValue {
50 | override def toString: String = "[" + values.mkString(",") + "]"
51 | }
52 |
53 | object JSONArray {
54 | def apply(values: JSONValue*): JSONArray = new JSONArray(values)
55 | }
56 |
57 |
58 |
59 | def apply(value: JSONValue): JSONBody = new JSONBody(value)
60 |
61 | // String escapement taken from scala-js
62 | private final val EscapeJSChars = "\\a\\b\\t\\n\\v\\f\\r\\\"\\\\"
63 |
64 | private def escapeJS(str: String): String = {
65 | // scalastyle:off return
66 | val end = str.length
67 | var i = 0
68 | while (i != end) {
69 | val c = str.charAt(i)
70 | if (c >= 32 && c <= 126 && c != '\\' && c != '"')
71 | i += 1
72 | else
73 | return createEscapeJSString(str)
74 | }
75 | str
76 | // scalastyle:on return
77 | }
78 |
79 | private def createEscapeJSString(str: String): String = {
80 | val sb = new java.lang.StringBuilder(2 * str.length)
81 | printEscapeJS(str, sb)
82 | sb.toString
83 | }
84 |
85 | private def printEscapeJS(str: String, out: java.lang.Appendable): Unit = {
86 | /* Note that Java and JavaScript happen to use the same encoding for
87 | * Unicode, namely UTF-16, which means that 1 char from Java always equals
88 | * 1 char in JavaScript. */
89 | val end = str.length()
90 | var i = 0
91 | /* Loop prints all consecutive ASCII printable characters starting
92 | * from current i and one non ASCII printable character (if it exists).
93 | * The new i is set at the end of the appended characters.
94 | */
95 | while (i != end) {
96 | val start = i
97 | var c: Int = str.charAt(i)
98 | // Find all consecutive ASCII printable characters from `start`
99 | while (i != end && c >= 32 && c <= 126 && c != 34 && c != 92) {
100 | i += 1
101 | if (i != end)
102 | c = str.charAt(i)
103 | }
104 | // Print ASCII printable characters from `start`
105 | if (start != i)
106 | out.append(str, start, i)
107 |
108 | // Print next non ASCII printable character
109 | if (i != end) {
110 | def escapeJSEncoded(c: Int): Unit = {
111 | if (6 < c && c < 14) {
112 | val i = 2 * (c - 7)
113 | out.append(EscapeJSChars, i, i + 2)
114 | } else if (c == 34) {
115 | out.append(EscapeJSChars, 14, 16)
116 | } else if (c == 92) {
117 | out.append(EscapeJSChars, 16, 18)
118 | } else {
119 | out.append(f"\\u$c%04x")
120 | }
121 | }
122 | escapeJSEncoded(c)
123 | i += 1
124 | }
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/body/MultiPartBody.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.body
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import monix.execution.Scheduler
6 | import monix.reactive.Observable
7 |
8 | import scala.util.Random
9 |
10 | /** A body made of multiple parts.
11 | *
12 | * Usage: A multipart body acts as a container for other bodies. For instance,
13 | * the multipart body is commonly used to send a form with binary attachments in conjunction with
14 | * the [[ByteBufferBody]].
15 | * For simple key/value pairs, use [[URLEncodedBody]] instead.
16 | *
17 | * Safety consideration: A random boundary is generated to separate parts. If the boundary was
18 | * to occur within a body part, it would mess up the whole body. In practice, the odds are extremely small though.
19 | *
20 | * @param parts The pieces of body. The key in the map is used as `name` for the `Content-Disposition` header
21 | * of each part.
22 | * @param subtype The exact multipart mime type as in `multipart/subtype`. Defaults to `form-data`.
23 | */
24 | class MultiPartBody(parts: Map[String, BodyPart], subtype: String = "form-data")(implicit scheduler: Scheduler)
25 | extends BodyPart {
26 |
27 | val boundary = "----" + Random.alphanumeric.take(24).mkString.toLowerCase
28 |
29 | override def contentType: String = s"multipart/$subtype; boundary=$boundary"
30 |
31 | override def content: Observable[ByteBuffer] = {
32 | parts.
33 | // Prepend multipart encapsulation boundary and body part headers to
34 | // each body part.
35 | map({ case (name, part) =>
36 | ByteBuffer.wrap(
37 | ("\r\n--" + boundary + "\r\n" +
38 | "Content-Disposition: form-data; name=\"" + name + "\"\r\n" +
39 | s"Content-Type: ${part.contentType}\r\n" +
40 | "\r\n").getBytes("utf-8")
41 | ) +: part.content
42 | }).
43 | // Join body parts
44 | reduceLeft((acc, elem) => acc ++ elem).
45 | // Append the closing boundary
46 | :+(ByteBuffer.wrap(s"\r\n--$boundary--\r\n".getBytes("utf-8")))
47 | }
48 | }
49 |
50 | object MultiPartBody {
51 | def apply(parts: (String, BodyPart)*)(implicit scheduler: Scheduler): MultiPartBody =
52 | new MultiPartBody(Map(parts: _*))
53 | }
54 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/body/PlainTextBody.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.body
2 |
3 | import java.nio.ByteBuffer
4 |
5 | /** Plain text body sent as `text/plain` mime type.
6 | *
7 | * @param text The plain text to send
8 | * @param charset Charset used for encoded (defaults to utf-8)
9 | */
10 | class PlainTextBody private(
11 | text: String,
12 | charset: String
13 | ) extends BulkBodyPart {
14 |
15 | override def contentType: String = "text/plain; charset=" + charset
16 | override def contentData: ByteBuffer = ByteBuffer.wrap(text.getBytes(charset))
17 | }
18 |
19 | object PlainTextBody {
20 | def apply(text: String, charset: String = "utf-8"): PlainTextBody = new PlainTextBody(text, charset)
21 | }
22 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/body/StreamBody.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.body
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import monix.reactive.Observable
6 |
7 | class StreamBody private(
8 | override val content: Observable[ByteBuffer],
9 | override val contentType: String
10 | ) extends BodyPart
11 |
12 | object StreamBody {
13 | def apply(data: Observable[ByteBuffer], contentType: String = "application/octet-stream"): StreamBody =
14 | new StreamBody(data, contentType)
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/body/URLEncodedBody.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.body
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import fr.hmil.roshttp.CrossPlatformUtils
6 |
7 | /** An urlencoded HTTP body.
8 | *
9 | * Usage: urlencoded bodies are best suited for simple key/value maps of strings. For more
10 | * structured data, use [[JSONBody]]. For binary data, use [[ByteBufferBody]] or [[MultiPartBody]].
11 | *
12 | * URLEncoded bodies are associated with the mime type "application/x-www-form-urlencoded"
13 | * and look like query string parameters (eg. key=value&key2=value2 ).
14 | *
15 | * @param values A map of key/value pairs to send with the request.
16 | */
17 | class URLEncodedBody private(values: Map[String, String]) extends BulkBodyPart {
18 |
19 | override def contentType: String = s"application/x-www-form-urlencoded"
20 |
21 | override def contentData: ByteBuffer = ByteBuffer.wrap(
22 | values.map({case (name, part) =>
23 | CrossPlatformUtils.encodeURIComponent(name) +
24 | "=" +
25 | CrossPlatformUtils.encodeURIComponent(part)
26 | }).mkString("&").getBytes("utf-8")
27 | )
28 | }
29 |
30 | object URLEncodedBody {
31 | def apply(values: (String, String)*): URLEncodedBody = new URLEncodedBody(Map(values: _*))
32 | }
33 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/exceptions/HttpException.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.exceptions
2 |
3 | import java.io.IOException
4 |
5 | import fr.hmil.roshttp.response.{HttpResponse, SimpleHttpResponse, StreamHttpResponse}
6 |
7 | /** Exception in the HTTP application layer.
8 | *
9 | * In other words, this exception occurs when a bad HTTP status code (>= 400) is received.
10 | */
11 | case class HttpException[+T <: HttpResponse] private(response: T)(message: String = null)
12 | extends IOException(message)
13 |
14 | object HttpException {
15 | def badStatus[T <: HttpResponse](response: T): HttpException[T] =
16 | new HttpException[T](response)(s"Server responded with status ${response.statusCode}")
17 | }
18 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/exceptions/RequestException.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.exceptions
2 |
3 | import java.io.IOException
4 |
5 | /** Captures network errors occurring during an HTTP request.
6 | *
7 | * @see [[ResponseException]]
8 | */
9 | case class RequestException(cause: Throwable)
10 | extends IOException("A network error occurred during HTTP request transmission.", cause)
11 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/exceptions/ResponseException.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.exceptions
2 |
3 | import java.io.IOException
4 |
5 | import fr.hmil.roshttp.response.HttpResponseHeader
6 |
7 | /** Captures network errors occurring during reception of an HTTP response body.
8 | *
9 | * When this exception occurs, HTTP headers have already been received.
10 | * The response header data is recovered in the header field.
11 | *
12 | * @see [[RequestException]]
13 | */
14 | case class ResponseException private[roshttp](
15 | cause: Throwable,
16 | header: HttpResponseHeader)
17 | extends IOException("A network error occurred during HTTP response transmission.", cause)
18 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/exceptions/TimeoutException.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.exceptions
2 |
3 | import java.io.IOException
4 |
5 | import fr.hmil.roshttp.response.HttpResponseHeader
6 |
7 | /** Captures timeout exceptions occurring during an HTTP response. */
8 | case class TimeoutException(header: Option[HttpResponseHeader] = None)
9 | extends IOException("HTTP response timed out.")
10 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/exceptions/UploadStreamException.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.exceptions
2 |
3 | import java.io.IOException
4 |
5 | /** Captures errors in the request body stream.
6 | *
7 | * This exception means that the stream which feeds request body data into the request broke.
8 | */
9 | case class UploadStreamException(cause: Throwable)
10 | extends IOException("An error occurred upstream while sending request data.", cause)
11 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/response/HttpResponse.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.response
2 |
3 | import fr.hmil.roshttp.util.HeaderMap
4 |
5 |
6 | private[roshttp] trait HttpResponse {
7 | val statusCode: Int
8 | val headers: HeaderMap[String]
9 | val body: Any
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/response/HttpResponseFactory.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.response
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import fr.hmil.roshttp.BackendConfig
6 | import fr.hmil.roshttp.util.HeaderMap
7 | import monix.execution.Scheduler
8 | import monix.reactive.Observable
9 |
10 | import scala.concurrent.Future
11 |
12 | private[roshttp] trait HttpResponseFactory[T <: HttpResponse] {
13 | def apply(
14 | header: HttpResponseHeader,
15 | bodyStream: Observable[ByteBuffer],
16 | config: BackendConfig)
17 | (implicit scheduler: Scheduler): Future[T]
18 | }
19 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/response/HttpResponseHeader.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.response
2 |
3 | import fr.hmil.roshttp.util.HeaderMap
4 |
5 | /** Data contained by the header section of an HTTP response message
6 | *
7 | * Most of the time, [[SimpleHttpResponse]] and [[StreamHttpResponse]] are the classes
8 | * you will want to use since they contain both header and body data.
9 | * However, if a network error occurs and the response body cannot be retreived, this class
10 | * is used to represent the header data received.
11 | */
12 | class HttpResponseHeader(val statusCode: Int, val headers: HeaderMap[String])
13 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/response/SimpleHttpResponse.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.response
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import fr.hmil.roshttp.BackendConfig
6 | import fr.hmil.roshttp.exceptions.ResponseException
7 | import fr.hmil.roshttp.util.{HeaderMap, Utils}
8 | import monix.execution.Scheduler
9 | import monix.reactive.Observable
10 |
11 | import scala.collection.mutable
12 | import scala.concurrent.{Future, Promise}
13 | import scala.util.{Failure, Success}
14 |
15 | /**
16 | * An HTTP response obtained via an [[fr.hmil.roshttp.HttpRequest]]
17 | */
18 | class SimpleHttpResponse(
19 | val statusCode: Int,
20 | val headers: HeaderMap[String],
21 | val body: String)
22 | extends HttpResponse
23 |
24 | object SimpleHttpResponse extends HttpResponseFactory[SimpleHttpResponse] {
25 | override def apply(
26 | header: HttpResponseHeader,
27 | bodyStream: Observable[ByteBuffer],
28 | config: BackendConfig)
29 | (implicit scheduler: Scheduler): Future[SimpleHttpResponse] = {
30 |
31 | val charset = Utils.charsetFromContentType(header.headers.getOrElse("content-type", null))
32 | val buffers = mutable.Queue[ByteBuffer]()
33 | val promise = Promise[SimpleHttpResponse]()
34 |
35 | val streamCollector = bodyStream.
36 | foreach(elem => buffers.enqueue(elem)).
37 | map({_ =>
38 | val body = recomposeBody(buffers, config.maxChunkSize, charset)
39 | new SimpleHttpResponse(header.statusCode, header.headers, body)
40 | })
41 |
42 | streamCollector.onComplete({
43 | case res:Success[SimpleHttpResponse] =>
44 | promise.trySuccess(res.value)
45 | case e:Failure[_] =>
46 | promise.tryFailure(new ResponseException(e.exception, header))
47 | })
48 |
49 | promise.future
50 | }
51 |
52 | private def recomposeBody(seq: mutable.Queue[ByteBuffer], maxChunkSize: Int, charset: String): String = {
53 | // Allocate maximum expected body length
54 | val buffer = ByteBuffer.allocate(seq.length * maxChunkSize)
55 | val totalBytes = seq.foldLeft(0)({ (count, chunk) =>
56 | buffer.put(chunk)
57 | count + chunk.limit()
58 | })
59 | buffer.limit(totalBytes)
60 | Utils.getStringFromBuffer(buffer, charset)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/response/StreamHttpResponse.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.response
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import fr.hmil.roshttp.BackendConfig
6 | import fr.hmil.roshttp.util.HeaderMap
7 | import monix.execution.Scheduler
8 | import monix.reactive.Observable
9 |
10 | import scala.concurrent.Future
11 |
12 |
13 | class StreamHttpResponse(
14 | val statusCode: Int,
15 | val headers: HeaderMap[String],
16 | val body: Observable[ByteBuffer])
17 | extends HttpResponse
18 |
19 | object StreamHttpResponse extends HttpResponseFactory[StreamHttpResponse] {
20 | override def apply(
21 | header: HttpResponseHeader,
22 | bodyStream: Observable[ByteBuffer],
23 | config: BackendConfig)
24 | (implicit scheduler: Scheduler): Future[StreamHttpResponse] =
25 | Future.successful(new StreamHttpResponse(header.statusCode, header.headers, bodyStream))
26 | }
27 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/tools/io/IO.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.tools.io
2 |
3 | import java.io.{ByteArrayOutputStream, OutputStream, Writer, _}
4 |
5 | import scala.annotation.tailrec
6 | import scala.reflect.ClassTag
7 |
8 | /** Very useful IO utilities shamelessly copied from org.scalajs.core.tools.io */
9 | private[roshttp] object IO {
10 | /** Returns the lines in an input stream.
11 | * Lines do not contain the new line characters.
12 | */
13 | def readLines(stream: InputStream): List[String] =
14 | readLines(new InputStreamReader(stream))
15 |
16 | /** Returns the lines in a string.
17 | * Lines do not contain the new line characters.
18 | */
19 | def readLines(content: String): List[String] =
20 | readLines(new StringReader(content))
21 |
22 | /** Returns the lines in a reader.
23 | * Lines do not contain the new line characters.
24 | */
25 | def readLines(reader: Reader): List[String] = {
26 | val br = new BufferedReader(reader)
27 | try {
28 | val builder = List.newBuilder[String]
29 | @tailrec
30 | def loop(): Unit = {
31 | val line = br.readLine()
32 | if (line ne null) {
33 | builder += line
34 | loop()
35 | }
36 | }
37 | loop()
38 | builder.result()
39 | } finally {
40 | br.close()
41 | }
42 | }
43 |
44 | /** Reads the entire content of a reader as a string. */
45 | def readReaderToString(reader: Reader): String = {
46 | val buffer = newBuffer[Char]
47 | val builder = new StringBuilder
48 | @tailrec
49 | def loop(): Unit = {
50 | val len = reader.read(buffer)
51 | if (len > 0) {
52 | builder.appendAll(buffer, 0, len)
53 | loop()
54 | }
55 | }
56 | loop()
57 | builder.toString()
58 | }
59 |
60 | /** Reads the entire content of an input stream as a UTF-8 string. */
61 | def readInputStreamToString(stream: InputStream): String = {
62 | val reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"))
63 | readReaderToString(reader)
64 | }
65 |
66 | /** Reads the entire content of an input stream as a byte array. */
67 | def readInputStreamToByteArray(stream: InputStream): Array[Byte] = {
68 | val builder = new ByteArrayOutputStream()
69 | pipe(stream, builder)
70 | builder.toByteArray
71 | }
72 | /** Pipes data from `in` to `out` */
73 | def pipe(in: InputStream, out: OutputStream): Unit = {
74 | val buffer = newBuffer[Byte]
75 |
76 | @tailrec
77 | def loop(): Unit = {
78 | val size = in.read(buffer)
79 | if (size > 0) {
80 | out.write(buffer, 0, size)
81 | loop()
82 | }
83 | }
84 | try {
85 | loop()
86 | } catch {
87 | // work around java quirk. See SO thread for details
88 | // http://stackoverflow.com/questions/10333257/java-io-ioexception-premature-eof
89 | case e:IOException if e.getMessage == "Premature EOF" => // ignore
90 | }
91 | }
92 |
93 | /** Pipes data from `in` to `out` */
94 | def pipe(in: Reader, out: Writer): Unit = {
95 | val buffer = newBuffer[Char]
96 |
97 | @tailrec
98 | def loop(): Unit = {
99 | val size = in.read(buffer)
100 | if (size > 0) {
101 | out.write(buffer, 0, size)
102 | loop()
103 | }
104 | }
105 | loop()
106 | }
107 |
108 | @inline
109 | private def newBuffer[T: ClassTag] = new Array[T](4096)
110 | }
111 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/util/HeaderMap.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.util
2 |
3 | import fr.hmil.roshttp.util.HeaderMap.CaseInsensitiveString
4 |
5 | import scala.collection.compat._
6 |
7 |
8 | /** A set of HTTP headers identified by case insensitive keys
9 | *
10 | * A Map[CaseInsensitiveString, String] would conform to the strict Map specification
11 | * but it would make the API ugly, forcing the explicit usage of CaseInsensitiveString
12 | * instead of string.
13 | *
14 | * That's why we have the HeaderMap class to represent HTTP headers in a map like
15 | * interface which is nice to use. It is however not *exactly* a map because
16 | * different keys can map to the same value if they are case-insensitive equivalent.
17 | *
18 | * @tparam B Required for MapLike implementation. Should always be set to String.
19 | */
20 | class HeaderMap[B >: String] private(map: Map[CaseInsensitiveString, B] = Map())
21 | // extends WithDefault[String, B](new HeaderMap[B](), Map()) {
22 | // extends MapOps[String, B, Map[String, B], HeaderMap[B]] {
23 | extends Map[String, B] {
24 |
25 | override def empty: HeaderMap[B] = new HeaderMap(Map())
26 |
27 | override def get(key: String): Option[B] = {
28 | map.get(new CaseInsensitiveString(key))
29 | }
30 |
31 | override def iterator: Iterator[(String, B)] = {
32 | map.map({ t => (t._1.value, t._2)}).iterator
33 | }
34 |
35 | override def +[B1 >: B](kv: (String, B1)): HeaderMap[B1] = {
36 | val key = new CaseInsensitiveString(kv._1)
37 | new HeaderMap[B1](map - key + (key -> kv._2))
38 | }
39 |
40 | def removed(key: String): HeaderMap[B] = {
41 | new HeaderMap[B](map - new CaseInsensitiveString(key))
42 | }
43 |
44 | override def updated[B1 >: B](key: String, value: B1): HeaderMap[B1] = {
45 | new HeaderMap[B1](map.updated(new CaseInsensitiveString(key), value))
46 | }
47 |
48 | override def toString: String = {
49 | map.map({ t => t._1 + ": " + t._2}).mkString("\n")
50 | }
51 | }
52 |
53 | object HeaderMap {
54 |
55 | /** Creates a HeaderMap from a map of string to string. */
56 | def apply(map: Map[String, String]): HeaderMap[String] = new HeaderMap(
57 | map.map(t => (new CaseInsensitiveString(t._1), t._2))
58 | )
59 |
60 | /** Creates an empty HeaderMap. */
61 | def apply(): HeaderMap[String] = HeaderMap(Map())
62 |
63 | /** A string whose equals and hashCode methods are case insensitive. */
64 | class CaseInsensitiveString(val value: String) {
65 |
66 | override def equals(other: Any): Boolean = other match {
67 | case s:CaseInsensitiveString => s.value.equalsIgnoreCase(value)
68 | case _ => false
69 | }
70 |
71 | override def hashCode(): Int = value.toLowerCase.hashCode
72 |
73 | override def toString: String = value
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/shared/src/main/scala/fr/hmil/roshttp/util/Utils.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp.util
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import fr.hmil.roshttp.CrossPlatformUtils
6 |
7 | object Utils {
8 |
9 | /**
10 | * Extracts the charset from a content-type header string
11 | * @param input content-type header value
12 | * @return the charset contained in the content-type or the default
13 | * one-byte encoding charset (to avoid tampering binary buffer).
14 | */
15 | def charsetFromContentType(input: String): String = {
16 | if (input == null) {
17 | oneByteCharset
18 | } else {
19 | // From W3C spec:
20 | // Content-Type := type "/" subtype *[";" parameter]
21 | // eg: text/html; charset=UTF-8
22 | input.split(';').toStream.drop(1).foldLeft(oneByteCharset)((acc, s) => {
23 | if (s.matches("^\\s*charset=.+$")) {
24 | s.substring(s.indexOf("charset") + 8)
25 | } else {
26 | acc
27 | }
28 | })
29 | }
30 | }
31 |
32 | /** urlencodes a query string by preserving key-value pairs. */
33 | def encodeQueryString(queryString: String): String = {
34 | queryString
35 | .split("&")
36 | .map(_
37 | .split("=")
38 | .map(encodeURIComponent)
39 | .mkString("="))
40 | .mkString("&")
41 | }
42 |
43 | def encodeURIComponent(input: String): String = CrossPlatformUtils.encodeURIComponent(input)
44 |
45 | def getStringFromBuffer(byteBuffer: ByteBuffer, charset: String): String = {
46 | if (byteBuffer.hasArray) {
47 | new String(byteBuffer.array(), 0, byteBuffer.limit(), charset)
48 | } else {
49 | val tmp = new Array[Byte](byteBuffer.limit)
50 | byteBuffer.get(tmp)
51 | new String(tmp, charset)
52 | }
53 | }
54 |
55 | private val oneByteCharset = "utf-8"
56 | }
57 |
--------------------------------------------------------------------------------
/shared/src/test/scala/fr/hmil/roshttp/HttpRequestSpec.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import fr.hmil.roshttp.body.Implicits._
6 | import fr.hmil.roshttp.body.JSONBody._
7 | import fr.hmil.roshttp.body._
8 | import fr.hmil.roshttp.exceptions._
9 | import fr.hmil.roshttp.response.SimpleHttpResponse
10 | import monix.execution.Scheduler.Implicits.global
11 | import monix.eval.Task
12 | import monix.reactive.Observable
13 | import utest._
14 |
15 | import scala.util.Failure
16 |
17 | object HttpRequestSpec extends TestSuite {
18 |
19 | private val SERVER_URL = "http://localhost:3000"
20 |
21 | /*
22 | * Status codes defined in HTTP/1.1 spec
23 | */
24 | private val goodStatus = List(
25 | // We do not support 1xx status codes
26 | 200, 201, 202, 203, 204, 205, 206,
27 | 300, 301, 302, 303, 304, 305, 306, 307
28 | )
29 |
30 | private def badStatus = {
31 | val base = List(
32 | 400, 401, 402, 403, 404, 405, 406, 408, 409,
33 | 410, 411, 412, 413, 414, 415, 416, 417,
34 | 500, 501, 502, 503, 504, 505
35 | )
36 | if (JsEnvUtils.isChrome) {
37 | // Chrome does not support userspace 407 error handling
38 | // see: https://bugs.chromium.org/p/chromium/issues/detail?id=372136
39 | base
40 | } else {
41 | 407 :: base
42 | }
43 | }
44 |
45 | private val statusText = Map(
46 | 200 -> "OK",
47 | 201 -> "Created",
48 | 202 -> "Accepted",
49 | 203 -> "Non-Authoritative Information",
50 | 204 -> "",
51 | 205 -> "",
52 | 206 -> "Partial Content",
53 | 300 -> "Multiple Choices",
54 | 301 -> "Moved Permanently",
55 | 302 -> "Found",
56 | 303 -> "See Other",
57 | 304 -> "",
58 | 305 -> "Use Proxy",
59 | 306 -> "306",
60 | 307 -> "Temporary Redirect",
61 | 400 -> "Bad Request",
62 | 401 -> "Unauthorized",
63 | 402 -> "Payment Required",
64 | 403 -> "Forbidden",
65 | 404 -> "Not Found",
66 | 405 -> "Method Not Allowed",
67 | 406 -> "Not Acceptable",
68 | 407 -> "Proxy Authentication Required",
69 | 408 -> "Request Timeout",
70 | 409 -> "Conflict",
71 | 410 -> "Gone",
72 | 411 -> "Length Required",
73 | 412 -> "Precondition Failed",
74 | 413 -> "Payload Too Large",
75 | 414 -> "URI Too Long",
76 | 415 -> "Unsupported Media Type",
77 | 416 -> "Range Not Satisfiable",
78 | 417 -> "Expectation Failed",
79 | 500 -> "Internal Server Error",
80 | 501 -> "Not Implemented",
81 | 502 -> "Bad Gateway",
82 | 503 -> "Service Unavailable",
83 | 504 -> "Gateway Timeout",
84 | 505 -> "HTTP Version Not Supported"
85 | )
86 |
87 | private val legalMethods = {
88 | val base = "GET" :: "POST" :: "HEAD" :: "OPTIONS" :: "PUT" :: "DELETE" :: Nil
89 | if (JsEnvUtils.isRealBrowser) {
90 | // The jvm cannot send PATCH requests
91 | "PATCH" :: base
92 | } else {
93 | // Browsers cannot send TRACE requests
94 | "TRACE" :: base
95 | }
96 | }
97 |
98 | private val IMAGE_BYTES: Array[Byte] = List[Int](
99 | 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00,
100 | 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x08, 0x06, 0x00, 0x00, 0x00, 0xC4, 0x0F, 0xBE, 0x8B, 0x00, 0x00, 0x00,
101 | 0x06, 0x62, 0x4B, 0x47, 0x44, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xA0, 0xBD, 0xA7, 0x93, 0x00, 0x00, 0x00,
102 | 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0B, 0x13, 0x00, 0x00, 0x0B, 0x13, 0x01, 0x00, 0x9A, 0x9C, 0x18,
103 | 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4D, 0x45, 0x07, 0xE0, 0x05, 0x0A, 0x0B, 0x1A, 0x39, 0x9E, 0xB0, 0x43,
104 | 0x04, 0x00, 0x00, 0x00, 0xF2, 0x49, 0x44, 0x41, 0x54, 0x18, 0xD3, 0x45, 0xCD, 0xBD, 0x4A, 0xC3, 0x50, 0x1C,
105 | 0x40, 0xF1, 0x93, 0xFB, 0xBF, 0xB9, 0x4D, 0xD2, 0x56, 0xD4, 0x98, 0xA1, 0x14, 0x05, 0x51, 0x50, 0xA8, 0x76,
106 | 0x13, 0x1C, 0x14, 0xF1, 0x19, 0x7C, 0x07, 0x71, 0xE9, 0x03, 0x08, 0x4E, 0xBE, 0x85, 0x83, 0x83, 0x9B, 0x8B,
107 | 0x8F, 0xA0, 0x93, 0x64, 0xB0, 0xA0, 0x45, 0x07, 0x6D, 0xD0, 0xCD, 0x8F, 0x45, 0x84, 0x54, 0xA9, 0xB1, 0xF9,
108 | 0x72, 0x50, 0xE8, 0x99, 0x7F, 0x70, 0x2C, 0xFE, 0xBB, 0xBF, 0xB8, 0x6C, 0x3E, 0x77, 0xF6, 0x9A, 0x55, 0xB7,
109 | 0xB6, 0x5E, 0x29, 0xF3, 0x35, 0x67, 0x94, 0x6E, 0xB4, 0xEE, 0x7A, 0xF3, 0x16, 0xC0, 0xA9, 0x1F, 0xEC, 0x9A,
110 | 0xA2, 0x38, 0xF2, 0x6C, 0x83, 0xA7, 0x2C, 0x6A, 0xA2, 0x09, 0x1C, 0x27, 0x9E, 0x7D, 0x8A, 0x26, 0x35, 0xC0,
111 | 0x57, 0x59, 0x5A, 0x43, 0xC7, 0x61, 0xA0, 0x35, 0x6F, 0x65, 0x41, 0x94, 0x7C, 0x23, 0x9F, 0xB1, 0x02, 0xD0,
112 | 0x00, 0xFE, 0xE2, 0xC2, 0xB5, 0x2B, 0xF6, 0xAD, 0x79, 0x7D, 0x59, 0x6A, 0xA5, 0x59, 0xA5, 0xE3, 0x78, 0x50,
113 | 0xAD, 0xCB, 0xF2, 0x20, 0xFE, 0x03, 0x3F, 0xFD, 0x68, 0xB5, 0xAE, 0xED, 0x76, 0x43, 0x14, 0x13, 0x22, 0xF4,
114 | 0x8B, 0x9C, 0x30, 0x4B, 0x13, 0x00, 0x05, 0xF0, 0x61, 0x2A, 0x61, 0xB7, 0xBD, 0x72, 0x76, 0xEC, 0x4F, 0x0D,
115 | 0x0F, 0xB5, 0xE2, 0x3C, 0x4B, 0xD9, 0x16, 0x71, 0xC7, 0x0B, 0xCA, 0xCD, 0xC6, 0x4D, 0x6F, 0x67, 0xCB, 0x18,
116 | 0x5C, 0x11, 0x8C, 0x36, 0xB8, 0xDA, 0x1E, 0x03, 0x94, 0x4A, 0x64, 0x26, 0x88, 0xF2, 0x74, 0xF4, 0xC0, 0xB4,
117 | 0xFF, 0x2E, 0x62, 0x85, 0x73, 0xDD, 0xAB, 0x93, 0xC7, 0xFD, 0x03, 0x7E, 0x01, 0x01, 0x9A, 0x49, 0xCF, 0xD0,
118 | 0xA6, 0xE4, 0x8F, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82).map(_.toByte).toArray
119 |
120 | val tests = this {
121 |
122 | test("Meta") - {
123 | test("The test server should be reachable") - {
124 | HttpRequest(SERVER_URL)
125 | .send().map({ s => s.statusCode ==> 200 })
126 | }
127 | }
128 |
129 | test("Responses") - {
130 | test("with status codes < 400") - {
131 | test("should complete the request with success") - {
132 | goodStatus.map(status => {
133 | HttpRequest(SERVER_URL)
134 | .withPath(s"/status/$status")
135 | .send()
136 | .map({ s =>
137 | s.statusCode ==> status
138 | })
139 | }).reduce((f1, f2) => f1.flatMap(_ => f2))
140 | }
141 | }
142 | test("with status codes >= 400") - {
143 | test("should complete the request with failure") - {
144 | badStatus.map(status =>
145 | HttpRequest(SERVER_URL)
146 | .withPath(s"/status/$status")
147 | .send()
148 | .map(r => r.headers("X-Status-Code") ==> r.statusCode)
149 | .failed.map(_ => "success")
150 | ).reduce((f1, f2) => f1.flatMap(_ => f2))
151 | }
152 | }
153 | test("with redirects") - {
154 | test("follow redirects") - {
155 | HttpRequest(SERVER_URL)
156 | .withPath("/redirect/temporary/echo/redirected")
157 | .send()
158 | .map(res => {
159 | res.body ==> "redirected"
160 | })
161 | }
162 | }
163 | test("with timeout") - {
164 | test("Throw the appropriate exception") - {
165 | HttpRequest(s"$SERVER_URL/no_response")
166 | .send().onComplete {
167 | case e: Failure[_] =>
168 | e.exception match {
169 | case TimeoutException(_) => () // success
170 | }
171 | case _ =>
172 | }
173 | }
174 | }
175 | }
176 |
177 | test("Buffered responses") - {
178 | test("with status code >= 400") - {
179 | test("should provide a response body in error handler") - {
180 | badStatus.map(status =>
181 | HttpRequest(SERVER_URL)
182 | .withPath(s"/status/$status")
183 | .send()
184 | .failed.map {
185 | case HttpException(res: SimpleHttpResponse) =>
186 | statusText(res.statusCode) ==> res.body
187 | case e => throw new java.lang.AssertionError("Unexpected failure", e)
188 | }
189 | ).reduce((f1, f2) => f1.flatMap(_ => f2))
190 | }
191 | test("can be empty") - {
192 | HttpRequest(s"$SERVER_URL/empty_body/400")
193 | .send()
194 | .failed
195 | .map {
196 | case HttpException(res: SimpleHttpResponse) =>
197 | res.body ==> ""
198 | }
199 | }
200 | }
201 | test("with status code < 400") - {
202 | test("can be empty") - {
203 | HttpRequest(s"$SERVER_URL/empty_body/200")
204 | .send()
205 | .map(response => response.body ==> "")
206 | }
207 | }
208 | test("can be chunked and recomposed") - {
209 | HttpRequest(s"$SERVER_URL/echo_repeat/foo")
210 | .withQueryParameters(
211 | "repeat" -> "4",
212 | "delay" -> "1000")
213 | .withBackendConfig(BackendConfig(maxChunkSize = 4))
214 | .send()
215 | .map(res => res.body ==> "foofoofoofoo")
216 | }
217 | test("can contain multibyte characters") - {
218 | val payload = "12\uD83D\uDCA978"
219 | HttpRequest(s"$SERVER_URL/multibyte_string")
220 | .send()
221 | .map(res => res.body ==> payload)
222 | }
223 | test("can contain multibyte characters split by chunk boundary") - {
224 | val payload = "12\uD83D\uDCA978"
225 | HttpRequest(s"$SERVER_URL/multibyte_string")
226 | .withBackendConfig(BackendConfig(
227 | maxChunkSize = 4
228 | ))
229 | .send()
230 | .map(res => res.body ==> payload)
231 | }
232 | }
233 |
234 | test("Streamed response body") - {
235 | test("work with a single chunk") - {
236 | val greeting_bytes: ByteBuffer = ByteBuffer.wrap("Hello World!".getBytes)
237 | HttpRequest(s"$SERVER_URL")
238 | .stream()
239 | .map({ r =>
240 | // Take only the first element because the body is so short we know it will fit in one buffer
241 | r.body.firstL.map(_.get ==> greeting_bytes)
242 | })
243 | }
244 | test("fail on bad status code") - {
245 | HttpRequest(SERVER_URL)
246 | .withPath(s"/status/400")
247 | .stream()
248 | .map(r => r.headers("X-Status-Code") ==> r.statusCode)
249 | .failed.map(_ => "success")
250 | }
251 | test("chunks are capped to chunkSize config") - {
252 | val config = BackendConfig(maxChunkSize = 128)
253 | HttpRequest(s"$SERVER_URL/resources/icon.png")
254 | .withBackendConfig(config)
255 | .stream()
256 | .flatMap(_
257 | .body
258 | .map({ buffer =>
259 | assert(buffer.limit() <= config.maxChunkSize)
260 | })
261 | .bufferTumbling(3)
262 | .firstL.runToFuture
263 | )
264 | }
265 | }
266 |
267 | test("Query string") - {
268 | test("set in constructor") - {
269 | test("vanilla") - {
270 | HttpRequest(s"$SERVER_URL/query?Hello%20world.")
271 | .send()
272 | .map(res => {
273 | res.body ==> "Hello world."
274 | })
275 | }
276 | test("with illegal characters") - {
277 | HttpRequest(s"$SERVER_URL/query?Heizölrückstoßabdämpfung%20+")
278 | .send()
279 | .map(res => {
280 | res.body ==> "Heizölrückstoßabdämpfung +"
281 | })
282 | }
283 | test("with key-value pairs") - {
284 | HttpRequest(s"$SERVER_URL/query/parsed?foo=bar&hello=world")
285 | .send()
286 | .map(res => {
287 | res.body ==> "{\"foo\":\"bar\",\"hello\":\"world\"}"
288 | })
289 | }
290 | }
291 | test("set in withQueryString") - {
292 | test("vanilla") - {
293 | HttpRequest(s"$SERVER_URL/query")
294 | .withQueryString("Hello world.")
295 | .send()
296 | .map(res => {
297 | res.body ==> "Hello world."
298 | })
299 | }
300 | test("with illegal characters") - {
301 | HttpRequest(s"$SERVER_URL/query")
302 | .withQueryString("Heizölrückstoßabdämpfung %20+")
303 | .send()
304 | .map(res => {
305 | res.body ==> "Heizölrückstoßabdämpfung %20+"
306 | })
307 | }
308 | test("is escaped") - {
309 | HttpRequest(s"$SERVER_URL/query")
310 | .withQueryString("Heizölrückstoßabdämpfung")
311 | .queryString.get ==> "Heiz%C3%B6lr%C3%BCcksto%C3%9Fabd%C3%A4mpfung"
312 | }
313 | }
314 | test("set in withRawQueryString") - {
315 | HttpRequest(s"$SERVER_URL/query")
316 | .withQueryStringRaw("Heiz%C3%B6lr%C3%BCcksto%C3%9Fabd%C3%A4mpfung")
317 | .send()
318 | .map(res => {
319 | res.body ==> "Heizölrückstoßabdämpfung"
320 | })
321 | }
322 | test("set in withQueryParameter") - {
323 | test("single") - {
324 | HttpRequest(s"$SERVER_URL/query/parsed")
325 | .withQueryParameter("device", "neon")
326 | .send()
327 | .map(res => {
328 | res.body ==> "{\"device\":\"neon\"}"
329 | })
330 | }
331 | test("added in batch") - {
332 | HttpRequest(s"$SERVER_URL/query/parsed")
333 | .withQueryParameters(
334 | "device" -> "neon",
335 | "element" -> "argon")
336 | .send()
337 | .map(res => {
338 | res.body ==> "{\"device\":\"neon\",\"element\":\"argon\"}"
339 | })
340 | }
341 | test("added in batch with illegal characters") - {
342 | HttpRequest(s"$SERVER_URL/query/parsed")
343 | .withQueryParameters(
344 | " zařízení" -> "topný olej vůle potlačující",
345 | "chäřac+=r&" -> "+Heizölrückstoßabdämpfung=r&")
346 | .send()
347 | .map(res => {
348 | res.body ==> "{\" zařízení\":\"topný olej vůle potlačující\"," +
349 | "\"chäřac+=r&\":\"+Heizölrückstoßabdämpfung=r&\"}"
350 | })
351 | }
352 | test("added in sequence") - {
353 | HttpRequest(s"$SERVER_URL/query/parsed")
354 | .withQueryParameters(
355 | "element" -> "argon",
356 | "device" -> "chair"
357 | )
358 | .withQueryParameter("tool", "hammer")
359 | .withQueryParameter("device", "neon")
360 | .send()
361 | .map(res => {
362 | res.body ==> "{\"element\":\"argon\",\"device\":[\"chair\",\"neon\"],\"tool\":\"hammer\"}"
363 | })
364 | }
365 | test("as list parameter") - {
366 | HttpRequest(s"$SERVER_URL/query/parsed")
367 | .withQuerySeqParameter("map", Seq("foo", "bar"))
368 | .send()
369 | .map(res => {
370 | res.body ==> "{\"map\":[\"foo\",\"bar\"]}"
371 | })
372 | }
373 | }
374 | test("removed") - {
375 | val req = HttpRequest(s"$SERVER_URL/query/parsed")
376 | .withQueryString("device=chair")
377 | .withoutQueryString()
378 |
379 | assert(req.queryString.isEmpty)
380 | }
381 | }
382 | test("Protocol") - {
383 | test("can be set to HTTP and HTTPS") - {
384 | HttpRequest()
385 | .withProtocol(Protocol.HTTP)
386 | .withProtocol(Protocol.HTTPS)
387 | }
388 | }
389 |
390 | test("Request headers") - {
391 | test("Can be set with a map") - {
392 | val headers = Map(
393 | "accept" -> "text/html, application/xhtml",
394 | "Cache-Control" -> "max-age=0",
395 | "custom" -> "foobar")
396 |
397 | val req = HttpRequest(s"$SERVER_URL/headers")
398 | .withHeaders(headers.toSeq: _*)
399 |
400 | // Test with corrected case
401 | req.headers ==> headers
402 |
403 | req.send().map(res => {
404 | assert(res.body.contains("\"accept\":\"text/html, application/xhtml\""))
405 | assert(res.body.contains("\"cache-control\":\"max-age=0\""))
406 | assert(res.body.contains("\"custom\":\"foobar\""))
407 | })
408 | }
409 | test("Can be set individually") - {
410 | val req = HttpRequest(s"$SERVER_URL/headers")
411 | .withHeader("cache-control", "max-age=0")
412 | .withHeader("Custom", "foobar")
413 |
414 | req.headers ==> Map(
415 | "cache-control" -> "max-age=0",
416 | "Custom" -> "foobar")
417 |
418 | req.send().map(res => {
419 | assert(res.body.contains("\"cache-control\":\"max-age=0\""))
420 | assert(res.body.contains("\"custom\":\"foobar\""))
421 | })
422 | }
423 | test("Overwrite previous value when set") - {
424 | val req = HttpRequest(s"$SERVER_URL/headers")
425 | .withHeaders(
426 | "accept" -> "text/html, application/xhtml",
427 | "Cache-Control" -> "max-age=0",
428 | "custom" -> "foobar"
429 | )
430 | .withHeaders(
431 | "Custom" -> "barbar",
432 | "Accept" -> "application/json"
433 | )
434 | .withHeader("cache-control", "max-age=128")
435 |
436 | req.headers ==> Map(
437 | "cache-control" -> "max-age=128",
438 | "custom" -> "barbar",
439 | "accept" -> "application/json")
440 |
441 | req.send().map(res => {
442 | assert(res.body.contains("\"cache-control\":\"max-age=128\""))
443 | assert(res.body.contains("\"custom\":\"barbar\""))
444 | assert(res.body.contains("\"accept\":\"application/json\""))
445 | })
446 | }
447 | test("Override body content-type") - {
448 | HttpRequest(s"$SERVER_URL/headers")
449 | .withBody(PlainTextBody("Hello world"))
450 | .withHeader("Content-Type", "text/html")
451 | .withMethod(Method.POST)
452 | .send()
453 | .map(res => {
454 | assert(res.body.contains("\"content-type\":\"text/html\""))
455 | assert(!res.body.contains("\"content-type\":\"text/plain\""))
456 | })
457 | }
458 | }
459 |
460 | test("Response headers") - {
461 | test("can be read in the general case") - {
462 | HttpRequest(s"$SERVER_URL/")
463 | .send()
464 | .map({
465 | res =>
466 | res.headers("X-Powered-By") ==> "Express"
467 | })
468 | }
469 | test("can be read in the error case") - {
470 | HttpRequest(s"$SERVER_URL/status/400")
471 | .send()
472 | .failed.map {
473 | case HttpException(res: SimpleHttpResponse) =>
474 | res.headers("X-Powered-By") ==> "Express"
475 | }
476 | }
477 | }
478 |
479 | test("Http method") - {
480 | test("can be set to any legal value") - {
481 | legalMethods.map(method =>
482 | HttpRequest(s"$SERVER_URL/method")
483 | .withMethod(Method(method))
484 | .send()
485 | .map(_.headers("X-Request-Method") ==> method)
486 | ).reduce((f1, f2) => f1.flatMap(_ => f2))
487 | }
488 | test("ignores case and capitalizes") - {
489 | legalMethods.map(method =>
490 | HttpRequest(s"$SERVER_URL/method")
491 | .withMethod(Method(method.toLowerCase))
492 | .send()
493 | .map(_.headers("X-Request-Method") ==> method)
494 | ).reduce((f1, f2) => f1.flatMap(_ => f2))
495 | }
496 | }
497 |
498 | test("Request body") - {
499 | test("Plain text") - {
500 | test("works with ASCII strings") - {
501 | HttpRequest(s"$SERVER_URL/body")
502 | .post(PlainTextBody("Hello world"))
503 | .map({ res =>
504 | res.body ==> "Hello world"
505 | res.headers("Content-Type").toLowerCase ==> "text/plain; charset=utf-8"
506 | })
507 | }
508 | test("works with non-ASCII strings") - {
509 | HttpRequest(s"$SERVER_URL/body")
510 | .post(PlainTextBody("Heizölrückstoßabdämpfung"))
511 | .map({ res =>
512 | res.body ==> "Heizölrückstoßabdämpfung"
513 | res.headers("Content-Type").toLowerCase ==> "text/plain; charset=utf-8"
514 | })
515 | }
516 | }
517 | test("Multipart") - {
518 | test( "works as intended") - {
519 | val part = MultiPartBody(
520 | "foo" -> PlainTextBody("bar"),
521 | "engine" -> PlainTextBody("Heizölrückstoßabdämpfung"))
522 | HttpRequest(s"$SERVER_URL/body")
523 | .post(part)
524 | .map({ res =>
525 | res.body ==> "{\"foo\":\"bar\",\"engine\":\"Heizölrückstoßabdämpfung\"}"
526 | res.headers("Content-Type").toLowerCase ==>
527 | s"multipart/form-data; boundary=${part.boundary}; charset=utf-8"
528 | })
529 | }
530 | }
531 | test("URL encoded") - {
532 | test("works as intended") - {
533 | val part = URLEncodedBody(
534 | "foo" -> "bar",
535 | "engine" -> "Heizölrückstoßabdämpfung")
536 | HttpRequest(s"$SERVER_URL/body")
537 | .post(part)
538 | .map({ res =>
539 | res.body ==> "{\"foo\":\"bar\",\"engine\":\"Heizölrückstoßabdämpfung\"}"
540 | res.headers("Content-Type").toLowerCase ==> s"application/x-www-form-urlencoded; charset=utf-8"
541 | })
542 | }
543 | }
544 |
545 | test("JSON") - {
546 | test("works as intended") - {
547 | val part = JSONObject(
548 | "foo" -> 42,
549 | "bar" -> true,
550 | "engine" -> "Heizölrückstoßabdämpfung",
551 | "\"quoted'" -> "Has \" quotes")
552 | HttpRequest(s"$SERVER_URL/body")
553 | .post(part)
554 | .map({ res =>
555 | res.body ==> "{\"foo\":42,\"bar\":true,\"engine\":\"Heizölrückstoßabdämpfung\"," +
556 | "\"\\\"quoted'\":\"Has \\\" quotes\"}"
557 | res.headers("Content-Type").toLowerCase ==> s"application/json; charset=utf-8"
558 | })
559 | }
560 | }
561 | test("Byte Buffer") - {
562 | test("can send a binary buffer") - {
563 | HttpRequest(s"$SERVER_URL/compare/icon.png")
564 | .post(ByteBufferBody(ByteBuffer.wrap(IMAGE_BYTES)))
565 | }
566 | }
567 | test("streamed") - {
568 | test("with wrapped array ByteBuffer") - {
569 | test("is properly sent") - {
570 | HttpRequest(s"$SERVER_URL/compare/icon.png")
571 | .post(
572 | // Splits the image bytes into chunks to create a streamed body
573 | StreamBody(
574 | Observable.fromIterable(Seq(IMAGE_BYTES: _*)
575 | .grouped(12)
576 | .toSeq
577 | ).map(b => ByteBuffer.wrap(b.toArray))
578 | )
579 | )
580 | }
581 | }
582 | test("with native ByteBuffer") - {
583 | test("is properly sent") - {
584 | val nativeBufferSeq = Seq(IMAGE_BYTES: _*)
585 | .grouped(12)
586 | .map({ chunk =>
587 | val b = ByteBuffer.allocateDirect(chunk.size)
588 | var i = 0
589 | while (i < chunk.size) {
590 | b.put(chunk(i))
591 | i += 1
592 | }
593 | b.rewind()
594 | b
595 | }).toSeq
596 | HttpRequest(s"$SERVER_URL/compare/icon.png")
597 | .post(StreamBody(Observable.fromIterable(nativeBufferSeq)))
598 | }
599 | }
600 | test("with read-only ByteBuffer") - {
601 | test("is properly sent") - {
602 | val readOnlyBuffers = Observable.fromIterable(
603 | Seq(IMAGE_BYTES: _*)
604 | .grouped(12)
605 | .toSeq)
606 | .map({ b =>
607 | val res = ByteBuffer.wrap(b.toArray).asReadOnlyBuffer()
608 | assert(!res.hasArray)
609 | res
610 | })
611 | HttpRequest(s"$SERVER_URL/compare/icon.png")
612 | .post(StreamBody(readOnlyBuffers))
613 | .recover {
614 | case e: UploadStreamException =>
615 | e.printStackTrace()
616 | throw e
617 | }
618 | }
619 | }
620 | test("embedded in multipart") - {
621 | test("handles errors correctly") - {
622 | def stateAction(i: Int) = {
623 | if (i == 0) throw new Exception("Stream error")
624 | (ByteBuffer.allocate(1), i - 1)
625 | }
626 |
627 | HttpRequest(s"$SERVER_URL/does_not_exist")
628 | .post(MultiPartBody("stream" -> StreamBody(Observable.fromStateAction(stateAction)(3))))
629 | .recover({
630 | case e: UploadStreamException => e
631 | })
632 | }
633 | test("is properly sent") - {
634 | val part = MultiPartBody(
635 | "stream" -> StreamBody(Observable.fromIterator(Task(new Iterator[ByteBuffer]() {
636 | private var emitted = false
637 |
638 | override def hasNext: Boolean = !emitted
639 |
640 | override def next(): ByteBuffer = {
641 | emitted = true
642 | ByteBuffer.wrap("Bonjour.".getBytes)
643 | }
644 | }))))
645 | HttpRequest(s"$SERVER_URL/body")
646 | .post(part)
647 | .map({ res =>
648 | res.body ==> "{\"stream\":\"Bonjour.\"}"
649 | })
650 | }
651 | }
652 | test("handles errors correctly") - {
653 | HttpRequest(s"$SERVER_URL/does_not_exist")
654 | .post(StreamBody(
655 | Observable.fromStateAction({ i: Int =>
656 | if (i == 0) throw new Exception("Stream error")
657 | (ByteBuffer.allocate(1), i - 1)
658 | })(3)
659 | ))
660 | .recover({
661 | case e: UploadStreamException => e
662 | })
663 | }
664 | }
665 | }
666 |
667 | test("CORS cookies") - {
668 | val currentDate = new java.util.Date().getTime().toDouble
669 |
670 | HttpRequest(s"$SERVER_URL/set_cookie")
671 | .withQueryParameters("token" -> currentDate.toString)
672 | .withCrossDomainCookies(true)
673 | .send()
674 | .map(res => {
675 | assert(res.statusCode == 200)
676 |
677 | HttpRequest(s"$SERVER_URL/verify_cookie")
678 | .withQueryParameters("token" -> currentDate.toString)
679 | .withCrossDomainCookies(true)
680 | .send()
681 | .map(res => {
682 | assert(res.statusCode == 200)
683 | })
684 | })
685 | }
686 |
687 | }
688 | }
689 |
--------------------------------------------------------------------------------
/shared/src/test/scala/fr/hmil/roshttp/StreamingPressureTest.scala:
--------------------------------------------------------------------------------
1 | package fr.hmil.roshttp
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import fr.hmil.roshttp.body.StreamBody
6 | import monix.reactive.Observable
7 | import monix.execution.Scheduler.Implicits.global
8 | import utest._
9 | import monix.eval.Task
10 |
11 | object StreamingPressureTest extends TestSuite {
12 |
13 | private val SERVER_URL = "http://localhost:3000"
14 |
15 | private val ONE_MILLION = 1000000
16 |
17 | val tests = this{
18 |
19 | // Send approx. 8 gigs of data to the server to check that there is no leak
20 | // test("Upload streams do not leak") - {
21 | // if (!JsEnvUtils.isRealBrowser) {
22 | // HttpRequest(s"$SERVER_URL/streams/in")
23 | // .post(StreamBody(Observable.fromIterator(Task(new Iterator[ByteBuffer]() {
24 | // override def hasNext: Boolean = true
25 | // override def next(): ByteBuffer = ByteBuffer.allocateDirect(8192)
26 | // }))
27 | // .take(ONE_MILLION)))
28 | // .map(r => r.body ==> "Received 8192000000 bytes.")
29 | // .recover({
30 | // case e: Throwable =>
31 | // e.printStackTrace()
32 | // })
33 | // }
34 | // }
35 |
36 | // Receive approx. 8 gigs of data to ensure that there is no leak
37 | // test("Download streams do not leak") - {
38 | // // Due to browser incompatibility and node memory leak, run this test only in the JVM
39 | // if (JsEnvUtils.userAgent == "jvm") {
40 | // HttpRequest(s"$SERVER_URL/streams/out")
41 | // .stream()
42 | // .flatMap(_.body.map(_.limit().asInstanceOf[Long]).reduce((l, r) => l + r).runAsyncGetFirst)
43 | // .map(_.get ==> 8192000000L)
44 | // }
45 | // }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/shared/src/test/scala/fr/hmil/roshttp_test/ReadmeSanityCheck.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is automatically generated by test/update-readme-sanity-check.sh
3 | * Do not edit this file manually!
4 | */
5 | package fr.hmil.roshttp_test
6 |
7 | import utest._
8 |
9 | object ReadmeSanityCheck extends TestSuite {
10 | // Shims libraryDependencies
11 | class Dep {
12 | def %%%(s: String): String = "%%%"
13 | def %%(s: String): String = "%%"
14 | def %(s: String): String = "%"
15 | }
16 | implicit def fromString(s: String): Dep = new Dep()
17 | var libraryDependencies = Set[Dep]()
18 |
19 | // Silence print output
20 | def println(s: String): Unit = ()
21 |
22 | // Test suite
23 | val tests = this {
24 | "Readme snippets compile and run successfully" - {
25 |
26 | import fr.hmil.roshttp.HttpRequest
27 | import monix.execution.Scheduler.Implicits.global
28 | import scala.util.{Failure, Success}
29 | import fr.hmil.roshttp.response.SimpleHttpResponse
30 |
31 | // Runs consistently on the jvm, in node.js and in the browser!
32 | val request = HttpRequest("https://schema.org/WebPage")
33 |
34 | request.send().onComplete({
35 | case res:Success[SimpleHttpResponse] => println(res.get.body)
36 | case e: Failure[SimpleHttpResponse] => println("Houston, we got a problem!")
37 | })
38 |
39 | import fr.hmil.roshttp.Protocol.HTTP
40 |
41 | HttpRequest()
42 | .withProtocol(HTTP)
43 | .withHost("localhost")
44 | .withPort(3000)
45 | .withPath("/weather")
46 | .withQueryParameter("city", "London")
47 | .send() // GET http://localhost:3000/weather?city=London
48 |
49 | // Sets the query string such that the target url ends in "?hello%20world"
50 | request.withQueryString("hello world")
51 |
52 | request
53 | .withQueryParameter("foo", "bar")
54 | .withQuerySeqParameter("table", Seq("a", "b", "c"))
55 | .withQueryObjectParameter("map", Seq(
56 | "d" -> "dval",
57 | "e" -> "e value"
58 | ))
59 | .withQueryParameters(
60 | "license" -> "MIT",
61 | "copy" -> "© 2016"
62 | )
63 | /* Query is now:
64 | foo=bar&table=a&table=b&table=c&map[d]=dval&map[e]=e%20value&license=MIT©=%C2%A9%202016
65 | */
66 |
67 | import fr.hmil.roshttp.Method.PUT
68 |
69 | request.withMethod(PUT).send()
70 |
71 | request.withHeader("Accept", "text/html")
72 |
73 | request.withHeaders(
74 | "Accept" -> "text/html",
75 | "User-Agent" -> "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"
76 | )
77 |
78 | import fr.hmil.roshttp.BackendConfig
79 |
80 | HttpRequest("long.source.of/data")
81 | .withBackendConfig(BackendConfig(
82 | // Uses stream chunks of at most 1024 bytes
83 | maxChunkSize = 1024
84 | ))
85 | .stream()
86 |
87 | request.withCrossDomainCookies(true)
88 |
89 | request.send().map({res =>
90 | println(res.headers("Set-Cookie"))
91 | })
92 |
93 | import fr.hmil.roshttp.body.Implicits._
94 | import fr.hmil.roshttp.body.URLEncodedBody
95 |
96 | val urlEncodedData = URLEncodedBody(
97 | "answer" -> "42",
98 | "platform" -> "jvm"
99 | )
100 | request.post(urlEncodedData)
101 | // or
102 | request.put(urlEncodedData)
103 |
104 | import fr.hmil.roshttp.body.Implicits._
105 | import fr.hmil.roshttp.body.JSONBody._
106 |
107 | val jsonData = JSONObject(
108 | "answer" -> 42,
109 | "platform" -> "node"
110 | )
111 | request.post(jsonData)
112 |
113 | import java.nio.ByteBuffer
114 | import fr.hmil.roshttp.body.ByteBufferBody
115 |
116 | val buffer = ByteBuffer.wrap(
117 | List[Byte](0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x0a)
118 | .toArray)
119 | request.post(ByteBufferBody(buffer))
120 |
121 | import fr.hmil.roshttp.body.Implicits._
122 | import fr.hmil.roshttp.body.JSONBody._
123 | import fr.hmil.roshttp.body._
124 |
125 | request.post(MultiPartBody(
126 | // The name part is sent as plain text
127 | "name" -> PlainTextBody("John"),
128 | // The skills part is a complex nested structure sent as JSON
129 | "skills" -> JSONObject(
130 | "programming" -> JSONObject(
131 | "C" -> 3,
132 | "PHP" -> 1,
133 | "Scala" -> 5
134 | ),
135 | "design" -> true
136 | ),
137 | "hobbies" -> JSONArray(
138 | "programming",
139 | "stargazing"
140 | ),
141 | // The picture is sent using a ByteBufferBody, assuming buffer is a ByteBuffer
142 | // containing the image data
143 | "picture" -> ByteBufferBody(buffer, "image/jpeg")
144 | ))
145 |
146 | import fr.hmil.roshttp.util.Utils._
147 |
148 | request
149 | .stream()
150 | .map({ r =>
151 | r.body.foreach(buffer => println(getStringFromBuffer(buffer, "UTF-8")))
152 | })
153 |
154 | import fr.hmil.roshttp.Method.POST
155 |
156 | request
157 | .withMethod(POST)
158 | .withBody(PlainTextBody("My upload data"))
159 | .stream()
160 | // The response will be streamed
161 |
162 | val inputStream = new java.io.ByteArrayInputStream(new Array[Byte](1))
163 |
164 | import fr.hmil.roshttp.body.Implicits._
165 |
166 | // On the JVM:
167 | // val inputStream = new java.io.FileInputStream("video.avi")
168 | request
169 | .post(inputStream)
170 | .onComplete({
171 | case _:Success[SimpleHttpResponse] => println("Data successfully uploaded")
172 | case _:Failure[SimpleHttpResponse] => println("Error: Could not upload stream")
173 | })
174 |
175 | import fr.hmil.roshttp.exceptions.HttpException
176 | import java.io.IOException
177 | request.send()
178 | .recover {
179 | case HttpException(e: SimpleHttpResponse) =>
180 | // Here we may have some detailed application-level insight about the error
181 | println("There was an issue with your request." +
182 | " Here is what the application server says: " + e.body)
183 | case e: IOException =>
184 | // By handling transport issues separately, you get a chance to apply
185 | // your own recovery strategy. Should you report to the user? Log the error?
186 | // Retry the request? Send an alert to your ops team?
187 | println("There was a network issue, please try again")
188 | }
189 |
190 | "Success"
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/test/resources/README:
--------------------------------------------------------------------------------
1 | Files in this folder are diffed with files uploaded to /upload.
2 | The server returns a success when the file matches and a failure otherwise
3 |
--------------------------------------------------------------------------------
/test/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmil/RosHTTP/7711894cebb11a85e44ea040deb69b0bce66c9ad/test/resources/icon.png
--------------------------------------------------------------------------------
/test/server/.gitignore:
--------------------------------------------------------------------------------
1 | runtime/
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/test/server/README:
--------------------------------------------------------------------------------
1 | This folder contains a nodejs web server which is used to test RösHTTP.
2 | The server serves both a testing API and the runtime scripts required by the sbt
3 | scala-js test runner.
4 |
5 | To setup the server, run `npm install` in this directory
6 | To run the server, run `node index.js`
7 |
--------------------------------------------------------------------------------
/test/server/index.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var app = express();
3 | var morgan = require('morgan');
4 | var querystring = require('querystring');
5 | var bodyParser = require('body-parser');
6 | var multipart = require('connect-multiparty');
7 | var fs = require('fs');
8 | var path = require('path');
9 | var cookieParser = require('cookie-parser')
10 | var cors = require('cors')
11 |
12 | app.use(morgan('combined'));
13 | app.use(cookieParser())
14 | app.use(cors({
15 | credentials: true,
16 | preflightContinue: true,
17 | origin: true,
18 | exposedHeaders: [ 'X-Powered-By', 'X-Request-Method'],
19 | methods: ["GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "PATCH", "TRACE"]
20 | })
21 | )
22 | app.use(multipart());
23 |
24 | // parse application/x-www-form-urlencoded
25 | app.use(bodyParser.urlencoded({ extended: false }));
26 |
27 | // parse application/json
28 | app.use(bodyParser.json());
29 |
30 | // parse all other types
31 | app.use(bodyParser.raw({
32 | type: 'text/plain'
33 | }));
34 |
35 | // Profiling middleware
36 | app.use(function(req, res, next) {
37 | var beginTime = Date.now();
38 | var end = res.end;
39 | res.end = function() {
40 | var endTime = Date.now();
41 | console.log("Spent " + (endTime - beginTime) + "ms on " + req.path);
42 | end.apply(this, arguments);
43 | };
44 | next();
45 | });
46 |
47 | app.get('/status/:statusCode', function(req, res) {
48 | res.set('X-Status-Code', req.params.statusCode);
49 | res.sendStatus(req.params.statusCode);
50 | });
51 |
52 | app.get('/redirect/temporary/*', function(req, res) {
53 | res.statusCode = 302;
54 | res.append('Location', '/' + req.params[0]);
55 | res.write("redirecting...");
56 | res.end();
57 | });
58 |
59 | app.get('/raw_greeting', function(req, res) {
60 | res.write("Hello world").end(); // Does not send Content-Type header
61 | });
62 |
63 | app.get('/redirect/permanent/*', function(req, res) {
64 | res.redirect(301 ,'/' + req.params[0]);
65 | });
66 |
67 |
68 | app.get('/', function (req, res) {
69 | res.send('Hello World!');
70 | });
71 |
72 | app.get('/echo/:text', function(req, res) {
73 | res.set('Content-Type', 'text/plain');
74 | res.send(req.params.text);
75 | });
76 |
77 | app.get('/echo_repeat/:text', function(req, res) {
78 | res.set('Content-Type', 'text/plain');
79 | var repeat = req.query.repeat || 1;
80 | var delay = req.query.delay || 1000;
81 | function sendOne() {
82 | res.write(req.params.text);
83 | repeat--;
84 | if (repeat > 0) {
85 | setTimeout(sendOne, delay);
86 | } else {
87 | res.end();
88 | }
89 | }
90 | sendOne();
91 | });
92 |
93 | app.get('/multibyte_string', function(req, res) {
94 | res.send("12\uD83D\uDCA978");
95 | });
96 |
97 | app.get('/query', function(req, res) {
98 | var url = req.url;
99 | var qPos = url.indexOf('?') + 1;
100 | if (qPos > 0) {
101 | var hPos = url.substr(qPos).indexOf('#');
102 | var query = url.substring(qPos, hPos < 0 ? undefined : hPos);
103 | res.send(querystring.unescape(query));
104 | } else {
105 | res.send('');
106 | }
107 | });
108 |
109 | app.get('/query/parsed', function(req, res) {
110 | res.send(req.query);
111 | });
112 |
113 | app.all('/headers', function(req, res) {
114 | res.set('Content-Type', 'text/plain');
115 | res.send(JSON.stringify(req.headers).replace(/"(?!\\),/g, '"\n').replace(/(^{|}$)/g, ''));
116 | });
117 |
118 | app.all('/method', function(req, res) {
119 | res.set('X-Request-Method', req.method);
120 | res.send(req.method);
121 | });
122 |
123 | app.options('/body', function(req, res) {
124 | res.end();
125 | });
126 |
127 | app.all('/body', function(req, res) {
128 | if (!req.headers.hasOwnProperty('content-type')) {
129 | res.status(400).send("No request body!");
130 | } else if(req.body.length === 0) {
131 | res.status(400).send("Empty request body!");
132 | } else {
133 | res.set('Content-Type', req.headers['content-type']);
134 | res.send(req.body);
135 | }
136 | });
137 |
138 | app.all('/empty_body/:statusCode', function(req, res) {
139 | res.set('X-Status-Code', req.params.statusCode);
140 | res.status(req.params.statusCode).send('');
141 | });
142 |
143 | app.post('/compare/:name', function(req, res) {
144 | bodyParser.raw()(req, res, function(err) {
145 | var fdata = fs.readFileSync(path.join(__dirname, "../resources", req.params.name));
146 |
147 | if (fdata.compare(req.body) == 0) {
148 | res.status(200).send('Ok');
149 | } else {
150 | res.status(400).send(req.body);
151 | }
152 | });
153 | });
154 |
155 | function logOverwrite(line) {
156 | process.stdout.clearLine(); // clear current text
157 | process.stdout.cursorTo(0); // move cursor to beginning of line
158 | process.stdout.write(line);
159 | }
160 |
161 | app.post('/streams/in', function(req, res) {
162 | var count = 0;
163 | req.on('data', function(chunk) {
164 | var prev = count;
165 | count += chunk.length
166 | if (prev % 16777216 > count % 16777216) {
167 | logOverwrite("Upload stream : " + count + " loaded");
168 | }
169 | });
170 | req.on('end', function() {
171 | process.stdout.write("\n");
172 | res.send('Received ' + count + ' bytes.');
173 | })
174 | });
175 |
176 | app.get('/set_cookie', function(req, res) {
177 | if (req.query.token !== undefined || req.cookies === undefined || Object.keys(req.cookies).length === 0) {
178 | res.cookie('test_cookie', req.query.token);
179 | res.status(200).send("Cookie request answered by sending cookie to client.");
180 | } else {
181 | res.status(500).send('Server sent no cookie to client!')
182 | }
183 | });
184 |
185 | app.get('/verify_cookie', function(req, res) {
186 | var testCookie = req.cookies['test_cookie'];
187 | if (testCookie === undefined) {
188 | res.status(500).send('Cookie not received from client!');
189 | } else {
190 | if (testCookie === req.query.token) {
191 | res.status(200).send('Cookie received from client.')
192 | } else {
193 | res.status(500).send('Cookie received from client, but contained data was wrong!')
194 | }
195 | }
196 | });
197 |
198 | var ONE_MILLION = 1000000;
199 | app.get('/streams/out', function(req, res) {
200 | var buff = Buffer.alloc(8192, 97);
201 | var remaining = ONE_MILLION;
202 | var increment = remaining / 100;
203 | res.set('Content-Type', 'application/octet-stream');
204 | res.set('Transfer-Encoding', 'chunked');
205 |
206 | function niceWrite() {
207 | while (remaining > 0) {
208 | remaining --;
209 | if (remaining % increment === 0) {
210 | logOverwrite("Download stream: " + remaining + " chunks to go.")
211 | }
212 | if (res.write(buff) === false) {
213 | res.once('drain', niceWrite);
214 | return;
215 | }
216 | }
217 | process.stdout.write("\n");
218 | res.end();
219 | }
220 | niceWrite();
221 | });
222 |
223 | app.use('/resources', express.static('../resources'));
224 | app.use('/runtime', express.static('runtime'));
225 |
226 | app.listen(3000, function () {
227 | console.log('Test server listening on port 3000.');
228 | });
229 |
--------------------------------------------------------------------------------
/test/server/npm-shrinkwrap.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "roshttp-test-server",
3 | "version": "0.0.1",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "accepts": {
8 | "version": "1.3.7",
9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
10 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
11 | "requires": {
12 | "mime-types": "2.1.27",
13 | "negotiator": "0.6.2"
14 | }
15 | },
16 | "array-flatten": {
17 | "version": "1.1.1",
18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
19 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
20 | },
21 | "basic-auth": {
22 | "version": "2.0.1",
23 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
24 | "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
25 | "requires": {
26 | "safe-buffer": "5.1.2"
27 | }
28 | },
29 | "body-parser": {
30 | "version": "1.15.1",
31 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.15.1.tgz",
32 | "integrity": "sha1-m87vBmm4+LlD8K2M5dlXFr10D9I=",
33 | "requires": {
34 | "bytes": "2.3.0",
35 | "content-type": "1.0.1",
36 | "debug": "2.2.0",
37 | "depd": "1.1.0",
38 | "http-errors": "1.4.0",
39 | "iconv-lite": "0.4.13",
40 | "on-finished": "2.3.0",
41 | "qs": "6.1.0",
42 | "raw-body": "2.1.6",
43 | "type-is": "1.6.12"
44 | },
45 | "dependencies": {
46 | "bytes": {
47 | "version": "2.3.0",
48 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.3.0.tgz",
49 | "integrity": "sha1-1baAoWW2IBc5rLYRVCqrwtjOsHA="
50 | },
51 | "content-type": {
52 | "version": "1.0.1",
53 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.1.tgz",
54 | "integrity": "sha1-oZ0iRzJ9wDgFDOYit6FU7FnF5gA="
55 | },
56 | "debug": {
57 | "version": "2.2.0",
58 | "resolved": "http://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
59 | "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
60 | "requires": {
61 | "ms": "0.7.1"
62 | },
63 | "dependencies": {
64 | "ms": {
65 | "version": "0.7.1",
66 | "resolved": "http://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
67 | "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg="
68 | }
69 | }
70 | },
71 | "depd": {
72 | "version": "1.1.0",
73 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz",
74 | "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM="
75 | },
76 | "http-errors": {
77 | "version": "1.4.0",
78 | "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.4.0.tgz",
79 | "integrity": "sha1-bAJC3qaz33r9oVPHEImzHG6Cqr8=",
80 | "requires": {
81 | "inherits": "2.0.1",
82 | "statuses": "1.2.1"
83 | },
84 | "dependencies": {
85 | "inherits": {
86 | "version": "2.0.1",
87 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
88 | "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
89 | },
90 | "statuses": {
91 | "version": "1.2.1",
92 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz",
93 | "integrity": "sha1-3e1FzBglbVHtQK7BQkidXGECbSg="
94 | }
95 | }
96 | },
97 | "iconv-lite": {
98 | "version": "0.4.13",
99 | "resolved": "http://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz",
100 | "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI="
101 | },
102 | "on-finished": {
103 | "version": "2.3.0",
104 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
105 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
106 | "requires": {
107 | "ee-first": "1.1.1"
108 | },
109 | "dependencies": {
110 | "ee-first": {
111 | "version": "1.1.1",
112 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
113 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
114 | }
115 | }
116 | },
117 | "qs": {
118 | "version": "6.1.0",
119 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz",
120 | "integrity": "sha1-7B0WJrJCeNmfD99FSeUk4k7O6yY="
121 | },
122 | "raw-body": {
123 | "version": "2.1.6",
124 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.6.tgz",
125 | "integrity": "sha1-nAUHN/4HztbZSk/QnGG2rYdNMQ8=",
126 | "requires": {
127 | "bytes": "2.3.0",
128 | "iconv-lite": "0.4.13",
129 | "unpipe": "1.0.0"
130 | },
131 | "dependencies": {
132 | "unpipe": {
133 | "version": "1.0.0",
134 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
135 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
136 | }
137 | }
138 | },
139 | "type-is": {
140 | "version": "1.6.12",
141 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.12.tgz",
142 | "integrity": "sha1-A1Kp37//BA/maMwVPMlYKcNUFz4=",
143 | "requires": {
144 | "media-typer": "0.3.0",
145 | "mime-types": "2.1.11"
146 | },
147 | "dependencies": {
148 | "media-typer": {
149 | "version": "0.3.0",
150 | "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
151 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
152 | },
153 | "mime-types": {
154 | "version": "2.1.11",
155 | "resolved": "http://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz",
156 | "integrity": "sha1-wlnEcb2oCKhdbNGTtDCl+uRHOzw=",
157 | "requires": {
158 | "mime-db": "1.23.0"
159 | },
160 | "dependencies": {
161 | "mime-db": {
162 | "version": "1.23.0",
163 | "resolved": "http://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz",
164 | "integrity": "sha1-oxtAcK2uon1zLqMzdApk0OyaZlk="
165 | }
166 | }
167 | }
168 | }
169 | }
170 | }
171 | },
172 | "bytes": {
173 | "version": "3.1.0",
174 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
175 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
176 | },
177 | "connect-multiparty": {
178 | "version": "2.0.0",
179 | "resolved": "http://registry.npmjs.org/connect-multiparty/-/connect-multiparty-2.0.0.tgz",
180 | "integrity": "sha1-V6e2HMezG27vSmKHjWDXcbI2mas=",
181 | "requires": {
182 | "multiparty": "4.1.2",
183 | "on-finished": "2.3.0",
184 | "qs": "4.0.0",
185 | "type-is": "1.6.12"
186 | },
187 | "dependencies": {
188 | "multiparty": {
189 | "version": "4.1.2",
190 | "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.1.2.tgz",
191 | "integrity": "sha1-VPjslxIFL6Hf2OyXUFbIIw1vI3A=",
192 | "requires": {
193 | "fd-slicer": "1.0.1"
194 | },
195 | "dependencies": {
196 | "fd-slicer": {
197 | "version": "1.0.1",
198 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
199 | "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
200 | "requires": {
201 | "pend": "1.2.0"
202 | },
203 | "dependencies": {
204 | "pend": {
205 | "version": "1.2.0",
206 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
207 | "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
208 | }
209 | }
210 | }
211 | }
212 | },
213 | "on-finished": {
214 | "version": "2.3.0",
215 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
216 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
217 | "requires": {
218 | "ee-first": "1.1.1"
219 | },
220 | "dependencies": {
221 | "ee-first": {
222 | "version": "1.1.1",
223 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
224 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
225 | }
226 | }
227 | },
228 | "qs": {
229 | "version": "4.0.0",
230 | "resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz",
231 | "integrity": "sha1-wx2bdOwn33XlQ6hseHKO2NRiNgc="
232 | },
233 | "type-is": {
234 | "version": "1.6.12",
235 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.12.tgz",
236 | "integrity": "sha1-A1Kp37//BA/maMwVPMlYKcNUFz4=",
237 | "requires": {
238 | "media-typer": "0.3.0",
239 | "mime-types": "2.1.11"
240 | },
241 | "dependencies": {
242 | "media-typer": {
243 | "version": "0.3.0",
244 | "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
245 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
246 | },
247 | "mime-types": {
248 | "version": "2.1.11",
249 | "resolved": "http://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz",
250 | "integrity": "sha1-wlnEcb2oCKhdbNGTtDCl+uRHOzw=",
251 | "requires": {
252 | "mime-db": "1.23.0"
253 | },
254 | "dependencies": {
255 | "mime-db": {
256 | "version": "1.23.0",
257 | "resolved": "http://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz",
258 | "integrity": "sha1-oxtAcK2uon1zLqMzdApk0OyaZlk="
259 | }
260 | }
261 | }
262 | }
263 | }
264 | }
265 | },
266 | "content-disposition": {
267 | "version": "0.5.3",
268 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
269 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
270 | "requires": {
271 | "safe-buffer": "5.1.2"
272 | }
273 | },
274 | "content-type": {
275 | "version": "1.0.4",
276 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
277 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
278 | },
279 | "cookie": {
280 | "version": "0.3.1",
281 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
282 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
283 | },
284 | "cookie-parser": {
285 | "version": "1.4.3",
286 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.3.tgz",
287 | "integrity": "sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU=",
288 | "requires": {
289 | "cookie": "0.3.1",
290 | "cookie-signature": "1.0.6"
291 | }
292 | },
293 | "cookie-signature": {
294 | "version": "1.0.6",
295 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
296 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
297 | },
298 | "cors": {
299 | "version": "2.8.4",
300 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.4.tgz",
301 | "integrity": "sha1-K9OB8usgECAQXNUOpZ2mMJBpRoY=",
302 | "requires": {
303 | "object-assign": "4.1.1",
304 | "vary": "1.1.2"
305 | }
306 | },
307 | "debug": {
308 | "version": "2.6.9",
309 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
310 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
311 | "requires": {
312 | "ms": "2.0.0"
313 | }
314 | },
315 | "depd": {
316 | "version": "1.1.2",
317 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
318 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
319 | },
320 | "destroy": {
321 | "version": "1.0.4",
322 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
323 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
324 | },
325 | "ee-first": {
326 | "version": "1.1.1",
327 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
328 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
329 | },
330 | "encodeurl": {
331 | "version": "1.0.2",
332 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
333 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
334 | },
335 | "escape-html": {
336 | "version": "1.0.3",
337 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
338 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
339 | },
340 | "etag": {
341 | "version": "1.8.1",
342 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
343 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
344 | },
345 | "express": {
346 | "version": "4.17.1",
347 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
348 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
349 | "requires": {
350 | "accepts": "1.3.7",
351 | "array-flatten": "1.1.1",
352 | "body-parser": "1.19.0",
353 | "content-disposition": "0.5.3",
354 | "content-type": "1.0.4",
355 | "cookie": "0.4.0",
356 | "cookie-signature": "1.0.6",
357 | "debug": "2.6.9",
358 | "depd": "1.1.2",
359 | "encodeurl": "1.0.2",
360 | "escape-html": "1.0.3",
361 | "etag": "1.8.1",
362 | "finalhandler": "1.1.2",
363 | "fresh": "0.5.2",
364 | "merge-descriptors": "1.0.1",
365 | "methods": "1.1.2",
366 | "on-finished": "2.3.0",
367 | "parseurl": "1.3.3",
368 | "path-to-regexp": "0.1.7",
369 | "proxy-addr": "2.0.6",
370 | "qs": "6.7.0",
371 | "range-parser": "1.2.1",
372 | "safe-buffer": "5.1.2",
373 | "send": "0.17.1",
374 | "serve-static": "1.14.1",
375 | "setprototypeof": "1.1.1",
376 | "statuses": "1.5.0",
377 | "type-is": "1.6.18",
378 | "utils-merge": "1.0.1",
379 | "vary": "1.1.2"
380 | },
381 | "dependencies": {
382 | "body-parser": {
383 | "version": "1.19.0",
384 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
385 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
386 | "requires": {
387 | "bytes": "3.1.0",
388 | "content-type": "1.0.4",
389 | "debug": "2.6.9",
390 | "depd": "1.1.2",
391 | "http-errors": "1.7.2",
392 | "iconv-lite": "0.4.24",
393 | "on-finished": "2.3.0",
394 | "qs": "6.7.0",
395 | "raw-body": "2.4.0",
396 | "type-is": "1.6.18"
397 | }
398 | },
399 | "cookie": {
400 | "version": "0.4.0",
401 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
402 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
403 | }
404 | }
405 | },
406 | "finalhandler": {
407 | "version": "1.1.2",
408 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
409 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
410 | "requires": {
411 | "debug": "2.6.9",
412 | "encodeurl": "1.0.2",
413 | "escape-html": "1.0.3",
414 | "on-finished": "2.3.0",
415 | "parseurl": "1.3.3",
416 | "statuses": "1.5.0",
417 | "unpipe": "1.0.0"
418 | }
419 | },
420 | "forwarded": {
421 | "version": "0.1.2",
422 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
423 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
424 | },
425 | "fresh": {
426 | "version": "0.5.2",
427 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
428 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
429 | },
430 | "http-errors": {
431 | "version": "1.7.2",
432 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
433 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
434 | "requires": {
435 | "depd": "1.1.2",
436 | "inherits": "2.0.3",
437 | "setprototypeof": "1.1.1",
438 | "statuses": "1.5.0",
439 | "toidentifier": "1.0.0"
440 | }
441 | },
442 | "iconv-lite": {
443 | "version": "0.4.24",
444 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
445 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
446 | "requires": {
447 | "safer-buffer": "2.1.2"
448 | }
449 | },
450 | "inherits": {
451 | "version": "2.0.3",
452 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
453 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
454 | },
455 | "ipaddr.js": {
456 | "version": "1.9.1",
457 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
458 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
459 | },
460 | "media-typer": {
461 | "version": "0.3.0",
462 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
463 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
464 | },
465 | "merge-descriptors": {
466 | "version": "1.0.1",
467 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
468 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
469 | },
470 | "methods": {
471 | "version": "1.1.2",
472 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
473 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
474 | },
475 | "mime": {
476 | "version": "1.6.0",
477 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
478 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
479 | },
480 | "mime-db": {
481 | "version": "1.44.0",
482 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
483 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg=="
484 | },
485 | "mime-types": {
486 | "version": "2.1.27",
487 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
488 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
489 | "requires": {
490 | "mime-db": "1.44.0"
491 | }
492 | },
493 | "morgan": {
494 | "version": "1.10.0",
495 | "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
496 | "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
497 | "requires": {
498 | "basic-auth": "2.0.1",
499 | "debug": "2.6.9",
500 | "depd": "2.0.0",
501 | "on-finished": "2.3.0",
502 | "on-headers": "1.0.2"
503 | },
504 | "dependencies": {
505 | "depd": {
506 | "version": "2.0.0",
507 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
508 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
509 | }
510 | }
511 | },
512 | "ms": {
513 | "version": "2.0.0",
514 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
515 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
516 | },
517 | "negotiator": {
518 | "version": "0.6.2",
519 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
520 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
521 | },
522 | "object-assign": {
523 | "version": "4.1.1",
524 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
525 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
526 | },
527 | "on-finished": {
528 | "version": "2.3.0",
529 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
530 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
531 | "requires": {
532 | "ee-first": "1.1.1"
533 | }
534 | },
535 | "on-headers": {
536 | "version": "1.0.2",
537 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
538 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
539 | },
540 | "parseurl": {
541 | "version": "1.3.3",
542 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
543 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
544 | },
545 | "path-to-regexp": {
546 | "version": "0.1.7",
547 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
548 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
549 | },
550 | "proxy-addr": {
551 | "version": "2.0.6",
552 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
553 | "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
554 | "requires": {
555 | "forwarded": "0.1.2",
556 | "ipaddr.js": "1.9.1"
557 | }
558 | },
559 | "qs": {
560 | "version": "6.7.0",
561 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
562 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
563 | },
564 | "range-parser": {
565 | "version": "1.2.1",
566 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
567 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
568 | },
569 | "raw-body": {
570 | "version": "2.4.0",
571 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
572 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
573 | "requires": {
574 | "bytes": "3.1.0",
575 | "http-errors": "1.7.2",
576 | "iconv-lite": "0.4.24",
577 | "unpipe": "1.0.0"
578 | }
579 | },
580 | "safe-buffer": {
581 | "version": "5.1.2",
582 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
583 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
584 | },
585 | "safer-buffer": {
586 | "version": "2.1.2",
587 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
588 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
589 | },
590 | "send": {
591 | "version": "0.17.1",
592 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
593 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
594 | "requires": {
595 | "debug": "2.6.9",
596 | "depd": "1.1.2",
597 | "destroy": "1.0.4",
598 | "encodeurl": "1.0.2",
599 | "escape-html": "1.0.3",
600 | "etag": "1.8.1",
601 | "fresh": "0.5.2",
602 | "http-errors": "1.7.2",
603 | "mime": "1.6.0",
604 | "ms": "2.1.1",
605 | "on-finished": "2.3.0",
606 | "range-parser": "1.2.1",
607 | "statuses": "1.5.0"
608 | },
609 | "dependencies": {
610 | "ms": {
611 | "version": "2.1.1",
612 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
613 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
614 | }
615 | }
616 | },
617 | "serve-static": {
618 | "version": "1.14.1",
619 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
620 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
621 | "requires": {
622 | "encodeurl": "1.0.2",
623 | "escape-html": "1.0.3",
624 | "parseurl": "1.3.3",
625 | "send": "0.17.1"
626 | }
627 | },
628 | "setprototypeof": {
629 | "version": "1.1.1",
630 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
631 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
632 | },
633 | "statuses": {
634 | "version": "1.5.0",
635 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
636 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
637 | },
638 | "toidentifier": {
639 | "version": "1.0.0",
640 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
641 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
642 | },
643 | "type-is": {
644 | "version": "1.6.18",
645 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
646 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
647 | "requires": {
648 | "media-typer": "0.3.0",
649 | "mime-types": "2.1.27"
650 | }
651 | },
652 | "unpipe": {
653 | "version": "1.0.0",
654 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
655 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
656 | },
657 | "utils-merge": {
658 | "version": "1.0.1",
659 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
660 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
661 | },
662 | "vary": {
663 | "version": "1.1.2",
664 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
665 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
666 | }
667 | }
668 | }
669 |
--------------------------------------------------------------------------------
/test/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "roshttp-test-server",
3 | "version": "0.0.1",
4 | "description": "A testing server for RösHTTP",
5 | "main": "index.js",
6 | "dependencies": {
7 | "body-parser": "^1.15.0",
8 | "connect-multiparty": "^2.0.0",
9 | "cookie-parser": "^1.4.3",
10 | "cors": "^2.8.4",
11 | "express": "^4.13.4",
12 | "morgan": "^1.7.0"
13 | },
14 | "devDependencies": {},
15 | "scripts": {
16 | "test": "echo \"Error: no test specified\" && exit 1"
17 | },
18 | "author": "Hadrien Milano ",
19 | "license": "MIT"
20 | }
21 |
--------------------------------------------------------------------------------
/test/update-readme-sanity-check.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | test_folder='shared/src/test/scala/fr/hmil/roshttp_test'
4 | test_name='ReadmeSanityCheck'
5 | fatalDiff='no'
6 | if [ "$1" = "--fatalDiff" ]; then
7 | fatalDiff='yes'
8 | fi
9 |
10 | # Filters lines from the README to output only lines of code enclosed by scala markdown code tags
11 | filter_code() {
12 | awk '/```/{e=0}/^```scala/{gsub("^```scala","",$0);e=1}{if(e==1){print}}'
13 | }
14 |
15 | format_code() {
16 | cat < $test_folder/$test_name.scala
55 | }
56 |
57 | echo "Verifying readme sanity check..."
58 | mv $test_folder/$test_name.scala $test_folder/$test_name.old
59 | update_sanity_check
60 | diff $test_folder/$test_name.scala $test_folder/$test_name.old
61 | status="$?"
62 | rm $test_folder/$test_name.old
63 | if [ ! "$status" -eq 0 ]; then
64 | if [ "$fatalDiff" = "yes" ]; then
65 | cat <