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