mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 21:46:35 +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",
|
||||
"url",
|
||||
"uuid",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4254,3 +4255,15 @@ dependencies = [
|
||||
"syn",
|
||||
"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"
|
||||
tagger = "4.0.1"
|
||||
textwrap = "0.14.2"
|
||||
zip = { version = "0.5.13", default-features = false, features = ["deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
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);
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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()
|
||||
}
|
||||
|
||||
#[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]
|
||||
pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
|
||||
@@ -1168,7 +1168,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
|
||||
"ttf" => (Viewtype::File, "font/ttf"),
|
||||
"vcard" => (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"),
|
||||
"weba" => (Viewtype::File, "audio/webm"),
|
||||
"webm" => (Viewtype::Video, "video/webm"),
|
||||
@@ -1705,7 +1705,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
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::context::Context;
|
||||
use crate::dc_tools::dc_open_file_std;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::{chat, EventType};
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{bail, ensure, format_err, Result};
|
||||
use lettre_email::mime::{self};
|
||||
use lettre_email::PartBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::convert::TryFrom;
|
||||
use std::io::Read;
|
||||
|
||||
pub const W30_SUFFIX: &str = "w30";
|
||||
|
||||
@@ -49,12 +51,16 @@ pub(crate) struct StatusUpdateItem {
|
||||
}
|
||||
|
||||
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) {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
let reader = std::io::Cursor::new(buf);
|
||||
if let Ok(mut archive) = zip::ZipArchive::new(reader) {
|
||||
if let Ok(_index_html) = archive.by_name("index.html") {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn create_status_update_record(
|
||||
@@ -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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -239,14 +265,35 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
assert!(
|
||||
!t.is_w30_file(
|
||||
"issue_523.txt",
|
||||
"bad-ext-no-zip.txt",
|
||||
include_bytes!("../test-data/message/issue_523.txt")
|
||||
)
|
||||
.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!(
|
||||
t.is_w30_file(
|
||||
"minimal.w30",
|
||||
"good-ext-good-zip.w30",
|
||||
include_bytes!("../test-data/w30/minimal.w30")
|
||||
)
|
||||
.await?
|
||||
@@ -255,10 +302,10 @@ mod tests {
|
||||
}
|
||||
|
||||
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)
|
||||
.await?
|
||||
.write_all("<html>ola!</html>".as_ref())
|
||||
.write_all(include_bytes!("../test-data/w30/minimal.w30"))
|
||||
.await?;
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
instance.set_file(file.to_str().unwrap(), None);
|
||||
@@ -279,7 +326,7 @@ mod tests {
|
||||
// send as .w30 file
|
||||
let instance = send_w30_instance(&t, chat_id).await?;
|
||||
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);
|
||||
|
||||
// sending using bad extension is not working, even when setting Viewtype to W30
|
||||
@@ -308,7 +355,7 @@ mod tests {
|
||||
.await?;
|
||||
let instance = t.get_last_msg().await;
|
||||
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(
|
||||
&t,
|
||||
@@ -596,14 +643,17 @@ mod tests {
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
let alice_instance = Message::load_from_db(&alice, alice_instance_id).await?;
|
||||
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);
|
||||
|
||||
// bob receives the instance together with the initial updates in a single message
|
||||
bob.recv_msg(&sent1).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
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("status-update.json"));
|
||||
assert!(sent1.payload().contains(r#""payload":{"foo":"bar"}"#));
|
||||
@@ -627,4 +677,63 @@ mod tests {
|
||||
.is_err());
|
||||
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
|
||||
|
||||
w30 with good extension;
|
||||
the mimetype is ignored then.
|
||||
the mimetype is ignored then,
|
||||
content is checked.
|
||||
|
||||
--==BREAK==
|
||||
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==--
|
||||
|
||||
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