Skip to content

Commit

Permalink
Http.jl (#88)
Browse files Browse the repository at this point in the history
* Use HTTP.jl

* Support SSLContext

* Get tests to pass

* Fix chat example

* examples/server.jl is working.

* Restrict tests to Julia v0.6 (v0.5 no longer supported).

* Almost done. Last test is nearly passing...
  • Loading branch information
Eric Forgy authored and hustf committed Feb 16, 2018
1 parent 77a8f8d commit 1dd534b
Show file tree
Hide file tree
Showing 16 changed files with 598 additions and 551 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ os:
- linux
- osx
julia:
- 0.5
- 0.6
sudo: false
notifications:
Expand Down
2 changes: 0 additions & 2 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
environment:
matrix:
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.6/julia-0.6-latest-win64.exe"
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.5/julia-0.5-latest-win64.exe"
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.6/julia-0.6-latest-win32.exe"
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.5/julia-0.5-latest-win32.exe"
# HttpCommon not building yet - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x64/julia-latest-win64.exe"
# HttpCommon not building yet - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe"
branches:
Expand Down
35 changes: 23 additions & 12 deletions examples/chat-client.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ <h1>Select a userinput</h1>
var $chatForm = document.querySelector("#say_message");
var $chatInput = $chatForm.querySelector("input[name=say]");


function uuid4() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
)
}
var id = uuid4();

function addContent(html) {
var $content = document.querySelector("#content");
var div = document.createElement("div");
Expand All @@ -86,20 +94,28 @@ <h1>Select a userinput</h1>
if( !uname.replace(/\s/gi,'').length ) {
alert("Please select a valid userinput");
} else {
connection.send('setusername:'+ uname);
var msg = {
"id": id,
"userName": uname
};
connection.send(JSON.stringify(msg));
$userName.innerHTML = uname;
$welcome.style.display = "none";
$chat.style.display = "block";
}
}

function whenChatMessage() {
var msg = $chatInput.value;
if(!msg.replace(/\s/gi,'').length) {
var content = $chatInput.value;
if( !content.replace(/\s/gi,'').length) {
/* nothing to do */
} else {
connection.send('say:'+ msg);
addContent(`<p class='sent'>${you}: ${msg}</p>`);
var msg = {
"id": id,
"say": content
};
connection.send(JSON.stringify(msg));
addContent(`<p class='sent'>${you}: ${content}</p>`);
$chatInput.focus();
}
}
Expand All @@ -110,19 +126,14 @@ <h1>Select a userinput</h1>
whenChatMessage();
return false;
})

$chatInput.addEventListener("keypress", (e) => {
if( e.keyCode === 13 ) { whenChatMessage(); } ;
return false;
}, false)


$userForm.addEventListener("submit", function(e){
e.preventDefault();
e.stopImmediatePropagation();
whenUserName();
return false;
});

const connection = new SocketConnection(onMessageReceived);
connection.start();

Expand Down
32 changes: 19 additions & 13 deletions examples/chat.jl
Original file line number Diff line number Diff line change
@@ -1,38 +1,44 @@
using HttpServer
using WebSockets
using JSON

struct User
name::String
client::WebSocket
end
#global Dict to store open connections in
global connections = Dict{Int,WebSocket}()
global usernames = Dict{Int,String}()
global connections = Dict{String,User}()

function decodeMessage( msg )
String(copy(msg))
JSON.parse(String(copy(msg)))
end

wsh = WebSocketHandler() do req, client
global connections
@show connections[client.id] = client
while true
msg = read(client)
msg = decodeMessage(msg)
if startswith(msg, "setusername:")
println("SETTING USERNAME: $msg")
usernames[client.id] = msg[13:end]
id = msg["id"]
if haskey(msg,"userName") && !haskey(connections,id)
uname = msg["userName"]
println("SETTING USERNAME: $(uname)")
connections[id] = User(uname,client)
end
if startswith(msg, "say:")
println("EMITTING MESSAGE: $msg")
if haskey(msg,"say")
content = msg["say"]
println("EMITTING MESSAGE: $(content)")
for (k,v) in connections
if k != client.id
write(v, (usernames[client.id] * ": " * msg[5:end]))
if k != id
write(v.client, (v.name * ": " * content))
end
end
end
end
end

onepage = readstring(Pkg.dir("WebSockets","examples","chat-client.html"))
httph = HttpHandler() do req::Request, res::Response
Response(onepage)
onepage = readstring(Pkg.dir("WebSockets","examples","chat-client.html"))
Response(onepage)
end

server = Server(httph, wsh)
Expand Down
6 changes: 0 additions & 6 deletions examples/server.jl
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
using HttpServer
using WebSockets

#global Dict to store open connections in
global connections = Dict{Int,WebSocket}()
global usernames = Dict{Int,String}()

function decodeMessage( msg )
String(copy(msg))
end
Expand All @@ -19,8 +15,6 @@ function eval_or_describe_error(strmsg)
end

wsh = WebSocketHandler() do req, client
global connections
connections[client.id] = client
while true
val = client |> read |> decodeMessage |> eval_or_describe_error
output = String(take!(Base.mystreamvar))
Expand Down
76 changes: 76 additions & 0 deletions src/HTTP.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
info("Loading HTTP methods...")

function open(f::Function, url; binary=false, verbose=false, kw...)

key = base64encode(rand(UInt8, 16))

headers = [
"Upgrade" => "websocket",
"Connection" => "Upgrade",
"Sec-WebSocket-Key" => key,
"Sec-WebSocket-Version" => "13"
]

HTTP.open("GET", url, headers;
reuse_limit=0, verbose=verbose ? 2 : 0, kw...) do http

HTTP.startread(http)

status = http.message.status
if status != 101
return
end

check_upgrade(http)

if HTTP.header(http, "Sec-WebSocket-Accept") != generate_websocket_key(key)
throw(WebSocketError(0, "Invalid Sec-WebSocket-Accept\n" *
"$(http.message)"))
end

io = HTTP.ConnectionPool.getrawstream(http)
f(WebSocket(io,false))
end
end

function upgrade(f::Function, http::HTTP.Stream; binary=false)

check_upgrade(http)
if !HTTP.hasheader(http, "Sec-WebSocket-Version", "13")
throw(WebSocketError(0, "Expected \"Sec-WebSocket-Version: 13\"!\n$(http.message)"))
end

HTTP.setstatus(http, 101)
HTTP.setheader(http, "Upgrade" => "websocket")
HTTP.setheader(http, "Connection" => "Upgrade")
key = HTTP.header(http, "Sec-WebSocket-Key")
HTTP.setheader(http, "Sec-WebSocket-Accept" => generate_websocket_key(key))

HTTP.startwrite(http)

io = HTTP.ConnectionPool.getrawstream(http)
f(WebSocket(io, true))
end

function check_upgrade(http)
if !HTTP.hasheader(http, "Upgrade", "websocket")
throw(WebSocketError(0, "Expected \"Upgrade: websocket\"!\n$(http.message)"))
end

if !HTTP.hasheader(http, "Connection", "upgrade")
throw(WebSocketError(0, "Expected \"Connection: upgrade\"!\n$(http.message)"))
end
end

function is_upgrade(r::HTTP.Message)
(r isa HTTP.Request && r.method == "GET" || r.status == 101) &&
HTTP.hasheader(r, "Connection", "upgrade") &&
HTTP.hasheader(r, "Upgrade", "websocket")
end

# function listen(f::Function, host::String="localhost", port::UInt16=UInt16(8081); binary=false, verbose=false)
# HTTP.listen(host, port; verbose=verbose) do http
# upgrade(f, http; binary=binary)
# end
# end

74 changes: 74 additions & 0 deletions src/HttpServer.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
info("Loading HttpServer methods...")

export WebSocketHandler


"""
Responds to a WebSocket handshake request.
Checks for required headers and subprotocols; sends Response(400) if they're missing or bad. Otherwise, transforms client key into accept value, and sends Reponse(101).
Function returns true for accepted handshakes.
"""
function websocket_handshake(request,client)
if !haskey(request.headers, "Sec-WebSocket-Key")
Base.write(client.sock, HttpServer.Response(400))
return false
end
if get(request.headers, "Sec-WebSocket-Version", "13") != "13"
response = HttpServer.Response(400)
response.headers["Sec-WebSocket-Version"] = "13"
Base.write(client.sock, response)
return false
end

key = request.headers["Sec-WebSocket-Key"]
if length(base64decode(key)) != 16 # Key must be 16 bytes
Base.write(client.sock, HttpServer.Response(400))
return false
end
resp_key = generate_websocket_key(key)

response = HttpServer.Response(101)
response.headers["Upgrade"] = "websocket"
response.headers["Connection"] = "Upgrade"
response.headers["Sec-WebSocket-Accept"] = resp_key

if haskey(request.headers, "Sec-WebSocket-Protocol")
if hasprotocol(request.headers["Sec-WebSocket-Protocol"])
response.headers["Sec-WebSocket-Protocol"] = request.headers["Sec-WebSocket-Protocol"]
else
Base.write(client.sock, HttpServer.Response(400))
return false
end
end

Base.write(client.sock, response)
return true
end

""" Implement the WebSocketInterface, for compatilibility with HttpServer."""
struct WebSocketHandler <: HttpServer.WebSocketInterface
handle::Function
end

"""
Performs handshake. If successfull, establishes WebSocket type and calls
handler with the WebSocket and the original request. On exit from handler, closes websocket. No return value.
"""
function HttpServer.handle(handler::WebSocketHandler, req::HttpServer.Request, client::HttpServer.Client)
websocket_handshake(req, client) || return
sock = WebSocket(client.sock,true)
handler.handle(req, sock)
if isopen(sock)
try
close(sock)
end
end
end

function HttpServer.is_websocket_handshake(handler::WebSocketHandler, req::HttpServer.Request)
is_get = req.method == "GET"
# "upgrade" for Chrome and "keep-alive, upgrade" for Firefox.
is_upgrade = contains(lowercase(get(req.headers, "Connection", "")),"upgrade")
is_websockets = lowercase(get(req.headers, "Upgrade", "")) == "websocket"
return is_get && is_upgrade && is_websockets
end
Loading

0 comments on commit 1dd534b

Please sign in to comment.