Compare commits

..

No commits in common. "master" and "v0.0.3" have entirely different histories.

18 changed files with 1573 additions and 1851 deletions

1728
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -4,5 +4,4 @@ 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 = 1_023_996;
pub const MAX_RECV_SIZE: usize = 16_383_996;
pub const FILE_CHUNK_SIZE: usize = 1023996;

View File

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

View File

@ -1,79 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -120,7 +120,7 @@ button:hover::after {
position: relative;
}
#avatarContainer .avatar {
font-size: 2.5em;
font-size: 4em;
}
#removeAvatar {
position: absolute;
@ -149,7 +149,8 @@ button:hover::after {
font-size: 0.9em;
}
#session_info .avatar {
font-size: 2.5em;
width: 6em;
height: 6em;
display: block;
margin: auto;
}
@ -197,6 +198,7 @@ button:hover::after {
padding: 10px;
display: flex;
align-items: center;
font-size: 1.7em;
cursor: pointer;
}
#me>div {
@ -205,7 +207,6 @@ button:hover::after {
flex-grow: 1;
}
#me p {
font-size: 1.7em;
margin: 0;
font-weight: bold;
display: inline;
@ -213,9 +214,6 @@ 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;
}
@ -237,7 +235,8 @@ button:hover::after {
flex-grow: 1;
}
#left_panel ul li .avatar {
font-size: .9em;
width: 2em;
height: 2em;
}
#left_panel ul li:hover, #left_panel ul li.current {
background-color: #333940;
@ -255,8 +254,7 @@ 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::after {
content: "";
#left_panel ul li .not_seen_marker {
width: 12px;
height: 12px;
background-color: var(--accent);
@ -297,6 +295,7 @@ button:hover::after {
flex-direction: row;
align-items: center;
padding: 20px 20px;
font-size: 1.5em;
}
#chat_header>div {
display: flex;
@ -308,7 +307,6 @@ button:hover::after {
color: var(--accent);
}
#chat_header>div>p { /*name*/
font-size: 1.5em;
font-weight: bold;
margin: 0;
}
@ -356,6 +354,7 @@ button:hover::after {
}
#file_transfer {
border-top: 2px solid var(--accent);
display: none;
position: relative;
}
#file_transfer.active {
@ -402,46 +401,31 @@ button:hover::after {
height: 100%;
background-color: var(--accent);
}
#message_box {
border-top: 2px solid var(--accent);
margin-bottom: 0;
}
#msg_log {
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;
font-size: 1.1em;
overflow-y: scroll;
white-space: pre;
}
#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: 3em;
}
#msg_log li .content p {
word-break: break-word;
margin-left: 2em;
margin-top: 5px;
margin-bottom: 10px;
}
#msg_log a {
color: #238cf5;
@ -468,149 +452,6 @@ button:hover::after {
width: 2em;
margin-left: 15px;
}
#message_box, #message_box.online #offline_warning, #chat_header, #msg_log, #file_transfer {
#message_box, #chat_header, #msg_log {
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,7 +3,6 @@
<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>
@ -42,10 +41,6 @@
</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>
@ -61,22 +56,10 @@
</div>
</div>
<div id="message_box">
<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>
<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>
</main>
@ -93,3 +76,4 @@
<script src="/static/index.js"></script>
</body>
</html>

View File

@ -1,13 +1,12 @@
"use strict";
let identityName = undefined;
let socket = new WebSocket("ws://"+location.hostname+":"+websocketPort+"/ws");;
let socket = null;
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()]
@ -15,7 +14,7 @@ let avatarTimestamps = new Map([
function onClickSession(event) {
let sessionId = event.currentTarget.getAttribute("data-sessionId");
if (sessionId != null && socket.readyState === WebSocket.OPEN) {
if (sessionId != null) {
currentSessionId = sessionId;
let session = sessionsData.get(sessionId);
if (!session.seen) {
@ -81,7 +80,6 @@ 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();
};
@ -105,9 +103,7 @@ document.getElementById("remove_contact").onclick = function() {
if (!session.isOnline) {
sessionsData.delete(currentSessionId);
msgHistory.get(currentSessionId).length = 0;
displayChatBottom();
}
pendingMsgs.delete(currentSessionId);
displayHeader();
displaySessions();
displayHistory();
@ -178,7 +174,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 > 16380000) {
if (files[i].size > 32760000) {
useLargeFileTransfer = true;
break;
}
@ -219,11 +215,7 @@ 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(text => {
if (text === "pending") {
newPendingMsg(currentSessionId, true, files[i].name);
}
});
response.text().then(uuid => onFileSent(currentSessionId, uuid, files[i].name));
} else {
console.log(response);
}
@ -234,19 +226,16 @@ document.getElementById("attach_file").onchange = function(event) {
document.getElementById("file_cancel").onclick = function() {
socket.send("abort "+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 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 profileDiv = document.querySelector("#me>div");
profileDiv.onclick = function() {
let profile_div = document.querySelector("#me>div");
profile_div.onclick = function() {
let mainDiv = document.createElement("div");
mainDiv.id = "profile_info";
let avatarContainer = document.createElement("div");
@ -335,35 +324,32 @@ profileDiv.onclick = function() {
changePasswordButton.textContent = "Change password";
changePasswordButton.onclick = function() {
let inputs = document.querySelectorAll("input[type=\"password\"]");
let newPassword, newPasswordConfirm, oldPassword;
let newPassword, newPasswordConfirm;
if (isIdentityProtected) {
oldPassword = inputs[0].value;
newPassword = inputs[1].value;
newPasswordConfirm = inputs[2].value;
newPassword = inputs[1];
newPasswordConfirm = inputs[2];
} else {
newPassword = inputs[0].value;
newPasswordConfirm = inputs[1].value;
newPassword = inputs[0];
newPasswordConfirm = inputs[1];
}
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
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
let msg = "change_password";
if (isIdentityProtected) {
msg += " "+b64EncodeUnicode(inputs[0].value);
}
if (newPasswordSet) {
msg += " "+b64EncodeUnicode(newPassword);
if (newPassword_set) {
msg += " "+b64EncodeUnicode(newPassword.value);
}
socket.send(msg);
} else {
removePopup();
}
} else {
newPassword = "";
newPasswordConfirm = "";
errorMsg.textContent = "Passwords don't match.";
newPassword.value = "";
newPasswordConfirm.value = "";
errorMsg.textContent = "Passwords don't match";
}
};
sectionPassword.appendChild(changePasswordButton);
@ -450,17 +436,8 @@ 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
@ -491,10 +468,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", parseTimestamp(args[3]), msg.data.slice(args[0].length+args[1].length+args[2].length+args[3].length+4));
onNewMessage(args[1], args[2] === "true", msg.data.slice(args[0].length+args[1].length+args[2].length+3));
break;
case "file":
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));
onFileReceived(args[1], args[2], msg.data.slice(args[0].length+args[1].length+args[2].length+3));
break;
case "files_transfer":
onNewFilesTransfer(args[1], args[2], msg.data.slice(args[0].length+args[1].length+args[2].length+3));
@ -523,15 +500,6 @@ 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;
@ -551,12 +519,6 @@ 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) {
@ -602,22 +564,6 @@ 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(' ');
}
@ -630,7 +576,6 @@ 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) {
@ -644,22 +589,16 @@ function onMsgOrFileReceived(sessionId, outgoing, body) {
}
if (document.hidden && !outgoing) {
if (notificationAllowed) {
let sessionName = sessionsData.get(sessionId).name;
new Notification(sessionName, {
"body": body,
"icon": "/avatar/"+sessionId+"/"+sessionName+"?"+avatarTimestamps.get(sessionId)
new Notification(sessionsData.get(sessionId).name, {
"body": body
});
}
}
}
function onNewMessage(sessionId, outgoing, timestamp, msg) {
msgHistory.get(sessionId).push([outgoing, timestamp, false, msg]);
function onNewMessage(sessionId, outgoing, msg) {
msgHistory.get(sessionId).push([outgoing, 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 = [];
@ -744,8 +683,7 @@ function onAskLargeFiles(sessionId, encodedDownloadLocation, filesInfo) {
showPopup(mainDiv, false);
if (document.hidden && notificationAllowed) {
new Notification(sessionName, {
"body": "Files download request",
"icon": "/avatar/"+sessionId+"/"+sessionName+"?"+avatarTimestamps.get(sessionId)
"body": "Files download request"
});
}
}
@ -788,32 +726,29 @@ function onIncFilesTransfer(sessionId, chunkSize) {
}
function onMsgsLoad(sessionId, strMsgs) {
let msgs = strMsgs.split(' ');
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;
}
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 (currentSessionId == sessionId) {
if (msgLog.scrollHeight - msgLog.scrollTop === msgLog.clientHeight) {
displayHistory();
} else {
let backupHeight = msgLog.scrollHeight;
displayHistory(false);
msgLog.scrollTop = msgLog.scrollHeight-backupHeight;
}
}
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;
}
}
}
@ -827,7 +762,6 @@ function onDisconnected(sessionId) {
}
if (currentSessionId == sessionId) {
displayChatBottom();
scrollHistoryToBottom();
}
if (currentSessionId == sessionId && !session.isContact) {
currentSessionId = -1;
@ -835,6 +769,16 @@ 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;
@ -981,11 +925,11 @@ function logout() {
window.location = "/logout";
}
function displayProfile() {
profileDiv.innerHTML = "";
profileDiv.appendChild(generateSelfAvatar(avatarTimestamps.get("self")));
profile_div.innerHTML = "";
profile_div.appendChild(generateSelfAvatar(avatarTimestamps.get("self")));
let p = document.createElement("p");
p.textContent = identityName;
profileDiv.appendChild(p);
profile_div.appendChild(p);
}
function displayHeader() {
chatHeader.children[0].innerHTML = "";
@ -1058,7 +1002,9 @@ function generateSession(sessionId, session) {
li.classList.add("is_verified");
}
if (!session.seen) {
li.classList.add("not_seen");
let marker = document.createElement("div");
marker.classList.add("not_seen_marker");
li.appendChild(marker);
}
if (sessionId == currentSessionId) {
li.classList.add("current");
@ -1081,27 +1027,20 @@ 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 divContainer = document.createElement("div");
let li = document.createElement("li");
if (typeof name !== "undefined") {
divContainer.appendChild(generateMsgHeader(name, sessionId));
li.appendChild(generateMsgHeader(name, sessionId));
}
divContainer.appendChild(div);
return divContainer;
li.appendChild(div);
return li;
}
function generateFile(name, sessionId, outgoing, fileInfo) {
function generateFile(name, sessionId, outgoing, file_info) {
let div1 = document.createElement("div");
div1.classList.add("file");
div1.classList.add("content");
@ -1113,24 +1052,20 @@ function generateFile(name, sessionId, outgoing, fileInfo) {
h4.textContent = "File received:";
}
div2.appendChild(h4);
div1.appendChild(div2);
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);
}
p.textContent = file_info[1];
div2.appendChild(p);
let divContainer = document.createElement("div");
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") {
divContainer.appendChild(generateMsgHeader(name, sessionId));
li.appendChild(generateMsgHeader(name, sessionId));
}
divContainer.appendChild(div1);
return divContainer;
li.appendChild(div1);
return li;
}
function generateFileInfo(fileName, fileSize, p) {
let span = document.createElement("span");
@ -1143,16 +1078,13 @@ function displayChatBottom(speed = undefined) {
let fileTransfer = document.getElementById("file_transfer");
let session = sessionsData.get(currentSessionId);
if (typeof session === "undefined") {
msgBox.classList.remove("active");
msgBox.removeAttribute("style");
fileTransfer.classList.remove("active");
} else {
if (session.isContact || session.isOnline) {
msgBox.classList.add("active");
}
if (session.isOnline) {
msgBox.classList.add("online");
msgBox.style.display = "flex";
} else {
msgBox.classList.remove("online");
msgBox.removeAttribute("style");
}
if (pendingFilesTransfers.has(currentSessionId)) {
let fileInfo = document.getElementById("file_info");
@ -1197,72 +1129,35 @@ function displayChatBottom(speed = undefined) {
}
}
}
function scrollHistoryToBottom() {
msgLog.scrollTop = msgLog.scrollHeight;
}
function displayHistory(scrollToBottom = true) {
msgLog.innerHTML = "";
msg_log.style.display = "block";
msg_log.innerHTML = "";
let session = sessionsData.get(currentSessionId);
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]);
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 {
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);
});
name = session.name;
sessionId = currentSessionId;
}
}
if (scrollToBottom) {
scrollHistoryToBottom();
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 (msgLog.scrollHeight <= msgLog.clientHeight && session.isContact) {
});
if (scrollToBottom) {
msg_log.scrollTop = msg_log.scrollHeight;
}
if (typeof session !== "undefined") {
if (msg_log.scrollHeight <= msg_log.clientHeight && session.isContact) {
socket.send("load_msgs "+currentSessionId);
}
}
}
}

View File

@ -3,7 +3,6 @@
<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 {
@ -59,7 +58,8 @@
padding: 10px 50px;
}
.avatar {
font-size: 3em;
width: 7em;
height: 7em;
}
#identity h2 {
text-align: center;

View File

@ -3,7 +3,6 @@
<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,11 +43,12 @@ fn get_database_path() -> String {
AppDirs::new(Some(constants::APPLICATION_FOLDER), false).unwrap().data_dir.join(constants::DB_NAME).to_str().unwrap().to_owned()
}
#[derive(Debug, Clone)]
pub struct Message {
pub outgoing: bool,
pub timestamp: u64,
pub data: Vec<u8>,
struct EncryptedIdentity {
name: String,
encrypted_keypair: Vec<u8>,
salt: Vec<u8>,
encrypted_master_key: Vec<u8>,
encrypted_use_padding: Vec<u8>,
}
pub struct Contact {
@ -59,14 +60,6 @@ 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,
@ -94,8 +87,8 @@ impl Identity {
};
Ok(Contact {
uuid: contact_uuid,
public_key,
name,
public_key: public_key,
name: name,
avatar: avatar_uuid,
verified: false,
seen: true,
@ -249,17 +242,16 @@ impl Identity {
Ok(file_uuid)
}
pub fn store_msg(&self, contact_uuid: &Uuid, message: &Message) -> Result<usize, rusqlite::Error> {
pub fn store_msg(&self, contact_uuid: &Uuid, outgoing: bool, data: &[u8]) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
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);
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (outgoing BLOB, data BLOB)", contact_uuid), [])?;
let outgoing_byte: u8 = bool_to_byte(outgoing);
let encrypted_outgoing = crypto::encrypt_data(&[outgoing_byte], &self.master_key).unwrap();
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])
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])
}
pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option<Vec<Message>> {
pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option<Vec<(bool, Vec<u8>)>> {
match Connection::open(get_database_path()) {
Ok(db) => {
if let Ok(mut stmt) = db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
@ -270,30 +262,24 @@ impl Identity {
if offset+count >= total {
count = total-offset;
}
let mut stmt = db.prepare(&format!("SELECT outgoing, timestamp, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)).unwrap();
let mut stmt = db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)).unwrap();
let mut rows = stmt.query([]).unwrap();
let mut msgs = Vec::new();
while let Ok(Some(row)) = rows.next() {
let encrypted_outgoing: Vec<u8> = row.get(0).unwrap();
match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){
Ok(outgoing) => {
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)
}
match byte_to_bool(outgoing[0]) {
Ok(outgoing) => {
let encrypted_data: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
Ok(data) => msgs.push((outgoing, data)),
Err(e) => print_error!(e)
}
Err(e) => print_error!(e)
}
Err(_) => {}
}
}
Err(e) => print_error!(e)
}
@ -378,8 +364,14 @@ 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] = match password {
Some(password) => match crypto::decrypt_master_key(&encrypted_identity.encrypted_master_key, password, &encrypted_identity.salt) {
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) {
Ok(master_key) => master_key,
Err(e) => return Err(
match e {
@ -388,11 +380,6 @@ 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) => {
@ -438,16 +425,13 @@ 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 = 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]
}
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
};
db.set(DBKeys::SALT, &salt)?;
let encrypted_use_padding = crypto::encrypt_data(&[bool_to_byte(true)], &master_key).unwrap();
@ -462,16 +446,13 @@ 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 = 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]
}
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
};
db.update(DBKeys::SALT, &salt)
}
@ -479,19 +460,20 @@ 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] = match old_password {
Some(old_password) => match crypto::decrypt_master_key(&encrypted_identity.encrypted_master_key, old_password, &encrypted_identity.salt) {
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) {
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> {
self.db.execute(&format!("INSERT INTO {} (key, value) VALUES (?1, ?2)", self.table_name), params![key, value])
Ok(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::{self, Cursor}, net::SocketAddr, str::{FromStr, from_utf8}, sync::{Arc, RwLock}, cmp::Ordering};
use std::{env, fs, io, net::SocketAddr, str::{FromStr, from_utf8}, sync::{Arc, RwLock}};
use image::GenericImageView;
use tokio::{net::TcpListener, runtime::Handle, sync::mpsc, task::JoinError};
use tungstenite::Message;
use actix_web::{App, HttpRequest, HttpResponse, HttpServer, http::header, cookie::CookieBuilder, web, web::Data};
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 tungstenite::Message;
use futures::{StreamExt, TryStreamExt};
use rand::{RngCore, rngs::OsRng};
use serde::{Deserialize, Serialize};
@ -25,36 +25,41 @@ use identity::Identity;
use session_manager::{SessionManager, SessionCommand};
use ui_interface::UiConnection;
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());
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());
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();
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();
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;
}
}
Err(e) => print_error!(e)
}
Err(e) => print_error!(e)
}
}
Err(e) => print_error!(e)
}
Err(e) => print_error!(e)
}
}
}
@ -65,6 +70,7 @@ async fn start_websocket_server(ui_auth_token: Arc<RwLock<Option<String>>>, sess
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 {
@ -79,18 +85,19 @@ fn discover_peers(session_manager: Arc<SessionManager>) {
});
}
fn load_msgs(session_manager: &SessionManager, ui_connection: &mut UiConnection, session_id: &usize) {
fn load_msgs(session_manager: Arc<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, session_manager: Arc<SessionManager>) -> Result<(), JoinError> {
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();
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, &mut ui_connection, &contact.0);
load_msgs(session_manager.clone(), &mut ui_connection, &contact.0);
});
session_manager.sessions.read().unwrap().iter().for_each(|session| {
ui_connection.on_new_session(
@ -105,28 +112,14 @@ async fn websocket_worker(mut ui_connection: UiConnection, session_manager: Arc<
{
let not_seen = session_manager.not_seen.read().unwrap();
if not_seen.len() > 0 {
ui_connection.set_not_seen(&not_seen);
ui_connection.set_not_seen(not_seen.clone());
}
}
session_manager.get_saved_msgs().into_iter().for_each(|msgs| {
if !msgs.1.is_empty() {
if msgs.1.len() > 0 {
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 {
@ -136,10 +129,10 @@ async fn websocket_worker(mut ui_connection: UiConnection, session_manager: Arc<
}
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();
tokio::task::spawn_blocking(move || {
std::thread::spawn(move || { //new thread needed to block on read_message() without blocking tokio tasks
loop {
match ui_connection.websocket.read_message() {
Ok(msg) => {
@ -147,12 +140,10 @@ async fn websocket_worker(mut ui_connection: UiConnection, session_manager: Arc<
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_whitespace().collect();
let args: Vec<&str> = msg.split(" ").collect();
match args[0] {
"set_seen" => {
let session_id: usize = args[1].parse().unwrap();
@ -169,14 +160,11 @@ async fn websocket_worker(mut ui_connection: UiConnection, session_manager: Arc<
"refresh" => discover_peers(session_manager.clone()),
"send" => {
let session_id: usize = args[1].parse().unwrap();
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);
}
}
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);
}
}
"large_files" => {
@ -185,9 +173,9 @@ async fn websocket_worker(mut ui_connection: UiConnection, session_manager: Arc<
for n in (2..args.len()).step_by(2) {
file_info.push((args[n].parse::<u64>().unwrap(), base64::decode(args[n+1]).unwrap()));
}
#[allow(unused_must_use)] {
session_manager.send_or_add_to_pending(&session_id, protocol::ask_large_files(file_info)).await;
}
session_manager.send_command(&session_id, SessionCommand::Send {
buff: protocol::ask_large_files(file_info)
}).await;
}
"download" => {
let session_id: usize = args[1].parse().unwrap();
@ -207,7 +195,7 @@ async fn websocket_worker(mut ui_connection: UiConnection, session_manager: Arc<
}
"load_msgs" => {
let session_id: usize = args[1].parse().unwrap();
load_msgs(&session_manager, &mut ui_connection, &session_id);
load_msgs(session_manager.clone(), &mut ui_connection, &session_id);
}
"contact" => {
let session_id: usize = args[1].parse().unwrap();
@ -218,7 +206,7 @@ async fn websocket_worker(mut ui_connection: UiConnection, session_manager: Arc<
}
"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)
}
@ -271,11 +259,11 @@ async fn websocket_worker(mut ui_connection: UiConnection, session_manager: Arc<
(None, Some(base64::decode(args[1]).unwrap()))
};
let result = Identity::change_password(old_password.as_deref(), new_password.as_deref());
if let Some(mut old_password) = old_password {
old_password.zeroize();
if old_password.is_some() {
old_password.unwrap().zeroize();
}
let is_identity_protected = if let Some(mut new_password) = new_password {
new_password.zeroize();
let is_identity_protected = if new_password.is_some() {
new_password.unwrap().zeroize();
true
} else {
false
@ -299,6 +287,7 @@ async fn websocket_worker(mut ui_connection: UiConnection, session_manager: Arc<
Err(e) => {
match e {
tungstenite::Error::ConnectionClosed => {
*worker_done.write().unwrap() = true;
break;
}
_ => print_error!(e)
@ -306,13 +295,13 @@ async fn websocket_worker(mut ui_connection: UiConnection, session_manager: Arc<
}
}
}
}).await
});
}
fn is_authenticated(req: &HttpRequest) -> bool {
if let Some(cookie) = req.cookie(constants::HTTP_COOKIE_NAME) {
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
if let Some(token) = global_vars.ui_auth_token.read().unwrap().as_ref() {
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
if let Some(token) = &global_vars.read().unwrap().ui_auth_token {
return token == cookie.value();
}
}
@ -321,7 +310,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();
let content_disposition = field.content_disposition().unwrap();
if let Some(name) = content_disposition.get_name() {
if name == "avatar" {
let mut buffer = Vec::new();
@ -333,15 +322,17 @@ 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 = 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 image = if width < height {
image.crop_imm(0, (height-width)/2, width, width)
} else if width > height {
image.crop_imm((width-height)/2, 0, height, height)
} else {
image
};
let mut avatar = Vec::new();
image.write_to(&mut Cursor::new(&mut avatar), format).unwrap();
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
let session_manager = &global_vars.session_manager;
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;
let is_authenticated = is_authenticated(&req);
let is_running = session_manager.is_identity_loaded();
if is_authenticated || !is_running {
@ -379,23 +370,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().next().unwrap_or('?').to_string()))
HttpResponse::Ok().content_type("image/svg+xml").body(svg.replace("LETTER", &name.chars().nth(0).unwrap_or('?').to_string()))
}
None => HttpResponse::InternalServerError().finish()
}
}
}
async fn handle_avatar(req: HttpRequest) -> HttpResponse {
let splits: Vec<&str> = req.path()[1..].split('/').collect();
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<GlobalVars>>().unwrap();
return reply_with_avatar(global_vars.session_manager.get_avatar(&session_id), Some(splits[2]));
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]));
}
}
HttpResponse::BadRequest().finish()
@ -406,13 +397,16 @@ struct FileInfo {
uuid: String,
file_name: String,
}
async fn handle_load_file(req: HttpRequest, file_info: web::Query<FileInfo>) -> HttpResponse {
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<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);
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 => {}
}
}
Err(e) => print_error!(e)
@ -425,27 +419,33 @@ 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();
let content_disposition = field.content_disposition().unwrap();
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().to_owned();
let filename = content_disposition.get_filename().unwrap();
let session_id = session_id.unwrap();
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
let global_vars_read = global_vars.read().unwrap();
if req.path() == "/send_file" {
let mut buffer = Vec::new();
while let Some(Ok(chunk)) = field.next().await {
buffer.extend(chunk);
}
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")
};
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)
}
}
} 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.session_manager.send_command(&session_id, SessionCommand::EncryptFileChunk{
if !global_vars_read.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.session_manager.send_command(&session_id, SessionCommand::SendEncryptedFileChunk {
if global_vars_read.session_manager.send_command(&session_id, SessionCommand::SendEncryptedFileChunk {
ack_sender: ack_sender.clone()
}).await {
should_continue
@ -506,31 +506,29 @@ 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<GlobalVars>>().unwrap();
if global_vars.session_manager.is_identity_loaded() {
*global_vars.ui_auth_token.write().unwrap() = None;
global_vars.session_manager.stop().await;
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;
}
if Identity::is_protected().unwrap_or(true) {
HttpResponse::Found().append_header((header::LOCATION, "/")).finish()
HttpResponse::Found().header(header::LOCATION, "/").finish()
} else {
#[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)
HttpResponse::Ok().body(include_str!("frontend/logout.html"))
}
} else {
HttpResponse::Unauthorized().finish()
}
}
fn login(identity: Identity, global_vars: &GlobalVars) -> HttpResponse {
let session_manager = global_vars.session_manager.clone();
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();
if !session_manager.is_identity_loaded() {
session_manager.set_identity(Some(identity));
global_vars.tokio_handle.spawn(async move {
if SessionManager::start_listener(session_manager).await.is_err() {
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() {
print_error!("You won't be able to receive incomming connections from other peers.");
}
});
@ -538,15 +536,15 @@ fn login(identity: Identity, global_vars: &GlobalVars) -> HttpResponse {
let mut raw_cookie = [0; 32];
OsRng.fill_bytes(&mut raw_cookie);
let cookie_value = base64::encode(raw_cookie);
*global_vars.ui_auth_token.write().unwrap() = Some(cookie_value.clone());
global_vars_write.ui_auth_token = Some(cookie_value.clone());
let cookie = CookieBuilder::new(constants::HTTP_COOKIE_NAME, cookie_value).max_age(time::Duration::hours(4)).finish();
HttpResponse::Found()
.append_header((header::LOCATION, "/"))
.insert_header((header::SET_COOKIE, cookie.to_string()))
.header(header::LOCATION, "/")
.set_header(header::SET_COOKIE, cookie.to_string())
.finish()
}
fn on_identity_loaded(identity: Identity, global_vars: &Arc<GlobalVars>) -> HttpResponse {
fn on_identity_loaded(identity: Identity, global_vars: &Arc<RwLock<GlobalVars>>) -> HttpResponse {
match Identity::clear_cache() {
Ok(_) => {},
Err(e) => print_error!(e)
@ -558,13 +556,13 @@ fn on_identity_loaded(identity: Identity, global_vars: &Arc<GlobalVars>) -> Http
struct LoginParams {
password: String,
}
async fn handle_login(req: HttpRequest, mut params: web::Form<LoginParams>) -> HttpResponse {
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<GlobalVars>>().unwrap();
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
on_identity_loaded(identity, global_vars)
}
Err(e) => generate_login_response(Some(&e))
Err(e) => generate_login_response(Some(&e.to_string()))
};
params.password.zeroize();
response
@ -613,15 +611,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.is_empty() { //no password
if params.password.len() == 0 { //no password
None
} else {
Some(params.password.as_bytes())
}
) {
Ok(identity) => {
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
login(identity, global_vars)
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
login(identity, global_vars.get_ref())
}
Err(e) => {
print_error!(e);
@ -636,7 +634,7 @@ async fn handle_create(req: HttpRequest, mut params: web::Form<CreateParams>) ->
response
}
fn index_not_logged_in(global_vars: &Arc<GlobalVars>) -> HttpResponse {
fn index_not_logged_in(global_vars: &Arc<RwLock<GlobalVars>>) -> HttpResponse {
if Identity::is_protected().unwrap_or(true) {
generate_login_response(None)
} else {
@ -648,21 +646,22 @@ fn index_not_logged_in(global_vars: &Arc<GlobalVars>) -> HttpResponse {
}
async fn handle_index(req: HttpRequest) -> HttpResponse {
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
let global_vars = req.app_data::<Data<Arc<RwLock<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 identity = global_vars.session_manager.identity.read().unwrap();
let identity = identity.as_ref().unwrap();
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();
HttpResponse::Ok().body(
html
.replace("IDENTITY_FINGERPRINT", &crypto::generate_fingerprint(&identity.get_public_key()))
.replace("WEBSOCKET_PORT", &global_vars.websocket_port.to_string())
.replace("IDENTITY_FINGERPRINT", &crypto::generate_fingerprint(&public_key))
.replace("WEBSOCKET_PORT", &global_vars_read.websocket_port.to_string())
.replace("IS_IDENTITY_PROTECTED", &Identity::is_protected().unwrap().to_string())
.replace("PSEC_PADDING", &identity.use_padding.to_string())
.replace("PSEC_PADDING", &use_padding)
)
} else {
index_not_logged_in(global_vars)
@ -683,8 +682,8 @@ fn replace_fields(file_path: &str) -> String {
content
}
async fn handle_static(req: HttpRequest) -> HttpResponse {
let splits: Vec<&str> = req.path()[1..].split('/').collect();
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] {
@ -708,8 +707,7 @@ async fn handle_static(req: HttpRequest) -> HttpResponse {
} else {
"none"
};
if let Some(body) = match splits[3] {
"logo" => Some(include_str!("frontend/imgs/icons/logo.svg")),
match match splits[3] {
"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")),
@ -724,8 +722,11 @@ async fn handle_static(req: HttpRequest) -> HttpResponse {
"profile" => Some(include_str!("frontend/imgs/icons/profile.svg")),
_ => None
} {
response_builder.content_type("image/svg+xml");
return response_builder.body(body.replace("FILL_COLOR", color))
Some(body) => {
response_builder.content_type("image/svg+xml");
return response_builder.body(body.replace("FILL_COLOR", color))
}
None => {}
}
} else if splits.len() == 3 {
match splits[2] {
@ -765,12 +766,13 @@ async fn handle_static(req: HttpRequest) -> HttpResponse {
}
"libs" => {
if splits.len() == 3 {
if let Some(body) = match splits[2] {
match 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
} {
return response_builder.content_type(JS_CONTENT_TYPE).body(body);
Some(body) => return response_builder.content_type(JS_CONTENT_TYPE).body(body),
None => {}
}
}
}
@ -780,15 +782,17 @@ async fn handle_static(req: HttpRequest) -> HttpResponse {
HttpResponse::NotFound().finish()
}
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");
#[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");
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()
.app_data(Data::new(global_vars.clone()))
.data(global_vars_clone)
.service(web::resource("/")
.route(web::get().to(handle_index))
.route(web::post().to(handle_create))
@ -798,8 +802,8 @@ async fn start_http_server(global_vars: 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))?;
@ -811,11 +815,10 @@ async fn start_http_server(global_vars: GlobalVars) -> io::Result<()> {
server.run().await
}
#[derive(Clone)]
struct GlobalVars {
session_manager: Arc<SessionManager>,
websocket_port: u16,
ui_auth_token: Arc<RwLock<Option<String>>>,
ui_auth_token: Option<String>,
tokio_handle: Handle,
}
@ -826,13 +829,13 @@ async fn main() {
print_error!(e);
}
}
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,
let global_vars = Arc::new(RwLock::new(GlobalVars {
session_manager: Arc::new(SessionManager::new()),
websocket_port: 0,
ui_auth_token: None,
tokio_handle: Handle::current(),
}).await.unwrap();
}
}));
let websocket_port = start_websocket_server(global_vars.clone()).await;
global_vars.write().unwrap().websocket_port = websocket_port;
start_http_server(global_vars).unwrap();
}

View File

@ -17,7 +17,7 @@ impl Headers {
pub const ABORT_FILES_TRANSFER: u8 = 0x0a;
}
pub fn new_message(message: &str) -> Vec<u8> {
pub fn new_message(message: String) -> Vec<u8> {
[&[Headers::MESSAGE], message.as_bytes()].concat()
}
@ -33,21 +33,17 @@ 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 get_file_name(buffer: &[u8]) -> Option<&str> {
pub fn parse_file<'a>(buffer: &'a [u8]) -> Option<(&'a [u8], &'a [u8])> {
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 {
return from_utf8(&buffer[3..3+file_name_len]).ok();
let file_name = &buffer[3..3+file_name_len];
return Some((file_name, &buffer[3+file_name_len..]));
}
}
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,7 +4,8 @@ use libmdns::Service;
use uuid::Uuid;
use platform_dirs::UserDirs;
use async_psec::{PUBLIC_KEY_LENGTH, Session, SessionWriteHalf, PsecWriter, PsecReader, PsecError};
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}};
use crate::{constants, protocol, crypto, discovery, identity::{Contact, Identity}, print_error, utils::{get_unix_timestamp, get_not_used_path}};
use crate::ui_interface::UiConnection;
pub enum SessionCommand {
Send {
@ -54,8 +55,7 @@ pub struct SessionManager {
ui_connection: Mutex<Option<UiConnection>>,
loaded_contacts: RwLock<HashMap<usize, Contact>>,
pub last_loaded_msg_offsets: RwLock<HashMap<usize, usize>>,
saved_msgs: RwLock<HashMap<usize, Vec<Message>>>,
pub pending_msgs: Mutex<HashMap<usize, Vec<Vec<u8>>>>,
pub saved_msgs: RwLock<HashMap<usize, Vec<(bool, Vec<u8>)>>>,
pub not_seen: RwLock<Vec<usize>>,
mdns_service: Mutex<Option<Service>>,
listener_stop_signal: Mutex<Option<Sender<()>>>,
@ -65,10 +65,11 @@ 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();
if let Some(ui_connection) = ui_connection_opt.as_mut() {
if ui_connection.is_valid {
match ui_connection_opt.as_mut() {
Some(ui_connection) => if ui_connection.is_valid {
f(ui_connection);
}
None => {}
}
}
@ -87,11 +88,11 @@ impl SessionManager {
Ok(())
}
pub fn store_msg(&self, session_id: &usize, message: Message) {
pub fn store_msg(&self, session_id: &usize, outgoing: bool, buffer: Vec<u8>) {
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, &message) {
match self.identity.read().unwrap().as_ref().unwrap().store_msg(&contact.uuid, outgoing, &buffer) {
Ok(_) => {
*offsets.get_mut(session_id).unwrap() += 1;
msg_saved = true;
@ -100,16 +101,16 @@ impl SessionManager {
}
}
if !msg_saved {
//can be None if session disconnected
if let Some(saved_msgs) = self.saved_msgs.write().unwrap().get_mut(&session_id) {
saved_msgs.push(message)
}
self.saved_msgs.write().unwrap().get_mut(&session_id).unwrap().push((outgoing, buffer));
}
}
fn get_session_sender(&self, session_id: &usize) -> Option<Sender<SessionCommand>> {
let sessions = self.sessions.read().unwrap();
sessions.get(session_id).map(|session_data| session_data.sender.clone())
let mut sessions = self.sessions.write().unwrap();
match sessions.get_mut(session_id) {
Some(session_data) => Some(session_data.sender.clone()),
None => None
}
}
pub async fn send_command(&self, session_id: &usize, session_command: SessionCommand) -> bool {
@ -126,21 +127,6 @@ 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);
@ -165,66 +151,23 @@ impl SessionManager {
});
}
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)
}
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);
}
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);
});
}
_ => {}
*file_ack_sender = None;
}
}
self.with_ui_connection(|ui_connection| {
ui_connection.on_msg_sent(session_id, &buff);
});
Ok(())
}
@ -280,7 +223,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_ms();
file_transfer.last_chunk = get_unix_timestamp();
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() {
@ -338,7 +281,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_file_transfer_aborted(&session_id);
ui_connection.on_received(&session_id, &buffer);
});
}
protocol::Headers::ASK_LARGE_FILES => {
@ -350,7 +293,7 @@ impl SessionManager {
file_name: info.1,
file_size: info.0,
transferred: 0,
last_chunk: get_unix_timestamp_ms(),
last_chunk: get_unix_timestamp(),
}
}).collect();
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = Some(LargeFilesDownload {
@ -426,52 +369,46 @@ impl SessionManager {
protocol::Headers::REMOVE_AVATAR => self.set_avatar_uuid(&session_id, None),
_ => {
let header = buffer[0];
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);
}
let buffer = match header {
protocol::Headers::FILE => {
if let Some((filename, content)) = protocol::parse_file(&buffer) {
if let Some((file_name, content)) = protocol::parse_file(&buffer) {
match self.store_file(&session_id, content) {
Ok(file_uuid) => {
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(),
});
Some([&[protocol::Headers::FILE][..], file_uuid.as_bytes(), file_name].concat())
}
Err(e) => {
print_error!(e);
None
}
Err(e) => print_error!(e)
}
} else {
None
}
}
protocol::Headers::ACCEPT_LARGE_FILES => {
_ => {
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 {
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 {
if e != PsecError::BrokenPipe && e != PsecError::ConnectionReset && e != PsecError::BufferTooLarge {
print_error!(e);
}
break;
@ -484,9 +421,11 @@ 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 } => {
@ -499,9 +438,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.is_empty() {
while msg_queue.len() > 0 {
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;
}
@ -522,28 +461,9 @@ 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()
@ -588,7 +508,7 @@ impl SessionManager {
outgoing,
peer_public_key,
ip,
sender,
sender: sender,
files_download: None,
};
let mut session_id = None;
@ -618,10 +538,17 @@ 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);
});
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)
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;
}
}
}
session_manager.session_worker(session_id, receiver, session).await;
session_manager.remove_session(&session_id);
}
}
@ -656,7 +583,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<Message>> {
pub fn get_saved_msgs(&self) -> HashMap<usize, Vec<(bool, Vec<u8>)>> {
self.saved_msgs.read().unwrap().clone()
}
@ -669,13 +596,16 @@ impl SessionManager {
}
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
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)
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)
}
}
}
None => {}
}
}
@ -685,22 +615,20 @@ 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.pending_msgs.lock().unwrap().remove(session_id);
self.last_loaded_msg_offsets.write().unwrap().remove(&session_id);
}
result
}
@ -725,13 +653,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(
self.loaded_contacts.read().unwrap().get(session_id).map(|contact| contact.uuid),
data
)
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)
}
pub fn load_msgs(&self, session_id: &usize, count: usize) -> Option<Vec<Message>> {
pub fn load_msgs(&self, session_id: &usize, count: usize) -> Option<Vec<(bool, Vec<u8>)>> {
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,
@ -815,18 +743,20 @@ impl SessionManager {
}
*identity_guard = identity;
if identity_guard.is_some() { //login
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;
});
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 => {}
}
}
}
@ -844,7 +774,6 @@ 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,7 +1,133 @@
use std::{fmt::Display, net::{IpAddr, TcpStream}, str::from_utf8};
use std::net::{IpAddr, TcpStream};
use tungstenite::{WebSocket, protocol::Role, Message};
use uuid::Uuid;
use crate::{identity, print_error, protocol, session_manager::{LargeFileDownload, LargeFilesDownload}, utils::to_uuid_bytes};
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))
}
}
pub struct UiConnection{
pub websocket: WebSocket<TcpStream>,
@ -11,138 +137,81 @@ pub struct UiConnection{
impl UiConnection {
pub fn new(websocket: WebSocket<TcpStream>) -> UiConnection {
UiConnection {
websocket,
websocket: websocket,
is_valid: true
}
}
pub fn write_message<T: Into<Message>>(&mut self, message: T) {
if self.websocket.write_message(message.into()).is_err() {
pub fn write_message(&mut self, message: Message) {
if self.websocket.write_message(message).is_err() {
self.is_valid = false
}
}
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_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())
}
}
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_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_session(&mut self, session_id: &usize, name: &str, outgoing: bool, fingerprint: &str, ip: IpAddr, files_transfer: Option<&LargeFilesDownload>) {
self.write_message(format!("new_session {} {} {} {} {}", session_id, outgoing, fingerprint, ip, name));
self.write_message(ui_messages::on_new_session(session_id, name, outgoing, fingerprint, ip));
if let Some(files_transfer) = 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())
}
self.write_message(ui_messages::new_files_transfer(session_id, files_transfer));
}
}
pub fn on_disconnected(&mut self, session_id: &usize) {
self.simple_event("disconnected", session_id);
self.write_message(ui_messages::on_disconnected(session_id));
}
pub fn on_name_told(&mut self, session_id: &usize, name: &str) {
self.write_message(format!("name_told {} {}", session_id, name));
self.write_message(ui_messages::on_name_told(session_id, name));
}
pub fn on_avatar_changed(&mut self, session_id: Option<&usize>) {
match session_id {
Some(session_id) => self.simple_event("avatar_changed", session_id),
None => self.write_message("avatar_changed self")
}
self.write_message(ui_messages::on_avatar_changed(session_id));
}
pub fn inc_files_transfer(&mut self, session_id: &usize, chunk_size: u64) {
self.write_message(format!("inc_file_transfer {} {}", session_id, chunk_size));
self.write_message(ui_messages::inc_files_transfer(session_id, chunk_size));
}
pub fn set_as_contact(&mut self, session_id: usize, name: &str, verified: bool, fingerprint: &str) {
self.write_message(format!("is_contact {} {} {} {}", session_id, verified, fingerprint, name));
self.write_message(ui_messages::set_as_contact(session_id, name, verified, fingerprint));
}
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 load_msgs(&mut self, session_id: &usize, msgs: &Vec<(bool, Vec<u8>)>) {
self.write_message(ui_messages::load_msgs(session_id, msgs));
}
pub fn set_not_seen(&mut self, session_ids: &[usize]) {
self.write_message(Self::data_list("not_seen", session_ids));
pub fn set_not_seen(&mut self, session_ids: Vec<usize>) {
self.write_message(ui_messages::set_not_seen(session_ids));
}
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_local_ips(&mut self, ips: Vec<IpAddr>) {
self.write_message(ui_messages::set_local_ips(ips));
}
pub fn set_name(&mut self, new_name: &str) {
self.write_message(format!("set_name {}", new_name));
self.write_message(ui_messages::set_name(new_name));
}
pub fn password_changed(&mut self, success: bool, is_protected: bool) {
self.write_message(format!("password_changed {} {}", success, is_protected));
self.write_message(ui_messages::password_changed(success, is_protected));
}
pub fn logout(&mut self) {
self.write_message("logout");
self.write_message(Message::from("logout"));
}
}

View File

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