Skip to main content

malwaredb_server/http/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use super::State;
4use malwaredb_api::digest::HashType;
5use malwaredb_api::{
6    GetAPIKeyRequest, GetAPIKeyResponse, GetUserInfoResponse, Labels, MDB_API_HEADER, NewSampleB64,
7    NewSampleBytes, Report, SearchRequest, SearchResponse, ServerError, ServerInfo, ServerResponse,
8    SimilarSamplesRequest, SimilarSamplesResponse, Sources, SupportedFileTypes, YaraSearchRequest,
9    YaraSearchRequestResponse, YaraSearchResponse,
10};
11
12use std::fmt::{Display, Formatter};
13use std::io::Cursor;
14use std::iter::once;
15use std::sync::Arc;
16
17use axum::body::Bytes;
18use axum::extract::{DefaultBodyLimit, Path, Request};
19use axum::http::{StatusCode, header};
20use axum::middleware::Next;
21use axum::response::{IntoResponse, Response};
22use axum::routing::{get, post};
23use axum::{Extension, Json, Router, middleware};
24use axum_cbor::Cbor;
25use base64::{Engine as _, engine::general_purpose};
26use constcat::concat;
27use http::{HeaderMap, HeaderName, HeaderValue};
28use sha2::{Digest, Sha256};
29use tower_http::compression::CompressionLayer;
30use tower_http::decompression::DecompressionLayer;
31use tower_http::limit::RequestBodyLimitLayer;
32use tower_http::sensitive_headers::SetSensitiveHeadersLayer;
33use uuid::Uuid;
34
35mod receive;
36
37const FAVICON_URL: &str = "/favicon.ico";
38
39/// Build the web service from the initial server state object
40pub fn app(state: Arc<State>) -> Router {
41    // This is an estimate, so err on the side of usability.
42    const UPLOAD_OVERHEAD: usize = std::mem::size_of::<Json<NewSampleB64>>() * 2;
43
44    let compression_layer = CompressionLayer::new()
45        .br(true)
46        .deflate(true)
47        .gzip(true)
48        .zstd(true);
49
50    let decompression_layer = DecompressionLayer::new()
51        .br(true)
52        .deflate(true)
53        .gzip(true)
54        .zstd(true);
55
56    let size_limit_layer = RequestBodyLimitLayer::new(UPLOAD_OVERHEAD + state.max_upload);
57
58    Router::new()
59        .route("/", get(health))
60        .route(FAVICON_URL, get(favicon))
61        .route(malwaredb_api::SERVER_INFO_URL, get(get_mdb_info))
62        .route(malwaredb_api::USER_LOGIN_URL, post(user_login))
63        .route(malwaredb_api::USER_LOGOUT_URL, get(user_logout))
64        .route(malwaredb_api::USER_INFO_URL, get(get_user_groups_sources))
65        .route(malwaredb_api::SUPPORTED_FILE_TYPES_URL, get(get_supported_types))
66        .route(malwaredb_api::LIST_LABELS_URL, get(get_labels))
67        .route(malwaredb_api::LIST_SOURCES_URL, get(get_sources))
68        .route(malwaredb_api::UPLOAD_SAMPLE_JSON_URL, post(upload_new_sample_json))
69        .route(malwaredb_api::UPLOAD_SAMPLE_CBOR_URL, post(upload_new_sample_cbor))
70        .route(malwaredb_api::SEARCH_URL, post(sample_search))
71        .route(concat!(malwaredb_api::DOWNLOAD_SAMPLE_URL, "/{hash}"), get(download_sample))
72        .route(
73            concat!(malwaredb_api::DOWNLOAD_SAMPLE_CART_URL, "/{hash}"),
74            get(download_sample_cart),
75        )
76        .route(concat!(malwaredb_api::SAMPLE_REPORT_URL, "/{hash}"), get(sample_report))
77        .route(malwaredb_api::SIMILAR_SAMPLES_URL, post(find_similar))
78        .route(malwaredb_api::YARA_SEARCH_URL, post(yara_search))
79        .route(concat!(malwaredb_api::YARA_SEARCH_URL, "/{uuid}"), get(yara_search_get))
80        .layer(DefaultBodyLimit::max(state.max_upload))
81        .layer(compression_layer)
82        .layer(decompression_layer)
83        .layer(size_limit_layer)
84        .layer(SetSensitiveHeadersLayer::new(once(HeaderName::from_static(MDB_API_HEADER))))
85        .route_layer(middleware::from_fn(response_header_middleware))
86        .layer(Extension(state))
87}
88
89/// User ID from the API key, generated by the middleware function
90struct UserInfo {
91    pub id: u32,
92}
93
94/// * Ask that content from Malware DB not be cached
95/// * One central place to get the API key and get the user's ID
96#[inline]
97async fn response_header_middleware(
98    Extension(state): Extension<Arc<State>>,
99    headers: HeaderMap,
100    mut req: Request,
101    next: Next,
102) -> Result<Response, HttpError> {
103    const ALWAYS_ALLOWED_ENDPOINTS: [&str; 4] = [
104        "/",
105        FAVICON_URL,
106        malwaredb_api::SERVER_INFO_URL,
107        malwaredb_api::USER_LOGIN_URL,
108    ];
109
110    if !ALWAYS_ALLOWED_ENDPOINTS.contains(&req.uri().path()) {
111        let uid = if let Some(key) = headers.get(MDB_API_HEADER) {
112            let key = key
113                .to_str()
114                .map_err(|_| HttpError(ServerError::Unauthorized, StatusCode::NOT_ACCEPTABLE))?;
115            state.db_type.get_uid(key).await.map_err(|e| {
116                tracing::warn!("Failed to get user ID from API key: {e}");
117                HttpError(ServerError::Unauthorized, StatusCode::UNAUTHORIZED)
118            })?
119        } else {
120            #[cfg(feature = "anonymous")]
121            {
122                if let Some(anon_uid) = state.db_config.anonymous_uid {
123                    tracing::info!("Using anonymous user ID: {anon_uid}");
124                    anon_uid
125                } else {
126                    return Err(HttpError(ServerError::Unauthorized, StatusCode::UNAUTHORIZED));
127                }
128            }
129            #[cfg(not(feature = "anonymous"))]
130            return Err(crate::http::HttpError(
131                ServerError::Unauthorized,
132                StatusCode::UNAUTHORIZED,
133            ));
134        };
135
136        req.extensions_mut().insert(Arc::new(UserInfo { id: uid }));
137    }
138
139    let mut response = next.run(req).await; // Run the next service in the chain
140
141    // `no-store` so an intermediate proxy doesn't save any data
142    // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control
143    response
144        .headers_mut()
145        .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
146
147    Ok(response)
148}
149
150#[inline]
151async fn health() -> StatusCode {
152    StatusCode::OK
153}
154
155#[inline]
156async fn favicon() -> Response {
157    const ICON: Bytes = Bytes::from_static(include_bytes!("../../MDB_Logo.ico"));
158
159    let mut bytes = ICON.into_response();
160    bytes
161        .headers_mut()
162        .insert(header::CONTENT_TYPE, HeaderValue::from_static("image/vnd.microsoft.icon"));
163
164    bytes
165}
166
167#[inline]
168async fn get_mdb_info(
169    Extension(state): Extension<Arc<State>>,
170) -> Result<Json<ServerResponse<ServerInfo>>, HttpError> {
171    let server_info = ServerResponse::Success(state.get_info().await?);
172    Ok(Json(server_info))
173}
174
175#[inline]
176async fn user_login(
177    Extension(state): Extension<Arc<State>>,
178    Json(payload): Json<GetAPIKeyRequest>,
179) -> Result<Json<ServerResponse<GetAPIKeyResponse>>, HttpError> {
180    let api_key = state
181        .db_type
182        .authenticate(&payload.user, &payload.password)
183        .await?;
184
185    Ok(Json(ServerResponse::Success(GetAPIKeyResponse {
186        key: api_key,
187        message: None,
188    })))
189}
190
191#[inline]
192async fn user_logout(
193    Extension(state): Extension<Arc<State>>,
194    Extension(user): Extension<Arc<UserInfo>>,
195) -> Result<StatusCode, HttpError> {
196    state.db_type.reset_own_api_key(user.id).await?;
197    Ok(StatusCode::OK)
198}
199
200#[inline]
201async fn get_user_groups_sources(
202    Extension(state): Extension<Arc<State>>,
203    Extension(user): Extension<Arc<UserInfo>>,
204) -> Result<Json<ServerResponse<GetUserInfoResponse>>, HttpError> {
205    let groups_sources = ServerResponse::Success(state.db_type.get_user_info(user.id).await?);
206    Ok(Json(groups_sources))
207}
208
209#[inline]
210async fn get_supported_types(
211    Extension(state): Extension<Arc<State>>,
212) -> Result<Json<ServerResponse<SupportedFileTypes>>, HttpError> {
213    let data_types = state.db_type.get_known_data_types().await?;
214    let file_types = ServerResponse::Success(SupportedFileTypes {
215        types: data_types.into_iter().map(Into::into).collect(),
216        message: None,
217    });
218    Ok(Json(file_types))
219}
220
221#[inline]
222async fn get_labels(
223    Extension(state): Extension<Arc<State>>,
224) -> Result<Json<ServerResponse<Labels>>, HttpError> {
225    let labels = ServerResponse::Success(state.db_type.get_labels().await?);
226    Ok(Json(labels))
227}
228
229#[inline]
230async fn get_sources(
231    Extension(state): Extension<Arc<State>>,
232    Extension(user): Extension<Arc<UserInfo>>,
233) -> Result<Json<ServerResponse<Sources>>, HttpError> {
234    let sources = ServerResponse::Success(state.db_type.get_user_sources(user.id).await?);
235    Ok(Json(sources))
236}
237
238#[inline]
239async fn upload_new_sample_json(
240    Extension(state): Extension<Arc<State>>,
241    Extension(user): Extension<Arc<UserInfo>>,
242    Json(payload): Json<NewSampleB64>,
243) -> Result<StatusCode, HttpError> {
244    let allowed = state
245        .db_type
246        .allowed_user_source(user.id, payload.source_id)
247        .await?;
248
249    if !allowed {
250        return Err(HttpError(ServerError::Unauthorized, StatusCode::UNAUTHORIZED));
251    }
252
253    let received_hash = hex::decode(&payload.sha256)?;
254    let bytes = general_purpose::STANDARD.decode(&payload.file_contents_b64)?;
255
256    let mut hasher = Sha256::new();
257    hasher.update(&bytes);
258    let result = hasher.finalize();
259
260    if result[..] != received_hash[..] {
261        return Err(HttpError(ServerError::Unauthorized, StatusCode::NOT_ACCEPTABLE));
262    }
263
264    receive::incoming_sample(state.clone(), bytes, user.id, payload.source_id, payload.file_name)
265        .await?;
266
267    Ok(StatusCode::OK)
268}
269
270#[inline]
271async fn upload_new_sample_cbor(
272    Extension(state): Extension<Arc<State>>,
273    Extension(user): Extension<Arc<UserInfo>>,
274    Cbor(payload): Cbor<NewSampleBytes>,
275) -> Result<StatusCode, HttpError> {
276    let allowed = state
277        .db_type
278        .allowed_user_source(user.id, payload.source_id)
279        .await?;
280
281    if !allowed {
282        return Err(HttpError(ServerError::Unauthorized, StatusCode::UNAUTHORIZED));
283    }
284
285    let received_hash = hex::decode(&payload.sha256)?;
286    let bytes = payload.file_contents;
287    let mut hasher = Sha256::new();
288    hasher.update(&bytes);
289    let result = hasher.finalize();
290
291    if result[..] != received_hash[..] {
292        return Err(HttpError(ServerError::Unauthorized, StatusCode::NOT_ACCEPTABLE));
293    }
294
295    receive::incoming_sample(state.clone(), bytes, user.id, payload.source_id, payload.file_name)
296        .await?;
297
298    Ok(StatusCode::OK)
299}
300
301#[inline]
302async fn sample_search(
303    Extension(state): Extension<Arc<State>>,
304    Extension(user): Extension<Arc<UserInfo>>,
305    Json(payload): Json<SearchRequest>,
306) -> Result<Json<ServerResponse<SearchResponse>>, HttpError> {
307    let hashes = ServerResponse::Success(state.db_type.partial_search(user.id, payload).await?);
308    Ok(Json(hashes))
309}
310
311#[inline]
312async fn download_sample(
313    Path(hash): Path<String>,
314    Extension(user): Extension<Arc<UserInfo>>,
315    Extension(state): Extension<Arc<State>>,
316) -> Result<Response, HttpError> {
317    if state.directory.is_none() {
318        return Err(NO_SAMPLES_STORED_ERROR);
319    }
320
321    let hash = HashType::try_from(hash.as_str())?;
322    let sha256 = state.db_type.retrieve_sample(user.id, &hash).await?;
323
324    // Ensure we're sending the content-digest as SHA-256, since we could have received MD5 or SHA1.
325    let hash = HashType::try_from(sha256.as_str())?;
326
327    let contents = state.retrieve_bytes(&sha256).await?;
328
329    let mut bytes = Bytes::from(contents).into_response();
330    let name_header_value = format!("attachment; filename=\"{sha256}\"");
331    bytes.headers_mut().insert(
332        header::CONTENT_DISPOSITION,
333        HeaderValue::from_str(&name_header_value)
334            .unwrap_or(HeaderValue::from_static("Unknown.bin")),
335    );
336
337    bytes
338        .headers_mut()
339        .insert("content-digest", HeaderValue::from_str(&hash.content_digest_header())?);
340
341    Ok(bytes)
342}
343
344#[inline]
345async fn download_sample_cart(
346    Path(hash): Path<String>,
347    Extension(user): Extension<Arc<UserInfo>>,
348    Extension(state): Extension<Arc<State>>,
349) -> Result<Response, HttpError> {
350    if state.directory.is_none() {
351        return Err(NO_SAMPLES_STORED_ERROR);
352    }
353
354    let hash = HashType::try_from(hash.as_str())?;
355    let sha256 = state.db_type.retrieve_sample(user.id, &hash).await?;
356    let report = state.db_type.get_sample_report(user.id, &hash).await?;
357
358    let contents = state.retrieve_bytes(&sha256).await?;
359    let contents_cursor = Cursor::new(contents);
360    let mut output_cursor = Cursor::new(vec![]);
361
362    let mut output_metadata = cart_container::JsonMap::new();
363    output_metadata.insert("sha384".into(), report.sha384.into());
364    output_metadata.insert("sha512".into(), report.sha512.into());
365    output_metadata.insert("entropy".into(), report.entropy.into());
366    if let Some(filecmd) = report.filecommand {
367        output_metadata.insert("file".into(), filecmd.into());
368    }
369
370    cart_container::pack_stream(
371        contents_cursor,
372        &mut output_cursor,
373        Some(output_metadata),
374        None,
375        cart_container::digesters::default_digesters(), // MD-5, SHA-1, SHA-256 applied here
376        None,
377    )?;
378
379    let mut hasher = Sha256::new();
380    hasher.update(output_cursor.get_ref());
381    let hash_b64 = general_purpose::STANDARD.encode(hasher.finalize());
382
383    let mut bytes = Bytes::from(output_cursor.into_inner()).into_response();
384    let name_header_value = format!("attachment; filename=\"{sha256}.cart\"");
385    bytes.headers_mut().insert(
386        header::CONTENT_DISPOSITION,
387        HeaderValue::from_str(&name_header_value)
388            .unwrap_or(HeaderValue::from_static("Unknown.cart")),
389    );
390    bytes
391        .headers_mut()
392        .insert("content-digest", HeaderValue::from_str(&format!("sha-256=:{hash_b64}:"))?);
393
394    Ok(bytes)
395}
396
397#[inline]
398async fn sample_report(
399    Path(hash): Path<String>,
400    Extension(user): Extension<Arc<UserInfo>>,
401    Extension(state): Extension<Arc<State>>,
402) -> Result<Json<ServerResponse<Report>>, HttpError> {
403    let hash = HashType::try_from(hash.as_str())?;
404    let report =
405        ServerResponse::<Report>::Success(state.db_type.get_sample_report(user.id, &hash).await?);
406    Ok(Json(report))
407}
408
409#[inline]
410async fn find_similar(
411    Extension(state): Extension<Arc<State>>,
412    Extension(user): Extension<Arc<UserInfo>>,
413    Json(payload): Json<SimilarSamplesRequest>,
414) -> Result<Json<ServerResponse<SimilarSamplesResponse>>, HttpError> {
415    let results = state
416        .db_type
417        .find_similar_samples(user.id, &payload.hashes)
418        .await?;
419
420    Ok(Json(ServerResponse::Success(SimilarSamplesResponse {
421        results,
422        message: None,
423    })))
424}
425
426#[inline]
427#[allow(unused_variables)]
428async fn yara_search(
429    Extension(state): Extension<Arc<State>>,
430    Extension(user): Extension<Arc<UserInfo>>,
431    Json(payload): Json<YaraSearchRequest>,
432) -> Result<Json<ServerResponse<YaraSearchRequestResponse>>, HttpError> {
433    #[cfg(feature = "yara")]
434    {
435        if state.directory.is_some() {
436            let yara_string = payload.rules.join("\n");
437            let rules = crate::yara::compile_yara_rules(payload)?;
438            let yara_bytes = rules.serialize()?;
439
440            let search_uuid = state
441                .db_type
442                .add_yara_search(user.id, &yara_string, &yara_bytes)
443                .await?;
444
445            Ok(Json(ServerResponse::Success(YaraSearchRequestResponse { uuid: search_uuid })))
446        } else {
447            Err(NO_SAMPLES_STORED_ERROR)
448        }
449    }
450
451    #[cfg(not(feature = "yara"))]
452    {
453        tracing::warn!("Received Yara search request, but Yara support is not enabled");
454        Err(HttpError(ServerError::Unsupported, StatusCode::INTERNAL_SERVER_ERROR))
455    }
456}
457
458#[inline]
459#[allow(unused_variables)]
460async fn yara_search_get(
461    Path(uuid): Path<Uuid>,
462    Extension(state): Extension<Arc<State>>,
463    Extension(user): Extension<Arc<UserInfo>>,
464) -> Result<Json<ServerResponse<YaraSearchResponse>>, HttpError> {
465    #[cfg(feature = "yara")]
466    {
467        if state.directory.is_some() {
468            Ok(Json(ServerResponse::Success(state.db_type.get_yara_results(uuid, user.id).await?)))
469        } else {
470            Err(NO_SAMPLES_STORED_ERROR)
471        }
472    }
473    #[cfg(not(feature = "yara"))]
474    {
475        tracing::warn!("Received Yara search request, but Yara support is not enabled");
476        Err(HttpError(ServerError::Unsupported, StatusCode::INTERNAL_SERVER_ERROR))
477    }
478}
479
480// How to use `anyhow::Error` with `axum`:
481// https://github.com/tokio-rs/axum/blob/c97967252de9741b602f400dc2b25c8a33216039/examples/anyhow-error-response/src/main.rs
482
483const NO_SAMPLES_STORED_ERROR: HttpError =
484    HttpError(ServerError::NoSamples, StatusCode::NOT_ACCEPTABLE);
485
486/// Custom error type to support Axum error handling
487pub struct HttpError(pub ServerError, pub StatusCode);
488
489/// Convert Anyhow error into an Axum response object
490impl IntoResponse for HttpError {
491    fn into_response(self) -> Response {
492        let response: ServerResponse<String> = ServerResponse::Error(self.0);
493        match serde_json::to_string(&response) {
494            Ok(json) => (self.1, json).into_response(),
495            Err(_) => self.1.into_response(),
496        }
497    }
498}
499
500impl Display for HttpError {
501    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
502        write!(f, "{}", self.0)
503    }
504}
505
506/// Enable the use of the ? operator in Axum handler functions
507impl<E> From<E> for HttpError
508where
509    E: Into<anyhow::Error>,
510{
511    fn from(_err: E) -> Self {
512        Self(ServerError::ServerError, StatusCode::INTERNAL_SERVER_ERROR)
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519    use crate::StateBuilder;
520    use crate::crypto::{EncryptionOption, FileEncryption};
521    use crate::db::DatabaseType;
522    use malwaredb_api::PartialHashSearchType;
523
524    use std::collections::HashMap;
525    use std::path::PathBuf;
526    use std::sync::{Once, RwLock};
527    use std::time::{Instant, SystemTime};
528    use std::{env, fs};
529
530    use anyhow::Context;
531    use axum::body::Body;
532    use axum::http::Request;
533    use chrono::Local;
534    use constcat::concat;
535    use deadpool_postgres::tokio_postgres::{Config, NoTls};
536    use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod};
537    use http::header::CONTENT_TYPE;
538    use http_body_util::BodyExt;
539    use rstest::rstest;
540    use tower::ServiceExt;
541    // for `app.oneshot()`
542    use uuid::Uuid;
543
544    const ADMIN_UNAME: &str = "admin";
545    const ADMIN_PASSWORD: &str = "password12345";
546    const SAMPLE_BYTES: &[u8] = include_bytes!("../../../types/testdata/elf/elf_haiku_x86");
547
548    static TRACING: Once = Once::new();
549
550    fn init_tracing() {
551        tracing_subscriber::fmt()
552            .with_max_level(tracing::Level::TRACE)
553            .init();
554    }
555
556    async fn state(compress: bool, encrypt: bool) -> (Arc<State>, u32) {
557        TRACING.call_once(init_tracing);
558
559        // Each test needs a separate file, or else they'll clobber each other.
560        let mut db_file = env::temp_dir();
561        db_file.push(format!("testing_sqlite_{}.db", Uuid::new_v4()));
562        if std::path::Path::new(&db_file).exists() {
563            fs::remove_file(&db_file)
564                .context(format!("failed to delete old SQLite file {db_file:?}"))
565                .unwrap();
566        }
567
568        let db_type =
569            DatabaseType::from_string(&format!("file:{}", db_file.to_str().unwrap()), None)
570                .await
571                .context(format!("failed to create SQLite instance for {db_file:?}"))
572                .unwrap();
573        if compress {
574            db_type.enable_compression().await.unwrap();
575        }
576        let keys = if encrypt {
577            let key = FileEncryption::from(EncryptionOption::Xor);
578            let key_id = db_type.add_file_encryption_key(&key).await.unwrap();
579            let mut keys = HashMap::new();
580            keys.insert(key_id, key);
581            keys
582        } else {
583            HashMap::new()
584        };
585        let db_config = db_type.get_config().await.unwrap();
586
587        let state = State {
588            port: 8080,
589            directory: Some(
590                tempfile::TempDir::with_prefix("mdb-temp-samples")
591                    .unwrap()
592                    .path()
593                    .into(),
594            ),
595            max_upload: 10 * 1024 * 1024,
596            ip: "127.0.0.1".parse().unwrap(),
597            db_type: Arc::new(db_type),
598            db_config,
599            keys,
600            started: SystemTime::now(),
601            #[cfg(feature = "vt")]
602            vt_client: None,
603            tls_config: None,
604            mdns: None,
605        };
606
607        state
608            .db_type
609            .set_password(ADMIN_UNAME, ADMIN_PASSWORD)
610            .await
611            .context("Failed to set admin password")
612            .unwrap();
613
614        let source_id = state
615            .db_type
616            .create_source("temp-source", None, None, Local::now(), true, Some(false))
617            .await
618            .unwrap();
619
620        state
621            .db_type
622            .add_group_to_source(0, source_id)
623            .await
624            .unwrap();
625
626        (Arc::new(state), source_id)
627    }
628
629    /// Get a new state and generate an API token
630    async fn state_and_token(pem_file: bool, postgres: bool) -> (State, u32, String) {
631        TRACING.call_once(init_tracing);
632
633        let mut builder = if postgres {
634            const CONNECTION_STRING: &str = "user=malwaredbtesting password=malwaredbtesting dbname=malwaredbtesting host=localhost sslmode=disable";
635
636            let pg_config = CONNECTION_STRING.parse::<Config>().unwrap();
637            let mgr_config = ManagerConfig {
638                recycling_method: RecyclingMethod::Fast,
639            };
640            let mgr = Manager::from_config(pg_config, NoTls, mgr_config);
641            let pool = Pool::builder(mgr)
642                .max_size(num_cpus::get().min(16))
643                .build()
644                .unwrap();
645
646            let client = pool.get().await.unwrap();
647            client
648                .batch_execute("DROP SCHEMA public CASCADE; CREATE SCHEMA public;")
649                .await
650                .unwrap();
651
652            StateBuilder::new(concat!("postgres ", CONNECTION_STRING), None)
653                .await
654                .unwrap()
655        } else {
656            let mut db_file = env::temp_dir();
657            db_file.push(format!("testing_sqlite_{}.db", Uuid::new_v4()));
658            if std::path::Path::new(&db_file).exists() {
659                fs::remove_file(&db_file)
660                    .context(format!("failed to delete old SQLite file {db_file:?}"))
661                    .unwrap();
662            }
663
664            StateBuilder::new(&format!("file:{}", db_file.display()), None)
665                .await
666                .unwrap()
667        };
668
669        builder = builder.directory(PathBuf::from(
670            tempfile::TempDir::with_prefix("mdb-temp-samples")
671                .unwrap()
672                .path(),
673        ));
674
675        if pem_file {
676            builder = builder.port(8443);
677            builder = builder
678                .tls(
679                    "../../testdata/server_ca_cert.pem".into(),
680                    "../../testdata/server_key.pem".into(),
681                )
682                .await
683                .unwrap();
684        } else {
685            builder = builder.port(8444);
686            builder = builder
687                .tls(
688                    "../../testdata/server_cert.der".into(),
689                    "../../testdata/server_key.der".into(),
690                )
691                .await
692                .unwrap();
693        }
694
695        let state = builder.into_state().await.unwrap();
696
697        state
698            .db_type
699            .set_password(ADMIN_UNAME, ADMIN_PASSWORD)
700            .await
701            .context("Failed to set admin password")
702            .unwrap();
703
704        let source_id = state
705            .db_type
706            .create_source("temp-source", None, None, Local::now(), true, Some(false))
707            .await
708            .unwrap();
709
710        state
711            .db_type
712            .add_group_to_source(0, source_id)
713            .await
714            .unwrap();
715
716        let token = state
717            .db_type
718            .authenticate(ADMIN_UNAME, ADMIN_PASSWORD)
719            .await
720            .unwrap();
721
722        (state, source_id, token)
723    }
724
725    async fn get_key(state: Arc<State>) -> String {
726        TRACING.call_once(init_tracing);
727
728        let key_request = serde_json::to_string(&GetAPIKeyRequest {
729            user: ADMIN_UNAME.into(),
730            password: ADMIN_PASSWORD.into(),
731        })
732        .context("Failed to convert API key request to JSON")
733        .unwrap();
734
735        let request = Request::builder()
736            .method("POST")
737            .uri(malwaredb_api::USER_LOGIN_URL)
738            .header(CONTENT_TYPE, "application/json")
739            .body(Body::from(key_request))
740            .unwrap();
741
742        let response = app(state)
743            .oneshot(request)
744            .await
745            .context("failed to send/receive login request")
746            .unwrap();
747
748        assert_eq!(response.status(), StatusCode::OK);
749        let bytes = response
750            .into_body()
751            .collect()
752            .await
753            .expect("failed to collect response body to bytes")
754            .to_bytes();
755        let json_response = String::from_utf8(bytes.to_ascii_lowercase())
756            .context("failed to convert response to string")
757            .unwrap();
758
759        let response: ServerResponse<GetAPIKeyResponse> = serde_json::from_str(&json_response)
760            .context("failed to convert json response to object")
761            .unwrap();
762
763        if let ServerResponse::Success(response) = response {
764            let key = response.key.clone();
765            assert_eq!(key.len(), 64);
766
767            key
768        } else {
769            panic!("failed to get API key response")
770        }
771    }
772
773    #[tokio::test]
774    async fn about_self() {
775        let (state, _) = state(false, false).await;
776        let api_key = get_key(state.clone()).await;
777
778        let request = Request::builder()
779            .method("GET")
780            .uri(malwaredb_api::USER_INFO_URL)
781            .header(MDB_API_HEADER, &api_key)
782            .body(Body::empty())
783            .unwrap();
784
785        let response = app(state.clone())
786            .oneshot(request)
787            .await
788            .context("failed to send/receive login request")
789            .unwrap();
790
791        assert_eq!(response.status(), StatusCode::OK);
792        let bytes = response
793            .into_body()
794            .collect()
795            .await
796            .expect("failed to collect response body to bytes")
797            .to_bytes();
798        let json_response = String::from_utf8(bytes.to_ascii_lowercase())
799            .context("failed to convert response to string")
800            .unwrap();
801
802        let response: ServerResponse<GetUserInfoResponse> = serde_json::from_str(&json_response)
803            .context("failed to convert json response to object")
804            .unwrap();
805
806        let response = response.unwrap();
807        assert_eq!(response.id, 0);
808        assert!(response.is_admin);
809        assert!(!response.is_readonly);
810        assert_eq!(response.username, "admin");
811
812        // Check labels, should be empty
813        let request = Request::builder()
814            .method("GET")
815            .uri(malwaredb_api::LIST_LABELS_URL)
816            .header(MDB_API_HEADER, &api_key)
817            .body(Body::empty())
818            .unwrap();
819
820        let response = app(state)
821            .oneshot(request)
822            .await
823            .context("failed to send/receive login request")
824            .unwrap();
825
826        assert_eq!(response.status(), StatusCode::OK);
827        let bytes = response
828            .into_body()
829            .collect()
830            .await
831            .expect("failed to collect response body to bytes")
832            .to_bytes();
833        let json_response = String::from_utf8(bytes.to_ascii_lowercase())
834            .context("failed to convert response to string")
835            .unwrap();
836
837        let response: ServerResponse<Labels> = serde_json::from_str(&json_response)
838            .context("failed to convert json response to object")
839            .unwrap();
840
841        let response = response.unwrap();
842        assert!(response.is_empty());
843    }
844
845    #[rstest]
846    #[case::elf_encrypt_cart(include_bytes!("../../../types/testdata/elf/elf_haiku_x86.cart"), false, true, true, false)]
847    #[case::pe32(include_bytes!("../../../types/testdata/exe/pe64_win32_gui_x86_64_gnu.exe"), false, false, false, false)]
848    #[case::pdf_encrypt(include_bytes!("../../../types/testdata/pdf/test.pdf"), false, true, false, false)]
849    #[case::rtf(include_bytes!("../../../types/testdata/rtf/hello.rtf"), false, false, false, false)]
850    #[case::elf_compress_encrypt(include_bytes!("../../../types/testdata/elf/elf_haiku_x86"), true, true, false, false)]
851    #[case::pe32_compress(include_bytes!("../../../types/testdata/exe/pe64_win32_gui_x86_64_gnu.exe"), true, false, false, false)]
852    #[case::pdf_compress_encrypt(include_bytes!("../../../types/testdata/pdf/test.pdf"), true, true, false, false)]
853    #[case::rtf_compress(include_bytes!("../../../types/testdata/rtf/hello.rtf"), true, false, false, false)]
854    #[case::icon_unknown_type_proxy(include_bytes!("../../../../MDB_Logo.ico"), false, false, false, true)]
855    #[tokio::test]
856    async fn submit_sample(
857        #[case] contents: &[u8],
858        #[case] compress: bool,
859        #[case] encrypt: bool,
860        #[case] cart: bool,
861        #[case] should_fail: bool,
862    ) {
863        let (state, source_id) = state(compress, encrypt).await;
864        let api_key = get_key(state.clone()).await;
865
866        let file_contents_b64 = general_purpose::STANDARD.encode(contents);
867        let mut hasher = Sha256::new();
868        hasher.update(contents);
869        let sha256 = hex::encode(hasher.finalize());
870
871        let upload = serde_json::to_string(&NewSampleB64 {
872            file_name: "some_sample".into(),
873            source_id,
874            file_contents_b64,
875            sha256: sha256.clone(),
876        })
877        .context("failed to create upload structure")
878        .unwrap();
879
880        let request = Request::builder()
881            .method("POST")
882            .uri(malwaredb_api::UPLOAD_SAMPLE_JSON_URL)
883            .header(CONTENT_TYPE, "application/json")
884            .header(MDB_API_HEADER, &api_key)
885            .body(Body::from(upload))
886            .unwrap();
887
888        let response = app(state.clone())
889            .oneshot(request)
890            .await
891            .context("failed to send/receive upload request/response")
892            .unwrap();
893
894        if should_fail {
895            assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
896            return;
897        }
898
899        assert_eq!(response.status(), StatusCode::OK);
900
901        let sha256 = if cart {
902            // We have to get the original SHA-256 hash, not the hash of the CaRT container.
903            // This way we can check that the file exists on disk, and request the file again later in this test.
904            let mut input_buffer = Cursor::new(contents);
905            let mut output_buffer = Cursor::new(vec![]);
906            let (_, footer) =
907                cart_container::unpack_stream(&mut input_buffer, &mut output_buffer, None)
908                    .expect("failed to decode CaRT file");
909            let footer = footer.expect("CaRT should have had a footer");
910            let sha256 = footer
911                .get("sha256")
912                .expect("CaRT footer should have had an entry for SHA-256")
913                .to_string();
914            sha256.replace('"', "") // Remove the quotes that get added by JSON
915        } else {
916            sha256
917        };
918
919        if let Some(dir) = &state.directory {
920            let mut sample_path = dir.clone();
921            sample_path.push(format!(
922                "{}/{}/{}/{}",
923                &sha256[0..2],
924                &sha256[2..4],
925                &sha256[4..6],
926                sha256
927            ));
928            eprintln!("Submitted sample should exist at {sample_path:?}.");
929            assert!(sample_path.exists());
930
931            if compress {
932                let sample_size_on_disk = sample_path.metadata().unwrap().len();
933                eprintln!("Original size: {}, compressed: {}", contents.len(), sample_size_on_disk);
934                assert!(sample_size_on_disk < contents.len() as u64);
935            }
936        } else {
937            panic!("Directory was set for the state, but is now `None`");
938        }
939
940        let request = Request::builder()
941            .method("GET")
942            .uri(format!("{}/{sha256}", malwaredb_api::SAMPLE_REPORT_URL))
943            .header(MDB_API_HEADER, &api_key)
944            .body(Body::empty())
945            .unwrap();
946
947        let response = app(state.clone())
948            .oneshot(request)
949            .await
950            .context("failed to send/receive upload request/response")
951            .unwrap();
952
953        println!("Response headers: {:?}", response.headers());
954
955        let bytes = response
956            .into_body()
957            .collect()
958            .await
959            .expect("failed to collect response body to bytes")
960            .to_bytes();
961        let json_response = String::from_utf8(bytes.to_ascii_lowercase())
962            .context("failed to convert response to string")
963            .unwrap();
964
965        let report: ServerResponse<Report> = serde_json::from_str(&json_response)
966            .context("failed to convert json response to object")
967            .unwrap();
968        let report = report.unwrap();
969
970        assert_eq!(report.sha256, sha256);
971        println!("Report: {report}");
972
973        let request = Request::builder()
974            .method("GET")
975            .uri(format!("{}/{sha256}", malwaredb_api::DOWNLOAD_SAMPLE_CART_URL))
976            .header(MDB_API_HEADER, api_key.clone())
977            .body(Body::empty())
978            .unwrap();
979
980        let response = app(state.clone())
981            .oneshot(request)
982            .await
983            .context("failed to send/receive upload request/response for CaRT")
984            .unwrap();
985
986        println!("CaRT Response headers: {:?}", response.headers());
987
988        let bytes = response
989            .into_body()
990            .collect()
991            .await
992            .expect("failed to collect response body to bytes")
993            .to_bytes();
994
995        let bytes = bytes.to_vec();
996        let bytes_input = Cursor::new(bytes);
997        let output = Cursor::new(vec![]);
998        match cart_container::unpack_stream(bytes_input, output, None) {
999            Ok((header, _)) => {
1000                let header = header.unwrap();
1001                assert_eq!(
1002                    header.get("sha384"),
1003                    Some(&serde_json::to_value(report.sha384).unwrap())
1004                );
1005            }
1006            Err(e) => panic!("{e}"),
1007        }
1008
1009        #[cfg(feature = "admin")]
1010        {
1011            use std::sync::atomic::{AtomicBool, AtomicU64};
1012
1013            let (new_key_id, keys) = {
1014                let new_key = FileEncryption::from(EncryptionOption::AES128);
1015                let new_key_id = state
1016                    .db_type
1017                    .add_file_encryption_key(&new_key)
1018                    .await
1019                    .unwrap();
1020                let mut keys = state.keys.clone();
1021                keys.insert(new_key_id, new_key);
1022                (new_key_id, keys)
1023            };
1024
1025            let state = State {
1026                port: state.port,
1027                directory: state.directory.clone(),
1028                max_upload: state.max_upload,
1029                ip: state.ip,
1030                db_type: state.db_type.clone(),
1031                started: state.started,
1032                db_config: crate::db::MDBConfig {
1033                    name: state.db_config.name.clone(),
1034                    compression: !state.db_config.compression,
1035                    send_samples_to_vt: false,
1036                    keep_unknown_files: false,
1037                    default_key: Some(new_key_id),
1038                    #[cfg(feature = "anonymous")]
1039                    anonymous_uid: None,
1040                },
1041                keys,
1042                #[cfg(feature = "vt")]
1043                vt_client: state.vt_client.clone(),
1044                tls_config: state.tls_config.clone(),
1045                mdns: state.mdns.clone(),
1046            };
1047
1048            let rewrite_counter = Arc::new(AtomicU64::new(0));
1049            let stop_signal = Arc::new(AtomicBool::new(false));
1050
1051            state
1052                .rewrite_files(rewrite_counter.clone(), stop_signal)
1053                .await
1054                .unwrap();
1055            eprintln!(
1056                "Rewrote {} files",
1057                rewrite_counter.load(std::sync::atomic::Ordering::Relaxed)
1058            );
1059
1060            let mut sample_path = state.directory.unwrap().clone();
1061            sample_path.push(format!(
1062                "{}/{}/{}/{}",
1063                &sha256[0..2],
1064                &sha256[2..4],
1065                &sha256[4..6],
1066                sha256
1067            ));
1068            eprintln!("Submitted sample should exist at {sample_path:?}.");
1069            assert!(sample_path.exists());
1070
1071            let contents = fs::read(&sample_path).unwrap();
1072            let sha256_disk = hex::encode(Sha256::digest(&contents));
1073            assert_ne!(sha256_disk, sha256, "New key added so disk hash should never match");
1074        }
1075    }
1076
1077    /// Integration test between Malware DB Server & Client. This was under "/tests" in the project
1078    /// but `cargo hack` would disable needed features, and there wasn't a way to use `cfg` to
1079    /// put a condition on the test. Additionally, such an integration test required additional
1080    /// fields of the Server's state to be public or to implement `Default`, which isn't ideal.
1081    #[rstest]
1082    #[case::ssl_pem_sqlite(true, false)]
1083    #[case::ssl_der_sqlite(false, false)]
1084    #[ignore = "don't run this in CI"]
1085    #[case::ssl_der_postgres(false, true)]
1086    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1087    async fn client_integration(#[case] pem: bool, #[case] postgres: bool) {
1088        TRACING.call_once(init_tracing);
1089
1090        // This other get state function was needed, or else there were complaints of
1091        // `state` being moved and unable to be used in `tokio::spawn()`
1092        let (state, source_id, token) = state_and_token(pem, postgres).await;
1093        let state_port = state.port; // copy before move below
1094
1095        let server = tokio::spawn(async move {
1096            state
1097                .serve()
1098                .await
1099                .expect("MalwareDB failed to .serve() in tokio::spawn()");
1100        });
1101        assert!(!server.is_finished());
1102
1103        // In case start-up time is needed
1104        tokio::time::sleep(std::time::Duration::new(1, 0)).await;
1105
1106        println!("SemVer of MalwareDB: {:?}", *crate::MDB_VERSION_SEMVER);
1107        assert_eq!(
1108            *malwaredb_client::MDB_VERSION_SEMVER,
1109            *crate::MDB_VERSION_SEMVER,
1110            "SemVer parsing of MDB version failed"
1111        );
1112
1113        let mdb_client = malwaredb_client::MdbClient::new(
1114            format!("https://127.0.0.1:{state_port}"),
1115            token.clone(),
1116            Some("../../testdata/ca_cert.pem".into()),
1117        )
1118        .unwrap();
1119
1120        assert!(!mdb_client.supported_types().await.unwrap().types.is_empty());
1121        assert!(mdb_client.server_info().await.is_ok());
1122
1123        let start = Instant::now();
1124        assert!(
1125            mdb_client
1126                .submit(SAMPLE_BYTES, String::from("elf_haiku_x86"), source_id)
1127                .await
1128                .context("failed to upload test file")
1129                .unwrap()
1130        );
1131        let duration = start.elapsed();
1132        println!("Initial upload and database record creation via base64 took {duration:?}");
1133
1134        let start = Instant::now();
1135        mdb_client
1136            .submit(SAMPLE_BYTES, String::from("elf_haiku_x86"), source_id)
1137            .await
1138            .context("failed to upload test file")
1139            .unwrap();
1140        let duration = start.elapsed();
1141        println!("Upload again via base64 took {duration:?}");
1142
1143        let start = Instant::now();
1144        mdb_client
1145            .submit_as_cbor(SAMPLE_BYTES, String::from("elf_haiku_x86"), source_id)
1146            .await
1147            .context("failed to upload test file")
1148            .unwrap();
1149        let duration = start.elapsed();
1150        println!("Upload again via cbor took {duration:?}");
1151
1152        let report = mdb_client
1153            .report("de10ba5e5402b46ea975b5cb8a45eb7df9e81dc81012fd4efd145ed2dce3a740")
1154            .await
1155            .expect("failed to get report for file just submitted");
1156        assert_eq!(report.md5, "82123011556b0e68801bee7bd71bb345");
1157
1158        let similar = mdb_client
1159            .similar(SAMPLE_BYTES)
1160            .await
1161            .expect("failed to query for files similar to what was just submitted");
1162        assert_eq!(similar.results.len(), 1);
1163
1164        let search = mdb_client
1165            .partial_search(
1166                Some((PartialHashSearchType::MD5, String::from(&report.md5[0..10]))),
1167                None,
1168                PartialHashSearchType::Any,
1169                10,
1170            )
1171            .await
1172            .unwrap();
1173        assert_eq!(search.hashes.len(), 1);
1174
1175        let search = mdb_client
1176            .partial_search(
1177                Some((PartialHashSearchType::Any, "AAAA".into())),
1178                None,
1179                PartialHashSearchType::Any,
1180                10,
1181            )
1182            .await
1183            .unwrap();
1184        assert!(search.hashes.is_empty());
1185
1186        #[cfg(feature = "yara")]
1187        {
1188            let elf_yara = "rule elf_file {
1189              condition:
1190                uint32(0) == 0x464c457f
1191            }";
1192
1193            let mut counter = 0;
1194            let mut successful = false;
1195
1196            let yara_result = mdb_client.yara_search(elf_yara).await.unwrap();
1197            while counter < 5 {
1198                if let Ok(yara_response) = mdb_client.yara_result(yara_result.uuid).await
1199                    && !yara_response.results.is_empty()
1200                {
1201                    println!("Yara search upload response: {:?}", yara_response.results);
1202                    assert_eq!(yara_response.results.get("elf_file").unwrap().len(), 1);
1203                    assert_eq!(yara_response.results.len(), 1);
1204                    successful = true;
1205                    break;
1206                }
1207                tokio::time::sleep(std::time::Duration::new(2, 0)).await;
1208                counter += 1;
1209            }
1210
1211            assert!(successful);
1212        }
1213
1214        mdb_client.reset_key().await.expect("failed to reset key");
1215        server.abort();
1216    }
1217
1218    #[allow(clippy::too_many_lines)]
1219    #[test]
1220    #[ignore = "don't run this in CI"]
1221    fn client_integration_blocking() {
1222        TRACING.call_once(init_tracing);
1223        let token = Arc::new(RwLock::new(String::new()));
1224        let token_clone = token.clone();
1225
1226        // There has to be a better way to do this. We have to create a new thread to run Tokio,
1227        // otherwise the closing of the functions used by the blocking client will cause
1228        // Tokio to panic with this error:
1229        // Cannot drop a runtime in a context where blocking is not allowed. This happens when a runtime is dropped from within an asynchronous context.
1230        // So we wrap that all in another thread.
1231        let thread = std::thread::spawn(move || {
1232            let rt = tokio::runtime::Builder::new_multi_thread()
1233                .enable_all()
1234                .build()
1235                .unwrap();
1236
1237            let mut db_file = env::temp_dir();
1238            db_file.push(format!("testing_sqlite_{}.db", Uuid::new_v4()));
1239            if std::path::Path::new(&db_file).exists() {
1240                fs::remove_file(&db_file)
1241                    .context(format!("failed to delete old SQLite file {db_file:?}"))
1242                    .unwrap();
1243            }
1244
1245            let (db_type, db_config) = rt.block_on(async {
1246                let db_type =
1247                    DatabaseType::from_string(&format!("file:{}", db_file.to_str().unwrap()), None)
1248                        .await
1249                        .context(format!("failed to create SQLite instance for {db_file:?}"))
1250                        .unwrap();
1251
1252                let db_config = db_type.get_config().await.unwrap();
1253                (db_type, db_config)
1254            });
1255
1256            let state = State {
1257                port: 9090,
1258                directory: Some(
1259                    tempfile::TempDir::with_prefix("mdb-temp-samples")
1260                        .unwrap()
1261                        .path()
1262                        .into(),
1263                ),
1264                max_upload: 10 * 1024 * 1024,
1265                ip: "127.0.0.1".parse().unwrap(),
1266                db_type: Arc::new(db_type),
1267                db_config,
1268                keys: HashMap::default(),
1269                started: SystemTime::now(),
1270                #[cfg(feature = "vt")]
1271                vt_client: None,
1272                tls_config: None,
1273                mdns: None,
1274            };
1275
1276            rt.block_on(async {
1277                state
1278                    .db_type
1279                    .set_password(ADMIN_UNAME, ADMIN_PASSWORD)
1280                    .await
1281                    .context("Failed to set admin password")
1282                    .unwrap();
1283
1284                let source_id = state
1285                    .db_type
1286                    .create_source("temp-source", None, None, Local::now(), true, Some(false))
1287                    .await
1288                    .unwrap();
1289
1290                state
1291                    .db_type
1292                    .add_group_to_source(0, source_id)
1293                    .await
1294                    .unwrap();
1295
1296                let token_string = state
1297                    .db_type
1298                    .authenticate(ADMIN_UNAME, ADMIN_PASSWORD)
1299                    .await
1300                    .unwrap();
1301
1302                if let Ok(mut token_lock) = token_clone.write() {
1303                    *token_lock = token_string;
1304                }
1305
1306                state
1307                    .serve()
1308                    .await
1309                    .expect("MalwareDB failed to .serve() in tokio::spawn()");
1310            });
1311        });
1312        std::thread::sleep(std::time::Duration::from_secs(1));
1313
1314        let mdb_client = malwaredb_client::blocking::MdbClient::new(
1315            String::from("http://127.0.0.1:9090"),
1316            token.read().unwrap().clone(),
1317            None,
1318        )
1319        .unwrap();
1320
1321        let types = match mdb_client.supported_types() {
1322            Ok(types) => types,
1323            Err(e) => panic!("{e}"),
1324        };
1325        assert!(!types.types.is_empty());
1326
1327        assert!(
1328            mdb_client
1329                .submit(SAMPLE_BYTES, String::from("elf_haiku_x86"), 1)
1330                .context("failed to upload test file")
1331                .unwrap()
1332        );
1333
1334        let report = mdb_client
1335            .report("de10ba5e5402b46ea975b5cb8a45eb7df9e81dc81012fd4efd145ed2dce3a740")
1336            .expect("failed to get report for file just submitted");
1337        assert_eq!(report.md5, "82123011556b0e68801bee7bd71bb345");
1338
1339        let similar = mdb_client
1340            .similar(SAMPLE_BYTES)
1341            .expect("failed to query for files similar to what was just submitted");
1342        assert_eq!(similar.results.len(), 1);
1343
1344        let search = mdb_client
1345            .partial_search(
1346                Some((PartialHashSearchType::Any, "AAAA".into())),
1347                None,
1348                PartialHashSearchType::Any,
1349                10,
1350            )
1351            .unwrap();
1352        assert!(search.hashes.is_empty());
1353
1354        mdb_client.reset_key().expect("failed to reset key");
1355        drop(thread); // force shutdown of the thread, as `thread.join()` just waits forever.
1356    }
1357}