check that the w30 app is actually an zip-archive with an index.html

This commit is contained in:
B. Petersen
2021-12-28 17:54:06 +01:00
committed by bjoern
parent bd988d805c
commit b656a60234
10 changed files with 195 additions and 18 deletions

13
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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"

View File

@@ -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.

View File

@@ -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() {

View File

@@ -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"))
);
}

View File

@@ -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(())
}
}

View File

@@ -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.

Binary file not shown.

Binary file not shown.