This commit is contained in:
Matéo Duparc 2021-05-25 21:59:16 +02:00
parent da4cc4995f
commit 8cc6f6b50f
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
16 changed files with 786 additions and 328 deletions

219
Cargo.lock generated
View File

@ -285,6 +285,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "aead"
version = "0.4.1"
@ -372,8 +378,10 @@ dependencies = [
"html-escape",
"html-minifier",
"if-addrs",
"image",
"lazy_static",
"libmdns",
"linked-hash-map",
"multicast_dns",
"platform-dirs",
"rand 0.7.3",
@ -395,9 +403,9 @@ dependencies = [
[[package]]
name = "async-psec"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58ab60657d7b3949c3bdb06570fc4855a4e83dbe6c9da73902c67355dd8f37a8"
checksum = "31dcd9dc064b59d84e03e9c42c97ca8bba139ff04a874afc0e93944d7f62e235"
dependencies = [
"aes-gcm",
"async-trait",
@ -510,6 +518,12 @@ version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe"
[[package]]
name = "bytemuck"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bed57e2090563b83ba8f83366628ce535a7584c9afa4c9fc0612a03925c6df58"
[[package]]
name = "byteorder"
version = "1.4.3"
@ -564,6 +578,12 @@ dependencies = [
"generic-array",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "const_fn"
version = "0.4.7"
@ -633,6 +653,51 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52fb27eab85b17fbb9f6fd667089e07d6a2eb8743d02639ee7f6a7a7729c9c94"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-utils",
"lazy_static",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
"lazy_static",
]
[[package]]
name = "crypto-mac"
version = "0.11.0"
@ -665,6 +730,16 @@ dependencies = [
"zeroize",
]
[[package]]
name = "deflate"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
dependencies = [
"adler32",
"byteorder",
]
[[package]]
name = "derive_more"
version = "0.99.14"
@ -809,7 +884,7 @@ dependencies = [
"cfg-if 1.0.0",
"crc32fast",
"libc",
"miniz_oxide",
"miniz_oxide 0.4.4",
]
[[package]]
@ -1004,6 +1079,16 @@ dependencies = [
"polyval",
]
[[package]]
name = "gif"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "h2"
version = "0.2.7"
@ -1173,6 +1258,25 @@ dependencies = [
"libc",
]
[[package]]
name = "image"
version = "0.23.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"gif",
"jpeg-decoder",
"num-iter",
"num-rational",
"num-traits",
"png",
"scoped_threadpool",
"tiff",
]
[[package]]
name = "indexmap"
version = "1.6.2"
@ -1228,6 +1332,15 @@ version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "jpeg-decoder"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
dependencies = [
"rayon",
]
[[package]]
name = "js-sys"
version = "0.3.51"
@ -1351,6 +1464,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]]
name = "memoffset"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d"
dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.16"
@ -1366,6 +1488,15 @@ dependencies = [
"macro-utils",
]
[[package]]
name = "miniz_oxide"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
dependencies = [
"adler32",
]
[[package]]
name = "miniz_oxide"
version = "0.4.4"
@ -1518,6 +1649,28 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.14"
@ -1706,6 +1859,18 @@ dependencies = [
"dirs-next",
]
[[package]]
name = "png"
version = "0.16.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
dependencies = [
"bitflags",
"crc32fast",
"deflate",
"miniz_oxide 0.3.7",
]
[[package]]
name = "polyval"
version = "0.5.0"
@ -1840,6 +2005,31 @@ dependencies = [
"rand_core 0.6.2",
]
[[package]]
name = "rayon"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90"
dependencies = [
"autocfg",
"crossbeam-deque",
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"lazy_static",
"num_cpus",
]
[[package]]
name = "redox_syscall"
version = "0.2.8"
@ -1954,6 +2144,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "scoped_threadpool"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -2265,6 +2461,17 @@ dependencies = [
"num_cpus",
]
[[package]]
name = "tiff"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
dependencies = [
"jpeg-decoder",
"miniz_oxide 0.4.4",
"weezl",
]
[[package]]
name = "time"
version = "0.2.26"
@ -2664,6 +2871,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "weezl"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e"
[[package]]
name = "widestring"
version = "0.4.3"

View File

@ -36,8 +36,10 @@ if-addrs = "0.6"
base64 = "0.13"
scrypt = "0.7"
zeroize = "1.2"
image = "0.23"
yaml-rust = "0.4" #only in debug mode
[build-dependencies]
html-minifier = "3.0"
yaml-rust = "0.4"
linked-hash-map = "0.5"

View File

@ -1,5 +1,9 @@
#[cfg(not(debug_assertions))]
use std::{env, fs::{File, read_to_string, create_dir}, path::Path, io::{Write, ErrorKind}};
use {
std::{env, fs::{File, read_to_string, create_dir}, path::Path, io::{Write, ErrorKind}},
yaml_rust::{YamlLoader, Yaml},
linked_hash_map::LinkedHashMap,
};
#[cfg(not(debug_assertions))]
fn minify_content(content: &str, language: &str) -> Option<String> {
@ -12,9 +16,14 @@ fn minify_content(content: &str, language: &str) -> Option<String> {
}
#[cfg(not(debug_assertions))]
fn generate_web_files() {
use yaml_rust::YamlLoader;
fn replace_fields(content: &mut String, fields: &LinkedHashMap<Yaml, Yaml>) {
fields.into_iter().for_each(|field| {
*content = content.replace(field.0.as_str().unwrap(), field.1.as_str().unwrap());
});
}
#[cfg(not(debug_assertions))]
fn generate_web_files() {
let out_dir = env::var("OUT_DIR").unwrap();
let out_dir = Path::new(&out_dir);
let src_dir = Path::new("src/frontend");
@ -26,7 +35,7 @@ fn generate_web_files() {
}
let config = &YamlLoader::load_from_str(&read_to_string("config.yml").unwrap()).unwrap()[0];
let css_values = config["css"].as_hash().unwrap();
let fields = config.as_hash().unwrap();
[
"login.html",
@ -40,9 +49,7 @@ fn generate_web_files() {
let extension = path.extension().unwrap().to_str().unwrap();
let mut content = read_to_string(src_dir.join(path)).unwrap();
if extension == "css" {
css_values.into_iter().for_each(|entry| {
content = content.replace(entry.0.as_str().unwrap(), entry.1.as_str().unwrap());
});
replace_fields(&mut content, fields);
}
if file_name == &"index.html" {
content = content.replace("AIRA_VERSION", env!("CARGO_PKG_VERSION"));
@ -51,6 +58,10 @@ fn generate_web_files() {
let mut dst = File::create(out_dir.join(path)).unwrap();
dst.write(minified_content.as_bytes()).unwrap();
});
let mut text_avatar = read_to_string("src/frontend/imgs/text_avatar.svg").unwrap();
replace_fields(&mut text_avatar, fields);
File::create(out_dir.join("text_avatar.svg")).unwrap().write(text_avatar.as_bytes()).unwrap();
}
fn main() {

View File

@ -1,2 +1 @@
css:
ACCENT_COLOR: "19a52c"
ACCENT_COLOR: "19a52c"

View File

@ -1,13 +1,17 @@
function generateAvatar(name) {
let span = document.createElement("span");
if (typeof name == "undefined") {
span.appendChild(document.createTextNode("?"));
} else if (name.length > 0) {
span.appendChild(document.createTextNode(name[0].toUpperCase()));
}
let div = document.createElement("div");
div.classList.add("avatar");
div.appendChild(span);
div.appendChild(document.createElement("div")); //used for background
return div;
function generateImgAvatar() {
let img = document.createElement("img");
img.classList.add("avatar");
return img;
}
function generateSelfAvatar(timestamp) {
let img = generateImgAvatar();
img.src = "/avatar/self?"+timestamp;
return img;
}
function generateAvatar(sessionId, name, timestamp) {
let img = generateImgAvatar();
img.src = "/avatar/"+sessionId+"/"+name+"?"+timestamp;
return img;
}

View File

@ -26,24 +26,8 @@ input[type="text"], input[type="password"] {
}
.avatar {
position: relative;
margin-right: .5em;
width: 1.5em;
height: 1.5em;
}
.avatar div {
width: 100%;
height: 100%;
background-color: var(--accent);
border-radius: 100%;
}
.avatar span {
position: absolute;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
margin: 0;
border-radius: 50%;
}

View File

@ -0,0 +1,4 @@
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50" fill="#ACCENT_COLOR"/>
<text x="50" y="50" text-anchor="middle" dominant-baseline="middle" fill="white" font-weight="bold" font-size="60" dy=".1em" font-family="Arial,Helvetica,Sans-Serif">LETTER</text>
</svg>

After

Width:  |  Height:  |  Size: 305 B

View File

@ -67,7 +67,7 @@ input[type="file"] {
left: 0;
right: 0;
margin: auto;
width: 40%;
width: 40vw;
max-height: 90vh;
overflow: auto;
box-sizing: border-box;
@ -102,29 +102,6 @@ input[type="file"] {
content: url("/static/imgs/icons/cancel");
background-color: unset;
}
.popup .avatar {
font-size: 4em;
margin: auto;
}
.popup section {
display: block;
margin-bottom: 20px;
border-top: 1px solid black;
}
.popup section:first-of-type {
border-top: unset;
}
.popup input {
display: block;
margin: 10px;
}
.popup span {
font-weight: bold;
}
.popup>div>div p {
font-weight: normal;
font-size: 0.9em;
}
.popup h2.warning::before {
content: url("/static/imgs/icons/warning/ACCENT_COLOR");
width: 9%;
@ -187,6 +164,57 @@ label {
.switch input:checked + span:before {
transform: translateX(26px);
}
#avatarContainer {
display: flex;
justify-content: center;
}
#avatarContainer .avatar {
font-size: 4em;
margin-right: unset;
}
#avatarContainer label:hover .avatar {
opacity: .4;
}
#avatarContainer>label {
position: relative;
}
#avatarContainer .avatar + p {
display: none;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
right: 0;
margin: auto;
text-align: center;
}
#avatarContainer label:hover p {
display: block;
}
#profile_info section {
display: block;
margin-bottom: 20px;
border-top: 1px solid black;
}
#profile_info section:first-of-type {
border-top: unset;
}
#profile_info input {
margin: 10px;
}
#profile_info span {
font-weight: bold;
}
#profile_info>div>div p {
font-weight: normal;
font-size: 0.9em;
}
#session_info .avatar {
width: 6em;
height: 6em;
display: block;
margin: auto;
}
#session_info .name {
display: flex;
justify-content: center;

View File

@ -8,6 +8,9 @@ let currentSessionId = -1;
let sessionsData = new Map();
let msgHistory = new Map();
let pendingFilesTransfers = new Map();
let avatarTimestamps = new Map([
["self", Date.now()]
]);
function onClickSession(event) {
let sessionId = event.currentTarget.getAttribute("data-sessionId");
@ -75,7 +78,7 @@ document.getElementById("delete_conversation").onclick = function() {
showPopup(mainDiv);
};
document.getElementById("add_contact").onclick = function() {
socket.send("contact "+currentSessionId+" "+sessionsData.get(currentSessionId).name);
socket.send("contact "+currentSessionId);
sessionsData.get(currentSessionId).isContact = true;
displayHeader();
displaySessions();
@ -234,8 +237,47 @@ msg_log.onscroll = function() {
let profile_div = document.querySelector("#me>div");
profile_div.onclick = function() {
let mainDiv = document.createElement("div");
let avatar = generateAvatar(identityName);
mainDiv.appendChild(avatar);
mainDiv.id = "profile_info";
let avatarContainer = document.createElement("div");
avatarContainer.id = "avatarContainer";
let labelAvatar = document.createElement("label");
labelAvatar.setAttribute("for", "avatar_input");
let inputAvatar = document.createElement("input");
inputAvatar.type = "file";
inputAvatar.id = "avatar_input";
inputAvatar.onchange = function(event) {
let file = event.target.files[0];
if (file.size < 10000000) {
let formData = new FormData();
formData.append("avatar", file);
fetch("/set_avatar", {method: "POST", body: formData}).then(response => {
if (response.ok) {
avatarTimestamps.set("self", Date.now());
document.querySelector("#avatarContainer .avatar").src = "/avatar/self?"+avatarTimestamps.get("self");
displayProfile();
if (currentSessionId != -1) {
displayHistory();
}
} else {
console.log(response);
}
});
} else {
let mainDiv = document.createElement("div");
mainDiv.appendChild(generatePopupWarningTitle());
let p = document.createElement("p");
p.textContent = "Avatar cannot be larger than 10MB.";
mainDiv.appendChild(p);
showPopup(mainDiv);
}
};
labelAvatar.appendChild(inputAvatar);
labelAvatar.appendChild(generateSelfAvatar(avatarTimestamps.get("self")));
let uploadP = document.createElement("p");
uploadP.textContent = "Upload";
labelAvatar.appendChild(uploadP);
avatarContainer.appendChild(labelAvatar);
mainDiv.appendChild(avatarContainer);
let fingerprint = document.createElement("pre");
fingerprint.id = "identity_fingerprint";
fingerprint.textContent = beautifyFingerprint(identityFingerprint);
@ -427,8 +469,8 @@ socket.onmessage = function(msg) {
console.log("Message: "+msg.data);
let args = msg.data.split(" ");
switch (args[0]) {
case "disconnected":
onDisconnected(args[1]);
case "inc_file_transfer":
onIncFilesTransfer(args[1], parseInt(args[2]));
break;
case "new_session":
onNewSession(args[1], args[2] === "true", args[3], args[4], msg.data.slice(args[0].length+args[1].length+args[2].length+args[3].length+args[4].length+5));
@ -451,15 +493,15 @@ socket.onmessage = function(msg) {
case "aborted":
onFilesTransferAborted(args[1]);
break;
case "inc_file_transfer":
onIncFilesTransfer(args[1], parseInt(args[2]));
break;
case "load_msgs":
onMsgsLoad(args[1], msg.data.slice(args[0].length+args[1].length+2));
break;
case "name_told":
onNameTold(args[1], msg.data.slice(args[0].length+args[1].length+2));
break;
case "avatar_set":
onAvatarSet(args[1]);
break;
case "is_contact":
onIsContact(args[1], args[2] === "true", args[3], msg.data.slice(args[0].length+args[1].length+args[2].length+args[3].length+4));
break;
@ -475,6 +517,9 @@ socket.onmessage = function(msg) {
case "password_changed":
onPasswordChanged(args[1] === "true", args[2] === "true");
break;
case "disconnected":
onDisconnected(args[1]);
break;
case "logout":
logout();
}
@ -509,6 +554,14 @@ function onNameTold(sessionId, name) {
}
displaySessions();
}
function onAvatarSet(sessionId) {
avatarTimestamps.set(sessionId, Date.now());
displaySessions();
if (sessionId === currentSessionId) {
displayHeader();
displayHistory(false);
}
}
function setNotSeen(strSessionIds) {
let sessionIds = strSessionIds.split(' ');
for (let i=0; i<sessionIds.length; ++i) {
@ -787,7 +840,7 @@ function showSessionInfoPopup() {
if (typeof session !== "undefined") {
let mainDiv = document.createElement("div");
mainDiv.id = "session_info";
mainDiv.appendChild(generateAvatar(session.name));
mainDiv.appendChild(generateAvatar(currentSessionId, session.name, avatarTimestamps.get(currentSessionId)));
let nameDiv = document.createElement("div");
nameDiv.classList.add("name");
let h2 = document.createElement("h2");
@ -796,7 +849,7 @@ function showSessionInfoPopup() {
if (session.isOnline) {
let button = document.createElement("button");
button.onclick = function() {
socket.send("ask_name "+currentSessionId);
socket.send("refresh_profile "+currentSessionId);
};
nameDiv.appendChild(button);
}
@ -839,6 +892,7 @@ function addSession(sessionId, name, outgoing, fingerprint, ip, isContact, isVer
"isOnline": isOnline,
});
msgHistory.set(sessionId, []);
avatarTimestamps.set(sessionId, Date.now());
displaySessions();
}
function displaySessions() {
@ -860,7 +914,7 @@ function logout() {
}
function displayProfile() {
profile_div.innerHTML = "";
profile_div.appendChild(generateAvatar(identityName));
profile_div.appendChild(generateSelfAvatar(avatarTimestamps.get("self")));
let p = document.createElement("p");
p.textContent = identityName;
profile_div.appendChild(p);
@ -872,7 +926,7 @@ function displayHeader() {
if (typeof session === "undefined") {
chatHeader.style.display = "none";
} else {
chatHeader.children[0].appendChild(generateAvatar(session.name));
chatHeader.children[0].appendChild(generateAvatar(currentSessionId, session.name, avatarTimestamps.get(currentSessionId)));
chatHeader.children[0].appendChild(generateName(session.name));
chatHeader.style.display = "flex";
if (session.isContact) {
@ -954,7 +1008,7 @@ function generateName(name) {
function generateSession(sessionId, session) {
let li = document.createElement("li");
li.setAttribute("data-sessionId", sessionId);
li.appendChild(generateAvatar(session.name));
li.appendChild(generateAvatar(sessionId, session.name, avatarTimestamps.get(sessionId)));
li.appendChild(generateName(session.name));
if (session.isContact) {
li.classList.add("is_contact");
@ -973,16 +1027,23 @@ function generateSession(sessionId, session) {
li.onclick = onClickSession;
return li;
}
function generateMsgHeader(name) {
function generateMsgHeader(name, sessionId) {
let p = document.createElement("p");
p.appendChild(document.createTextNode(name));
let div = document.createElement("div");
div.classList.add("header");
div.appendChild(generateAvatar(name));
let timestamp = avatarTimestamps.get(sessionId);
let avatar;
if (typeof sessionId === "undefined") {
avatar = generateSelfAvatar(timestamp);
} else {
avatar = generateAvatar(sessionId, name, timestamp);
}
div.appendChild(avatar);
div.appendChild(p);
return div;
}
function generateMessage(name, msg) {
function generateMessage(name, sessionId, msg) {
let p = document.createElement("p");
p.appendChild(document.createTextNode(msg));
let div = document.createElement("div");
@ -990,12 +1051,12 @@ function generateMessage(name, msg) {
div.appendChild(linkifyElement(p));
let li = document.createElement("li");
if (typeof name !== "undefined") {
li.appendChild(generateMsgHeader(name));
li.appendChild(generateMsgHeader(name, sessionId));
}
li.appendChild(div);
return li;
}
function generateFile(name, outgoing, file_info) {
function generateFile(name, sessionId, outgoing, file_info) {
let div1 = document.createElement("div");
div1.classList.add("file");
div1.classList.add("content");
@ -1017,7 +1078,7 @@ function generateFile(name, outgoing, file_info) {
div1.appendChild(a);
let li = document.createElement("li");
if (typeof name !== "undefined") {
li.appendChild(generateMsgHeader(name));
li.appendChild(generateMsgHeader(name, sessionId));
}
li.appendChild(div1);
return li;
@ -1090,18 +1151,20 @@ function displayHistory(scrollToBottom = true) {
let previousOutgoing = undefined;
msgHistory.get(currentSessionId).forEach(entry => {
let name = undefined;
let sessionId = undefined;
if (previousOutgoing != entry[0]) {
previousOutgoing = entry[0];
if (entry[0]) { //outgoing msg
name = identityName;
} else {
name = session.name;
sessionId = currentSessionId;
}
}
if (entry[1]) { //is file
msg_log.appendChild(generateFile(name, entry[0], entry[2]));
msg_log.appendChild(generateFile(name, sessionId, entry[0], entry[2]));
} else {
msg_log.appendChild(generateMessage(name, entry[2]));
msg_log.appendChild(generateMessage(name, sessionId, entry[2]));
}
});
if (scrollToBottom) {

View File

@ -63,9 +63,9 @@
padding: 10px 50px;
}
.avatar {
width: 2em;
height: 2em;
font-size: 3em;
width: 7em;
height: 7em;
display: block;
margin: auto;
}
#identity h2 {
@ -157,7 +157,7 @@
}
} else {
let identity = document.getElementById("identity");
identity.appendChild(generateAvatar(identity_name));
identity.appendChild(generateSelfAvatar(Date.now()));
let h2 = document.createElement("h2");
h2.textContent = identity_name;
identity.appendChild(h2);

View File

@ -11,6 +11,7 @@ use crate::{constants, crypto, key_value_table::KeyValueTable, print_error, util
const MAIN_TABLE: &str = "main";
const CONTACTS_TABLE: &str = "contacts";
const FILES_TABLE: &str = "files";
const AVATARS_TABLE: &str = "avatars";
const DATABASE_CORRUPED_ERROR: &str = "Database corrupted";
@ -21,6 +22,7 @@ impl<'a> DBKeys {
pub const SALT: &'a str = "salt";
pub const MASTER_KEY: &'a str = "master_key";
pub const USE_PADDING: &'a str = "use_padding";
pub const AVATAR: &'a str = "avatar";
}
fn bool_to_byte(b: bool) -> u8 {
@ -53,6 +55,7 @@ pub struct Contact {
pub uuid: Uuid,
pub public_key: [u8; PUBLIC_KEY_LENGTH],
pub name: String,
pub avatar: Option<Uuid>,
pub verified: bool,
pub seen: bool,
}
@ -70,19 +73,23 @@ impl Identity {
self.keypair.public.to_bytes()
}
pub fn add_contact(&self, name: String, public_key: [u8; PUBLIC_KEY_LENGTH]) -> Result<Contact, rusqlite::Error> {
pub fn add_contact(&self, name: String, avatar_uuid: Option<Uuid>, public_key: [u8; PUBLIC_KEY_LENGTH]) -> Result<Contact, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
db.execute(&("CREATE TABLE IF NOT EXISTS ".to_owned()+CONTACTS_TABLE+"(uuid BLOB PRIMARY KEY, name BLOB, key BLOB, verified BLOB, seen BLOB)"), [])?;
db.execute(&("CREATE TABLE IF NOT EXISTS ".to_owned()+CONTACTS_TABLE+"(uuid BLOB PRIMARY KEY, name BLOB, avatar BLOB, key BLOB, verified BLOB, seen BLOB)"), [])?;
let contact_uuid = Uuid::new_v4();
let encrypted_name = crypto::encrypt_data(name.as_bytes(), &self.master_key).unwrap();
let encrypted_public_key = crypto::encrypt_data(&public_key, &self.master_key).unwrap();
let encrypted_verified = crypto::encrypt_data(&[bool_to_byte(false)], &self.master_key).unwrap();
let encrypted_seen = crypto::encrypt_data(&[bool_to_byte(true)], &self.master_key).unwrap();
db.execute(&("INSERT INTO ".to_owned()+CONTACTS_TABLE+" (uuid, name, key, verified, seen) VALUES (?1, ?2, ?3, ?4, ?5)"), params![&contact_uuid.as_bytes()[..], encrypted_name, encrypted_public_key, encrypted_verified, encrypted_seen])?;
match avatar_uuid {
Some(avatar_uuid) => db.execute(&format!("INSERT INTO {} (uuid, name, avatar, key, verified, seen) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", CONTACTS_TABLE), params![&contact_uuid.as_bytes()[..], encrypted_name, &avatar_uuid.as_bytes()[..], encrypted_public_key, encrypted_verified, encrypted_seen])?,
None => db.execute(&format!("INSERT INTO {} (uuid, name, key, verified, seen) VALUES (?1, ?2, ?3, ?4, ?5)", CONTACTS_TABLE), params![&contact_uuid.as_bytes()[..], encrypted_name, encrypted_public_key, encrypted_verified, encrypted_seen])?
};
Ok(Contact {
uuid: contact_uuid,
public_key: public_key,
name: name,
avatar: avatar_uuid,
verified: false,
seen: true,
})
@ -91,7 +98,7 @@ impl Identity {
pub fn remove_contact(uuid: &Uuid) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
Identity::delete_conversation(uuid)?;
db.execute(&("DELETE FROM ".to_owned()+CONTACTS_TABLE+" WHERE uuid=?"), [&uuid.as_bytes()[..]])
db.execute(&format!("DELETE FROM {} WHERE uuid=?", CONTACTS_TABLE), [&uuid.as_bytes()[..]])
}
pub fn set_verified(&self, uuid: &Uuid) -> Result<usize, rusqlite::Error> {
@ -106,6 +113,11 @@ impl Identity {
db.execute(&format!("UPDATE {} SET name=?1 WHERE uuid=?2", CONTACTS_TABLE), [encrypted_name.as_slice(), uuid.as_bytes()])
}
pub fn set_contact_avatar(&self, contact_uuid: &Uuid, avatar_uuid: &Uuid) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
db.execute(&format!("UPDATE {} SET avatar=?1 WHERE uuid=?2", CONTACTS_TABLE), params![&avatar_uuid.as_bytes()[..], &contact_uuid.as_bytes()[..]])
}
pub fn set_contact_seen(&self, uuid: &Uuid, seen: bool) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
let encrypted_seen = crypto::encrypt_data(&[bool_to_byte(seen)], &self.master_key).unwrap();
@ -115,107 +127,89 @@ impl Identity {
pub fn load_contacts(&self) -> Option<Vec<Contact>> {
match Connection::open(get_database_path()) {
Ok(db) => {
match db.prepare(&("SELECT uuid, name, key, verified, seen FROM ".to_owned()+CONTACTS_TABLE)) {
Ok(mut stmt) => {
let mut rows = stmt.query([]).unwrap();
let mut contacts = Vec::new();
while let Some(row) = rows.next().unwrap() {
let encrypted_public_key: Vec<u8> = row.get(2).unwrap();
match crypto::decrypt_data(encrypted_public_key.as_slice(), &self.master_key) {
Ok(public_key) => {
if public_key.len() == PUBLIC_KEY_LENGTH {
let encrypted_name: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_name.as_slice(), &self.master_key) {
Ok(name) => {
let encrypted_verified: Vec<u8> = row.get(3).unwrap();
match crypto::decrypt_data(encrypted_verified.as_slice(), &self.master_key) {
Ok(verified) => {
let encrypted_seen: Vec<u8> = row.get(4).unwrap();
match crypto::decrypt_data(encrypted_seen.as_slice(), &self.master_key) {
Ok(seen) => {
let uuid: Vec<u8> = row.get(0).unwrap();
match to_uuid_bytes(&uuid) {
Some(uuid_bytes) => {
contacts.push(Contact {
uuid: Uuid::from_bytes(uuid_bytes),
public_key: public_key.try_into().unwrap(),
name: std::str::from_utf8(name.as_slice()).unwrap().to_owned(),
verified: byte_to_bool(verified[0]).unwrap(),
seen: byte_to_bool(seen[0]).unwrap(),
})
}
None => {}
}
}
Err(e) => print_error!(e)
}
if let Ok(mut stmt) = db.prepare(&("SELECT uuid, name, avatar, key, verified, seen FROM ".to_owned()+CONTACTS_TABLE)) {
let mut rows = stmt.query([]).unwrap();
let mut contacts = Vec::new();
while let Ok(Some(row)) = rows.next() {
let encrypted_public_key: Vec<u8> = row.get(3).unwrap();
match crypto::decrypt_data(encrypted_public_key.as_slice(), &self.master_key) {
Ok(public_key) => {
let encrypted_name: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_name.as_slice(), &self.master_key) {
Ok(name) => {
let encrypted_verified: Vec<u8> = row.get(4).unwrap();
match crypto::decrypt_data(encrypted_verified.as_slice(), &self.master_key) {
Ok(verified) => {
let encrypted_seen: Vec<u8> = row.get(5).unwrap();
match crypto::decrypt_data(encrypted_seen.as_slice(), &self.master_key) {
Ok(seen) => {
let contact_uuid: Vec<u8> = row.get(0).unwrap();
let avatar_result: Result<Vec<u8>, rusqlite::Error> = row.get(2);
let avatar = match avatar_result {
Ok(avatar_uuid) => Some(Uuid::from_bytes(to_uuid_bytes(&avatar_uuid).unwrap())),
Err(_) => None
};
contacts.push(Contact {
uuid: Uuid::from_bytes(to_uuid_bytes(&contact_uuid).unwrap()),
public_key: public_key.try_into().unwrap(),
name: std::str::from_utf8(name.as_slice()).unwrap().to_owned(),
avatar,
verified: byte_to_bool(verified[0]).unwrap(),
seen: byte_to_bool(seen[0]).unwrap(),
})
}
Err(e) => print_error!(e)
}
}
Err(e) => print_error!(e)
}
} else {
print_error!("Invalid public key length: database corrupted");
}
Err(e) => print_error!(e)
}
Err(e) => print_error!(e)
}
Err(e) => print_error!(e)
}
Some(contacts)
}
Err(e) => {
print_error!(e);
None
}
return Some(contacts);
}
}
Err(e) => {
print_error!(e);
None
}
Err(e) => print_error!(e)
}
None
}
pub fn clear_temporary_files() -> Result<usize, rusqlite::Error> {
#[allow(unused_must_use)]
pub fn clear_cache() -> Result<(), rusqlite::Error> {
let db = Connection::open(get_database_path())?;
db.execute(&format!("DELETE FROM {} WHERE contact_uuid IS NULL", FILES_TABLE), [])
db.execute(&format!("DELETE FROM {} WHERE contact_uuid IS NULL", FILES_TABLE), []);
db.execute(&format!("DELETE FROM {} WHERE uuid NOT IN (SELECT avatar FROM {})", AVATARS_TABLE, CONTACTS_TABLE), []);
Ok(())
}
pub fn load_file(&self, uuid: Uuid) -> Option<Vec<u8>> {
match Connection::open(get_database_path()) {
Ok(db) => {
match db.prepare(&format!("SELECT uuid, data FROM \"{}\"", FILES_TABLE)) {
Ok(mut stmt) => {
let mut rows = stmt.query([]).unwrap();
while let Some(row) = rows.next().unwrap() {
let encrypted_uuid: Vec<u8> = row.get(0).unwrap();
match crypto::decrypt_data(encrypted_uuid.as_slice(), &self.master_key){
Ok(test_uuid) => {
if test_uuid == uuid.as_bytes() {
let encrypted_data: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
Ok(data) => return Some(data),
Err(e) => print_error!(e)
}
}
let mut stmt = db.prepare(&format!("SELECT uuid, data FROM \"{}\"", FILES_TABLE)).unwrap();
let mut rows = stmt.query([]).unwrap();
while let Ok(Some(row)) = rows.next() {
let encrypted_uuid: Vec<u8> = row.get(0).unwrap();
match crypto::decrypt_data(encrypted_uuid.as_slice(), &self.master_key){
Ok(test_uuid) => {
if test_uuid == uuid.as_bytes() {
let encrypted_data: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
Ok(data) => return Some(data),
Err(e) => print_error!(e)
}
Err(e) => print_error!(e)
}
}
None
}
Err(e) => {
print_error!(e);
None
Err(e) => print_error!(e)
}
}
}
Err(e) => {
print_error!(e);
None
}
Err(e) => print_error!(e)
}
None
}
pub fn store_file(&self, contact_uuid: Option<Uuid>, data: &[u8]) -> Result<Uuid, rusqlite::Error> {
@ -244,73 +238,44 @@ impl Identity {
pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option<Vec<(bool, Vec<u8>)>> {
match Connection::open(get_database_path()) {
Ok(db) => {
match db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
Ok(mut stmt) => {
let mut rows = stmt.query([]).unwrap();
match rows.next() {
Ok(row) => if row.is_some() {
let total: usize = row.unwrap().get(0).unwrap();
if offset >= total {
None
} else {
if offset+count >= total {
count = total-offset;
}
match db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)) {
Ok(mut stmt) => {
let mut rows = stmt.query([]).unwrap();
let mut msgs = Vec::new();
while let Some(row) = rows.next().unwrap() {
let encrypted_outgoing: Vec<u8> = row.get(0).unwrap();
match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){
Ok(outgoing) => {
match byte_to_bool(outgoing[0]) {
Ok(outgoing) => {
let encrypted_data: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
Ok(data) => {
msgs.push(
(
outgoing,
data
)
)
},
Err(e) => print_error!(e)
}
}
Err(_) => {}
}
}
if let Ok(mut stmt) = db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
let mut rows = stmt.query([]).unwrap();
if let Ok(Some(row)) = rows.next() {
let total: usize = row.get(0).unwrap();
if offset < total {
if offset+count >= total {
count = total-offset;
}
let mut stmt = db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)).unwrap();
let mut rows = stmt.query([]).unwrap();
let mut msgs = Vec::new();
while let Ok(Some(row)) = rows.next() {
let encrypted_outgoing: Vec<u8> = row.get(0).unwrap();
match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){
Ok(outgoing) => {
match byte_to_bool(outgoing[0]) {
Ok(outgoing) => {
let encrypted_data: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
Ok(data) => msgs.push((outgoing, data)),
Err(e) => print_error!(e)
}
}
Some(msgs)
}
Err(e) => {
print_error!(e);
None
Err(_) => {}
}
}
Err(e) => print_error!(e)
}
} else {
None
}
Err(_) => None
return Some(msgs);
}
}
Err(e) => {
print_error!(e);
None
}
}
}
Err(e) => {
print_error!(e);
None
}
Err(e) => print_error!(e)
}
None
}
#[allow(unused_must_use)]
@ -336,6 +301,29 @@ impl Identity {
db.update(DBKeys::USE_PADDING, &encrypted_use_padding)
}
pub fn store_avatar(&self, avatar: &[u8]) -> Result<Uuid, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (uuid BLOB PRIMARY KEY, data BLOB)", AVATARS_TABLE), [])?;
let uuid = Uuid::new_v4();
let encrypted_avatar = crypto::encrypt_data(avatar, &self.master_key).unwrap();
db.execute(&format!("INSERT INTO {} (uuid, data) VALUES (?1, ?2)", AVATARS_TABLE), params![&uuid.as_bytes()[..], encrypted_avatar])?;
Ok(uuid)
}
pub fn get_avatar(&self, avatar_uuid: &Uuid) -> Option<Vec<u8>> {
let db = Connection::open(get_database_path()).ok()?;
let mut stmt = db.prepare(&format!("SELECT data FROM {} WHERE uuid=?", AVATARS_TABLE)).unwrap();
let mut rows = stmt.query(params![&avatar_uuid.as_bytes()[..]]).unwrap();
let encrypted_avatar: Vec<u8> = rows.next().ok()??.get(0).unwrap();
match crypto::decrypt_data(&encrypted_avatar, &self.master_key) {
Ok(avatar) => Some(avatar),
Err(e) => {
print_error!(e);
None
}
}
}
pub fn zeroize(&mut self){
self.master_key.zeroize();
self.keypair.secret.zeroize();
@ -480,6 +468,16 @@ impl Identity {
}
}
pub fn set_identity_avatar(avatar: &[u8]) -> Result<usize, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
db.upsert(DBKeys::AVATAR, avatar)
}
pub fn get_identity_avatar() -> Result<Vec<u8>, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
db.get(DBKeys::AVATAR)
}
pub fn delete_identity() -> Result<(), std::io::Error> {
std::fs::remove_file(get_database_path())
}

View File

@ -28,7 +28,7 @@ impl<'a> KeyValueTable<'a> {
pub fn update(&self, key: &str, value: &[u8]) -> Result<usize, Error> {
self.db.execute(&format!("UPDATE {} SET value=? WHERE key=\"{}\"", self.table_name, key), params![value])
}
/*pub fn upsert(&self, key: &str, value: &[u8]) -> Result<usize, Error> {
pub fn upsert(&self, key: &str, value: &[u8]) -> Result<usize, Error> {
self.db.execute(&format!("INSERT INTO {} (key, value) VALUES(?1, ?2) ON CONFLICT(key) DO UPDATE SET value=?3", self.table_name), params![key, value, value])
}*/
}
}

View File

@ -9,6 +9,7 @@ mod constants;
mod discovery;
use std::{env, fs, io, net::SocketAddr, str::{FromStr, from_utf8}, sync::{Arc, RwLock}};
use image::GenericImageView;
use tokio::{net::TcpListener, runtime::Handle, sync::mpsc};
use actix_web::{App, HttpMessage, HttpRequest, HttpResponse, HttpServer, http::{header, CookieBuilder}, web, web::Data};
use actix_multipart::Multipart;
@ -198,7 +199,7 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
}
"contact" => {
let session_id: usize = args[1].parse().unwrap();
match session_manager.add_contact(session_id, msg[args[0].len()+args[1].len()+2..].to_string()) {
match session_manager.add_contact(session_id) {
Ok(_) => {},
Err(e) => print_error!(e)
}
@ -224,10 +225,10 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
Err(e) => print_error!(e)
}
}
"ask_name" => {
"refresh_profile" => {
let session_id: usize = args[1].parse().unwrap();
session_manager.send_command(&session_id, SessionCommand::Send {
buff: protocol::ask_name()
buff: protocol::ask_profile_info()
}).await;
}
"set_use_padding" => {
@ -303,12 +304,86 @@ fn is_authenticated(req: &HttpRequest) -> bool {
false
}
async fn handle_set_avatar(req: HttpRequest, mut payload: Multipart) -> HttpResponse {
if is_authenticated(&req) {
while let Ok(Some(mut field)) = payload.try_next().await {
let content_disposition = field.content_disposition().unwrap();
if let Some(name) = content_disposition.get_name() {
if name == "avatar" {
let mut buffer = Vec::new();
while let Some(Ok(chunk)) = field.next().await {
buffer.extend(chunk);
}
match image::guess_format(&buffer) {
Ok(format) => {
match image::load_from_memory_with_format(&buffer, format) {
Ok(image) => {
let (width, height) = image.dimensions();
let image = if width < height {
image.crop_imm(0, (height-width)/2, width, width)
} else if width > height {
image.crop_imm((width-height)/2, 0, height, height)
} else {
image
};
let mut avatar = Vec::new();
image.write_to(&mut avatar, format).unwrap();
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
match global_vars.read().unwrap().session_manager.set_avatar(&avatar).await {
Ok(_) => {
return HttpResponse::Ok().finish();
}
Err(e) => {
print_error!(e);
return HttpResponse::InternalServerError().finish();
}
}
}
Err(e) => print_error!(e)
}
}
Err(e) => print_error!(e)
}
}
}
}
}
HttpResponse::BadRequest().finish()
}
fn reply_with_avatar(avatar: Option<Vec<u8>>, name: &str) -> HttpResponse {
match avatar {
Some(avatar) => HttpResponse::Ok().content_type("image/*").body(avatar),
None => {
#[cfg(not(debug_assertions))]
let svg = include_str!(concat!(env!("OUT_DIR"), "/text_avatar.svg"));
#[cfg(debug_assertions)]
let svg = replace_fields("src/frontend/imgs/text_avatar.svg");
HttpResponse::Ok().content_type("image/svg+xml").body(svg.replace("LETTER", &name.chars().nth(0).unwrap().to_string()))
}
}
}
fn handle_avatar(req: HttpRequest) -> HttpResponse {
let splits: Vec<&str> = req.path()[1..].split("/").collect();
if splits.len() == 2 {
if splits[1] == "self" {
return reply_with_avatar(Identity::get_identity_avatar().ok(), &Identity::get_identity_name().unwrap());
}
} else if splits.len() == 3 {
if let Ok(session_id) = splits[1].parse() {
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
return reply_with_avatar(global_vars.read().unwrap().session_manager.get_avatar(&session_id), splits[2]);
}
}
HttpResponse::BadRequest().finish()
}
#[derive(Deserialize, Serialize, Debug)]
struct FileInfo {
uuid: String,
file_name: String,
}
fn handle_load_file(req: HttpRequest, file_info: web::Query<FileInfo>) -> HttpResponse {
if is_authenticated(&req) {
match Uuid::from_str(&file_info.uuid) {
@ -368,7 +443,9 @@ async fn handle_send_file(req: HttpRequest, mut payload: Multipart) -> HttpRespo
loop {
chunk_buffer.extend(&pending_buffer);
pending_buffer.clear();
println!("Calling next()");
while let Some(Ok(chunk)) = field.next().await {
println!("Not None");
if chunk_buffer.len()+chunk.len() <= constants::FILE_CHUNK_SIZE {
chunk_buffer.extend(chunk);
} else if chunk_buffer.len() == constants::FILE_CHUNK_SIZE {
@ -381,6 +458,7 @@ async fn handle_send_file(req: HttpRequest, mut payload: Multipart) -> HttpRespo
break;
}
}
println!("May be None");
if !global_vars_read.session_manager.send_command(&session_id, SessionCommand::EncryptFileChunk{
plain_text: chunk_buffer.clone()
}).await {
@ -457,7 +535,7 @@ fn login(identity: Identity, global_vars: &Arc<RwLock<GlobalVars>>) -> HttpRespo
}
fn on_identity_loaded(identity: Identity, global_vars: &Arc<RwLock<GlobalVars>>) -> HttpResponse {
match Identity::clear_temporary_files() {
match Identity::clear_cache() {
Ok(_) => {},
Err(e) => print_error!(e)
}
@ -571,13 +649,13 @@ async fn handle_index(req: HttpRequest) -> HttpResponse {
const JS_CONTENT_TYPE: &str = "text/javascript";
#[cfg(debug_assertions)]
fn replace_css(file_path: &str) -> String {
fn replace_fields(file_path: &str) -> String {
use yaml_rust::YamlLoader;
let mut content = fs::read_to_string(file_path).unwrap();
let config = &YamlLoader::load_from_str(&fs::read_to_string("config.yml").unwrap()).unwrap()[0];
let css_values = config["css"].as_hash().unwrap();
css_values.into_iter().for_each(|entry| {
content = content.replace(entry.0.as_str().unwrap(), entry.1.as_str().unwrap());
let fields = config.as_hash().unwrap();
fields.into_iter().for_each(|field| {
content = content.replace(field.0.as_str().unwrap(), field.1.as_str().unwrap());
});
content
}
@ -596,7 +674,7 @@ fn handle_static(req: HttpRequest) -> HttpResponse {
}
"index.css" => {
#[cfg(debug_assertions)]
return response_builder.body(replace_css("src/frontend/index.css"));
return response_builder.body(replace_fields("src/frontend/index.css"));
#[cfg(not(debug_assertions))]
return response_builder.body(include_str!(concat!(env!("OUT_DIR"), "/index.css")));
}
@ -655,7 +733,7 @@ fn handle_static(req: HttpRequest) -> HttpResponse {
}
"style.css" => {
#[cfg(debug_assertions)]
return response_builder.body(replace_css("src/frontend/commons/style.css"));
return response_builder.body(replace_fields("src/frontend/commons/style.css"));
#[cfg(not(debug_assertions))]
return response_builder.body(include_str!(concat!(env!("OUT_DIR"), "/commons/style.css")));
}
@ -700,6 +778,8 @@ async fn start_http_server(global_vars: Arc<RwLock<GlobalVars>>) -> io::Result<(
.route("/send_file", web::post().to(handle_send_file))
.route("/send_large_file", web::post().to(handle_send_file))
.route("/load_file", web::get().to(handle_load_file))
.route("/set_avatar", web::post().to(handle_set_avatar))
.route("/avatar/*", web::get().to(handle_avatar))
.route("/static/.*", web::get().to(handle_static))
.route("/logout", web::get().to(handle_logout))
}

View File

@ -5,26 +5,27 @@ pub struct Headers;
impl Headers {
pub const MESSAGE: u8 = 0x00;
pub const ASK_NAME: u8 = 0x01;
pub const TELL_NAME: u8 = 0x02;
pub const FILE: u8 = 0x03;
pub const ASK_LARGE_FILES: u8 = 0x04;
pub const ACCEPT_LARGE_FILES: u8 = 0x05;
pub const LARGE_FILE_CHUNK: u8 = 0x06;
pub const ACK_CHUNK: u8 = 0x07;
pub const ABORT_FILES_TRANSFER: u8 = 0x08;
pub const FILE: u8 = 0x01;
pub const ASK_PROFILE_INFO: u8 = 0x02;
pub const NAME: u8 = 0x03;
pub const AVATAR: u8 = 0x04;
pub const ASK_LARGE_FILES: u8 = 0x05;
pub const ACCEPT_LARGE_FILES: u8 = 0x06;
pub const LARGE_FILE_CHUNK: u8 = 0x07;
pub const ACK_CHUNK: u8 = 0x08;
pub const ABORT_FILES_TRANSFER: u8 = 0x09;
}
pub fn new_message(message: String) -> Vec<u8> {
[&[Headers::MESSAGE], message.as_bytes()].concat()
}
pub fn ask_name() -> Vec<u8> {
vec![Headers::ASK_NAME]
pub fn ask_profile_info() -> Vec<u8> {
vec![Headers::ASK_PROFILE_INFO]
}
pub fn tell_name(name: &str) -> Vec<u8> {
[&[Headers::TELL_NAME], name.as_bytes()].concat()
pub fn name(name: &str) -> Vec<u8> {
[&[Headers::NAME], name.as_bytes()].concat()
}
pub fn file(file_name: &str, buffer: &[u8]) -> Vec<u8> {
@ -80,3 +81,7 @@ pub fn parse_ask_files(buffer: &[u8]) -> Option<Vec<(u64, String)>> {
}
Some(files_info)
}
pub fn avatar(avatar: &[u8]) -> Vec<u8> {
[&[Headers::AVATAR], avatar].concat()
}

View File

@ -40,6 +40,7 @@ pub struct LargeFilesDownload {
#[derive(Clone)]
pub struct SessionData {
pub name: String,
avatar: Option<Uuid>,
pub outgoing: bool,
pub peer_public_key: [u8; PUBLIC_KEY_LENGTH],
pub ip: IpAddr,
@ -54,7 +55,7 @@ pub struct SessionManager {
ui_connection: Mutex<Option<UiConnection>>,
loaded_contacts: RwLock<HashMap<usize, Contact>>,
pub last_loaded_msg_offsets: RwLock<HashMap<usize, usize>>,
pub saved_msgs: Mutex<HashMap<usize, Vec<(bool, Vec<u8>)>>>,
pub saved_msgs: RwLock<HashMap<usize, Vec<(bool, Vec<u8>)>>>,
pub not_seen: RwLock<Vec<usize>>,
mdns_service: Mutex<Option<Service>>,
listener_stop_signal: Mutex<Option<Sender<()>>>,
@ -72,6 +73,10 @@ impl SessionManager {
}
}
fn get_all_senders(&self) -> Vec<Sender<SessionCommand>> {
self.sessions.read().unwrap().iter().map(|i| i.1.sender.clone()).collect()
}
async fn encrypt_and_send<T: PsecWriter>(&self, writer: &mut T, buff: &[u8]) -> Result<(), PsecError> {
let use_padding = self.identity.read().unwrap().as_ref().unwrap().use_padding;
writer.encrypt_and_send(buff, use_padding).await
@ -96,7 +101,7 @@ impl SessionManager {
}
}
if !msg_saved {
self.saved_msgs.lock().unwrap().get_mut(&session_id).unwrap().push((false, buffer));
self.saved_msgs.write().unwrap().get_mut(&session_id).unwrap().push((outgoing, buffer));
}
}
@ -127,7 +132,7 @@ impl SessionManager {
ui_connection.on_disconnected(&session_id);
});
self.sessions.write().unwrap().remove(session_id);
self.saved_msgs.lock().unwrap().remove(session_id);
self.saved_msgs.write().unwrap().remove(session_id);
self.not_seen.write().unwrap().retain(|x| x != session_id);
}
@ -172,64 +177,6 @@ impl SessionManager {
let session_read = result.1;
receiving.set(session_read.into_receive_and_decrypt());
match buffer[0] {
protocol::Headers::ASK_NAME => {
let name = {
self.identity.read().unwrap().as_ref().and_then(|identity| Some(identity.name.clone()))
};
if name.is_some() { //can be None if we log out just before locking the identity mutex
if let Err(e) = self.encrypt_and_send(&mut session_write, &protocol::tell_name(&name.unwrap())).await {
print_error!(e);
break;
}
}
}
protocol::Headers::TELL_NAME => {
match from_utf8(&buffer[1..]) {
Ok(new_name) => {
let new_name = new_name.replace('\n', " ");
self.with_ui_connection(|ui_connection| {
ui_connection.on_name_told(&session_id, &new_name);
});
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
if let Some(contact) = loaded_contacts.get_mut(&session_id) {
contact.name = new_name.to_string();
if let Err(e) = self.identity.read().unwrap().as_ref().unwrap().change_contact_name(&contact.uuid, &new_name) {
print_error!(e);
}
} else {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().name = new_name.to_string();
}
}
Err(e) => print_error!(e)
}
}
protocol::Headers::ASK_LARGE_FILES => {
if self.sessions.read().unwrap().get(&session_id).unwrap().files_download.is_none() && !is_sending { //don't accept 2 file transfers at the same time
if let Some(files_info) = protocol::parse_ask_files(&buffer) {
let download_location = UserDirs::new().unwrap().download_dir;
let files: Vec<LargeFileDownload> = files_info.into_iter().map(|info| {
LargeFileDownload {
file_name: info.1,
file_size: info.0,
transferred: 0,
last_chunk: get_unix_timestamp(),
}
}).collect();
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = Some(LargeFilesDownload {
download_location: download_location.clone(),
accepted: false,
index: 0,
files: files.clone(),
});
self.with_ui_connection(|ui_connection| {
ui_connection.on_ask_large_files(&session_id, &files, download_location.to_str().unwrap());
})
}
} else if let Err(e) = self.encrypt_and_send(&mut session_write, &[protocol::Headers::ABORT_FILES_TRANSFER]).await {
print_error!(e);
break;
}
}
protocol::Headers::LARGE_FILE_CHUNK => {
let mut should_accept_chunk = false;
{
@ -322,6 +269,103 @@ impl SessionManager {
ui_connection.on_received(&session_id, &buffer);
});
}
protocol::Headers::ASK_LARGE_FILES => {
if self.sessions.read().unwrap().get(&session_id).unwrap().files_download.is_none() && !is_sending { //don't accept 2 file transfers at the same time
if let Some(files_info) = protocol::parse_ask_files(&buffer) {
let download_location = UserDirs::new().unwrap().download_dir;
let files: Vec<LargeFileDownload> = files_info.into_iter().map(|info| {
LargeFileDownload {
file_name: info.1,
file_size: info.0,
transferred: 0,
last_chunk: get_unix_timestamp(),
}
}).collect();
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = Some(LargeFilesDownload {
download_location: download_location.clone(),
accepted: false,
index: 0,
files: files.clone(),
});
self.with_ui_connection(|ui_connection| {
ui_connection.on_ask_large_files(&session_id, &files, download_location.to_str().unwrap());
})
}
} else if let Err(e) = self.encrypt_and_send(&mut session_write, &[protocol::Headers::ABORT_FILES_TRANSFER]).await {
print_error!(e);
break;
}
}
protocol::Headers::ASK_PROFILE_INFO => {
let identity = {
self.identity.read().unwrap().clone()
};
if let Some(identity) = identity { //can be None if we log out just before locking the identity mutex
match self.encrypt_and_send(&mut session_write, &protocol::name(&identity.name)).await {
Ok(_) => {
if let Ok(avatar) = Identity::get_identity_avatar() {
if let Err(e) = self.encrypt_and_send(&mut session_write, &protocol::avatar(&avatar)).await {
print_error!(e);
break;
}
}
}
Err(e) => {
print_error!(e);
break;
}
}
}
}
protocol::Headers::NAME => {
match from_utf8(&buffer[1..]) {
Ok(new_name) => {
let new_name = new_name.replace('\n', " ");
self.with_ui_connection(|ui_connection| {
ui_connection.on_name_told(&session_id, &new_name);
});
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
if let Some(contact) = loaded_contacts.get_mut(&session_id) {
contact.name = new_name.to_string();
if let Err(e) = self.identity.read().unwrap().as_ref().unwrap().change_contact_name(&contact.uuid, &new_name) {
print_error!(e);
}
} else {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().name = new_name.to_string();
}
}
Err(e) => print_error!(e)
}
}
protocol::Headers::AVATAR => {
if buffer.len() < 10000000 {
match image::load_from_memory(&buffer[1..]) {
Ok(image) => {
drop(image);
let identity_opt = self.identity.read().unwrap();
let identity = identity_opt.as_ref().unwrap();
match identity.store_avatar(&buffer[1..]) {
Ok(avatar_uuid) => {
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
if let Some(contact) = loaded_contacts.get_mut(&session_id) {
contact.avatar = Some(avatar_uuid);
if let Err(e) = identity.set_contact_avatar(&contact.uuid, &avatar_uuid) {
print_error!(e);
}
} else {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().avatar = Some(avatar_uuid);
}
self.with_ui_connection(|ui_connection| {
ui_connection.on_avatar_set(&session_id);
});
}
Err(e) => print_error!(e)
}
}
Err(e) => print_error!(e)
}
}
}
_ => {
let header = buffer[0];
let buffer = match header {
@ -421,11 +465,7 @@ impl SessionManager {
let mut peer_public_key = [0; PUBLIC_KEY_LENGTH];
let session = {
let identity = {
let identity_opt = session_manager.identity.read().unwrap();
match identity_opt.as_ref() {
Some(identity) => Some(identity.clone()),
None => None
}
session_manager.identity.read().unwrap().clone()
};
match identity {
Some(identity) => {
@ -461,8 +501,9 @@ impl SessionManager {
}
if is_new_session && session_manager.is_identity_loaded() { //check if we didn't logged out during the handshake
let (sender, receiver) = mpsc::channel(32);
let session_data = SessionData{
let session_data = SessionData {
name: ip.to_string(),
avatar: None,
outgoing,
peer_public_key,
ip,
@ -485,7 +526,7 @@ impl SessionManager {
*session_counter += 1;
}
let session_id = session_id.unwrap();
session_manager.saved_msgs.lock().unwrap().insert(session_id, Vec::new());
session_manager.saved_msgs.write().unwrap().insert(session_id, Vec::new());
Some((session_id, receiver))
} else {
None
@ -497,7 +538,7 @@ impl SessionManager {
ui_connection.on_new_session(&session_id, &ip.to_string(), outgoing, &crypto::generate_fingerprint(&peer_public_key), ip, None);
});
if !is_contact {
match session_manager.encrypt_and_send(&mut session, &protocol::ask_name()).await {
match session_manager.encrypt_and_send(&mut session, &protocol::ask_profile_info()).await {
Ok(_) => {}
Err(e) => {
print_error!(e);
@ -542,7 +583,7 @@ impl SessionManager {
}
pub fn get_saved_msgs(&self) -> HashMap<usize, Vec<(bool, Vec<u8>)>> {
self.saved_msgs.lock().unwrap().clone()
self.saved_msgs.read().unwrap().clone()
}
pub fn set_seen(&self, session_id: usize, seen: bool) {
@ -567,8 +608,10 @@ impl SessionManager {
}
}
pub fn add_contact(&self, session_id: usize, name: String) -> Result<(), rusqlite::Error> {
let contact = self.identity.read().unwrap().as_ref().unwrap().add_contact(name, self.sessions.read().unwrap().get(&session_id).unwrap().peer_public_key)?;
pub fn add_contact(&self, session_id: usize) -> Result<(), rusqlite::Error> {
let sessions = self.sessions.read().unwrap();
let session = sessions.get(&session_id).unwrap();
let contact = self.identity.read().unwrap().as_ref().unwrap().add_contact(session.name.clone(), session.avatar, session.peer_public_key)?;
self.loaded_contacts.write().unwrap().insert(session_id, contact);
self.last_loaded_msg_offsets.write().unwrap().insert(session_id, 0);
Ok(())
@ -581,6 +624,7 @@ impl SessionManager {
if let Some(contact) = loaded_contacts.remove(&session_id) {
if let Some(session) = self.sessions.write().unwrap().get_mut(&session_id) {
session.name = contact.name;
session.avatar = contact.avatar;
}
}
self.last_loaded_msg_offsets.write().unwrap().remove(&session_id);
@ -602,7 +646,7 @@ impl SessionManager {
let result = Identity::delete_conversation(&self.loaded_contacts.read().unwrap().get(&session_id).unwrap().uuid);
if result.is_ok() {
self.last_loaded_msg_offsets.write().unwrap().insert(session_id, 0);
self.saved_msgs.lock().unwrap().insert(session_id, Vec::new());
self.saved_msgs.write().unwrap().insert(session_id, Vec::new());
}
result
}
@ -627,15 +671,32 @@ impl SessionManager {
msgs
}
pub fn get_avatar(&self, session_id: &usize) -> Option<Vec<u8>> {
let avatar_uuid = match self.loaded_contacts.read().unwrap().get(session_id) {
Some(contact) => contact.avatar?,
None => self.sessions.read().unwrap().get(session_id)?.avatar?
};
self.identity.read().unwrap().as_ref().unwrap().get_avatar(&avatar_uuid)
}
#[allow(unused_must_use)]
pub async fn set_avatar(&self, avatar: &[u8]) -> Result<(), rusqlite::Error> {
Identity::set_identity_avatar(&avatar)?;
let avatar_msg = protocol::avatar(&avatar);
for sender in self.get_all_senders().into_iter() {
sender.send(SessionCommand::Send {
buff: avatar_msg.clone()
}).await;
}
Ok(())
}
#[allow(unused_must_use)]
pub async fn change_name(&self, new_name: String) -> Result<usize, rusqlite::Error> {
let telling_name = protocol::tell_name(&new_name);
let telling_name = protocol::name(&new_name);
let result = self.identity.write().unwrap().as_mut().unwrap().change_name(new_name);
if result.is_ok() {
let senders: Vec<Sender<SessionCommand>> = {
self.sessions.read().unwrap().iter().map(|i| i.1.sender.clone()).collect()
};
for sender in senders.into_iter() {
for sender in self.get_all_senders().into_iter() {
sender.send(SessionCommand::Send {
buff: telling_name.clone()
}).await;
@ -659,7 +720,7 @@ impl SessionManager {
*self.ui_connection.lock().unwrap() = None;
*self.session_counter.write().unwrap() = 0;
self.loaded_contacts.write().unwrap().clear();
self.saved_msgs.lock().unwrap().clear();
self.saved_msgs.write().unwrap().clear();
}
pub fn is_identity_loaded(&self) -> bool {
@ -703,7 +764,7 @@ impl SessionManager {
ui_connection: Mutex::new(None),
loaded_contacts: RwLock::new(HashMap::new()),
last_loaded_msg_offsets: RwLock::new(HashMap::new()),
saved_msgs: Mutex::new(HashMap::new()),
saved_msgs: RwLock::new(HashMap::new()),
not_seen: RwLock::new(Vec::new()),
mdns_service: Mutex::new(None),
listener_stop_signal: Mutex::new(None),

View File

@ -112,6 +112,9 @@ mod ui_messages {
pub fn on_name_told(session_id: &usize, name: &str) -> Message {
Message::from(format!("name_told {} {}", session_id, name))
}
pub fn on_avatar_set(session_id: &usize) -> Message {
simple_event("avatar_set", session_id)
}
pub fn set_as_contact(session_id: usize, name: &str, verified: bool, fingerprint: &str) -> Message {
Message::from(format!("is_contact {} {} {} {}", session_id, verified, fingerprint, name))
}
@ -179,6 +182,9 @@ impl UiConnection {
pub fn on_name_told(&mut self, session_id: &usize, name: &str) {
self.write_message(ui_messages::on_name_told(session_id, name));
}
pub fn on_avatar_set(&mut self, session_id: &usize) {
self.write_message(ui_messages::on_avatar_set(session_id));
}
pub fn inc_files_transfer(&mut self, session_id: &usize, chunk_size: u64) {
self.write_message(ui_messages::inc_files_transfer(session_id, chunk_size));