From cca484940551a3e9d46fb299550a76278a148df3 Mon Sep 17 00:00:00 2001 From: hustf Date: Sat, 9 Jun 2018 21:32:35 +0200 Subject: [PATCH] Stricter checks open client wsuri modified: README.md Replace client example new:examples/client_repl_input.jl Same as README.md modified: examples/minimal_server.jl More prints, as in README modified: examples/minimal_server_HTTP.jl More prints modified: src/HTTP.jl Stricter wsuri checks modified: test/client_test.jl Check errors bad wsuri --- README.md | 77 ++++++++++++++++----------------- examples/client_repl_input.jl | 33 ++++++++++++++ examples/minimal_server.jl | 12 ++--- examples/minimal_server_HTTP.jl | 8 ++-- src/HTTP.jl | 13 +++++- test/client_test.jl | 21 +++++++++ 6 files changed, 114 insertions(+), 50 deletions(-) create mode 100644 examples/client_repl_input.jl diff --git a/README.md b/README.md index 8d712a7..05f56da 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ You should also have a look at alternative Julia packages: [DandelionWebSockets] ## Server side example -As a first example, we can create a WebSockets echo server. We use named function arguments for more readable stacktraces while debugging. +As a first example, we can create a WebSockets echo server. We use named function arguments for more readable stacktraces while debugging. ```julia using HttpServer @@ -74,10 +74,8 @@ function coroutine(ws) while isopen(ws) data, = readguarded(ws) s = String(data) - if s == "" - break - end - println(s) + s == "" && break + println("Received: ", s) if s[1] == "P" writeguarded(ws, "No, I'm not!") else @@ -87,10 +85,12 @@ function coroutine(ws) end function gatekeeper(req, ws) - if origin(req) == "http://127.0.0.1:8080" || origin(req) == "http://localhost:8080" + println("\nOrigin:", origin(req), " Target:", target(req), " subprotocol:", subprotocol(req)) + # Non-browser clients don't send Origin. We liberally accept in this case. + if origin(req) == "" || origin(req) == "http://127.0.0.1:8080" || origin(req) == "http://localhost:8080" coroutine(ws) else - println(origin(req)) + println("Inacceptable request") end end @@ -99,7 +99,7 @@ handle(req, res) = Response(200) server = Server(HttpHandler(handle), WebSocketHandler(gatekeeper)) -run(server, 8080) +@async run(server, 8080) ``` Now open a browser on http://127.0.0.1:8080/ and press F12. In the console, type the lines following ≫: @@ -131,55 +131,54 @@ You could replace 'using HttpServer' with 'using HTTP'. Also: ## Client side example -You need to use [HTTP.jl](https://github.com/JuliaWeb/HttpServer.jl). - -What you can't do is use a browser as the server side. The server side can be the example above, running in an asyncronous task. The server can also be running in a separate REPL, or in a a parallel task. The benchmarks puts the `client` side on a parallel task. +Clients need to use [HTTP.jl](https://github.com/JuliaWeb/HttpServer.jl). -The following example -- runs server in an asyncronous task, client in the REPL control flow -- uses [Do-Block-Syntax](https://docs.julialang.org/en/v0.6.3/manual/functions/#Do-Block-Syntax-for-Function-Arguments-1), which is a style choice -- the server `ugrade` skips checking origin(req)` -- the server is invoked with `listen(..)` instead of `serve()` -- read(ws) and write(ws, msg) instead of readguarded(ws), writeguarded(ws) ```julia - using HTTP using WebSockets - -const PORT = 8080 - -# Server side -@async HTTP.listen("127.0.0.1", PORT) do http - if WebSockets.is_upgrade(http.message) - WebSockets.upgrade(http) do req, ws - while isopen(ws) - msg = String(read(ws)) - write(ws, msg) - end +function client_one_message(ws) + print_with_color(:green, STDOUT, "\nws|client input > ") + msg = readline(STDIN) + if writeguarded(ws, msg) + msg, stillopen = readguarded(ws) + println("Received:", String(msg)) + if stillopen + println("The connection is active, but we leave. WebSockets.jl will close properly.") + else + println("Disconnect during reading.") end + else + println("Disconnect during writing.") end end - -sleep(2) - - -WebSockets.open("ws://127.0.0.1:$PORT") do ws - write(ws, "Peer, about your hunting") - println("echo received:" * String(read(ws))) +function main() + while true + println("\nWebSocket client side. WebSocket URI format:") + println("ws:// host [ \":\" port ] path [ \"?\" query ]") + println("Example:\nws://127.0.0.1:8080") + println("Where do you want to connect? Empty line to exit") + print_with_color(:green, STDOUT, "\nclient_repl_input > ") + wsuri = readline(STDIN) + wsuri == "" && break + res = WebSockets.open(client_one_message, wsuri) + !isa(res, HTTP.Response) && println(res) + end + println("Have a nice day") end + +main() ``` -The output from the example in a console session is barely readable. Output from asyncronous tasks are intermixed. To build real-time applications, we need more code. See other examples in /test, /benchmark/ and /examples. +See other examples in /test, /benchmark/ and /examples. Some logging utilties for a running relay server are available in /logutils. -Some logging utilties for a running relay server are available in /logutils. ## Errors after updating? The introduction of client side websockets to this package may require changes in your code: - `using HttpServer` (or import) prior to `using WebSockets` (or import). - The `WebSocket.id` field is no longer supported. You can generate unique counters by code similar to 'bencmark/functions_open_browsers.jl' COUNTBROWSER. -- You may want to modify error handling code. Examine WebSocketsClosedError.message. +- You may want to modify you error handling code. Examine WebSocketsClosedError.message. - You may want to use `readguarded` and `writeguarded` to save on error handling code. ## Switching from HttpServer to HTTP? diff --git a/examples/client_repl_input.jl b/examples/client_repl_input.jl new file mode 100644 index 0000000..e8fec44 --- /dev/null +++ b/examples/client_repl_input.jl @@ -0,0 +1,33 @@ +using HTTP +using WebSockets +function client_one_message(ws) + print_with_color(:green, STDOUT, "\nws|client input > ") + msg = readline(STDIN) + if writeguarded(ws, msg) + msg, stillopen = readguarded(ws) + println("Received:", String(msg)) + if stillopen + println("The connection is active, but we leave. WebSockets.jl will close properly.") + else + println("Disconnect during reading.") + end + else + println("Disconnect during writing.") + end +end +function main() + while true + println("\nWebSocket client side. WebSocket URI format:") + println("ws:// host [ \":\" port ] path [ \"?\" query ]") + println("Example:\nws://127.0.0.1:8080") + println("Where do you want to connect? Empty line to exit") + print_with_color(:green, STDOUT, "\nclient_repl_input > ") + wsuri = readline(STDIN) + wsuri == "" && break + res = WebSockets.open(client_one_message, wsuri) + !isa(res, HTTP.Response) && println(res) + end + println("Have a nice day") +end + +main() diff --git a/examples/minimal_server.jl b/examples/minimal_server.jl index f19ff2e..b1e172e 100644 --- a/examples/minimal_server.jl +++ b/examples/minimal_server.jl @@ -5,10 +5,8 @@ function coroutine(ws) while isopen(ws) data, = readguarded(ws) s = String(data) - if s == "" - break - end - println(s) + s == "" && break + println("Received: ", s) if s[1] == "P" writeguarded(ws, "No, I'm not!") else @@ -18,10 +16,12 @@ function coroutine(ws) end function gatekeeper(req, ws) - if origin(req) == "http://127.0.0.1:8080" || origin(req) == "http://localhost:8080" + println("\nOrigin:", origin(req), " Target:", target(req), " subprotocol:", subprotocol(req)) + # Non-browser clients don't send Origin. We liberally accept in this case. + if origin(req) == "" || origin(req) == "http://127.0.0.1:8080" || origin(req) == "http://localhost:8080" coroutine(ws) else - println(origin(req)) + println("Inacceptable request") end end diff --git a/examples/minimal_server_HTTP.jl b/examples/minimal_server_HTTP.jl index 2450f5b..35c3329 100644 --- a/examples/minimal_server_HTTP.jl +++ b/examples/minimal_server_HTTP.jl @@ -8,7 +8,7 @@ function coroutine(ws) if s == "" break end - println(s) + println("Received: ", s) if s[1] == "P" writeguarded(ws, "No, I'm not!") else @@ -18,10 +18,12 @@ function coroutine(ws) end function gatekeeper(req, ws) - if origin(req) == "http://127.0.0.1:8080" || origin(req) == "http://localhost:8080" + println("\nOrigin:", origin(req), " Target:", target(req), " subprotocol:", subprotocol(req)) + # Non-browser clients don't send Origin. We liberally accept in this case. + if origin(req) == "" || origin(req) == "http://127.0.0.1:8080" || origin(req) == "http://localhost:8080" coroutine(ws) else - println(origin(req)) + println("Inacceptable request") end end diff --git a/src/HTTP.jl b/src/HTTP.jl index 5de35ef..fd2de45 100644 --- a/src/HTTP.jl +++ b/src/HTTP.jl @@ -32,13 +32,22 @@ function open(f::Function, url; verbose=false, subprotocol = "", kw...) push!(headers, "Sec-WebSocket-Protocol" => subprotocol ) end + if in('#', url) + throw(ArgumentError(" replace '#' with %23 in url: $url")) + end + + uri = HTTP.URIs.URI(url) + if uri.scheme != "ws" && uri.scheme != "wss" + throw(ArgumentError(" bad argument url: Scheme not ws or wss. Input scheme: $(uri.scheme)")) + end + + try - HTTP.open("GET", url, headers; + HTTP.open("GET", uri, headers; reuse_limit=0, verbose=verbose ? 2 : 0, kw...) do http _openstream(f, http, key) end catch err - # TODO don't pass on WebSocketClosedError and other known ones. if typeof(err) <: Base.UVError throw(WebSocketClosedError(" while open ws|client: $(string(err))")) elseif typeof(err) <: HTTP.ExceptionRequest.StatusError diff --git a/test/client_test.jl b/test/client_test.jl index 0b66a4b..5e83e77 100644 --- a/test/client_test.jl +++ b/test/client_test.jl @@ -45,6 +45,27 @@ end @test typeof(caughterr) <: WebSockets.WebSocketClosedError @test caughterr.message == " while open ws|client: connect: connection refused (ECONNREFUSED)" +info("try open with uknown scheme\n") +sleep(1) +caughterr = ArgumentError("") +try +WebSockets.open((_)->nothing, "ww://127.0.0.1:8099"); +catch err + caughterr = err +end +@test typeof(caughterr) <: ArgumentError +@test caughterr.msg == " bad argument url: Scheme not ws or wss. Input scheme: ww" + + +caughterr = ArgumentError("") +try +WebSockets.open((_)->nothing, "ws://127.0.0.1:8099/svg/#"); +catch err + caughterr = err +end +@test typeof(caughterr) <: ArgumentError +@test caughterr.msg == " replace '#' with %23 in url: ws://127.0.0.1:8099/svg/#" + info("start a client websocket that irritates by closing the TCP stream connection without a websocket closing handshake. This throws an error in the server task\n")