1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
pub mod bundle;
mod install;
mod query;
use std::io::{self, ErrorKind};
use std::sync::Arc;
use crate::bundle::{BundleManifest, ManifestVerification};
use calimero_primitives::application::{
Application, ApplicationBlob, ApplicationId, ApplicationSource,
};
use calimero_primitives::blobs::BlobId;
use calimero_primitives::hash::Hash;
use calimero_store::{key, types};
use camino::{Utf8Path, Utf8PathBuf};
use eyre::bail;
use flate2::read::GzDecoder;
use futures_util::{io::Cursor, TryStreamExt};
use reqwest::Url;
use sha2::{Digest, Sha256};
use std::fs;
use tar::Archive;
use tokio::fs::File;
use tokio_util::compat::TokioAsyncReadCompatExt;
use tracing::{debug, trace, warn};
use super::NodeClient;
const MAX_ERROR_BODY_LEN: usize = 256;
impl NodeClient {
pub fn get_application(
&self,
application_id: &ApplicationId,
) -> eyre::Result<Option<Application>> {
let handle = self.datastore.handle();
let key = key::ApplicationMeta::new(*application_id);
let Some(application) = handle.get(&key)? else {
return Ok(None);
};
let services = application
.services
.iter()
.map(|s| {
(
s.name.to_string(),
ApplicationBlob {
bytecode: s.bytecode.blob_id(),
compiled: s.compiled.blob_id(),
},
)
})
.collect();
let mut app = Application::new(
*application_id,
ApplicationBlob {
bytecode: application.bytecode.blob_id(),
compiled: application.compiled.blob_id(),
},
application.size,
application.source.parse()?,
application.metadata.into_vec(),
)
.with_bundle_info(
application.signer_id.to_string(),
application.package.to_string(),
application.version.to_string(),
);
app.services = services;
Ok(Some(app))
}
pub async fn get_application_bytes(
&self,
application_id: &ApplicationId,
service_name: Option<&str>,
) -> eyre::Result<Option<Arc<[u8]>>> {
let handle = self.datastore.handle();
let key = key::ApplicationMeta::new(*application_id);
let Some(application) = handle.get(&key)? else {
return Ok(None);
};
// Determine if this is a bundle by checking package/version
// Bundles have real package/version values, non-bundles use "unknown"/"0.0.0"
// This avoids repeated decompression on every get_application_bytes call
let is_bundle =
application.package.as_ref() != "unknown" && application.version.as_ref() != "0.0.0";
// Get blob bytes
let Some(blob_bytes) = self
.get_blob_bytes(&application.bytecode.blob_id(), None)
.await?
else {
bail!("fatal: application points to dangling blob");
};
if is_bundle {
// This is a bundle, extract WASM from extracted directory or bundle.
// The bundle was already admitted (possibly unsigned in dev mode),
// so use the unsigned-tolerant extractor here.
let blob_bytes_clone = Arc::clone(&blob_bytes);
let (_, manifest) = tokio::task::spawn_blocking(move || {
bundle::extract_manifest_allow_unsigned(&blob_bytes_clone)
})
.await??;
let package = &manifest.package;
let version = &manifest.app_version;
// Resolve relative path against node root (must be done before spawn_blocking)
let blobstore_root = self.blob_manager.root_path();
let node_root = blobstore_root
.parent()
.ok_or_else(|| eyre::eyre!("blobstore root has no parent"))?
.to_path_buf();
let extract_dir = node_root
.join("applications")
.join(package)
.join(version)
.join("extracted");
// Resolve WASM path from manifest, supporting both single and multi-service bundles
let wasm_relative_path = match service_name {
Some(name) => {
// Multi-service: find the named service in the services array
manifest
.services
.as_ref()
.and_then(|svcs| svcs.iter().find(|s| s.name == name))
.map(|s| s.wasm.path.as_str())
.ok_or_else(|| {
eyre::eyre!("service '{}' not found in bundle manifest", name)
})?
}
None => {
// Single-service: use top-level wasm field (fallback to "app.wasm")
manifest
.wasm
.as_ref()
.map(|w| w.path.as_str())
.unwrap_or("app.wasm")
}
};
// Validate WASM path to prevent path traversal attacks
// Check that the relative path doesn't contain ".." components that would escape
if wasm_relative_path.contains("..") {
bail!(
"WASM path traversal detected: {} contains '..' component",
wasm_relative_path
);
}
let wasm_path = extract_dir.join(wasm_relative_path);
// Additional validation: ensure the resolved path stays within extract_dir
// Use canonicalize if path exists, otherwise validate components
if wasm_path.exists() {
let canonical_wasm = wasm_path.canonicalize_utf8()?;
// extract_dir might not exist if wasm_relative_path contains subdirectories
// Reconstruct canonical extract_dir from wasm_path by removing relative path components
let canonical_extract = if extract_dir.exists() {
extract_dir.canonicalize_utf8()?
} else {
// Reconstruct extract_dir from wasm_path by removing wasm_relative_path components
// Since we validated wasm_relative_path doesn't contain "..", this is safe
let wasm_parent = wasm_path
.parent()
.ok_or_else(|| eyre::eyre!("WASM path has no parent directory"))?;
let wasm_parent_canonical = wasm_parent.canonicalize_utf8()?;
// Count depth of wasm_relative_path (number of path components)
let relative_depth = wasm_relative_path
.split('/')
.filter(|s| !s.is_empty())
.count()
.saturating_sub(1); // Subtract 1 for the filename itself
// Go up relative_depth levels from wasm_parent to get extract_dir
let mut canonical_extract_candidate = wasm_parent_canonical.clone();
for _ in 0..relative_depth {
if let Some(parent) = canonical_extract_candidate.parent() {
canonical_extract_candidate = parent.to_path_buf();
} else {
bail!("Cannot reconstruct extract_dir from WASM path");
}
}
canonical_extract_candidate.try_into().map_err(|_| {
eyre::eyre!("Failed to convert extract_dir path to Utf8PathBuf")
})?
};
// Ensure canonical_wasm is within canonical_extract
if !canonical_wasm.starts_with(&canonical_extract) {
bail!(
"WASM path traversal detected: {} escapes extraction directory {}",
wasm_relative_path,
extract_dir
);
}
}
if wasm_path.exists() {
let wasm_bytes = tokio::fs::read(&wasm_path).await?;
return Ok(Some(wasm_bytes.into()));
} else {
// Fallback: re-extract from bundle blob if extracted files missing
warn!(
wasm_path = %wasm_path,
"extracted WASM not found, attempting to re-extract from bundle and persist to disk"
);
// Remove marker file if it exists (files were deleted, marker is stale)
let marker_file_path = extract_dir.join(".extracted");
if marker_file_path.exists() {
let _ = tokio::fs::remove_file(&marker_file_path).await;
}
// Re-extract entire bundle to disk (not just WASM) so future calls don't need to re-extract
// This handles the case where sync_context_config installed the app before blob arrived
// Wrap blocking I/O in spawn_blocking to avoid blocking async runtime
let blob_bytes_clone = Arc::clone(&blob_bytes);
let manifest_clone = manifest.clone();
let extract_dir_clone = extract_dir.to_path_buf();
let node_root_clone = node_root.to_path_buf();
let package_clone = package.to_string();
let version_clone = version.to_string();
tokio::task::spawn_blocking(move || {
Self::extract_bundle_artifacts(
&blob_bytes_clone,
&manifest_clone,
&extract_dir_clone,
&node_root_clone,
&package_clone,
&version_clone,
)
})
.await??;
// Now read the WASM file that was just extracted
if wasm_path.exists() {
let wasm_bytes = tokio::fs::read(&wasm_path).await?;
return Ok(Some(wasm_bytes.into()));
}
bail!("WASM file not found in bundle after extraction");
}
}
// Single WASM installation (existing behavior)
// Reuse blob_bytes that were already fetched for bundle detection
Ok(Some(blob_bytes))
}
pub fn has_application(&self, application_id: &ApplicationId) -> eyre::Result<bool> {
let handle = self.datastore.handle();
let key = key::ApplicationMeta::new(*application_id);
if let Some(application) = handle.get(&key)? {
return self.has_blob(&application.bytecode.blob_id());
}
Ok(false)
}
// Installation, uninstallation, and bundle extraction are in `install` submodule.
// Query and management functions are in `query` submodule.
}
#[cfg(test)]
mod tests;