Compare commits

...

17 Commits

18 changed files with 1857 additions and 1579 deletions

1736
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>AIRA - Login</title> <title>AIRA - Login</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/commons/style.css">
<style> <style>
body { body {
@ -58,8 +59,7 @@
padding: 10px 50px; padding: 10px 50px;
} }
.avatar { .avatar {
width: 7em; font-size: 3em;
height: 7em;
} }
#identity h2 { #identity h2 {
text-align: center; text-align: center;

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>AIRA - Logged out</title> <title>AIRA - Logged out</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/commons/style.css">
<style> <style>
body { body {

View File

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

View File

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

View File

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

View File

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

View File

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