Skip to content
forked from BOINC/boinc

Commit

Permalink
client: fix bug that broke HTTP GUI RPC for authenticated ops
Browse files Browse the repository at this point in the history
electron sample GUI: add support for starting the client,
and for attaching a project
  • Loading branch information
davidpanderson committed Nov 20, 2019
1 parent 1858d1e commit db09770
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 57 deletions.
13 changes: 10 additions & 3 deletions client/gui_rpc_server_ops.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1637,8 +1637,7 @@ static int handle_rpc_aux(GUI_RPC_CONN& grc) {
return 0;
}

// see if we got a complete HTTP POST request,
// and if so remove HTTP header from buffer
// see if we got a complete HTTP POST request
//
static bool is_http_post_request(char* buf) {
if (strstr(buf, "POST") != buf) return false;
Expand All @@ -1650,10 +1649,17 @@ static bool is_http_post_request(char* buf) {
if (!p) return false;
p += 4;
if ((int)strlen(p) < n) return false;
strcpy_overlap(buf, p);
return true;
}

// remove HTTP header from request
//
static void strip_http_header(char* buf) {
char* p = strstr(buf, "\r\n\r\n");
p += 4;
strcpy_overlap(buf, p);
}

static bool is_http_get_request(char* buf) {
return (strstr(buf, "GET") == buf);
}
Expand Down Expand Up @@ -1782,6 +1788,7 @@ int GUI_RPC_CONN::handle_rpc() {
got_auth1 = got_auth2 = true;
auth_needed = false;
}
strip_http_header(request_msg);
} else {
p = strchr(request_msg, 3);
if (p) {
Expand Down
5 changes: 3 additions & 2 deletions samples/electron_gui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
<!-- <canvas class="chart"></canvas> -->
<p>
<input type="button" value = "Suspend" onclick="suspend()">
<input type="button" value = "Attach" onclick="attach()">
<p>

Host: <span id="domain_name"></span>
Status: <span id="status"></span>
<p>
<h3>Projects:</h3>
<span id="projects"></span>
Expand All @@ -40,4 +41,4 @@ <h3>Tasks:</h3>

</body>

</html>
</html>
213 changes: 161 additions & 52 deletions samples/electron_gui/window.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
// This is a proof of concept for an electron-based BOINC GUI,
// showing how to do GUI RPCs to the client.
// showing how to start the client and do GUI RPCs to it.
//
// I developed this by starting with one of electron's "simple samples" (activity-monitor)
// and replacing the files index.html, styles.css, and window.js
//
// Run this on a host with a running BOINC client,
// in a directory with the GUI password file (gui_rpc_auth.cfg).
// Run this in a directory with the GUI password file (gui_rpc_auth.cfg).
//
// Logic for connecting to and starting the client:
// try an RPC. If it fails, start a client.
// If the client exits, wait N seconds before restarting.
//
// Some next steps:
// - do periodic RPCs to update state information
// - add menus and dialogs
// - Turn the Suspend button into a never/prefs/always control

const os = require('os')
const fs = require('fs')
const crypto = require('crypto')
const { execFile } = require('child_process');

var auth_id = 0;
var auth_seqno = 1;
Expand All @@ -30,8 +33,10 @@ function read_password() {
try {
p = fs.readFileSync("gui_rpc_auth.cfg")
passwd = String(p).trim();
return true
} catch (err) {
console.log("can't read RPC password file");
return false
}
}

Expand All @@ -44,103 +49,207 @@ function gui_rpc(request) {
http.onreadystatechange = function() {
if (http.readyState != 4) return;
if (http.status == 200) {
console.log('got response: ' + http.responseText);
resolve(http.responseText);
//console.log('got response: ' + http.responseText);
resolve(http.responseText)
} else {
reject(http);
reject(http)
}
};
http.open("POST", "http://localhost:31416", true);
http.open("POST", "http://localhost:31416", true)
if (auth_id) {
http.setRequestHeader("Auth-ID", auth_id);
console.log("request "+request+ " auth ID " + auth_id + " seqno " + auth_seqno);
http.setRequestHeader("Auth-Seqno", auth_seqno);
var seqno = String(auth_seqno);
var salt = String(auth_salt);
var x = crypto.createHash('md5').update(seqno+passwd+salt+request).digest("hex");
http.setRequestHeader("Auth-Hash", x);
auth_seqno++;
http.setRequestHeader("Auth-ID", auth_id)
console.log("request "+request+ " auth ID " + auth_id + " seqno " + auth_seqno)
http.setRequestHeader("Auth-Seqno", auth_seqno)
var seqno = String(auth_seqno)
var salt = String(auth_salt)
var x = crypto.createHash('md5').update(seqno+passwd+salt+request).digest("hex")
http.setRequestHeader("Auth-Hash", x)
auth_seqno++
}
http.send(request);
http.send(request)
});
}

// Do a set_run_mode() RPC
//
function set_run_mode(mode) {
return gui_rpc("<set_run_mode><"+mode+"/></set_run_mode>").then(function(reply) {
console.log("set run mode");
return gui_rpc("<set_run_mode><"+mode+"/></set_run_mode>").then((reply)=>{
console.log("set run mode: "+mode)
});
}

// get authentication ID
//
function authorize() {
read_password();
if (!passwd) {
return new Promise(function(resolve, reject) {
resolve();
if (!read_password()) {
return new Promise((resolve, reject)=>{
reject();
});
}
return gui_rpc("<get_auth_id/>").then(function(reply) {
return gui_rpc("<get_auth_id/>").then((reply)=>{
// TODO: error checking
x = new DOMParser().parseFromString(reply, "text/xml");
auth_id = x.getElementsByTagName("auth_id")[0].childNodes[0].nodeValue;
auth_salt = x.getElementsByTagName("auth_salt")[0].childNodes[0].nodeValue;
console.log("auth_id: "+auth_id);
console.log("auth_salt: "+auth_salt);
x = new DOMParser().parseFromString(reply, "text/xml")
auth_id = x.getElementsByTagName("auth_id")[0].childNodes[0].nodeValue
auth_salt = x.getElementsByTagName("auth_salt")[0].childNodes[0].nodeValue
//console.log("auth_id: "+auth_id)
//console.log("auth_salt: "+auth_salt)
});
}

// do a get_state() RPC
// do a get_state() RPC; return promise
//
function get_state() {
return gui_rpc("<get_state/>").then(function(reply) {
parser = new DOMParser();
state = parser.parseFromString(reply, "text/xml");
return gui_rpc("<get_state/>").then((reply)=>{
parser = new DOMParser()
state = parser.parseFromString(reply, "text/xml")
})
.catch((err)=>{
show_status('disconnected')
console.log(err)
});
}

function suspend() {
set_run_mode("never").then(function() {
set_run_mode("never").then(()=>{
});
}

function parse_reply(reply) {
x = new DOMParser().parseFromString(reply, "text/xml")
if (x.getElementsByTagName('success')) {
return 0
}
return x.getElementsByTagName("status")[0].childNodes[0].nodeValue
}

function attach_poll() {
console.log('doing poll')
req = '<project_attach_poll/>'
gui_rpc(req).then((reply)=>{
console.log(reply)
r = parse_reply(reply)
switch(r) {
case -199:
setTimeout(attach_poll, 1000)
break
case 0:
console.log('attached')
break
default:
console.log('attach failed: ', reply)
break
}
})
}

function attach() {
console.log('attach')
url = 'https://boinc.berkeley.edu/test/'
auth = 'xxx'
req = '<project_attach>'
req += '<project_url>'+url+'</project_url>'
req += '<authenticator>'+auth+'</authenticator>'
req += '<project_name>BOINC test project</project_name>'
req += '</project_attach>'
gui_rpc(req).then((reply)=>{
console.log('did attach RPC')
attach_poll()
})
}

function client_exited(error, stdout, stderr) {
console.log('client exited');
if (error) {
throw error
}
console.log(stdout)
console.log(stderr)
}

function show_status(s) {
document.querySelector('#status').innerHTML = s
}

// Start the BOINC client.
// On Win, the client will find its data directory in the registry.
// May need to set current dir on other platforms.
// If there's already a client running,
// the new one will detect this and exit.
//
function start_client() {
const child = execFile(
'c:/Users/David/Documents/BOINC_git/boinc/win_build/Build/x64/Debug/boinc.exe',
['--redirectio', '--launched_by_manager'],
{},
client_exited
);
}

// display the result of get_state()
//
function show_state() {
// show host name
//
d = state.querySelector("domain_name").textContent;
console.log('domain name', d);
document.querySelector('#domain_name').innerHTML = d;
show_status('Connected to ' + state.querySelector("domain_name").textContent)

// show projects
//
x = '';
x = ''
state.querySelectorAll("project").forEach((p)=>{
x += '<br>'+ p.querySelector('project_name').textContent;
x += '<br>'+ p.querySelector('project_name').textContent
});
document.querySelector('#projects').innerHTML = x;
document.querySelector('#projects').innerHTML = x

// show tasks
x = '';
x = ''
state.querySelectorAll("result").forEach((r)=>{
x += '<br>'+ r.querySelector('name').textContent;
//console.log(r)
name = r.querySelector('name').textContent
x += '<br>'+name
active_task = r.querySelector('active_task')
if (active_task) {
cpu_time = active_task.querySelector('current_cpu_time')
x += ' CPU time: '+cpu_time.textContent
}
});
document.querySelector('#tasks').innerHTML = x
}

function main_loop() {
get_state().then(()=>{
show_state()
setTimeout(main_loop, 1000)
})
.catch(()=>{
show_status('disconnected')
});
document.querySelector('#tasks').innerHTML = x;
}

var started_client = false

$(() => { // shorthand for document ready
// get authorization ID
//
console.log('starting');
function startup() {
authorize().then(function() {
// then get state and show it
//
get_state().then(function() {
show_state();
});
console.log('authorized')
main_loop()
})
.catch(()=>{
if (passwd == null) {
show_status('no password file')
return
}
if (started_client) {
console.log('waiting for client')
} else {
console.log('auth failed - starting client')
show_status('starting client')
start_client()
started_client = true
}
setTimeout(startup, 1000)
});
}

$(() => { // shorthand for document ready
console.log('starting');
startup();
})

0 comments on commit db09770

Please sign in to comment.