Compare commits

...

17 Commits

18 changed files with 1857 additions and 1579 deletions

1736
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "aira"
version = "0.0.3"
version = "0.1.1"
authors = ["Hardcore Sushi <hardcore.sushi@disroot.org>"]
edition = "2018"
exclude = ["src/frontend"]
@ -9,37 +9,41 @@ exclude = ["src/frontend"]
rand = "0.8"
rand-7 = { package = "rand", version = "0.7.3" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "net", "io-util"] }
async-psec = { version = "0.3", features = ["split"] }
async-psec = { version = "0.4", features = ["split"] }
lazy_static = "1.4"
socket2 = "0.4"
rusqlite = { version = "0.25.1", features = ["bundled"] }
ed25519-dalek = "1" #for singing
sha2 = "0.9"
rusqlite = { version = "0.27", features = ["bundled"] }
ed25519-dalek = "1" #for singatures
sha2 = "0.10"
aes-gcm = "0.9"
aes-gcm-siv = "0.10" #Database
hkdf = "0.11"
aes-gcm-siv = "0.10" #database encryption
hkdf = "0.12"
hex = "0.4"
actix-web = "3"
actix-multipart = "0.3"
time = "0.2" #needed for actix cookies
actix-web = "4"
env_logger = "0.9"
actix-multipart = "0.4"
time = "0.3" #needed for actix cookies
futures = "0.3"
tungstenite = "0.13" #websocket
serde = "1.0" #serialization
tungstenite = "0.17" #websocket
serde = { version = "1.0", features = ["derive"] } #serialization
html-escape = "0.2"
sanitize-filename = "0.3"
platform-dirs = "0.3"
uuid = { version = "0.8", features = ["v4"] }
webbrowser = "0.5"
uuid = { version = "1.0", features = ["v4"] }
webbrowser = "0.7"
libmdns = "0.6" #mDNS advertiser
multicast_dns = "0.5" #mDNS browser
if-addrs = "0.6"
if-addrs = "0.7"
base64 = "0.13"
scrypt = "0.7"
zeroize = "1.2"
image = "0.23"
scrypt = "0.10"
zeroize = "1.5"
image = "0.24"
yaml-rust = "0.4" #only in debug mode
[build-dependencies]
html-minifier = "3.0"
yaml-rust = "0.4"
linked-hash-map = "0.5"
[profile.dev.package.scrypt]
opt-level = 3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -4,4 +4,5 @@ pub const APPLICATION_FOLDER: &str = "AIRA";
pub const DB_NAME: &str = "AIRA.db";
pub const HTTP_COOKIE_NAME: &str = "aira_auth";
pub const MSG_LOADING_COUNT: usize = 20;
pub const FILE_CHUNK_SIZE: usize = 1023996;
pub const FILE_CHUNK_SIZE: usize = 1_023_996;
pub const MAX_RECV_SIZE: usize = 16_383_996;

View File

@ -34,8 +34,8 @@ label {
.avatar {
margin-right: .5em;
width: 1.5em;
height: 1.5em;
width: 2.5em;
height: 2.5em;
border-radius: 50%;
}

View File

@ -0,0 +1,79 @@
<svg width="191.7mm" height="168.39mm" version="1.1" viewBox="0 0 191.7 168.39" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-9.1475 -20.806)">
<path d="m81.908 189.19c14.507-12.545 13.641-67.907 13.641-67.907l18.56 2e-5s-0.86524 55.362 13.641 67.907z" fill="#803300"/>
<g fill="#19f52c">
<g fill-rule="evenodd">
<circle cx="19.053" cy="67.31" r="8.6808"/>
<circle cx="40.006" cy="88.061" r="8.6808"/>
<circle cx="79.997" cy="99.739" r="8.6808"/>
<circle cx="68.203" cy="133.44" r="8.6808"/>
<circle cx="105.86" cy="74.321" r="8.6808"/>
<circle cx="38.754" cy="131.9" r="8.6808"/>
<circle cx="127.11" cy="124.1" r="8.6808"/>
<circle cx="97.819" cy="119.49" r="8.6808"/>
<circle cx="19.67" cy="109.42" r="8.6808"/>
<circle cx="48.138" cy="59.715" r="8.6808"/>
<circle cx="95.703" cy="44.581" r="8.6808"/>
<circle cx="77.134" cy="67.671" r="8.6808"/>
<circle cx="121.04" cy="29.486" r="8.6808"/>
<circle cx="119.73" cy="98.423" r="8.6808"/>
<circle cx="51.945" cy="108.83" r="8.6808"/>
</g>
<g stroke="#19f52c" stroke-width="1.8939">
<path d="m38.754 131.9-19.084-22.481"/>
<path d="m19.67 109.42 20.336-21.356"/>
<path d="m19.053 67.31 20.953 20.75"/>
<path d="m51.945 108.83 16.258 24.602"/>
<path d="m51.945 108.83 28.052-9.0944"/>
<path d="m48.138 59.715-8.132 28.346"/>
<path d="m48.138 59.715 28.996 7.956"/>
<path d="m77.134 67.671 28.73 6.6502"/>
<path d="m77.134 67.671 18.569-23.089"/>
<path d="m95.703 44.581 25.333-15.095"/>
<path d="m105.86 74.321 13.863 24.103"/>
<path d="m73.582 133.41 24.237-13.916"/>
<path d="m97.819 119.49 21.907-21.071"/>
<path d="m97.819 119.49 29.289 4.6056"/>
<path d="m17.828 153.33 20.926-21.434"/>
</g>
<circle cx="17.828" cy="153.33" r="8.6808" fill-rule="evenodd"/>
<circle cx="53.865" cy="159.2" r="8.6808" fill-rule="evenodd"/>
<path d="m38.754 131.9 15.111 27.303" stroke="#19f52c" stroke-width="1.8939"/>
<path d="m127.11 124.1 13.863 24.103" stroke="#19f52c" stroke-width="1.8939"/>
<circle cx="140.97" cy="148.2" r="8.6808" fill-rule="evenodd"/>
<path d="m140.97 148.2 20.559-17.279" stroke="#19f52c" stroke-width="1.8939"/>
<g fill-rule="evenodd">
<circle cx="161.53" cy="130.92" r="8.6808"/>
<circle cx="148.77" cy="105.42" r="8.6808"/>
<circle cx="161.93" cy="80.961" r="8.6808"/>
<circle cx="134.44" cy="73.351" r="8.6808"/>
<circle cx="172.46" cy="44.584" r="8.6808"/>
<circle cx="192.17" cy="132.75" r="8.6808"/>
<circle cx="175.39" cy="155.03" r="8.6808"/>
<circle cx="68.229" cy="33.867" r="8.6808"/>
<circle cx="143.95" cy="45.376" r="8.6808"/>
</g>
<g stroke="#19f52c" stroke-width="1.8939">
<path d="m161.53 130.92 13.863 24.103"/>
<path d="m175.39 155.03 16.778-22.271"/>
<path d="m119.73 98.423 29.041 6.9934"/>
<path d="m148.77 105.42 12.763 25.506"/>
<path d="m105.86 74.321 28.58-0.96983"/>
<path d="m148.77 105.42 29.455-1.4191"/>
<path d="m143.95 45.376 28.511-0.79158"/>
<path d="m121.04 29.486 22.914 15.89"/>
<path d="m38.754 131.9 29.449 1.5377"/>
<path d="m77.134 67.671 2.8632 32.068"/>
<path d="m68.229 33.867 27.474 10.715"/>
<path d="m48.138 59.715 20.091-25.848"/>
<path d="m143.95 45.376-9.5064 27.975"/>
</g>
<circle cx="178.22" cy="104" r="8.6808" fill-rule="evenodd"/>
<path d="m172.46 44.584 16.663 26.606" stroke="#19f52c" stroke-width="1.8939"/>
<path d="m134.44 73.351 27.488 7.6103" stroke="#19f52c" stroke-width="1.8939"/>
<circle cx="189.12" cy="71.19" r="8.6808" fill-rule="evenodd"/>
<path d="m189.12 71.19-27.193 9.7712" stroke="#19f52c" stroke-width="1.8939"/>
<path d="m161.93 80.961 16.292 23.036" stroke="#19f52c" stroke-width="1.8939"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -120,7 +120,7 @@ button:hover::after {
position: relative;
}
#avatarContainer .avatar {
font-size: 4em;
font-size: 2.5em;
}
#removeAvatar {
position: absolute;
@ -149,8 +149,7 @@ button:hover::after {
font-size: 0.9em;
}
#session_info .avatar {
width: 6em;
height: 6em;
font-size: 2.5em;
display: block;
margin: auto;
}
@ -198,7 +197,6 @@ button:hover::after {
padding: 10px;
display: flex;
align-items: center;
font-size: 1.7em;
cursor: pointer;
}
#me>div {
@ -207,6 +205,7 @@ button:hover::after {
flex-grow: 1;
}
#me p {
font-size: 1.7em;
margin: 0;
font-weight: bold;
display: inline;
@ -214,6 +213,9 @@ button:hover::after {
#me>div:hover p {
color: var(--accent);
}
#me .avatar {
font-size: 1.2em;
}
#left_panel ul:last-of-type, #msg_log {
flex-grow: 1;
}
@ -235,8 +237,7 @@ button:hover::after {
flex-grow: 1;
}
#left_panel ul li .avatar {
width: 2em;
height: 2em;
font-size: .9em;
}
#left_panel ul li:hover, #left_panel ul li.current {
background-color: #333940;
@ -254,7 +255,8 @@ button:hover::after {
#left_panel ul li.is_verified p::after {
content: url("/static/imgs/icons/verified/ACCENT_COLOR");
}
#left_panel ul li .not_seen_marker {
#left_panel ul li.not_seen::after {
content: "";
width: 12px;
height: 12px;
background-color: var(--accent);
@ -295,7 +297,6 @@ button:hover::after {
flex-direction: row;
align-items: center;
padding: 20px 20px;
font-size: 1.5em;
}
#chat_header>div {
display: flex;
@ -307,6 +308,7 @@ button:hover::after {
color: var(--accent);
}
#chat_header>div>p { /*name*/
font-size: 1.5em;
font-weight: bold;
margin: 0;
}
@ -354,7 +356,6 @@ button:hover::after {
}
#file_transfer {
border-top: 2px solid var(--accent);
display: none;
position: relative;
}
#file_transfer.active {
@ -401,31 +402,46 @@ button:hover::after {
height: 100%;
background-color: var(--accent);
}
#message_box {
border-top: 2px solid var(--accent);
margin-bottom: 0;
}
#msg_log {
font-size: 1.1em;
overflow-y: scroll;
white-space: pre;
overflow-y: auto;
white-space: pre-wrap;
}
#msg_log li {
display: flex;
align-items: end;
gap: 10px;
margin-bottom: 10px;
padding-right: 10px;
}
#msg_log li>div {
flex-grow: 1;
}
#msg_log li .timestamp {
opacity: .5;
font-family: "Liberation Sans", Arial, sans-serif;
font-size: .8em;
}
#msg_log p {
font-size: 1.1em;
margin: 0;
}
#msg_log .avatar {
font-size: .8em;
}
#msg_log li .header {
display: flex;
align-items: center;
margin-top: 15px;
}
#msg_log li .header p {
color: var(--accent);
font-weight: bold;
margin-left: .5em;
}
#msg_log li .content {
margin-left: 2em;
margin-top: 5px;
margin-bottom: 10px;
margin-left: 3em;
}
#msg_log li .content p {
word-break: break-word;
}
#msg_log a {
color: #238cf5;
@ -452,6 +468,149 @@ button:hover::after {
width: 2em;
margin-left: 15px;
}
#message_box, #chat_header, #msg_log {
#message_box, #message_box.online #offline_warning, #chat_header, #msg_log, #file_transfer {
display: none;
}
#message_box.active {
display: block;
}
#message_box {
border-top: 2px solid red;
margin-bottom: 0;
}
#message_box>div:nth-child(2) {
display: flex;
}
#message_box.online {
border-top-color: var(--accent);
}
#offline_warning {
margin-left: 20px;
display: flex;
align-items: center;
gap: 25px;
}
#offline_warning::before {
content: url("/static/imgs/icons/warning/ff0000");
display: block;
width: 2em;
}
#offline_warning h3 {
color: red;
display: inline-block;
margin-bottom: .3em;
}
#offline_warning p {
margin-top: 0;
}
#msg_log li.pending_msgs_divider {
border-top: 1px solid grey;
padding-top: 10px;
margin-top: 30px;
margin-left: 100px;
margin-right: 100px;
}
#msg_log li.pending_msgs_divider h4 {
margin: auto;
opacity: .5;
}
.lds-spinner {
color: official;
position: relative;
width: 82px;
height: 82px;
}
.lds-spinner div {
transform-origin: 40px 40px;
animation: lds-spinner 1.2s linear infinite;
}
.lds-spinner div:after {
content: " ";
display: block;
position: absolute;
top: 3px;
left: 37px;
width: 6px;
height: 18px;
border-radius: 20%;
background: #fff;
}
.lds-spinner div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -1.1s;
}
.lds-spinner div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -1s;
}
.lds-spinner div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.9s;
}
.lds-spinner div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.8s;
}
.lds-spinner div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.7s;
}
.lds-spinner div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.6s;
}
.lds-spinner div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.5s;
}
.lds-spinner div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.4s;
}
.lds-spinner div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.3s;
}
.lds-spinner div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.2s;
}
.lds-spinner div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.1s;
}
.lds-spinner div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
}
@keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
#pending_msgs_indicator {
display: none;
align-items: center;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
#pending_msgs_indicator.sending {
display: flex;
}
#disconnected {
display: none;
height: 100%;
align-items: center;
justify-content: center;
}
#disconnected.disconnected {
display: flex;
}
#disconnected img {
width: 70px;
height: 70px;
}

View File

@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<title>AIRA</title>
<link rel="icon" type="image/svg" href="/static/imgs/icons/logo">
<link rel="stylesheet" href="/static/commons/style.css">
<link rel="stylesheet" href="/static/index.css">
</head>
@ -41,6 +42,10 @@
</div>
<ul id="msg_log">
</ul>
<div id="pending_msgs_indicator">
<div class="lds-spinner"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
<h3>Sending pending messages...</h3>
</div>
<div id="file_transfer">
<div id="file_control">
<button id="file_cancel" title="Cancel"></button>
@ -56,10 +61,22 @@
</div>
</div>
<div id="message_box">
<input type="text" id="message_input" placeholder="Send a message...">
<label title="Send file" class="file_picker">
<input type="file" id="attach_file" multiple>
</label>
<div id="offline_warning">
<div>
<h3>Your contact seems to be offline.</h3>
<p>Sent messages will be stored until a connection is established.</p>
</div>
</div>
<div>
<input type="text" id="message_input" placeholder="Send a message...">
<label title="Send file" class="file_picker">
<input type="file" id="attach_file" multiple>
</label>
</div>
</div>
<div id="disconnected">
<img src="/static/imgs/icons/warning/ff0000">
<h1>Websocket connection closed</h1>
</div>
</div>
</main>
@ -76,4 +93,3 @@
<script src="/static/index.js"></script>
</body>
</html>

View File

@ -1,12 +1,13 @@
"use strict";
let identityName = undefined;
let socket = null;
let socket = new WebSocket("ws://"+location.hostname+":"+websocketPort+"/ws");;
let notificationAllowed = false;
let localIps = [];
let currentSessionId = -1;
let sessionsData = new Map();
let msgHistory = new Map();
let pendingMsgs = new Map();
let pendingFilesTransfers = new Map();
let avatarTimestamps = new Map([
["self", Date.now()]
@ -14,7 +15,7 @@ let avatarTimestamps = new Map([
function onClickSession(event) {
let sessionId = event.currentTarget.getAttribute("data-sessionId");
if (sessionId != null) {
if (sessionId != null && socket.readyState === WebSocket.OPEN) {
currentSessionId = sessionId;
let session = sessionsData.get(sessionId);
if (!session.seen) {
@ -80,6 +81,7 @@ document.getElementById("delete_conversation").onclick = function() {
document.getElementById("add_contact").onclick = function() {
socket.send("contact "+currentSessionId);
sessionsData.get(currentSessionId).isContact = true;
pendingMsgs.set(currentSessionId, []);
displayHeader();
displaySessions();
};
@ -103,7 +105,9 @@ document.getElementById("remove_contact").onclick = function() {
if (!session.isOnline) {
sessionsData.delete(currentSessionId);
msgHistory.get(currentSessionId).length = 0;
displayChatBottom();
}
pendingMsgs.delete(currentSessionId);
displayHeader();
displaySessions();
displayHistory();
@ -174,7 +178,7 @@ document.getElementById("attach_file").onchange = function(event) {
let files = event.target.files;
let useLargeFileTransfer = false;
for (let i=0; i<files.length; ++i) {
if (files[i].size > 32760000) {
if (files[i].size > 16380000) {
useLargeFileTransfer = true;
break;
}
@ -215,7 +219,11 @@ document.getElementById("attach_file").onchange = function(event) {
formData.append("", files[i]);
fetch("/send_file", {method: "POST", body: formData}).then(response => {
if (response.ok) {
response.text().then(uuid => onFileSent(currentSessionId, uuid, files[i].name));
response.text().then(text => {
if (text === "pending") {
newPendingMsg(currentSessionId, true, files[i].name);
}
});
} else {
console.log(response);
}
@ -226,16 +234,19 @@ document.getElementById("attach_file").onchange = function(event) {
document.getElementById("file_cancel").onclick = function() {
socket.send("abort "+currentSessionId);
};
let msg_log = document.getElementById("msg_log");
msg_log.onscroll = function() {
if (sessionsData.get(currentSessionId).isContact) {
if (msg_log.scrollTop < 30) {
socket.send("load_msgs "+currentSessionId);
let msgLog = document.getElementById("msg_log");
msgLog.onscroll = function() {
let session = sessionsData.get(currentSessionId);
if (typeof session !== "undefined") {
if (session.isContact) {
if (msgLog.scrollTop < 30) {
socket.send("load_msgs "+currentSessionId);
}
}
}
};
let profile_div = document.querySelector("#me>div");
profile_div.onclick = function() {
let profileDiv = document.querySelector("#me>div");
profileDiv.onclick = function() {
let mainDiv = document.createElement("div");
mainDiv.id = "profile_info";
let avatarContainer = document.createElement("div");
@ -324,32 +335,35 @@ profile_div.onclick = function() {
changePasswordButton.textContent = "Change password";
changePasswordButton.onclick = function() {
let inputs = document.querySelectorAll("input[type=\"password\"]");
let newPassword, newPasswordConfirm;
let newPassword, newPasswordConfirm, oldPassword;
if (isIdentityProtected) {
newPassword = inputs[1];
newPasswordConfirm = inputs[2];
oldPassword = inputs[0].value;
newPassword = inputs[1].value;
newPasswordConfirm = inputs[2].value;
} else {
newPassword = inputs[0];
newPasswordConfirm = inputs[1];
newPassword = inputs[0].value;
newPasswordConfirm = inputs[1].value;
}
if (newPassword.value == newPasswordConfirm.value) {
let newPassword_set = newPassword.value.length > 0;
if (isIdentityProtected || newPassword_set) { //don't change password if identity is not protected and new password is blank
if (newPassword == newPasswordConfirm) {
let newPasswordSet = newPassword.length > 0;
if (isIdentityProtected && oldPassword.length == 0) {
errorMsg.textContent = "Current password cannot be empty.";
} else if (isIdentityProtected || newPasswordSet) { //don't change password if identity is not protected and new password is blank
let msg = "change_password";
if (isIdentityProtected) {
msg += " "+b64EncodeUnicode(inputs[0].value);
}
if (newPassword_set) {
msg += " "+b64EncodeUnicode(newPassword.value);
if (newPasswordSet) {
msg += " "+b64EncodeUnicode(newPassword);
}
socket.send(msg);
} else {
removePopup();
}
} else {
newPassword.value = "";
newPasswordConfirm.value = "";
errorMsg.textContent = "Passwords don't match";
newPassword = "";
newPasswordConfirm = "";
errorMsg.textContent = "Passwords don't match.";
}
};
sectionPassword.appendChild(changePasswordButton);
@ -436,8 +450,17 @@ function getCookie(cname) {
}
return "";
}
function parseTimestamp(timestamp) {
return new Date(Number(timestamp) * 1000);
}
function toTwoNumbers(n) {
if (n < 10) {
return '0'+n;
} else {
return n;
}
}
socket = new WebSocket("ws://"+location.hostname+":"+websocketPort+"/ws");
socket.onopen = function() {
console.log("Connected");
socket.send(getCookie("aira_auth")); //authenticating websocket connection
@ -468,10 +491,10 @@ socket.onmessage = function(msg) {
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));
break;
case "new_message":
onNewMessage(args[1], args[2] === "true", msg.data.slice(args[0].length+args[1].length+args[2].length+3));
onNewMessage(args[1], args[2] === "true", parseTimestamp(args[3]), msg.data.slice(args[0].length+args[1].length+args[2].length+args[3].length+4));
break;
case "file":
onFileReceived(args[1], args[2], msg.data.slice(args[0].length+args[1].length+args[2].length+3));
onNewFile(args[1], args[2] === "true", parseTimestamp(args[3]), args[4], msg.data.slice(args[0].length+args[1].length+args[2].length+args[3].length+args[4].length+5));
break;
case "files_transfer":
onNewFilesTransfer(args[1], args[2], msg.data.slice(args[0].length+args[1].length+args[2].length+3));
@ -500,6 +523,15 @@ socket.onmessage = function(msg) {
case "not_seen":
setNotSeen(msg.data.slice(args[0].length+1));
break;
case "pending":
newPendingMsg(args[1], args[2] === "true", msg.data.slice(args[0].length+args[1].length+args[2].length+3));
break;
case "sending_pending_msgs":
onSendingPendingMsgs(args[1]);
break;
case "pending_msgs_sent":
onPendingMsgsSent(args[1]);
break;
case "local_ips":
setLocalIps(msg.data.slice(args[0].length+1));
break;
@ -519,6 +551,12 @@ socket.onmessage = function(msg) {
};
socket.onclose = function() {
console.log("Disconnected");
currentSessionId = -1;
displayHistory();
displayHeader();
displayChatBottom();
displaySessions();
document.getElementById("disconnected").classList.add("disconnected");
};
function onNewSession(sessionId, outgoing, fingerprint, ip, name) {
@ -564,6 +602,22 @@ function setNotSeen(strSessionIds) {
}
displaySessions();
}
function newPendingMsg(sessionId, isFile, data) {
pendingMsgs.get(sessionId).push([isFile, data]);
if (sessionId == currentSessionId) {
displayHistory();
}
}
function onSendingPendingMsgs(sessionId) {
document.getElementById("pending_msgs_indicator").classList.add("sending");
pendingMsgs.get(sessionId).length = 0;
if (sessionId == currentSessionId) {
displayHistory();
}
}
function onPendingMsgsSent(sessionId) {
document.getElementById("pending_msgs_indicator").classList.remove("sending");
}
function setLocalIps(strIPs) {
localIps = strIPs.split(' ');
}
@ -576,6 +630,7 @@ function onIsContact(sessionId, verified, fingerprint, name) {
} else {
addSession(sessionId, name, undefined, fingerprint, undefined, true, verified, false);
}
pendingMsgs.set(sessionId, []);
}
function onMsgOrFileReceived(sessionId, outgoing, body) {
if (currentSessionId == sessionId) {
@ -589,16 +644,22 @@ function onMsgOrFileReceived(sessionId, outgoing, body) {
}
if (document.hidden && !outgoing) {
if (notificationAllowed) {
new Notification(sessionsData.get(sessionId).name, {
"body": body
let sessionName = sessionsData.get(sessionId).name;
new Notification(sessionName, {
"body": body,
"icon": "/avatar/"+sessionId+"/"+sessionName+"?"+avatarTimestamps.get(sessionId)
});
}
}
}
function onNewMessage(sessionId, outgoing, msg) {
msgHistory.get(sessionId).push([outgoing, false, msg]);
function onNewMessage(sessionId, outgoing, timestamp, msg) {
msgHistory.get(sessionId).push([outgoing, timestamp, false, msg]);
onMsgOrFileReceived(sessionId, outgoing, msg);
}
function onNewFile(sessionId, outgoing, timestamp, uuid, filename) {
msgHistory.get(sessionId).push([outgoing, timestamp, true, [uuid, filename]]);
onMsgOrFileReceived(sessionId, outgoing, filename);
}
function onNewFilesTransfer(sessionId, index, filesInfo) {
let split = filesInfo.split(' ');
let files = [];
@ -683,7 +744,8 @@ function onAskLargeFiles(sessionId, encodedDownloadLocation, filesInfo) {
showPopup(mainDiv, false);
if (document.hidden && notificationAllowed) {
new Notification(sessionName, {
"body": "Files download request"
"body": "Files download request",
"icon": "/avatar/"+sessionId+"/"+sessionName+"?"+avatarTimestamps.get(sessionId)
});
}
}
@ -726,29 +788,32 @@ function onIncFilesTransfer(sessionId, chunkSize) {
}
function onMsgsLoad(sessionId, strMsgs) {
let msgs = strMsgs.split(' ');
let n = 0;
while (n < msgs.length) {
let outgoing = msgs[n+1] === "true";
switch (msgs[n]) {
case 'm':
let msg = b64DecodeUnicode(msgs[n+2]);
msgHistory.get(sessionId).unshift([outgoing, false, msg]);
n += 3;
break;
case 'f':
let uuid = msgs[n+2];
let fileName = b64DecodeUnicode(msgs[n+3]);
msgHistory.get(sessionId).unshift([outgoing, true, [uuid, fileName]]);
n += 4;
if (msgs.length > 3) {
let n = 0;
while (n < msgs.length) {
let outgoing = msgs[n+1] === "true";
let timestamp = parseTimestamp(msgs[n+2]);
switch (msgs[n]) {
case 'm':
let msg = b64DecodeUnicode(msgs[n+3]);
msgHistory.get(sessionId).unshift([outgoing, timestamp, false, msg]);
n += 4;
break;
case 'f':
let uuid = msgs[n+3];
let fileName = b64DecodeUnicode(msgs[n+4]);
msgHistory.get(sessionId).unshift([outgoing, timestamp, true, [uuid, fileName]]);
n += 5;
}
}
}
if (currentSessionId == sessionId) {
if (msg_log.scrollHeight - msg_log.scrollTop === msg_log.clientHeight) {
displayHistory();
} else {
let backupHeight = msg_log.scrollHeight;
displayHistory(false);
msg_log.scrollTop = msg_log.scrollHeight-backupHeight;
if (currentSessionId == sessionId) {
if (msgLog.scrollHeight - msgLog.scrollTop === msgLog.clientHeight) {
displayHistory();
} else {
let backupHeight = msgLog.scrollHeight;
displayHistory(false);
msgLog.scrollTop = msgLog.scrollHeight-backupHeight;
}
}
}
}
@ -762,6 +827,7 @@ function onDisconnected(sessionId) {
}
if (currentSessionId == sessionId) {
displayChatBottom();
scrollHistoryToBottom();
}
if (currentSessionId == sessionId && !session.isContact) {
currentSessionId = -1;
@ -769,16 +835,6 @@ function onDisconnected(sessionId) {
}
displaySessions();
}
function onFileReceived(sessionId, uuid, file_name) {
msgHistory.get(sessionId).push([false, true, [uuid, file_name]]);
onMsgOrFileReceived(sessionId, false, file_name);
}
function onFileSent(sessionId, uuid, file_name) {
msgHistory.get(sessionId).push([true, true, [uuid, file_name]]);
if (currentSessionId == sessionId) {
displayHistory();
}
}
function onNameSet(newName) {
removePopup();
identityName = newName;
@ -925,11 +981,11 @@ function logout() {
window.location = "/logout";
}
function displayProfile() {
profile_div.innerHTML = "";
profile_div.appendChild(generateSelfAvatar(avatarTimestamps.get("self")));
profileDiv.innerHTML = "";
profileDiv.appendChild(generateSelfAvatar(avatarTimestamps.get("self")));
let p = document.createElement("p");
p.textContent = identityName;
profile_div.appendChild(p);
profileDiv.appendChild(p);
}
function displayHeader() {
chatHeader.children[0].innerHTML = "";
@ -1002,9 +1058,7 @@ function generateSession(sessionId, session) {
li.classList.add("is_verified");
}
if (!session.seen) {
let marker = document.createElement("div");
marker.classList.add("not_seen_marker");
li.appendChild(marker);
li.classList.add("not_seen");
}
if (sessionId == currentSessionId) {
li.classList.add("current");
@ -1027,20 +1081,27 @@ function generateMsgHeader(name, sessionId) {
div.appendChild(p);
return div;
}
function generateMessageTimestamp(timestamp) {
let p = document.createElement("p");
p.classList.add("timestamp");
p.title = timestamp;
p.textContent = toTwoNumbers(timestamp.getHours())+":"+toTwoNumbers(timestamp.getMinutes());
return p;
}
function generateMessage(name, sessionId, msg) {
let p = document.createElement("p");
p.appendChild(document.createTextNode(msg));
let div = document.createElement("div");
div.classList.add("content");
div.appendChild(linkifyElement(p));
let li = document.createElement("li");
let divContainer = document.createElement("div");
if (typeof name !== "undefined") {
li.appendChild(generateMsgHeader(name, sessionId));
divContainer.appendChild(generateMsgHeader(name, sessionId));
}
li.appendChild(div);
return li;
divContainer.appendChild(div);
return divContainer;
}
function generateFile(name, sessionId, outgoing, file_info) {
function generateFile(name, sessionId, outgoing, fileInfo) {
let div1 = document.createElement("div");
div1.classList.add("file");
div1.classList.add("content");
@ -1052,20 +1113,24 @@ function generateFile(name, sessionId, outgoing, file_info) {
h4.textContent = "File received:";
}
div2.appendChild(h4);
let p = document.createElement("p");
p.textContent = file_info[1];
div2.appendChild(p);
div1.appendChild(div2);
let a = document.createElement("a");
a.href = "/load_file?uuid="+file_info[0]+"&file_name="+encodeURIComponent(file_info[1]);
a.target = "_blank";
div1.appendChild(a);
let li = document.createElement("li");
if (typeof name !== "undefined") {
li.appendChild(generateMsgHeader(name, sessionId));
let p = document.createElement("p");
if (typeof fileInfo === "string") { //pending
p.textContent = fileInfo;
} else {
p.textContent = fileInfo[1];
let a = document.createElement("a");
a.href = "/load_file?uuid="+fileInfo[0]+"&file_name="+encodeURIComponent(fileInfo[1]);
a.target = "_blank";
div1.appendChild(a);
}
li.appendChild(div1);
return li;
div2.appendChild(p);
let divContainer = document.createElement("div");
if (typeof name !== "undefined") {
divContainer.appendChild(generateMsgHeader(name, sessionId));
}
divContainer.appendChild(div1);
return divContainer;
}
function generateFileInfo(fileName, fileSize, p) {
let span = document.createElement("span");
@ -1078,13 +1143,16 @@ function displayChatBottom(speed = undefined) {
let fileTransfer = document.getElementById("file_transfer");
let session = sessionsData.get(currentSessionId);
if (typeof session === "undefined") {
msgBox.removeAttribute("style");
msgBox.classList.remove("active");
fileTransfer.classList.remove("active");
} else {
if (session.isContact || session.isOnline) {
msgBox.classList.add("active");
}
if (session.isOnline) {
msgBox.style.display = "flex";
msgBox.classList.add("online");
} else {
msgBox.removeAttribute("style");
msgBox.classList.remove("online");
}
if (pendingFilesTransfers.has(currentSessionId)) {
let fileInfo = document.getElementById("file_info");
@ -1129,35 +1197,72 @@ function displayChatBottom(speed = undefined) {
}
}
}
function scrollHistoryToBottom() {
msgLog.scrollTop = msgLog.scrollHeight;
}
function displayHistory(scrollToBottom = true) {
msg_log.style.display = "block";
msg_log.innerHTML = "";
msgLog.innerHTML = "";
let session = sessionsData.get(currentSessionId);
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;
if (typeof session === "undefined") {
msgLog.style.display = "none";
} else {
msgLog.style.display = "block";
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;
}
}
let div;
if (entry[2]) { //is file
div = generateFile(name, sessionId, entry[0], entry[3]);
} else {
name = session.name;
sessionId = currentSessionId;
div = generateMessage(name, sessionId, entry[3]);
}
let li = document.createElement("li");
li.appendChild(div);
li.appendChild(generateMessageTimestamp(entry[1]));
msgLog.appendChild(li);
});
if (session.isContact) {
let msgs = pendingMsgs.get(currentSessionId);
if (msgs.length > 0) {
let li = document.createElement("li");
li.classList.add("pending_msgs_divider");
let h4 = document.createElement("h4");
h4.textContent = "Pending messages:";
li.appendChild(h4);
msgLog.appendChild(li);
msgs.forEach(entry => {
let name = undefined;
if (previousOutgoing != true) {
previousOutgoing = true;
name = identityName;
}
let div;
if (entry[0]) { //is file
div = generateFile(name, undefined, true, entry[1]);
} else {
div = generateMessage(name, undefined, entry[1]);
}
let li = document.createElement("li");
li.appendChild(div);
msgLog.appendChild(li);
});
}
}
if (entry[1]) { //is file
msg_log.appendChild(generateFile(name, sessionId, entry[0], entry[2]));
} else {
msg_log.appendChild(generateMessage(name, sessionId, entry[2]));
if (scrollToBottom) {
scrollHistoryToBottom();
}
});
if (scrollToBottom) {
msg_log.scrollTop = msg_log.scrollHeight;
}
if (typeof session !== "undefined") {
if (msg_log.scrollHeight <= msg_log.clientHeight && session.isContact) {
if (msgLog.scrollHeight <= msgLog.clientHeight && session.isContact) {
socket.send("load_msgs "+currentSessionId);
}
}
}
}

View File

@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<title>AIRA - Login</title>
<link rel="icon" type="image/svg" href="/static/imgs/icons/logo">
<link rel="stylesheet" href="/static/commons/style.css">
<style>
body {
@ -58,8 +59,7 @@
padding: 10px 50px;
}
.avatar {
width: 7em;
height: 7em;
font-size: 3em;
}
#identity h2 {
text-align: center;

View File

@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<title>AIRA - Logged out</title>
<link rel="icon" type="image/svg" href="/static/imgs/icons/logo">
<link rel="stylesheet" href="/static/commons/style.css">
<style>
body {

View File

@ -43,12 +43,11 @@ fn get_database_path() -> String {
AppDirs::new(Some(constants::APPLICATION_FOLDER), false).unwrap().data_dir.join(constants::DB_NAME).to_str().unwrap().to_owned()
}
struct EncryptedIdentity {
name: String,
encrypted_keypair: Vec<u8>,
salt: Vec<u8>,
encrypted_master_key: Vec<u8>,
encrypted_use_padding: Vec<u8>,
#[derive(Debug, Clone)]
pub struct Message {
pub outgoing: bool,
pub timestamp: u64,
pub data: Vec<u8>,
}
pub struct Contact {
@ -60,6 +59,14 @@ pub struct Contact {
pub seen: bool,
}
struct EncryptedIdentity {
name: String,
encrypted_keypair: Vec<u8>,
salt: Vec<u8>,
encrypted_master_key: Vec<u8>,
encrypted_use_padding: Vec<u8>,
}
pub struct Identity {
pub name: String,
pub keypair: Keypair,
@ -87,8 +94,8 @@ impl Identity {
};
Ok(Contact {
uuid: contact_uuid,
public_key: public_key,
name: name,
public_key,
name,
avatar: avatar_uuid,
verified: false,
seen: true,
@ -242,16 +249,17 @@ impl Identity {
Ok(file_uuid)
}
pub fn store_msg(&self, contact_uuid: &Uuid, outgoing: bool, data: &[u8]) -> Result<usize, rusqlite::Error> {
pub fn store_msg(&self, contact_uuid: &Uuid, message: &Message) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (outgoing BLOB, data BLOB)", contact_uuid), [])?;
let outgoing_byte: u8 = bool_to_byte(outgoing);
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (outgoing BLOB, timestamp BLOB, data BLOB)", contact_uuid), [])?;
let outgoing_byte: u8 = bool_to_byte(message.outgoing);
let encrypted_outgoing = crypto::encrypt_data(&[outgoing_byte], &self.master_key).unwrap();
let encrypted_data = crypto::encrypt_data(data, &self.master_key).unwrap();
db.execute(&format!("INSERT INTO \"{}\" (outgoing, data) VALUES (?1, ?2)", contact_uuid), params![encrypted_outgoing, encrypted_data])
let encrypted_timestamp = crypto::encrypt_data(&message.timestamp.to_be_bytes(), &self.master_key).unwrap();
let encrypted_data = crypto::encrypt_data(&message.data, &self.master_key).unwrap();
db.execute(&format!("INSERT INTO \"{}\" (outgoing, timestamp, data) VALUES (?1, ?2, ?3)", contact_uuid), params![encrypted_outgoing, encrypted_timestamp, encrypted_data])
}
pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option<Vec<(bool, Vec<u8>)>> {
pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option<Vec<Message>> {
match Connection::open(get_database_path()) {
Ok(db) => {
if let Ok(mut stmt) = db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
@ -262,24 +270,30 @@ impl Identity {
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 stmt = db.prepare(&format!("SELECT outgoing, timestamp, 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)
if let Ok(outgoing) = byte_to_bool(outgoing[0]) {
let encrypted_timestamp: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(&encrypted_timestamp, &self.master_key) {
Ok(timestamp) => {
let encrypted_data: Vec<u8> = row.get(2).unwrap();
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
Ok(data) => msgs.push(Message {
outgoing,
timestamp: u64::from_be_bytes(timestamp.try_into().unwrap()),
data,
}),
Err(e) => print_error!(e)
}
}
Err(e) => print_error!(e)
}
Err(_) => {}
}
}
Err(e) => print_error!(e)
}
@ -364,14 +378,8 @@ impl Identity {
pub fn load_identity(password: Option<&[u8]>) -> Result<Identity, String> {
match Identity::load_encrypted_identity() {
Ok(encrypted_identity) => {
let master_key: [u8; crypto::MASTER_KEY_LEN] = if password.is_none() {
if encrypted_identity.encrypted_master_key.len() == crypto::MASTER_KEY_LEN {
encrypted_identity.encrypted_master_key.try_into().unwrap()
} else {
return Err(String::from(DATABASE_CORRUPED_ERROR))
}
} else {
match crypto::decrypt_master_key(&encrypted_identity.encrypted_master_key, password.unwrap(), &encrypted_identity.salt) {
let master_key: [u8; crypto::MASTER_KEY_LEN] = match password {
Some(password) => match crypto::decrypt_master_key(&encrypted_identity.encrypted_master_key, password, &encrypted_identity.salt) {
Ok(master_key) => master_key,
Err(e) => return Err(
match e {
@ -380,6 +388,11 @@ impl Identity {
}
)
}
None => if encrypted_identity.encrypted_master_key.len() == crypto::MASTER_KEY_LEN {
encrypted_identity.encrypted_master_key.try_into().unwrap()
} else {
return Err(String::from(DATABASE_CORRUPED_ERROR))
}
};
match crypto::decrypt_data(&encrypted_identity.encrypted_keypair, &master_key) {
Ok(keypair) => {
@ -425,13 +438,16 @@ impl Identity {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
db.set(DBKeys::NAME, name.as_bytes())?;
db.set(DBKeys::KEYPAIR, &encrypted_keypair)?;
let salt = if password.is_none() { //no password
db.set(DBKeys::MASTER_KEY, &master_key)?; //storing master_key in plaintext
[0; crypto::SALT_LEN]
} else {
let (salt, encrypted_master_key) = crypto::encrypt_master_key(master_key, password.unwrap());
db.set(DBKeys::MASTER_KEY, &encrypted_master_key)?;
salt
let salt = match password {
Some(password) => {
let (salt, encrypted_master_key) = crypto::encrypt_master_key(master_key, password);
db.set(DBKeys::MASTER_KEY, &encrypted_master_key)?;
salt
}
None => {
db.set(DBKeys::MASTER_KEY, &master_key)?; //storing master_key in plaintext
[0; crypto::SALT_LEN]
}
};
db.set(DBKeys::SALT, &salt)?;
let encrypted_use_padding = crypto::encrypt_data(&[bool_to_byte(true)], &master_key).unwrap();
@ -446,13 +462,16 @@ impl Identity {
fn update_master_key(master_key: [u8; crypto::MASTER_KEY_LEN], new_password: Option<&[u8]>) -> Result<usize, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
let salt = if new_password.is_none() { //no password
db.update(DBKeys::MASTER_KEY, &master_key)?;
[0; crypto::SALT_LEN]
} else {
let (salt, encrypted_master_key) = crypto::encrypt_master_key(master_key, new_password.unwrap());
db.update(DBKeys::MASTER_KEY, &encrypted_master_key)?;
salt
let salt = match new_password {
Some(new_password) => {
let (salt, encrypted_master_key) = crypto::encrypt_master_key(master_key, new_password);
db.update(DBKeys::MASTER_KEY, &encrypted_master_key)?;
salt
}
None => {
db.update(DBKeys::MASTER_KEY, &master_key)?;
[0; crypto::SALT_LEN]
}
};
db.update(DBKeys::SALT, &salt)
}
@ -460,20 +479,19 @@ impl Identity {
pub fn change_password(old_password: Option<&[u8]>, new_password: Option<&[u8]>) -> Result<bool, String> {
match Identity::load_encrypted_identity() {
Ok(encrypted_identity) => {
let master_key: [u8; crypto::MASTER_KEY_LEN] = if old_password.is_none() {
if encrypted_identity.encrypted_master_key.len() == crypto::MASTER_KEY_LEN {
encrypted_identity.encrypted_master_key.try_into().unwrap()
} else {
return Err(String::from(DATABASE_CORRUPED_ERROR))
}
} else {
match crypto::decrypt_master_key(&encrypted_identity.encrypted_master_key, old_password.unwrap(), &encrypted_identity.salt) {
let master_key: [u8; crypto::MASTER_KEY_LEN] = match old_password {
Some(old_password) => match crypto::decrypt_master_key(&encrypted_identity.encrypted_master_key, old_password, &encrypted_identity.salt) {
Ok(master_key) => master_key,
Err(e) => return match e {
CryptoError::DecryptionFailed => Ok(false),
CryptoError::InvalidLength => Err(String::from(DATABASE_CORRUPED_ERROR))
}
}
None => if encrypted_identity.encrypted_master_key.len() == crypto::MASTER_KEY_LEN {
encrypted_identity.encrypted_master_key.try_into().unwrap()
} else {
return Err(String::from(DATABASE_CORRUPED_ERROR))
}
};
match Identity::update_master_key(master_key, new_password) {
Ok(_) => Ok(true),

View File

@ -12,7 +12,7 @@ impl<'a> KeyValueTable<'a> {
Ok(KeyValueTable {db, table_name})
}
pub fn set(&self, key: &str, value: &[u8]) -> Result<usize, Error> {
Ok(self.db.execute(&format!("INSERT INTO {} (key, value) VALUES (?1, ?2)", self.table_name), params![key, value])?)
self.db.execute(&format!("INSERT INTO {} (key, value) VALUES (?1, ?2)", self.table_name), params![key, value])
}
pub fn get(&self, key: &str) -> Result<Vec<u8>, Error> {
let mut stmt = self.db.prepare(&format!("SELECT value FROM {} WHERE key=\"{}\"", self.table_name, key))?;

View File

@ -8,12 +8,12 @@ mod ui_interface;
mod constants;
mod discovery;
use std::{env, fs, io, net::SocketAddr, str::{FromStr, from_utf8}, sync::{Arc, RwLock}};
use std::{env, fs, io::{self, Cursor}, net::SocketAddr, str::{FromStr, from_utf8}, sync::{Arc, RwLock}, cmp::Ordering};
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;
use tokio::{net::TcpListener, runtime::Handle, sync::mpsc, task::JoinError};
use tungstenite::Message;
use actix_web::{App, HttpRequest, HttpResponse, HttpServer, http::header, cookie::CookieBuilder, web, web::Data};
use actix_multipart::Multipart;
use futures::{StreamExt, TryStreamExt};
use rand::{RngCore, rngs::OsRng};
use serde::{Deserialize, Serialize};
@ -25,41 +25,36 @@ use identity::Identity;
use session_manager::{SessionManager, SessionCommand};
use ui_interface::UiConnection;
async fn start_websocket_server(global_vars: Arc<RwLock<GlobalVars>>) -> u16 {
let websocket_bind_addr = env::var("AIRA_WEBSOCKET_ADDR").unwrap_or("127.0.0.1".to_owned());
let websocket_port = env::var("AIRA_WEBSOCKET_PORT").unwrap_or("0".to_owned());
async fn start_websocket_server(ui_auth_token: Arc<RwLock<Option<String>>>, session_manager: Arc<SessionManager>) -> u16 {
let websocket_bind_addr = env::var("AIRA_WEBSOCKET_ADDR").unwrap_or_else(|_| "127.0.0.1".to_owned());
let websocket_port = env::var("AIRA_WEBSOCKET_PORT").unwrap_or_else(|_| "0".to_owned());
let server = TcpListener::bind(websocket_bind_addr+":"+&websocket_port).await.unwrap();
let websocket_port = server.local_addr().unwrap().port();
tokio::spawn(async move {
let worker_done = Arc::new(RwLock::new(true));
loop {
let (stream, _addr) = server.accept().await.unwrap();
if *worker_done.read().unwrap() {
let ui_auth_token = {
global_vars.clone().read().unwrap().ui_auth_token.clone()
};
if let Some(ui_auth_token) = ui_auth_token {
let stream = stream.into_std().unwrap();
stream.set_nonblocking(false).unwrap();
match tungstenite::accept(stream.try_clone().unwrap()) {
Ok(mut websocket) => {
if let Ok(message) = websocket.read_message() { //waiting for auth token
match message.into_text() {
Ok(token) => {
if token == ui_auth_token {
let ui_connection = UiConnection::new(websocket);
let global_vars = global_vars.clone();
global_vars.read().unwrap().session_manager.set_ui_connection(ui_connection.clone());
*worker_done.write().unwrap() = false;
websocket_worker(ui_connection, global_vars, worker_done.clone()).await;
}
let ui_auth_token = {
ui_auth_token.read().unwrap().clone()
};
if let Some(ui_auth_token) = ui_auth_token {
let stream = stream.into_std().unwrap();
stream.set_nonblocking(false).unwrap();
match tungstenite::accept(stream) {
Ok(mut websocket) => {
if let Ok(message) = websocket.read_message() { //waiting for auth token
match message.into_text() {
Ok(token) => {
if token == ui_auth_token {
let ui_connection = UiConnection::new(websocket);
session_manager.set_ui_connection(ui_connection.clone());
websocket_worker(ui_connection, session_manager.clone()).await.unwrap();
}
Err(e) => print_error!(e)
}
Err(e) => print_error!(e)
}
}
Err(e) => print_error!(e)
}
Err(e) => print_error!(e)
}
}
}
@ -70,7 +65,6 @@ async fn start_websocket_server(global_vars: Arc<RwLock<GlobalVars>>) -> u16 {
fn discover_peers(session_manager: Arc<SessionManager>) {
tokio::spawn(async move {
discovery::discover_peers(move |discovery_manager, ip| {
println!("New peer discovered: {}", ip);
let session_manager = session_manager.clone();
if session_manager.is_identity_loaded() {
tokio::spawn( async move {
@ -85,19 +79,18 @@ fn discover_peers(session_manager: Arc<SessionManager>) {
});
}
fn load_msgs(session_manager: Arc<SessionManager>, ui_connection: &mut UiConnection, session_id: &usize) {
fn load_msgs(session_manager: &SessionManager, ui_connection: &mut UiConnection, session_id: &usize) {
if let Some(msgs) = session_manager.load_msgs(session_id, constants::MSG_LOADING_COUNT) {
ui_connection.load_msgs(session_id, &msgs);
}
}
async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLock<GlobalVars>>, worker_done: Arc<RwLock<bool>>) {
let session_manager = global_vars.read().unwrap().session_manager.clone();
async fn websocket_worker(mut ui_connection: UiConnection, session_manager: Arc<SessionManager>) -> Result<(), JoinError> {
ui_connection.set_name(&session_manager.identity.read().unwrap().as_ref().unwrap().name);
session_manager.list_contacts().into_iter().for_each(|contact|{
ui_connection.set_as_contact(contact.0, &contact.1, contact.2, &crypto::generate_fingerprint(&contact.3));
session_manager.last_loaded_msg_offsets.write().unwrap().insert(contact.0, 0);
load_msgs(session_manager.clone(), &mut ui_connection, &contact.0);
load_msgs(&session_manager, &mut ui_connection, &contact.0);
});
session_manager.sessions.read().unwrap().iter().for_each(|session| {
ui_connection.on_new_session(
@ -112,14 +105,28 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
{
let not_seen = session_manager.not_seen.read().unwrap();
if not_seen.len() > 0 {
ui_connection.set_not_seen(not_seen.clone());
ui_connection.set_not_seen(&not_seen);
}
}
session_manager.get_saved_msgs().into_iter().for_each(|msgs| {
if msgs.1.len() > 0 {
if !msgs.1.is_empty() {
ui_connection.load_msgs(&msgs.0, &msgs.1);
}
});
session_manager.pending_msgs.lock().unwrap().iter().for_each(|entry| {
entry.1.iter().for_each(|buff| {
match buff[0] {
protocol::Headers::MESSAGE => match from_utf8(&buff[1..]) {
Ok(msg) => ui_connection.new_pending_msg(entry.0, false, msg),
Err(e) => print_error!(e)
}
protocol::Headers::FILE => if let Some(filename) = protocol::get_file_name(buff) {
ui_connection.new_pending_msg(entry.0, true, filename);
}
_ => {}
}
});
});
let mut ips = Vec::new();
match if_addrs::get_if_addrs() {
Ok(ifaces) => for iface in ifaces {
@ -129,10 +136,10 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
}
Err(e) => print_error!(e)
}
ui_connection.set_local_ips(ips);
ui_connection.set_local_ips(&ips);
discover_peers(session_manager.clone());
let handle = Handle::current();
std::thread::spawn(move || { //new thread needed to block on read_message() without blocking tokio tasks
tokio::task::spawn_blocking(move || {
loop {
match ui_connection.websocket.read_message() {
Ok(msg) => {
@ -140,10 +147,12 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
ui_connection.write_message(Message::Pong(Vec::new())); //not sure if I'm doing this right
} else if msg.is_text() {
let msg = msg.into_text().unwrap();
#[cfg(debug_assertions)]
println!("Message: {}", msg);
let mut ui_connection = ui_connection.clone();
let session_manager = session_manager.clone();
handle.spawn(async move {
let args: Vec<&str> = msg.split(" ").collect();
let args: Vec<&str> = msg.split_whitespace().collect();
match args[0] {
"set_seen" => {
let session_id: usize = args[1].parse().unwrap();
@ -160,11 +169,14 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
"refresh" => discover_peers(session_manager.clone()),
"send" => {
let session_id: usize = args[1].parse().unwrap();
let buffer = protocol::new_message(msg[args[0].len()+args[1].len()+2..].to_string());
if session_manager.send_command(&session_id, SessionCommand::Send {
buff: buffer.clone()
}).await {
session_manager.store_msg(&session_id, true, buffer);
let msg_content = &msg[args[0].len()+args[1].len()+2..];
let buffer = protocol::new_message(msg_content);
#[allow(unused_must_use)] {
if let Ok(sent) = session_manager.send_or_add_to_pending(&session_id, buffer).await {
if !sent {
ui_connection.new_pending_msg(&session_id, false, msg_content);
}
}
}
}
"large_files" => {
@ -173,9 +185,9 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
for n in (2..args.len()).step_by(2) {
file_info.push((args[n].parse::<u64>().unwrap(), base64::decode(args[n+1]).unwrap()));
}
session_manager.send_command(&session_id, SessionCommand::Send {
buff: protocol::ask_large_files(file_info)
}).await;
#[allow(unused_must_use)] {
session_manager.send_or_add_to_pending(&session_id, protocol::ask_large_files(file_info)).await;
}
}
"download" => {
let session_id: usize = args[1].parse().unwrap();
@ -195,7 +207,7 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
}
"load_msgs" => {
let session_id: usize = args[1].parse().unwrap();
load_msgs(session_manager.clone(), &mut ui_connection, &session_id);
load_msgs(&session_manager, &mut ui_connection, &session_id);
}
"contact" => {
let session_id: usize = args[1].parse().unwrap();
@ -206,7 +218,7 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
}
"uncontact" => {
let session_id: usize = args[1].parse().unwrap();
match session_manager.remove_contact(session_id) {
match session_manager.remove_contact(&session_id) {
Ok(_) => {},
Err(e) => print_error!(e)
}
@ -259,11 +271,11 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
(None, Some(base64::decode(args[1]).unwrap()))
};
let result = Identity::change_password(old_password.as_deref(), new_password.as_deref());
if old_password.is_some() {
old_password.unwrap().zeroize();
if let Some(mut old_password) = old_password {
old_password.zeroize();
}
let is_identity_protected = if new_password.is_some() {
new_password.unwrap().zeroize();
let is_identity_protected = if let Some(mut new_password) = new_password {
new_password.zeroize();
true
} else {
false
@ -287,7 +299,6 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
Err(e) => {
match e {
tungstenite::Error::ConnectionClosed => {
*worker_done.write().unwrap() = true;
break;
}
_ => print_error!(e)
@ -295,13 +306,13 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
}
}
}
});
}).await
}
fn is_authenticated(req: &HttpRequest) -> bool {
if let Some(cookie) = req.cookie(constants::HTTP_COOKIE_NAME) {
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
if let Some(token) = &global_vars.read().unwrap().ui_auth_token {
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
if let Some(token) = global_vars.ui_auth_token.read().unwrap().as_ref() {
return token == cookie.value();
}
}
@ -310,7 +321,7 @@ fn is_authenticated(req: &HttpRequest) -> bool {
async fn handle_set_avatar(req: HttpRequest, mut payload: Multipart) -> HttpResponse {
if let Ok(Some(mut field)) = payload.try_next().await {
let content_disposition = field.content_disposition().unwrap();
let content_disposition = field.content_disposition();
if let Some(name) = content_disposition.get_name() {
if name == "avatar" {
let mut buffer = Vec::new();
@ -322,17 +333,15 @@ async fn handle_set_avatar(req: HttpRequest, mut payload: Multipart) -> HttpResp
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 image = match width.cmp(&height) {
Ordering::Greater => image.crop_imm((width-height)/2, 0, height, height),
Ordering::Less => image.crop_imm(0, (height-width)/2, width, width),
Ordering::Equal => image,
};
let mut avatar = Vec::new();
image.write_to(&mut avatar, format).unwrap();
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
let session_manager = &global_vars.read().unwrap().session_manager;
image.write_to(&mut Cursor::new(&mut avatar), format).unwrap();
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
let session_manager = &global_vars.session_manager;
let is_authenticated = is_authenticated(&req);
let is_running = session_manager.is_identity_loaded();
if is_authenticated || !is_running {
@ -370,23 +379,23 @@ fn reply_with_avatar(avatar: Option<Vec<u8>>, name: Option<&str>) -> HttpRespons
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_or('?').to_string()))
HttpResponse::Ok().content_type("image/svg+xml").body(svg.replace("LETTER", &name.chars().next().unwrap_or('?').to_string()))
}
None => HttpResponse::InternalServerError().finish()
}
}
}
fn handle_avatar(req: HttpRequest) -> HttpResponse {
let splits: Vec<&str> = req.path()[1..].split("/").collect();
async 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().ok().as_deref());
}
} else if splits.len() == 3 && is_authenticated(&req) {
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), Some(splits[2]));
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
return reply_with_avatar(global_vars.session_manager.get_avatar(&session_id), Some(splits[2]));
}
}
HttpResponse::BadRequest().finish()
@ -397,16 +406,13 @@ struct FileInfo {
uuid: String,
file_name: String,
}
fn handle_load_file(req: HttpRequest, file_info: web::Query<FileInfo>) -> HttpResponse {
async fn handle_load_file(req: HttpRequest, file_info: web::Query<FileInfo>) -> HttpResponse {
if is_authenticated(&req) {
match Uuid::from_str(&file_info.uuid) {
Ok(uuid) => {
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
match global_vars.read().unwrap().session_manager.identity.read().unwrap().as_ref().unwrap().load_file(uuid) {
Some(buffer) => {
return HttpResponse::Ok().header("Content-Disposition", format!("attachment; filename=\"{}\"", escape_double_quote(html_escape::decode_html_entities(&file_info.file_name).to_string()))).content_type("application/octet-stream").body(buffer);
}
None => {}
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
if let Some(buffer) = global_vars.session_manager.identity.read().unwrap().as_ref().unwrap().load_file(uuid) {
return HttpResponse::Ok().append_header(("Content-Disposition", format!("attachment; filename=\"{}\"", escape_double_quote(html_escape::decode_html_entities(&file_info.file_name).to_string())))).content_type("application/octet-stream").body(buffer);
}
}
Err(e) => print_error!(e)
@ -419,33 +425,27 @@ async fn handle_send_file(req: HttpRequest, mut payload: Multipart) -> HttpRespo
if is_authenticated(&req) {
let mut session_id: Option<usize> = None;
while let Ok(Some(mut field)) = payload.try_next().await {
let content_disposition = field.content_disposition().unwrap();
let content_disposition = field.content_disposition();
if let Some(name) = content_disposition.get_name() {
if name == "session_id" {
if let Some(Ok(raw_id)) = field.next().await {
session_id = Some(from_utf8(&raw_id).unwrap().parse().unwrap());
}
} else if session_id.is_some() {
let filename = content_disposition.get_filename().unwrap();
let filename = content_disposition.get_filename().unwrap().to_owned();
let session_id = session_id.unwrap();
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
let global_vars_read = global_vars.read().unwrap();
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
if req.path() == "/send_file" {
let mut buffer = Vec::new();
while let Some(Ok(chunk)) = field.next().await {
buffer.extend(chunk);
}
if global_vars_read.session_manager.send_command(&session_id, SessionCommand::Send {
buff: protocol::file(filename, &buffer)
}).await {
match global_vars_read.session_manager.store_file(&session_id, &buffer) {
Ok(file_uuid) => {
let msg = [&[protocol::Headers::FILE][..], file_uuid.as_bytes(), filename.as_bytes()].concat();
global_vars_read.session_manager.store_msg(&session_id, true, msg);
return HttpResponse::Ok().body(file_uuid.to_string());
}
Err(e) => print_error!(e)
}
if let Ok(sent) = global_vars.session_manager.send_or_add_to_pending(&session_id, protocol::file(&filename, &buffer)).await {
return if sent {
HttpResponse::Ok().finish()
} else {
HttpResponse::Ok().body("pending")
};
}
} else {
let (ack_sender, mut ack_receiver) = mpsc::channel(1);
@ -469,7 +469,7 @@ async fn handle_send_file(req: HttpRequest, mut payload: Multipart) -> HttpRespo
break;
}
}
if !global_vars_read.session_manager.send_command(&session_id, SessionCommand::EncryptFileChunk{
if !global_vars.session_manager.send_command(&session_id, SessionCommand::EncryptFileChunk{
plain_text: chunk_buffer.clone()
}).await {
return HttpResponse::InternalServerError().finish();
@ -477,7 +477,7 @@ async fn handle_send_file(req: HttpRequest, mut payload: Multipart) -> HttpRespo
if !match ack_receiver.recv().await {
Some(should_continue) => {
//send previous encrypted chunk even if transfert is aborted to keep PSEC nonces syncrhonized
if global_vars_read.session_manager.send_command(&session_id, SessionCommand::SendEncryptedFileChunk {
if global_vars.session_manager.send_command(&session_id, SessionCommand::SendEncryptedFileChunk {
ack_sender: ack_sender.clone()
}).await {
should_continue
@ -506,29 +506,31 @@ async fn handle_send_file(req: HttpRequest, mut payload: Multipart) -> HttpRespo
async fn handle_logout(req: HttpRequest) -> HttpResponse {
if is_authenticated(&req) {
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
let mut global_vars_write = global_vars.write().unwrap();
if global_vars_write.session_manager.is_identity_loaded() {
global_vars_write.ui_auth_token = None;
global_vars_write.session_manager.stop().await;
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
if global_vars.session_manager.is_identity_loaded() {
*global_vars.ui_auth_token.write().unwrap() = None;
global_vars.session_manager.stop().await;
}
if Identity::is_protected().unwrap_or(true) {
HttpResponse::Found().header(header::LOCATION, "/").finish()
HttpResponse::Found().append_header((header::LOCATION, "/")).finish()
} else {
HttpResponse::Ok().body(include_str!("frontend/logout.html"))
#[cfg(debug_assertions)]
let html = fs::read_to_string("src/frontend/logout.html").unwrap();
#[cfg(not(debug_assertions))]
let html = include_str!("frontend/logout.html");
HttpResponse::Ok().body(html)
}
} else {
HttpResponse::Unauthorized().finish()
}
}
fn login(identity: Identity, global_vars: &Arc<RwLock<GlobalVars>>) -> HttpResponse {
let mut global_vars_write = global_vars.write().unwrap();
let session_manager = global_vars_write.session_manager.clone();
fn login(identity: Identity, global_vars: &GlobalVars) -> HttpResponse {
let session_manager = global_vars.session_manager.clone();
if !session_manager.is_identity_loaded() {
global_vars_write.session_manager.set_identity(Some(identity));
global_vars_write.tokio_handle.clone().spawn(async move {
if SessionManager::start_listener(session_manager.clone()).await.is_err() {
session_manager.set_identity(Some(identity));
global_vars.tokio_handle.spawn(async move {
if SessionManager::start_listener(session_manager).await.is_err() {
print_error!("You won't be able to receive incomming connections from other peers.");
}
});
@ -536,15 +538,15 @@ fn login(identity: Identity, global_vars: &Arc<RwLock<GlobalVars>>) -> HttpRespo
let mut raw_cookie = [0; 32];
OsRng.fill_bytes(&mut raw_cookie);
let cookie_value = base64::encode(raw_cookie);
global_vars_write.ui_auth_token = Some(cookie_value.clone());
*global_vars.ui_auth_token.write().unwrap() = Some(cookie_value.clone());
let cookie = CookieBuilder::new(constants::HTTP_COOKIE_NAME, cookie_value).max_age(time::Duration::hours(4)).finish();
HttpResponse::Found()
.header(header::LOCATION, "/")
.set_header(header::SET_COOKIE, cookie.to_string())
.append_header((header::LOCATION, "/"))
.insert_header((header::SET_COOKIE, cookie.to_string()))
.finish()
}
fn on_identity_loaded(identity: Identity, global_vars: &Arc<RwLock<GlobalVars>>) -> HttpResponse {
fn on_identity_loaded(identity: Identity, global_vars: &Arc<GlobalVars>) -> HttpResponse {
match Identity::clear_cache() {
Ok(_) => {},
Err(e) => print_error!(e)
@ -556,13 +558,13 @@ fn on_identity_loaded(identity: Identity, global_vars: &Arc<RwLock<GlobalVars>>)
struct LoginParams {
password: String,
}
fn handle_login(req: HttpRequest, mut params: web::Form<LoginParams>) -> HttpResponse {
async fn handle_login(req: HttpRequest, mut params: web::Form<LoginParams>) -> HttpResponse {
let response = match Identity::load_identity(Some(params.password.as_bytes())) {
Ok(identity) => {
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
on_identity_loaded(identity, global_vars)
}
Err(e) => generate_login_response(Some(&e.to_string()))
Err(e) => generate_login_response(Some(&e))
};
params.password.zeroize();
response
@ -611,15 +613,15 @@ async fn handle_create(req: HttpRequest, mut params: web::Form<CreateParams>) ->
let response = if params.password == params.password_confirm {
match Identity::create_identidy(
&params.name,
if params.password.len() == 0 { //no password
if params.password.is_empty() { //no password
None
} else {
Some(params.password.as_bytes())
}
) {
Ok(identity) => {
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
login(identity, global_vars.get_ref())
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
login(identity, global_vars)
}
Err(e) => {
print_error!(e);
@ -634,7 +636,7 @@ async fn handle_create(req: HttpRequest, mut params: web::Form<CreateParams>) ->
response
}
fn index_not_logged_in(global_vars: &Arc<RwLock<GlobalVars>>) -> HttpResponse {
fn index_not_logged_in(global_vars: &Arc<GlobalVars>) -> HttpResponse {
if Identity::is_protected().unwrap_or(true) {
generate_login_response(None)
} else {
@ -646,22 +648,21 @@ fn index_not_logged_in(global_vars: &Arc<RwLock<GlobalVars>>) -> HttpResponse {
}
async fn handle_index(req: HttpRequest) -> HttpResponse {
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
if is_authenticated(&req) {
let global_vars_read = global_vars.read().unwrap();
#[cfg(debug_assertions)]
let html = fs::read_to_string("src/frontend/index.html").unwrap()
.replace("AIRA_VERSION", env!("CARGO_PKG_VERSION"));
#[cfg(not(debug_assertions))]
let html = include_str!(concat!(env!("OUT_DIR"), "/index.html"));
let public_key = global_vars_read.session_manager.identity.read().unwrap().as_ref().unwrap().get_public_key();
let use_padding = global_vars_read.session_manager.identity.read().unwrap().as_ref().unwrap().use_padding.to_string();
let identity = global_vars.session_manager.identity.read().unwrap();
let identity = identity.as_ref().unwrap();
HttpResponse::Ok().body(
html
.replace("IDENTITY_FINGERPRINT", &crypto::generate_fingerprint(&public_key))
.replace("WEBSOCKET_PORT", &global_vars_read.websocket_port.to_string())
.replace("IDENTITY_FINGERPRINT", &crypto::generate_fingerprint(&identity.get_public_key()))
.replace("WEBSOCKET_PORT", &global_vars.websocket_port.to_string())
.replace("IS_IDENTITY_PROTECTED", &Identity::is_protected().unwrap().to_string())
.replace("PSEC_PADDING", &use_padding)
.replace("PSEC_PADDING", &identity.use_padding.to_string())
)
} else {
index_not_logged_in(global_vars)
@ -682,8 +683,8 @@ fn replace_fields(file_path: &str) -> String {
content
}
fn handle_static(req: HttpRequest) -> HttpResponse {
let splits: Vec<&str> = req.path()[1..].split("/").collect();
async fn handle_static(req: HttpRequest) -> HttpResponse {
let splits: Vec<&str> = req.path()[1..].split('/').collect();
if splits[0] == "static" {
let mut response_builder = HttpResponse::Ok();
match splits[1] {
@ -707,7 +708,8 @@ fn handle_static(req: HttpRequest) -> HttpResponse {
} else {
"none"
};
match match splits[3] {
if let Some(body) = match splits[3] {
"logo" => Some(include_str!("frontend/imgs/icons/logo.svg")),
"verified" => Some(include_str!("frontend/imgs/icons/verified.svg")),
"add_contact" => Some(include_str!("frontend/imgs/icons/add_contact.svg")),
"remove_contact" => Some(include_str!("frontend/imgs/icons/remove_contact.svg")),
@ -722,11 +724,8 @@ fn handle_static(req: HttpRequest) -> HttpResponse {
"profile" => Some(include_str!("frontend/imgs/icons/profile.svg")),
_ => None
} {
Some(body) => {
response_builder.content_type("image/svg+xml");
return response_builder.body(body.replace("FILL_COLOR", color))
}
None => {}
response_builder.content_type("image/svg+xml");
return response_builder.body(body.replace("FILL_COLOR", color))
}
} else if splits.len() == 3 {
match splits[2] {
@ -766,13 +765,12 @@ fn handle_static(req: HttpRequest) -> HttpResponse {
}
"libs" => {
if splits.len() == 3 {
match match splits[2] {
if let Some(body) = match splits[2] {
"linkify.min.js" => Some(include_str!("frontend/libs/linkify.min.js")),
"linkify-element.min.js" => Some(include_str!("frontend/libs/linkify-element.min.js")),
_ => None
} {
Some(body) => return response_builder.content_type(JS_CONTENT_TYPE).body(body),
None => {}
return response_builder.content_type(JS_CONTENT_TYPE).body(body);
}
}
}
@ -782,17 +780,15 @@ fn handle_static(req: HttpRequest) -> HttpResponse {
HttpResponse::NotFound().finish()
}
#[actix_web::main]
async fn start_http_server(global_vars: Arc<RwLock<GlobalVars>>) -> io::Result<()> {
let http_addr = env::var("AIRA_HTTP_ADDR").unwrap_or("127.0.0.1".to_owned()).parse().expect("AIRA_HTTP_ADDR invalid");
async fn start_http_server(global_vars: GlobalVars) -> io::Result<()> {
let http_addr = env::var("AIRA_HTTP_ADDR").unwrap_or_else(|_| "127.0.0.1".to_owned()).parse().expect("AIRA_HTTP_ADDR invalid");
let http_port = match env::var("AIRA_HTTP_PORT") {
Ok(port) => port.parse().expect("AIRA_HTTP_PORT invalid"),
Err(_) => constants::UI_PORT
};
let server = HttpServer::new(move || {
let global_vars_clone = global_vars.clone();
App::new()
.data(global_vars_clone)
.app_data(Data::new(global_vars.clone()))
.service(web::resource("/")
.route(web::get().to(handle_index))
.route(web::post().to(handle_create))
@ -802,8 +798,8 @@ async fn start_http_server(global_vars: Arc<RwLock<GlobalVars>>) -> io::Result<(
.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("/avatar/{_}*", web::get().to(handle_avatar))
.route("/static/{_}*", web::get().to(handle_static))
.route("/logout", web::get().to(handle_logout))
}
).bind(SocketAddr::new(http_addr, http_port))?;
@ -815,10 +811,11 @@ async fn start_http_server(global_vars: Arc<RwLock<GlobalVars>>) -> io::Result<(
server.run().await
}
#[derive(Clone)]
struct GlobalVars {
session_manager: Arc<SessionManager>,
websocket_port: u16,
ui_auth_token: Option<String>,
ui_auth_token: Arc<RwLock<Option<String>>>,
tokio_handle: Handle,
}
@ -829,13 +826,13 @@ async fn main() {
print_error!(e);
}
}
let global_vars = Arc::new(RwLock::new(GlobalVars {
session_manager: Arc::new(SessionManager::new()),
websocket_port: 0,
ui_auth_token: None,
let ui_auth_token = Arc::new(RwLock::new(None));
let session_manager = Arc::new(SessionManager::new());
let websocket_port = start_websocket_server(ui_auth_token.clone(), session_manager.clone()).await;
start_http_server(GlobalVars {
session_manager,
websocket_port,
ui_auth_token,
tokio_handle: Handle::current(),
}));
let websocket_port = start_websocket_server(global_vars.clone()).await;
global_vars.write().unwrap().websocket_port = websocket_port;
start_http_server(global_vars).unwrap();
}
}).await.unwrap();
}

View File

@ -17,7 +17,7 @@ impl Headers {
pub const ABORT_FILES_TRANSFER: u8 = 0x0a;
}
pub fn new_message(message: String) -> Vec<u8> {
pub fn new_message(message: &str) -> Vec<u8> {
[&[Headers::MESSAGE], message.as_bytes()].concat()
}
@ -33,17 +33,21 @@ pub fn file(file_name: &str, buffer: &[u8]) -> Vec<u8> {
[&[Headers::FILE], &(file_name.len() as u16).to_be_bytes()[..], file_name.as_bytes(), buffer].concat()
}
pub fn parse_file<'a>(buffer: &'a [u8]) -> Option<(&'a [u8], &'a [u8])> {
pub fn get_file_name(buffer: &[u8]) -> Option<&str> {
if buffer.len() > 3 {
let file_name_len = u16::from_be_bytes([buffer[1], buffer[2]]) as usize;
if buffer.len() > 3+file_name_len {
let file_name = &buffer[3..3+file_name_len];
return Some((file_name, &buffer[3+file_name_len..]));
return from_utf8(&buffer[3..3+file_name_len]).ok();
}
}
None
}
pub fn parse_file(buffer: &[u8]) -> Option<(&str, &[u8])> {
let file_name = get_file_name(buffer)?;
Some((file_name, &buffer[3+file_name.len()..]))
}
pub fn ask_large_files(file_info: Vec<(u64, Vec<u8>)>) -> Vec<u8> {
let mut buff = vec![Headers::ASK_LARGE_FILES];
file_info.into_iter().for_each(|info| {

View File

@ -4,8 +4,7 @@ use libmdns::Service;
use uuid::Uuid;
use platform_dirs::UserDirs;
use async_psec::{PUBLIC_KEY_LENGTH, Session, SessionWriteHalf, PsecWriter, PsecReader, PsecError};
use crate::{constants, protocol, crypto, discovery, identity::{Contact, Identity}, print_error, utils::{get_unix_timestamp, get_not_used_path}};
use crate::ui_interface::UiConnection;
use crate::{constants, crypto, discovery, identity::{Contact, Identity, Message}, ui_interface::UiConnection, print_error, protocol, utils::{get_not_used_path, get_unix_timestamp_ms, get_unix_timestamp_sec}};
pub enum SessionCommand {
Send {
@ -55,7 +54,8 @@ 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: RwLock<HashMap<usize, Vec<(bool, Vec<u8>)>>>,
saved_msgs: RwLock<HashMap<usize, Vec<Message>>>,
pub pending_msgs: Mutex<HashMap<usize, Vec<Vec<u8>>>>,
pub not_seen: RwLock<Vec<usize>>,
mdns_service: Mutex<Option<Service>>,
listener_stop_signal: Mutex<Option<Sender<()>>>,
@ -65,11 +65,10 @@ impl SessionManager {
fn with_ui_connection<F>(&self, f: F) where F: FnOnce(&mut UiConnection) {
let mut ui_connection_opt = self.ui_connection.lock().unwrap();
match ui_connection_opt.as_mut() {
Some(ui_connection) => if ui_connection.is_valid {
if let Some(ui_connection) = ui_connection_opt.as_mut() {
if ui_connection.is_valid {
f(ui_connection);
}
None => {}
}
}
@ -88,11 +87,11 @@ impl SessionManager {
Ok(())
}
pub fn store_msg(&self, session_id: &usize, outgoing: bool, buffer: Vec<u8>) {
pub fn store_msg(&self, session_id: &usize, message: Message) {
let mut msg_saved = false;
if let Some(contact) = self.loaded_contacts.read().unwrap().get(session_id) {
let mut offsets = self.last_loaded_msg_offsets.write().unwrap(); //locking mutex before modifying the DB to prevent race conditions with load_msgs()
match self.identity.read().unwrap().as_ref().unwrap().store_msg(&contact.uuid, outgoing, &buffer) {
match self.identity.read().unwrap().as_ref().unwrap().store_msg(&contact.uuid, &message) {
Ok(_) => {
*offsets.get_mut(session_id).unwrap() += 1;
msg_saved = true;
@ -101,16 +100,16 @@ impl SessionManager {
}
}
if !msg_saved {
self.saved_msgs.write().unwrap().get_mut(&session_id).unwrap().push((outgoing, buffer));
//can be None if session disconnected
if let Some(saved_msgs) = self.saved_msgs.write().unwrap().get_mut(&session_id) {
saved_msgs.push(message)
}
}
}
fn get_session_sender(&self, session_id: &usize) -> Option<Sender<SessionCommand>> {
let mut sessions = self.sessions.write().unwrap();
match sessions.get_mut(session_id) {
Some(session_data) => Some(session_data.sender.clone()),
None => None
}
let sessions = self.sessions.read().unwrap();
sessions.get(session_id).map(|session_data| session_data.sender.clone())
}
pub async fn send_command(&self, session_id: &usize, session_command: SessionCommand) -> bool {
@ -127,6 +126,21 @@ impl SessionManager {
}
}
pub async fn send_or_add_to_pending(&self, session_id: &usize, buff: Vec<u8>) -> Result<bool, ()> {
if let Some(sender) = self.get_session_sender(session_id) {
match sender.send(SessionCommand::Send { buff }).await {
Ok(_) => Ok(true),
Err(e) => {
print_error!(e);
Err(())
}
}
} else {
self.pending_msgs.lock().unwrap().get_mut(session_id).unwrap().push(buff);
Ok(false)
}
}
fn remove_session(&self, session_id: &usize) {
self.with_ui_connection(|ui_connection| {
ui_connection.on_disconnected(&session_id);
@ -151,23 +165,66 @@ impl SessionManager {
});
}
async fn send_msg(&self, session_id: usize, session_write: &mut SessionWriteHalf, buff: &[u8], is_sending: &mut bool, file_ack_sender: &mut Option<Sender<bool>>) -> Result<(), PsecError> {
self.encrypt_and_send(session_write, &buff).await?;
if buff[0] == protocol::Headers::ACCEPT_LARGE_FILES {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download.as_mut().unwrap().accepted = true;
} else if buff[0] == protocol::Headers::ABORT_FILES_TRANSFER {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = None;
*is_sending = false;
if let Some(ack_sender) = file_ack_sender {
if let Err(e) = ack_sender.send(false).await {
print_error!(e);
async fn send_store_and_inform<T: PsecWriter>(&self, session_id: usize, session_writer: &mut T, buff: Vec<u8>) -> Result<Option<Vec<u8>>, PsecError> {
self.encrypt_and_send(session_writer, &buff).await?;
let timestamp = get_unix_timestamp_sec();
Ok(match buff[0] {
protocol::Headers::MESSAGE => {
let msg = Message {
outgoing: true,
timestamp,
data: buff,
};
self.with_ui_connection(|ui_connection| {
ui_connection.on_new_msg(&session_id, &msg);
});
self.store_msg(&session_id, msg);
None
}
protocol::Headers::FILE => {
if let Some((filename, content)) = protocol::parse_file(&buff) {
match self.store_file(&session_id, content) {
Ok(file_uuid) => {
let msg = [&[protocol::Headers::FILE][..], file_uuid.as_bytes(), filename.as_bytes()].concat();
self.store_msg(&session_id, Message {
outgoing: true,
timestamp,
data: msg,
});
self.with_ui_connection(|ui_connection| {
ui_connection.on_new_file(&session_id, true, timestamp, filename, file_uuid);
});
}
Err(e) => print_error!(e)
}
}
*file_ack_sender = None;
None
}
_ => Some(buff)
})
}
async fn send_msg(&self, session_id: usize, session_write: &mut SessionWriteHalf, buff: Vec<u8>, is_sending: &mut bool, file_ack_sender: &mut Option<Sender<bool>>) -> Result<(), PsecError> {
if let Some(buff) = self.send_store_and_inform(session_id, session_write, buff).await? {
//not a message or a file
match buff[0] {
protocol::Headers::ACCEPT_LARGE_FILES => self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download.as_mut().unwrap().accepted = true,
protocol::Headers::ABORT_FILES_TRANSFER => {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = None;
*is_sending = false;
if let Some(ack_sender) = file_ack_sender {
if let Err(e) = ack_sender.send(false).await {
print_error!(e);
}
*file_ack_sender = None;
}
self.with_ui_connection(|ui_connection| {
ui_connection.on_file_transfer_aborted(&session_id);
});
}
_ => {}
}
}
self.with_ui_connection(|ui_connection| {
ui_connection.on_msg_sent(session_id, &buff);
});
Ok(())
}
@ -223,7 +280,7 @@ impl SessionManager {
let mut sessions = self.sessions.write().unwrap();
let files_transfer = sessions.get_mut(&session_id).unwrap().files_download.as_mut().unwrap();
let file_transfer = &mut files_transfer.files[files_transfer.index];
file_transfer.last_chunk = get_unix_timestamp();
file_transfer.last_chunk = get_unix_timestamp_ms();
file_transfer.transferred += chunk_size;
if file_transfer.transferred >= file_transfer.file_size { //we downloaded all the file
if files_transfer.index+1 == files_transfer.files.len() {
@ -281,7 +338,7 @@ impl SessionManager {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = None;
local_file_handle = None;
self.with_ui_connection(|ui_connection| {
ui_connection.on_received(&session_id, &buffer);
ui_connection.on_file_transfer_aborted(&session_id);
});
}
protocol::Headers::ASK_LARGE_FILES => {
@ -293,7 +350,7 @@ impl SessionManager {
file_name: info.1,
file_size: info.0,
transferred: 0,
last_chunk: get_unix_timestamp(),
last_chunk: get_unix_timestamp_ms(),
}
}).collect();
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = Some(LargeFilesDownload {
@ -369,46 +426,52 @@ impl SessionManager {
protocol::Headers::REMOVE_AVATAR => self.set_avatar_uuid(&session_id, None),
_ => {
let header = buffer[0];
let buffer = match header {
let timestamp = get_unix_timestamp_sec();
match header {
protocol::Headers::MESSAGE => {
let msg = Message {
outgoing: false,
timestamp,
data: buffer,
};
self.set_seen(session_id, false);
self.with_ui_connection(|ui_connection| {
ui_connection.on_new_msg(&session_id, &msg);
});
self.store_msg(&session_id, msg);
}
protocol::Headers::FILE => {
if let Some((file_name, content)) = protocol::parse_file(&buffer) {
if let Some((filename, content)) = protocol::parse_file(&buffer) {
match self.store_file(&session_id, content) {
Ok(file_uuid) => {
Some([&[protocol::Headers::FILE][..], file_uuid.as_bytes(), file_name].concat())
}
Err(e) => {
print_error!(e);
None
self.set_seen(session_id, false);
self.with_ui_connection(|ui_connection| {
ui_connection.on_new_file(&session_id, false, timestamp, filename, file_uuid);
});
self.store_msg(&session_id, Message {
outgoing: false,
timestamp,
data: [&[protocol::Headers::FILE][..], file_uuid.as_bytes(), filename.as_bytes()].concat(),
});
}
Err(e) => print_error!(e)
}
} else {
None
}
}
_ => {
Some(buffer)
}
};
if buffer.is_some() {
let is_classical_message = header == protocol::Headers::MESSAGE || header == protocol::Headers::FILE;
if is_classical_message {
self.set_seen(session_id, false);
} else if header == protocol::Headers::ACCEPT_LARGE_FILES {
protocol::Headers::ACCEPT_LARGE_FILES => {
is_sending = true;
last_chunks_sizes = Some(Vec::new());
self.with_ui_connection(|ui_connection| {
ui_connection.on_large_files_accepted(&session_id);
})
}
self.with_ui_connection(|ui_connection| {
ui_connection.on_received(&session_id, buffer.as_ref().unwrap());
});
if is_classical_message {
self.store_msg(&session_id, false, buffer.unwrap());
}
_ => {}
}
}
}
}
Err(e) => {
if e != PsecError::BrokenPipe && e != PsecError::ConnectionReset && e != PsecError::BufferTooLarge {
if e != PsecError::BrokenPipe && e != PsecError::ConnectionReset {
print_error!(e);
}
break;
@ -421,11 +484,9 @@ impl SessionManager {
//don't send msg if we already encrypted a file chunk (keep PSEC nonces synchronized)
if is_sending {
msg_queue.push(buff);
} else {
if let Err(e) = self.send_msg(session_id, &mut session_write, &buff, &mut is_sending, &mut file_ack_sender).await {
print_error!(e);
break;
}
} else if let Err(e) = self.send_msg(session_id, &mut session_write, buff, &mut is_sending, &mut file_ack_sender).await {
print_error!(e);
break;
}
}
SessionCommand::EncryptFileChunk { plain_text } => {
@ -438,9 +499,9 @@ impl SessionManager {
Ok(_) => {
file_ack_sender = Some(ack_sender);
//once the pre-encrypted chunk is sent, we can send the pending messages
while msg_queue.len() > 0 {
while !msg_queue.is_empty() {
let msg = msg_queue.remove(0);
if let Err(e) = self.send_msg(session_id, &mut session_write, &msg, &mut is_sending, &mut file_ack_sender).await {
if let Err(e) = self.send_msg(session_id, &mut session_write, msg, &mut is_sending, &mut file_ack_sender).await {
print_error!(e);
break;
}
@ -461,9 +522,28 @@ impl SessionManager {
}
}
async fn on_session_initialized(&self, session: &mut Session, session_id: usize, is_contact: bool) -> Result<(), PsecError> {
if is_contact {
let pending_msgs = self.pending_msgs.lock().unwrap().get_mut(&session_id).unwrap().split_off(0);
self.with_ui_connection(|ui_connection| {
ui_connection.on_sending_pending_msgs(&session_id);
});
for buff in pending_msgs {
self.send_store_and_inform(session_id, session, buff).await?;
}
self.with_ui_connection(|ui_connection| {
ui_connection.on_pending_msgs_sent(&session_id);
});
Ok(())
} else {
self.encrypt_and_send(session, &protocol::ask_profile_info()).await
}
}
fn handle_new_session(session_manager: Arc<SessionManager>, mut session: Session, outgoing: bool) {
tokio::spawn(async move {
let mut peer_public_key = [0; PUBLIC_KEY_LENGTH];
session.set_max_recv_size(constants::MAX_RECV_SIZE, false);
let session = {
let identity = {
session_manager.identity.read().unwrap().clone()
@ -508,7 +588,7 @@ impl SessionManager {
outgoing,
peer_public_key,
ip,
sender: sender,
sender,
files_download: None,
};
let mut session_id = None;
@ -538,17 +618,10 @@ impl SessionManager {
session_manager.with_ui_connection(|ui_connection| {
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_profile_info()).await {
Ok(_) => {}
Err(e) => {
print_error!(e);
session_manager.remove_session(&session_id);
return;
}
}
match session_manager.on_session_initialized(&mut session, session_id, is_contact).await {
Ok(_) => session_manager.session_worker(session_id, receiver, session).await,
Err(e) => print_error!(e)
}
session_manager.session_worker(session_id, receiver, session).await;
session_manager.remove_session(&session_id);
}
}
@ -583,7 +656,7 @@ impl SessionManager {
self.loaded_contacts.read().unwrap().iter().map(|c| (*c.0, c.1.name.clone(), c.1.verified, c.1.public_key)).collect()
}
pub fn get_saved_msgs(&self) -> HashMap<usize, Vec<(bool, Vec<u8>)>> {
pub fn get_saved_msgs(&self) -> HashMap<usize, Vec<Message>> {
self.saved_msgs.read().unwrap().clone()
}
@ -596,16 +669,13 @@ impl SessionManager {
}
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
match loaded_contacts.get_mut(&session_id) {
Some(contact) => {
if contact.seen != seen {
match self.identity.read().unwrap().as_ref().unwrap().set_contact_seen(&contact.uuid, seen) {
Ok(_) => contact.seen = seen,
Err(e) => print_error!(e)
}
if let Some(contact) = loaded_contacts.get_mut(&session_id) {
if contact.seen != seen {
match self.identity.read().unwrap().as_ref().unwrap().set_contact_seen(&contact.uuid, seen) {
Ok(_) => contact.seen = seen,
Err(e) => print_error!(e)
}
}
None => {}
}
}
@ -615,20 +685,22 @@ impl SessionManager {
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);
self.pending_msgs.lock().unwrap().insert(session_id, Vec::new());
Ok(())
}
pub fn remove_contact(&self, session_id: usize) -> Result<usize, rusqlite::Error> {
pub fn remove_contact(&self, session_id: &usize) -> Result<usize, rusqlite::Error> {
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
let result = Identity::remove_contact(&loaded_contacts.get(&session_id).unwrap().uuid);
let result = Identity::remove_contact(&loaded_contacts.get(session_id).unwrap().uuid);
if result.is_ok() {
if let Some(contact) = loaded_contacts.remove(&session_id) {
if let Some(session) = self.sessions.write().unwrap().get_mut(&session_id) {
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);
self.last_loaded_msg_offsets.write().unwrap().remove(session_id);
self.pending_msgs.lock().unwrap().remove(session_id);
}
result
}
@ -653,13 +725,13 @@ impl SessionManager {
}
pub fn store_file(&self, session_id: &usize, data: &[u8]) -> Result<Uuid, rusqlite::Error> {
self.identity.read().unwrap().as_ref().unwrap().store_file(match self.loaded_contacts.read().unwrap().get(session_id) {
Some(contact) => Some(contact.uuid),
None => None
}, data)
self.identity.read().unwrap().as_ref().unwrap().store_file(
self.loaded_contacts.read().unwrap().get(session_id).map(|contact| contact.uuid),
data
)
}
pub fn load_msgs(&self, session_id: &usize, count: usize) -> Option<Vec<(bool, Vec<u8>)>> {
pub fn load_msgs(&self, session_id: &usize, count: usize) -> Option<Vec<Message>> {
let mut offsets = self.last_loaded_msg_offsets.write().unwrap();
let msgs = self.identity.read().unwrap().as_ref().unwrap().load_msgs(
&self.loaded_contacts.read().unwrap().get(session_id)?.uuid,
@ -743,20 +815,18 @@ impl SessionManager {
}
*identity_guard = identity;
if identity_guard.is_some() { //login
match identity_guard.as_ref().unwrap().load_contacts() {
Some(contacts) => {
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
let mut session_counter = self.session_counter.write().unwrap();
let mut not_seen = self.not_seen.write().unwrap();
contacts.into_iter().for_each(|contact| {
if !contact.seen {
not_seen.push(*session_counter);
}
loaded_contacts.insert(*session_counter, contact);
*session_counter += 1;
})
}
None => {}
if let Some(contacts) = identity_guard.as_ref().unwrap().load_contacts() {
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
let mut session_counter = self.session_counter.write().unwrap();
let mut not_seen = self.not_seen.write().unwrap();
contacts.into_iter().for_each(|contact| {
if !contact.seen {
not_seen.push(*session_counter);
}
loaded_contacts.insert(*session_counter, contact);
self.pending_msgs.lock().unwrap().insert(*session_counter, Vec::new());
*session_counter += 1;
});
}
}
}
@ -774,6 +844,7 @@ impl SessionManager {
loaded_contacts: RwLock::new(HashMap::new()),
last_loaded_msg_offsets: RwLock::new(HashMap::new()),
saved_msgs: RwLock::new(HashMap::new()),
pending_msgs: Mutex::new(HashMap::new()),
not_seen: RwLock::new(Vec::new()),
mdns_service: Mutex::new(None),
listener_stop_signal: Mutex::new(None),

View File

@ -1,133 +1,7 @@
use std::net::{IpAddr, TcpStream};
use std::{fmt::Display, net::{IpAddr, TcpStream}, str::from_utf8};
use tungstenite::{WebSocket, protocol::Role, Message};
use crate::{protocol, session_manager::{LargeFileDownload, LargeFilesDownload}};
mod ui_messages {
use std::{fmt::Display, iter::FromIterator, net::IpAddr, str::from_utf8};
use tungstenite::Message;
use uuid::Uuid;
use crate::{print_error, session_manager::{LargeFileDownload, LargeFilesDownload}, protocol, utils::to_uuid_bytes};
fn simple_event(command: &str, session_id: &usize) -> Message {
Message::from(format!("{} {}", command, session_id))
}
fn data_list<T: Display>(command: &str, data: Vec<T>) -> Message {
Message::from(command.to_owned()+&String::from_iter(data.into_iter().map(|i| {
format!(" {}", i)
})))
}
pub fn on_disconnected(session_id: &usize) -> Message {
simple_event("disconnected", session_id)
}
pub fn on_new_session(session_id: &usize, name: &str, outgoing: bool, fingerprint: &str, ip: IpAddr) -> Message {
Message::from(format!("new_session {} {} {} {} {}", session_id, outgoing, fingerprint, ip, name))
}
pub fn on_file_received(session_id: &usize, buffer: &[u8]) -> Option<Message> {
let uuid = Uuid::from_bytes(to_uuid_bytes(&buffer[1..17])?);
match from_utf8(&buffer[17..]) {
Ok(file_name) => Some(Message::from(format!("file {} {} {}", session_id, uuid.to_string(), file_name))),
Err(e) => {
print_error!(e);
None
}
}
}
pub fn new_files_transfer(session_id: &usize, files_transfer: &LargeFilesDownload) -> Message {
if files_transfer.accepted {
let mut s = format!(
"files_transfer {} {}",
session_id,
files_transfer.index
);
files_transfer.files.iter().for_each(|file| {
s.push_str(&format!(
" {} {} {} {}",
base64::encode(&file.file_name),
file.file_size,
file.transferred,
file.last_chunk,
));
});
Message::from(s)
} else {
on_ask_large_files(session_id, &files_transfer.files, files_transfer.download_location.to_str().unwrap())
}
}
pub fn on_ask_large_files(session_id: &usize, files: &Vec<LargeFileDownload>, download_location: &str) -> Message {
let mut s = format!("ask_large_files {} {}", session_id, base64::encode(download_location));
files.into_iter().for_each(|file| {
s.push_str(&format!(
" {} {}",
base64::encode(&file.file_name),
file.file_size,
));
});
Message::from(s)
}
pub fn on_large_files_accepted(session_id: &usize) -> Message {
simple_event("files_accepted", session_id)
}
pub fn on_file_transfer_aborted(session_id: &usize) -> Message {
simple_event("aborted", session_id)
}
pub fn on_new_message(session_id: &usize, outgoing: bool, buffer: &[u8]) -> Option<Message> {
match from_utf8(&buffer[1..]) {
Ok(msg) => Some(Message::from(format!("{} {} {} {}", "new_message", session_id, outgoing, msg))),
Err(e) => {
print_error!(e);
None
}
}
}
pub fn inc_files_transfer(session_id: &usize, chunk_size: u64) -> Message {
Message::from(format!("inc_file_transfer {} {}", session_id, chunk_size))
}
pub fn load_msgs(session_id: &usize, msgs: &Vec<(bool, Vec<u8>)>) -> Message {
let mut s = format!("load_msgs {}", session_id);
msgs.into_iter().rev().for_each(|entry| {
match entry.1[0] {
protocol::Headers::MESSAGE => match from_utf8(&entry.1[1..]) {
Ok(msg) => s.push_str(&format!(" m {} {}", entry.0, base64::encode(msg))),
Err(e) => print_error!(e)
}
protocol::Headers::FILE => {
let uuid = Uuid::from_bytes(to_uuid_bytes(&entry.1[1..17]).unwrap());
match from_utf8(&entry.1[17..]) {
Ok(file_name) => s.push_str(&format!(" f {} {} {}", entry.0, uuid.to_string(), base64::encode(file_name))),
Err(e) => print_error!(e)
}
}
_ => {}
}
});
Message::from(s)
}
pub fn set_not_seen(session_ids: Vec<usize>) -> Message {
data_list("not_seen", session_ids)
}
pub fn set_local_ips(ips: Vec<IpAddr>) -> Message {
data_list("local_ips", ips)
}
pub fn on_name_told(session_id: &usize, name: &str) -> Message {
Message::from(format!("name_told {} {}", session_id, name))
}
pub fn on_avatar_changed(session_id: Option<&usize>) -> Message {
match session_id {
Some(session_id) => simple_event("avatar_changed", session_id),
None => Message::from("avatar_changed self")
}
}
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))
}
pub fn set_name(new_name: &str) -> Message {
Message::from(format!("set_name {}", new_name))
}
pub fn password_changed(success: bool, is_protected: bool) -> Message {
Message::from(format!("password_changed {} {}", success, is_protected))
}
}
use uuid::Uuid;
use crate::{identity, print_error, protocol, session_manager::{LargeFileDownload, LargeFilesDownload}, utils::to_uuid_bytes};
pub struct UiConnection{
pub websocket: WebSocket<TcpStream>,
@ -137,81 +11,138 @@ pub struct UiConnection{
impl UiConnection {
pub fn new(websocket: WebSocket<TcpStream>) -> UiConnection {
UiConnection {
websocket: websocket,
websocket,
is_valid: true
}
}
pub fn write_message(&mut self, message: Message) {
if self.websocket.write_message(message).is_err() {
pub fn write_message<T: Into<Message>>(&mut self, message: T) {
if self.websocket.write_message(message.into()).is_err() {
self.is_valid = false
}
}
pub fn on_received(&mut self, session_id: &usize, buffer: &[u8]) {
let ui_message = match buffer[0] {
protocol::Headers::MESSAGE => ui_messages::on_new_message(session_id, false, buffer),
protocol::Headers::FILE => ui_messages::on_file_received(session_id, buffer),
protocol::Headers::ACCEPT_LARGE_FILES => Some(ui_messages::on_large_files_accepted(session_id)),
protocol::Headers::ABORT_FILES_TRANSFER => Some(ui_messages::on_file_transfer_aborted(session_id)),
_ => None
};
if ui_message.is_some() {
self.write_message(ui_message.unwrap())
fn simple_event(&mut self, command: &str, session_id: &usize) {
self.write_message(format!("{} {}", command, session_id));
}
fn data_list<T: Display>(command: &str, data: &[T]) -> String {
command.to_string()+&data.iter().map(|i| {
format!(" {}", i)
}).collect::<String>()
}
pub fn on_ask_large_files(&mut self, session_id: &usize, files: &[LargeFileDownload], download_location: &str) {
let mut s = format!("ask_large_files {} {}", session_id, base64::encode(download_location));
files.iter().for_each(|file| {
s.push_str(&format!(
" {} {}",
base64::encode(&file.file_name),
file.file_size,
));
});
self.write_message(s);
}
pub fn on_large_files_accepted(&mut self, session_id: &usize) {
self.simple_event("files_accepted", session_id);
}
pub fn on_file_transfer_aborted(&mut self, session_id: &usize) {
self.simple_event("aborted", session_id);
}
pub fn on_new_msg(&mut self, session_id: &usize, message: &identity::Message) {
match from_utf8(&message.data[1..]) {
Ok(msg) => self.write_message(format!("new_message {} {} {} {}", session_id, message.outgoing, message.timestamp, msg)),
Err(e) => print_error!(e)
}
}
pub fn on_ask_large_files(&mut self, session_id: &usize, files: &Vec<LargeFileDownload>, download_location: &str) {
self.write_message(ui_messages::on_ask_large_files(session_id, files, download_location))
}
pub fn on_msg_sent(&mut self, session_id: usize, buffer: &[u8]) {
match buffer[0] {
protocol::Headers::MESSAGE => match ui_messages::on_new_message(&session_id, true, buffer) {
Some(msg) => self.write_message(msg),
None => {}
}
protocol::Headers::ABORT_FILES_TRANSFER => self.write_message(ui_messages::on_file_transfer_aborted(&session_id)),
_ => {}
}
pub fn on_new_file(&mut self, session_id: &usize, outgoing: bool, timestamp: u64, filename: &str, uuid: Uuid) {
self.write_message(format!("file {} {} {} {} {}", session_id, outgoing, timestamp, uuid.to_string(), filename));
}
pub fn on_new_session(&mut self, session_id: &usize, name: &str, outgoing: bool, fingerprint: &str, ip: IpAddr, files_transfer: Option<&LargeFilesDownload>) {
self.write_message(ui_messages::on_new_session(session_id, name, outgoing, fingerprint, ip));
self.write_message(format!("new_session {} {} {} {} {}", session_id, outgoing, fingerprint, ip, name));
if let Some(files_transfer) = files_transfer {
self.write_message(ui_messages::new_files_transfer(session_id, files_transfer));
if files_transfer.accepted {
let mut s = format!(
"files_transfer {} {}",
session_id,
files_transfer.index
);
files_transfer.files.iter().for_each(|file| {
s.push_str(&format!(
" {} {} {} {}",
base64::encode(&file.file_name),
file.file_size,
file.transferred,
file.last_chunk,
));
});
self.write_message(s);
} else {
self.on_ask_large_files(session_id, &files_transfer.files, files_transfer.download_location.to_str().unwrap())
}
}
}
pub fn on_disconnected(&mut self, session_id: &usize) {
self.write_message(ui_messages::on_disconnected(session_id));
self.simple_event("disconnected", session_id);
}
pub fn on_name_told(&mut self, session_id: &usize, name: &str) {
self.write_message(ui_messages::on_name_told(session_id, name));
self.write_message(format!("name_told {} {}", session_id, name));
}
pub fn on_avatar_changed(&mut self, session_id: Option<&usize>) {
self.write_message(ui_messages::on_avatar_changed(session_id));
match session_id {
Some(session_id) => self.simple_event("avatar_changed", session_id),
None => self.write_message("avatar_changed self")
}
}
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));
self.write_message(format!("inc_file_transfer {} {}", session_id, chunk_size));
}
pub fn set_as_contact(&mut self, session_id: usize, name: &str, verified: bool, fingerprint: &str) {
self.write_message(ui_messages::set_as_contact(session_id, name, verified, fingerprint));
self.write_message(format!("is_contact {} {} {} {}", session_id, verified, fingerprint, name));
}
pub fn load_msgs(&mut self, session_id: &usize, msgs: &Vec<(bool, Vec<u8>)>) {
self.write_message(ui_messages::load_msgs(session_id, msgs));
pub fn load_msgs(&mut self, session_id: &usize, msgs: &[identity::Message]) {
let mut s = format!("load_msgs {}", session_id);
msgs.iter().rev().for_each(|message| {
match message.data[0] {
protocol::Headers::MESSAGE => match from_utf8(&message.data[1..]) {
Ok(msg) => s.push_str(&format!(" m {} {} {}", message.outgoing, message.timestamp, base64::encode(msg))),
Err(e) => print_error!(e)
}
protocol::Headers::FILE => {
let uuid = Uuid::from_bytes(to_uuid_bytes(&message.data[1..17]).unwrap());
match from_utf8(&message.data[17..]) {
Ok(file_name) => s.push_str(&format!(" f {} {} {} {}", message.outgoing, message.timestamp, uuid.to_string(), base64::encode(file_name))),
Err(e) => print_error!(e)
}
}
_ => {}
}
});
self.write_message(s);
}
pub fn set_not_seen(&mut self, session_ids: Vec<usize>) {
self.write_message(ui_messages::set_not_seen(session_ids));
pub fn set_not_seen(&mut self, session_ids: &[usize]) {
self.write_message(Self::data_list("not_seen", session_ids));
}
pub fn set_local_ips(&mut self, ips: Vec<IpAddr>) {
self.write_message(ui_messages::set_local_ips(ips));
pub fn new_pending_msg(&mut self, session_id: &usize, is_file: bool, data: &str) {
self.write_message(format!("pending {} {} {}", session_id, is_file, data));
}
pub fn on_sending_pending_msgs(&mut self, session_id: &usize) {
self.simple_event("sending_pending_msgs", session_id);
}
pub fn on_pending_msgs_sent(&mut self, session_id: &usize) {
self.simple_event("pending_msgs_sent", session_id);
}
pub fn set_local_ips(&mut self, ips: &[IpAddr]) {
self.write_message(Self::data_list("local_ips", ips));
}
pub fn set_name(&mut self, new_name: &str) {
self.write_message(ui_messages::set_name(new_name));
self.write_message(format!("set_name {}", new_name));
}
pub fn password_changed(&mut self, success: bool, is_protected: bool) {
self.write_message(ui_messages::password_changed(success, is_protected));
self.write_message(format!("password_changed {} {}", success, is_protected));
}
pub fn logout(&mut self) {
self.write_message(Message::from("logout"));
self.write_message("logout");
}
}

View File

@ -1,4 +1,4 @@
use std::{convert::TryInto, time::{SystemTime, UNIX_EPOCH}, path::PathBuf};
use std::{convert::TryInto, time::{SystemTime, UNIX_EPOCH}, path::Path};
use uuid::Bytes;
use crate::print_error;
@ -16,11 +16,15 @@ pub fn escape_double_quote(origin: String) -> String {
origin.replace("\"", "\\\"")
}
pub fn get_unix_timestamp() -> u128 {
pub fn get_unix_timestamp_ms() -> u128 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis()
}
pub fn get_not_used_path(file_name: &str, parent_directory: &PathBuf) -> String {
pub fn get_unix_timestamp_sec() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()
}
pub fn get_not_used_path(file_name: &str, parent_directory: &Path) -> String {
let has_extension = file_name.matches('.').count() > 0;
let mut path = parent_directory.join(&file_name);
let mut n = 1;