diff --git a/baas/cloud-list.html b/baas/cloud-list.html index 45fdeee33060393787105224ae22bb96826827e1..ac96d0cb9a250da7b79161e78afd560f15a73de0 100644 --- a/baas/cloud-list.html +++ b/baas/cloud-list.html @@ -127,13 +127,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. $('#cloud-error small').show(); $("#login").hide(); } else { - $.ajax({ - type : 'POST', - url : url + '/tokens', - }) - .done(function(data) { + var cert_path = $("#cert-path").html(); + try { + var astakos = getClient('astakos_notoken', url, null, cert_path); + } catch (err) { + console.log(err); + return; + } + + function handle_result(data) { $("#login").show(); - var endpoints = data.access.serviceCatalog + var endpoints = JSON.parse(data).access.serviceCatalog $.each(endpoints, function(i, endpoint) { switch(endpoint.type) { case 'object-store': try { @@ -148,15 +152,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. break; } }); - }) - .fail ( function(xhr, status, msg) { + } + + function handle_err(err) { pithos_public = null; - url_error = xhr.status + ' ' + msg; - console.log(xhr.status + ' ' + xhr.responseText); + url_error = err; + console.log(url_error); $('#cloud-error small').text(errors.cloud_inaccessible + ' [' + url_error + ']'); $('#cloud-error small').show(); $("#login").hide(); - }); + } + astakos.post('/tokens', null, null, 200, handle_result, handle_err); } } @@ -171,31 +177,33 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. } else { var url = $('#cloud-url').val().replace(/^\s+|\s+$/gm,''); if (pithos_public && url) { - $.ajax({ - type: 'POST', - url: url + '/tokens', - beforeSend : function(req) { - req.setRequestHeader('X-Auth-Token', token); - }, - headers: { - 'Content-Type': 'application/json' - }, - data: JSON.stringify({auth: {token: {id: token}}}) - }) - .done(function(data) { - auth_error = null; - $('#token-error small').hide(); - uuid = data.access.token.tenant.id; - }) - .fail(function(xhr, status, msg) { - auth_error = xhr.status + ' ' + msg; - console.log(xhr.status + ' ' + xhr.responseText); + var cert_path = $("#cert-path").html(); + try { + var astakos = + getClient('astakos', url, token, cert_path); + } catch (err) { + handle_err(err); + return; + } + + function handle_err(err) { + auth_error = err; + console.log(auth_error); $('#token-error small').text( errors.token_error + ' [' + auth_error + ']'); $('#token-error small').show(); - - }); - + } + function handle_result(data) { + auth_error = null; + $('#token-error small').hide(); + uuid = JSON.parse(data).access.token.tenant.id; + } + var send_data = JSON.stringify({ auth: { token: { id: token } } }); + astakos.post( + '/tokens', null, send_data, 200, + handle_result, + handle_err + ); } else { $('#token-error small').text(errors.token_cloudless); $('#token-error small').show(); @@ -215,25 +223,41 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. $('#cloud-name-error small').hide(); $('#cloud-error small').hide(); $('#token-error small').hide(); + $('#cert-path-error small').hide(); if(cloud) { $("#cloud-name").val(cloud.name); $("#cloud-url").val(cloud.auth_url); $("#token").val(cloud.token); + if(cloud.cert) { + $("#cert-path-div").show(); + $("#cert-path").html(cloud.cert); + $("#use-cert").prop("checked", true); + } else { + $("#cert-path-div").hide(); + $("#cert-path").html(DEFAULT_CERT); + $("#use-cert").prop("checked", false); + } } else { $("#cloud-name").val(''); $("#cloud-url").val(''); $("#token").val(''); $("#login").hide(); + $("#cert-path-div").hide(); + $("#use-cert").prop("checked", false); + $("#cert-path").html(DEFAULT_CERT); + activate_li("dummy"); } $("#cloud_details").show(); + toggle_default_button(); } function edit_add_cloud() { var cloud_name = $("#cloud-name").val().replace(/^\s+|\s+$/gm,''); var url = $("#cloud-url").val().replace(/^\s+|\s+$/gm,''); var token = $("#token").val().replace(/^\s+|\s+$/gm,''); + var cert = $("#cert-path").html().replace(/^\s+|\s+$/gm,''); var cloud = {}; cloud.name = cloud_name; @@ -241,7 +265,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. cloud.token = token; cloud.pithos_public = pithos_public; cloud.uuid = uuid; - + if($("#use-cert").is(":checked")) { + cloud.cert = cert; + } else { + cloud.cert = ""; + } clouds[cloud_name] = cloud; render_clouds(""); activate_li("li_" + cloud.name); @@ -309,13 +337,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. }); } + function toggle_default_button() { + if($("#cert-path").html() == DEFAULT_CERT) { + $("#default-button").addClass("disabled"); + $("#default-button").prop("disabled", true); + } else { + $("#default-button").removeClass("disabled"); + $("#default-button").prop("disabled", false); + } + } + window.setInterval(function() { if($("#clouds_page").attr("id") && $("#cloud-url").val()) { check_cloud_url(); check_token(); } - }, 500); + }, 1000); </script> <div class="row" id="clouds_page"> @@ -388,6 +426,44 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. and click on <u>API access</u>. Copy the Authentication URL and Token above. </div> </div> + <div class="clearfix"> + <div class="small-3 columns"> + <label id="cert-show-label" for="cert-path-div" class="right inline"> + Show certificate settings + </label> + </div> + <div class="small-9 columns"> + <input type="checkbox" id="use-cert" + onclick="$('#cert-path-div').toggle(this.checked); + toggle_default_button()"/> + </div> + </div> + <div class="clearfix hide" id="cert-path-div"> + <div class="small-3 columns"> + <label id="cert-path-label" for="cert-path" class="right inline"> + Certificate + </label> + </div> + <div id="cert-path" class="small-5 columns dir-field"> </div> + <div id="dirdialogue_label" onclick="$('#choose-cert-path').trigger('click');" + class="small-1 columns" id="dirpick"> + <i class="fa fa-folder-o blue-folder"> </i> + </div> + <input type="file" id="choose-cert-path" nwfile + style="display:none;" + onchange="$('#cert-path').html($(this).val()); + toggle_default_button()"/> + <div class="small-3 columns"> + <a href="#" onclick="$('#cert-path').html(DEFAULT_CERT); + toggle_default_button()" + class="button tiny radius" id="default-button"> + Use default + </a> + </div> + </div> + <div class="clearfix"> + <div class="small-12 columns"> </div> + </div> <div class="clearfix"> <div class="small-8 columns"></div> <a id="save_button" class="button radius right small" diff --git a/baas/config.js b/baas/config.js index fe7503b50d5aa249e32820bbb3a01b768b7a2958..486ceac08223385b5cad700e140484be37880c2e 100644 --- a/baas/config.js +++ b/baas/config.js @@ -36,6 +36,8 @@ if(process.platform == 'darwin') { process.env['PATH'] = process.env['PATH'] + ':/usr/local/bin'; } +var DEFAULT_CERT = path.join(exec_path, 'cacert.pem'); + var CYGWIN_BASH = path.join(exec_path, "cygwin", "bin", "bash.exe"); function get_user_home() { @@ -164,3 +166,20 @@ function escape_quote_str(str) { var escaped_quoted = str.replace(/'/g, "'\\''"); return "'" + escaped_quoted + "'"; } + +var clients = { }; +var kamaki = require('./static/js/kamaki.js'); + +/** + * Return a client + * Create or update a Client object if it's missing or is outdated + */ +function getClient(name, URL, token, CAPath) { + if (clients[name] && clients[name].equalsURL(URL)) + clients[name].setToken(token); + else if (clients[name]) + clients[name].setURL(URL); + else clients[name] = new kamaki.Client(URL, token, CAPath); + if (clients[name].getCA() !== CAPath) clients[name].setCA(CAPath); + return clients[name]; +} diff --git a/baas/static/js/kamaki.js b/baas/static/js/kamaki.js new file mode 100644 index 0000000000000000000000000000000000000000..4a82f8f4fdb6dab89eef45701d973420fe8565c5 --- /dev/null +++ b/baas/static/js/kamaki.js @@ -0,0 +1,154 @@ +// Copyright (C) 2015 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +var fs = require('fs'); +var util = require('util'); +var url = require('url'); + +/** @return http: --> http, https: --> https, null otherwise */ +function getProtocol(prefix) { + switch (prefix) { + case "http:": return require('http'); + case "https:": return require('https'); + } + + return null; +} +/** + * Read the path (utf8), chop it to individual certificates + * @return {[String, ...]} An array with all the certificates + */ +function splitCA(CAPath) { + chain = fs.readFileSync(CAPath, 'utf8').split('\n') + ca = []; + for (i=0, cert=[]; i < chain.length; i++) { + cert.push(chain[i]); + line = chain[i]; + if (line.match(/-END CERTIFICATE-/)) { + ca.push(cert.join('\n')); + cert = []; + } + + } + + return ca; +} + +/** + * An HTTP(S) client to make requests to the cloud. An instance of this class + * should be used to make requests to a specific service e.g., Identity, + * Storage, Compute, etc. + * + * It provides RESTfull methods: get, head, post, put, delete, update + * + * @param {String} endpointURL - trailing / is removed automatically + * @param {String} token - modify it as a property e.g. client.token = "tkn" + * @param {String } CAPath - the path of the CA certificates bundle file + */ +var Client = function(endpointURL, token, CAPath) { + var _options = {host: null, headers: { }, }; + var _url, _parser, _protocol, _token; + + this.setURL = function(newURL) { + _url = newURL; + _parser = url.parse(newURL); + _options.host = _parser.host; + _protocol = getProtocol(_parser.protocol); + if (_protocol) _endpoint = _parser.pathname.replace(/\/$/, ""); + else throw "Unknown URL protocol"; + }; + this.getURL = function() { return _url; }; + this.equalsURL = function(URL) { return URL === _url; }; + + this.setToken = function(newToken) { + _token = newToken; + if (_token) util._extend(_options.headers, { 'X-Auth-Token': _token, }); + else delete _options.headers['X-Auth-Token']; + }; + this.getToken = function() { return _token; } + + this.setCA = function(newCAPath) { + this.CAPath = newCAPath; + if (newCAPath) _options.ca = splitCA(newCAPath); + else delete _options.ca; + }; + this.getCA = function() { return _options.ca; }; + + this.setURL(endpointURL); + this.setToken(token); + this.setCA(CAPath); + + this.post = function(path, headers, send_data, status, handle_res, handle_err) { + handle_res = handle_res || function(r){ console.log(r); }; + handle_err = handle_err || function(e){ console.log(e); }; + + var post_opts = util._extend({ + method: 'POST', + path: _endpoint + path, + }, _options); + post_opts.headers = util._extend({}, _options.headers); + + util._extend(post_opts.headers, headers); + if (send_data) { + h = post_opts.headers; + util._extend(post_opts.headers, { + 'Content-Type': h['Content-Type'] || 'application/json', + 'Content-Length': h['Content-Length'] || Buffer.byteLength(send_data), + }); + }; + + _req = _protocol.request(post_opts, function(res){ + _recv_data = ''; + res.on('data', function(d) { _recv_data+=d; }); + + res.on('end', function() { + if ((status || 200) !== res.statusCode) { + handle_err(res.statusCode + " " + res.statusMessage, res.headers); + } else handle_res(_recv_data, res.headers); + + }); + }); + + if (send_data) _req.write(send_data); + _req.end(); + _req.on('error', handle_err); + }; + +}; + +module.exports = { Client: Client }; + +/** + * Test if client is working correctly. + */ +function testClient(token) { + var astakos = new Client( + "https://accounts.okeanos.grnet.gr/identity/v2.0", + token, + "/etc/ssl/certs/ca-certificates.crt" + ); + + data2send = { auth: { token: { id: token } } }; + + function print_user_name(data, headers) { + var jdata = JSON.parse(data); + console.log(jdata.access.user.name); + console.log(headers); + } + + // astakos.post('/tokens', null, null, 200, function(m){console.log("Result: " + m)}); + astakos.post('/tokens', null, JSON.stringify(data2send), 200, print_user_name); +} + +// testClient("t0k3n"); diff --git a/baas/static/stylesheets/custom.css b/baas/static/stylesheets/custom.css index f5ca8fd54eb68314d8e201b0a8069e73d38f3da1..a1b3af9181454a2a5f390857c04efa4f9a276d17 100644 --- a/baas/static/stylesheets/custom.css +++ b/baas/static/stylesheets/custom.css @@ -90,6 +90,7 @@ body legend, .template-list-title { } .dir-field { overflow-x: auto; + word-break: break-word; } .template-list li a i { visibility: hidden; diff --git a/copy_cacert.py b/copy_cacert.py new file mode 100644 index 0000000000000000000000000000000000000000..166255c51b2c5808215b7510370a01c22924d3b4 --- /dev/null +++ b/copy_cacert.py @@ -0,0 +1,13 @@ +import os +import shutil + +def main(): + os.chdir(os.path.dirname(os.path.realpath(__file__))) + os.system("pip install certifi") + + print "Copying certifi's cacert.pem" + import certifi + shutil.copy2(certifi.where(), './cacert.pem') + +if __name__ == '__main__': + main() diff --git a/make_package.sh b/make_package.sh index 5febf50809916e96fc74f50065d19e1d3143fdbd..46b126ed5e8e6c3a3625104899ca2910b40df756 100755 --- a/make_package.sh +++ b/make_package.sh @@ -38,6 +38,7 @@ NW_VERSION=0.12.3 cd "$(dirname "$0")" ROOTPATH=$(pwd) +python copy_cacert.py cd baas npm install cd .. @@ -59,6 +60,7 @@ then RESOURCES=$DIST/baas.app/Contents/Resources else RESOURCES=$DIST fi cp src/timeview.py $RESOURCES +cp cacert.pem $RESOURCES echo Copying duplicity cp -r build/duplicity/* $RESOURCES