mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 17:36:29 +03:00
check that the w30 app is actually an zip-archive with an index.html
This commit is contained in:
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -1131,6 +1131,7 @@ dependencies = [
|
|||||||
"toml",
|
"toml",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4254,3 +4255,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "0.5.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"crc32fast",
|
||||||
|
"flate2",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ humansize = "1"
|
|||||||
qrcodegen = "1.7.0"
|
qrcodegen = "1.7.0"
|
||||||
tagger = "4.0.1"
|
tagger = "4.0.1"
|
||||||
textwrap = "0.14.2"
|
textwrap = "0.14.2"
|
||||||
|
zip = { version = "0.5.13", default-features = false, features = ["deflate"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
ansi_term = "0.12.0"
|
ansi_term = "0.12.0"
|
||||||
|
|||||||
@@ -3675,6 +3675,20 @@ char* dc_msg_get_filename (const dc_msg_t* msg);
|
|||||||
char* dc_msg_get_filemime (const dc_msg_t* msg);
|
char* dc_msg_get_filemime (const dc_msg_t* msg);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return file from inside an archive.
|
||||||
|
* Currently, this works for W30 messages only.
|
||||||
|
*
|
||||||
|
* @param msg The W30 instance.
|
||||||
|
* @param filename The name inside the archive,
|
||||||
|
* must be given as a relative path (no leading `/`).
|
||||||
|
* @param ret_bytes Pointer to a size_t. The size of the blob will be written here.
|
||||||
|
* @return The blob must be released using dc_str_unref() after usage.
|
||||||
|
* NULL if there is no such file in the archive or on errors.
|
||||||
|
*/
|
||||||
|
char* dc_msg_get_blob_from_archive (const dc_msg_t* msg, const char* filename, size_t* ret_bytes);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the size of the file. Returns the size of the file associated with a
|
* Get the size of the file. Returns the size of the file associated with a
|
||||||
* message, if applicable.
|
* message, if applicable.
|
||||||
|
|||||||
@@ -3070,6 +3070,39 @@ pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c
|
|||||||
ffi_msg.message.get_filename().unwrap_or_default().strdup()
|
ffi_msg.message.get_filename().unwrap_or_default().strdup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_msg_get_blob_from_archive(
|
||||||
|
msg: *mut dc_msg_t,
|
||||||
|
filename: *const libc::c_char,
|
||||||
|
ret_bytes: *mut libc::size_t,
|
||||||
|
) -> *mut libc::c_char {
|
||||||
|
if msg.is_null() || filename.is_null() || ret_bytes.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_msg_get_blob_from_archive()");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
let ffi_msg = &*msg;
|
||||||
|
let ctx = &*ffi_msg.context;
|
||||||
|
let blob = block_on(async move {
|
||||||
|
ffi_msg
|
||||||
|
.message
|
||||||
|
.get_blob_from_archive(ctx, &to_string_lossy(filename))
|
||||||
|
.await
|
||||||
|
});
|
||||||
|
match blob {
|
||||||
|
Ok(blob) => {
|
||||||
|
// TODO: introduce dc_blob_t to avoid malloc and returning size by pointer and to save copying data
|
||||||
|
*ret_bytes = blob.len();
|
||||||
|
let ptr = libc::malloc(*ret_bytes);
|
||||||
|
libc::memcpy(ptr, blob.as_ptr() as *mut libc::c_void, *ret_bytes);
|
||||||
|
ptr as *mut libc::c_char
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("failed read blob from archive: {}", err);
|
||||||
|
ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||||
if msg.is_null() {
|
if msg.is_null() {
|
||||||
|
|||||||
@@ -1168,7 +1168,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
|
|||||||
"ttf" => (Viewtype::File, "font/ttf"),
|
"ttf" => (Viewtype::File, "font/ttf"),
|
||||||
"vcard" => (Viewtype::File, "text/vcard"),
|
"vcard" => (Viewtype::File, "text/vcard"),
|
||||||
"vcf" => (Viewtype::File, "text/vcard"),
|
"vcf" => (Viewtype::File, "text/vcard"),
|
||||||
"w30" => (Viewtype::W30, "application/html+w30"),
|
"w30" => (Viewtype::W30, "application/w30+zip"),
|
||||||
"wav" => (Viewtype::File, "audio/wav"),
|
"wav" => (Viewtype::File, "audio/wav"),
|
||||||
"weba" => (Viewtype::File, "audio/webm"),
|
"weba" => (Viewtype::File, "audio/webm"),
|
||||||
"webm" => (Viewtype::Video, "video/webm"),
|
"webm" => (Viewtype::Video, "video/webm"),
|
||||||
@@ -1705,7 +1705,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
guess_msgtype_from_suffix(Path::new("foo/file.w30")),
|
guess_msgtype_from_suffix(Path::new("foo/file.w30")),
|
||||||
Some((Viewtype::W30, "application/html+w30"))
|
Some((Viewtype::W30, "application/w30+zip"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
135
src/w30.rs
135
src/w30.rs
@@ -2,16 +2,18 @@
|
|||||||
|
|
||||||
use crate::constants::Viewtype;
|
use crate::constants::Viewtype;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
|
use crate::dc_tools::dc_open_file_std;
|
||||||
use crate::message::{Message, MessageState, MsgId};
|
use crate::message::{Message, MessageState, MsgId};
|
||||||
use crate::mimeparser::SystemMessage;
|
use crate::mimeparser::SystemMessage;
|
||||||
use crate::param::Param;
|
use crate::param::Param;
|
||||||
use crate::{chat, EventType};
|
use crate::{chat, EventType};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, ensure, format_err, Result};
|
||||||
use lettre_email::mime::{self};
|
use lettre_email::mime::{self};
|
||||||
use lettre_email::PartBuilder;
|
use lettre_email::PartBuilder;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
pub const W30_SUFFIX: &str = "w30";
|
pub const W30_SUFFIX: &str = "w30";
|
||||||
|
|
||||||
@@ -49,13 +51,17 @@ pub(crate) struct StatusUpdateItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
pub(crate) async fn is_w30_file(&self, filename: &str, _decoded_data: &[u8]) -> Result<bool> {
|
pub(crate) async fn is_w30_file(&self, filename: &str, buf: &[u8]) -> Result<bool> {
|
||||||
if filename.ends_with(W30_SUFFIX) {
|
if filename.ends_with(W30_SUFFIX) {
|
||||||
Ok(true)
|
let reader = std::io::Cursor::new(buf);
|
||||||
} else {
|
if let Ok(mut archive) = zip::ZipArchive::new(reader) {
|
||||||
Ok(false)
|
if let Ok(_index_html) = archive.by_name("index.html") {
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_status_update_record(
|
async fn create_status_update_record(
|
||||||
&self,
|
&self,
|
||||||
@@ -221,6 +227,26 @@ impl Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Message {
|
||||||
|
/// Return file form inside an archive.
|
||||||
|
/// Currently, this works only if the message is an w30 instance.
|
||||||
|
pub async fn get_blob_from_archive(&self, context: &Context, name: &str) -> Result<Vec<u8>> {
|
||||||
|
ensure!(self.viewtype == Viewtype::W30, "No w30 instance.");
|
||||||
|
|
||||||
|
let archive = self
|
||||||
|
.get_file(context)
|
||||||
|
.ok_or_else(|| format_err!("No w30 instance file."))?;
|
||||||
|
let archive = dc_open_file_std(context, archive)?;
|
||||||
|
let mut archive = zip::ZipArchive::new(archive)?;
|
||||||
|
|
||||||
|
let mut file = archive.by_name(name)?;
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
file.read_to_end(&mut buf)?;
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -239,14 +265,35 @@ mod tests {
|
|||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
assert!(
|
assert!(
|
||||||
!t.is_w30_file(
|
!t.is_w30_file(
|
||||||
"issue_523.txt",
|
"bad-ext-no-zip.txt",
|
||||||
include_bytes!("../test-data/message/issue_523.txt")
|
include_bytes!("../test-data/message/issue_523.txt")
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
!t.is_w30_file(
|
||||||
|
"bad-ext-good-zip.txt",
|
||||||
|
include_bytes!("../test-data/w30/minimal.w30")
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!t.is_w30_file(
|
||||||
|
"good-ext-no-zip.w30",
|
||||||
|
include_bytes!("../test-data/message/issue_523.txt")
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!t.is_w30_file(
|
||||||
|
"good-ext-no-index-html.w30",
|
||||||
|
include_bytes!("../test-data/w30/no-index-html.w30")
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
t.is_w30_file(
|
t.is_w30_file(
|
||||||
"minimal.w30",
|
"good-ext-good-zip.w30",
|
||||||
include_bytes!("../test-data/w30/minimal.w30")
|
include_bytes!("../test-data/w30/minimal.w30")
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
@@ -255,10 +302,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn create_w30_instance(t: &TestContext) -> Result<Message> {
|
async fn create_w30_instance(t: &TestContext) -> Result<Message> {
|
||||||
let file = t.get_blobdir().join("index.w30");
|
let file = t.get_blobdir().join("minimal.w30");
|
||||||
File::create(&file)
|
File::create(&file)
|
||||||
.await?
|
.await?
|
||||||
.write_all("<html>ola!</html>".as_ref())
|
.write_all(include_bytes!("../test-data/w30/minimal.w30"))
|
||||||
.await?;
|
.await?;
|
||||||
let mut instance = Message::new(Viewtype::File);
|
let mut instance = Message::new(Viewtype::File);
|
||||||
instance.set_file(file.to_str().unwrap(), None);
|
instance.set_file(file.to_str().unwrap(), None);
|
||||||
@@ -279,7 +326,7 @@ mod tests {
|
|||||||
// send as .w30 file
|
// send as .w30 file
|
||||||
let instance = send_w30_instance(&t, chat_id).await?;
|
let instance = send_w30_instance(&t, chat_id).await?;
|
||||||
assert_eq!(instance.viewtype, Viewtype::W30);
|
assert_eq!(instance.viewtype, Viewtype::W30);
|
||||||
assert_eq!(instance.get_filename(), Some("index.w30".to_string()));
|
assert_eq!(instance.get_filename(), Some("minimal.w30".to_string()));
|
||||||
assert_eq!(instance.chat_id, chat_id);
|
assert_eq!(instance.chat_id, chat_id);
|
||||||
|
|
||||||
// sending using bad extension is not working, even when setting Viewtype to W30
|
// sending using bad extension is not working, even when setting Viewtype to W30
|
||||||
@@ -308,7 +355,7 @@ mod tests {
|
|||||||
.await?;
|
.await?;
|
||||||
let instance = t.get_last_msg().await;
|
let instance = t.get_last_msg().await;
|
||||||
assert_eq!(instance.viewtype, Viewtype::W30);
|
assert_eq!(instance.viewtype, Viewtype::W30);
|
||||||
assert_eq!(instance.get_filename(), Some("index.w30".to_string()));
|
assert_eq!(instance.get_filename(), Some("minimal.w30".to_string()));
|
||||||
|
|
||||||
dc_receive_imf(
|
dc_receive_imf(
|
||||||
&t,
|
&t,
|
||||||
@@ -596,14 +643,17 @@ mod tests {
|
|||||||
let sent1 = alice.pop_sent_msg().await;
|
let sent1 = alice.pop_sent_msg().await;
|
||||||
let alice_instance = Message::load_from_db(&alice, alice_instance_id).await?;
|
let alice_instance = Message::load_from_db(&alice, alice_instance_id).await?;
|
||||||
assert_eq!(alice_instance.viewtype, Viewtype::W30);
|
assert_eq!(alice_instance.viewtype, Viewtype::W30);
|
||||||
assert_eq!(alice_instance.get_filename(), Some("index.w30".to_string()));
|
assert_eq!(
|
||||||
|
alice_instance.get_filename(),
|
||||||
|
Some("minimal.w30".to_string())
|
||||||
|
);
|
||||||
assert_eq!(alice_instance.chat_id, alice_chat_id);
|
assert_eq!(alice_instance.chat_id, alice_chat_id);
|
||||||
|
|
||||||
// bob receives the instance together with the initial updates in a single message
|
// bob receives the instance together with the initial updates in a single message
|
||||||
bob.recv_msg(&sent1).await;
|
bob.recv_msg(&sent1).await;
|
||||||
let bob_instance = bob.get_last_msg().await;
|
let bob_instance = bob.get_last_msg().await;
|
||||||
assert_eq!(bob_instance.viewtype, Viewtype::W30);
|
assert_eq!(bob_instance.viewtype, Viewtype::W30);
|
||||||
assert_eq!(bob_instance.get_filename(), Some("index.w30".to_string()));
|
assert_eq!(bob_instance.get_filename(), Some("minimal.w30".to_string()));
|
||||||
assert!(sent1.payload().contains("Content-Type: application/json"));
|
assert!(sent1.payload().contains("Content-Type: application/json"));
|
||||||
assert!(sent1.payload().contains("status-update.json"));
|
assert!(sent1.payload().contains("status-update.json"));
|
||||||
assert!(sent1.payload().contains(r#""payload":{"foo":"bar"}"#));
|
assert!(sent1.payload().contains(r#""payload":{"foo":"bar"}"#));
|
||||||
@@ -627,4 +677,63 @@ mod tests {
|
|||||||
.is_err());
|
.is_err());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_get_blob_from_archive() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||||
|
let instance = send_w30_instance(&t, chat_id).await?;
|
||||||
|
|
||||||
|
let buf = instance.get_blob_from_archive(&t, "index.html").await?;
|
||||||
|
assert_eq!(buf.len(), 188);
|
||||||
|
assert!(String::from_utf8_lossy(&buf).contains("document.write"));
|
||||||
|
|
||||||
|
assert!(instance
|
||||||
|
.get_blob_from_archive(&t, "not-existent.html")
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_get_blob_from_archive_subdirs() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||||
|
let file = t.get_blobdir().join("some-files.w30");
|
||||||
|
File::create(&file)
|
||||||
|
.await?
|
||||||
|
.write_all(include_bytes!("../test-data/w30/some-files.w30"))
|
||||||
|
.await?;
|
||||||
|
let mut instance = Message::new(Viewtype::W30);
|
||||||
|
instance.set_file(file.to_str().unwrap(), None);
|
||||||
|
chat_id.set_draft(&t, Some(&mut instance)).await?;
|
||||||
|
|
||||||
|
let buf = instance.get_blob_from_archive(&t, "index.html").await?;
|
||||||
|
assert_eq!(buf.len(), 65);
|
||||||
|
assert!(String::from_utf8_lossy(&buf).contains("many files"));
|
||||||
|
|
||||||
|
let buf = instance.get_blob_from_archive(&t, "subdir/bla.txt").await?;
|
||||||
|
assert_eq!(buf.len(), 4);
|
||||||
|
assert!(String::from_utf8_lossy(&buf).starts_with("bla"));
|
||||||
|
|
||||||
|
let buf = instance
|
||||||
|
.get_blob_from_archive(&t, "subdir/subsubdir/text.md")
|
||||||
|
.await?;
|
||||||
|
assert_eq!(buf.len(), 24);
|
||||||
|
assert!(String::from_utf8_lossy(&buf).starts_with("this is a markdown file"));
|
||||||
|
|
||||||
|
let buf = instance
|
||||||
|
.get_blob_from_archive(&t, "subdir/subsubdir/text2.md")
|
||||||
|
.await?;
|
||||||
|
assert_eq!(buf.len(), 22);
|
||||||
|
assert!(String::from_utf8_lossy(&buf).starts_with("another markdown"));
|
||||||
|
|
||||||
|
let buf = instance
|
||||||
|
.get_blob_from_archive(&t, "anotherdir/anothersubsubdir/foo.txt")
|
||||||
|
.await?;
|
||||||
|
assert_eq!(buf.len(), 4);
|
||||||
|
assert!(String::from_utf8_lossy(&buf).starts_with("foo"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,19 @@ Content-Type: multipart/mixed; boundary="==BREAK=="
|
|||||||
Content-Type: text/plain; charset=utf-8
|
Content-Type: text/plain; charset=utf-8
|
||||||
|
|
||||||
w30 with good extension;
|
w30 with good extension;
|
||||||
the mimetype is ignored then.
|
the mimetype is ignored then,
|
||||||
|
content is checked.
|
||||||
|
|
||||||
--==BREAK==
|
--==BREAK==
|
||||||
Content-Type: text/html
|
Content-Type: text/html
|
||||||
Content-Disposition: attachment; filename=index.w30
|
Content-Disposition: attachment; filename=minimal.w30
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
<html>hey!<html>
|
UEsDBBQAAgAIAFJqnVOItjSofAAAALwAAAAKABwAaW5kZXguaHRtbFVUCQADG1LMYV1SzGF1eAsAAQ
|
||||||
|
T1AQAABBQAAACzUXTxdw6JDHBVyCjJzbHjsoFQCgo2SfkplSCGgkJJanGJIphlU5xclFlQAhFWUEjJ
|
||||||
|
Ty7NTc0r0SsvyixJ1VAqyU8EqlTStIYo1kdWbZOXj6o5uiQjsxhodkWJQnFGfmlOikJefolCUiqIV5
|
||||||
|
4XCzUCWZeNPsRNNvoQRwIAUEsBAh4DFAACAAgAUmqdU4i2NKh8AAAAvAAAAAoAGAAAAAAAAQAAAKSB
|
||||||
|
AAAAAGluZGV4Lmh0bWxVVAUAAxtSzGF1eAsAAQT1AQAABBQAAABQSwUGAAAAAAEAAQBQAAAAwAAAAA
|
||||||
|
AA
|
||||||
|
|
||||||
--==BREAK==--
|
--==BREAK==--
|
||||||
|
|||||||
Binary file not shown.
BIN
test-data/w30/no-index-html.w30
Normal file
BIN
test-data/w30/no-index-html.w30
Normal file
Binary file not shown.
BIN
test-data/w30/some-files.w30
Normal file
BIN
test-data/w30/some-files.w30
Normal file
Binary file not shown.
Reference in New Issue
Block a user