samply_server/
lib.rs

1use flate2::read::GzDecoder;
2use hyper::body::Bytes;
3use hyper::server::conn::AddrIncoming;
4use hyper::server::Builder;
5use hyper::service::{make_service_fn, service_fn};
6use hyper::{header, Body, Request, Response, Server};
7use hyper::{Method, StatusCode};
8use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
9use rand::RngCore;
10use serde_derive::Deserialize;
11use std::collections::HashMap;
12use std::convert::Infallible;
13use std::ffi::{OsStr, OsString};
14use std::io::BufReader;
15use std::net::SocketAddr;
16use std::ops::Range;
17use std::path::{Path, PathBuf};
18use std::str::FromStr;
19use std::sync::Arc;
20use symsrv::NtSymbolPathEntry;
21use tokio::io::AsyncReadExt;
22use wholesym::debugid::{CodeId, DebugId};
23use wholesym::{LibraryInfo, SymbolManager, SymbolManagerConfig};
24
25pub use symsrv;
26pub use wholesym::samply_symbols;
27
28const BAD_CHARS: &AsciiSet = &CONTROLS.add(b':').add(b'/');
29
30#[test]
31fn test_is_send_and_sync() {
32    use symsrv::FileContents;
33    fn assert_is_send<T: Send>() {}
34    fn assert_is_sync<T: Sync>() {}
35    assert_is_send::<FileContents>();
36    assert_is_sync::<FileContents>();
37}
38
39#[derive(Clone, Debug)]
40pub enum PortSelection {
41    OnePort(u16),
42    TryMultiple(Range<u16>),
43}
44
45impl PortSelection {
46    pub fn try_from_str(s: &str) -> std::result::Result<Self, <u16 as FromStr>::Err> {
47        if s.ends_with('+') {
48            let start = s.trim_end_matches('+').parse()?;
49            let end = start + 100;
50            Ok(PortSelection::TryMultiple(start..end))
51        } else {
52            Ok(PortSelection::OnePort(s.parse()?))
53        }
54    }
55}
56
57pub async fn start_server(
58    profile_filename: Option<&Path>,
59    port_selection: PortSelection,
60    symbol_path: Vec<NtSymbolPathEntry>,
61    verbose: bool,
62    open_in_browser: bool,
63) {
64    let libinfo_map = if let Some(profile_filename) = profile_filename {
65        // Read the profile.json file and parse it as JSON.
66        // Build a map (debugName, breakpadID) -> debugPath from the information
67        // in profile(\.processes\[\d+\])*(\.threads\[\d+\])?\.libs.
68        let file = std::fs::File::open(profile_filename).expect("couldn't read file");
69        let reader = BufReader::new(file);
70
71        // Handle .gz profiles
72        if profile_filename.extension() == Some(&OsString::from("gz")) {
73            let decoder = GzDecoder::new(reader);
74            let reader = BufReader::new(decoder);
75            parse_libinfo_map_from_profile(reader).expect("couldn't parse json")
76        } else {
77            parse_libinfo_map_from_profile(reader).expect("couldn't parse json")
78        }
79    } else {
80        HashMap::new()
81    };
82
83    let (builder, addr) = make_builder_at_port(port_selection);
84
85    let token = generate_token();
86    let path_prefix = format!("/{}", token);
87    let server_origin = format!("http://{}", addr);
88    let symbol_server_url = format!("{}{}", server_origin, path_prefix);
89    let mut template_values: HashMap<&'static str, String> = HashMap::new();
90    template_values.insert("SERVER_URL", server_origin.clone());
91    template_values.insert("PATH_PREFIX", path_prefix.clone());
92
93    let profiler_url = if profile_filename.is_some() {
94        let profile_url = format!("{}/profile.json", symbol_server_url);
95
96        let env_profiler_override = std::env::var("PROFILER_URL").ok();
97        let profiler_origin = match &env_profiler_override {
98            Some(s) => s.trim_end_matches('/'),
99            None => "https://profiler.firefox.com",
100        };
101
102        let encoded_profile_url = utf8_percent_encode(&profile_url, BAD_CHARS).to_string();
103        let encoded_symbol_server_url =
104            utf8_percent_encode(&symbol_server_url, BAD_CHARS).to_string();
105        let profiler_url = format!(
106            "{}/from-url/{}/?symbolServer={}",
107            profiler_origin, encoded_profile_url, encoded_symbol_server_url
108        );
109        template_values.insert("PROFILER_URL", profiler_url.clone());
110        template_values.insert("PROFILE_URL", profile_url);
111        Some(profiler_url)
112    } else {
113        None
114    };
115
116    let template_values = Arc::new(template_values);
117
118    let config = SymbolManagerConfig::new()
119        .verbose(verbose)
120        .with_nt_symbol_path(symbol_path);
121    let mut symbol_manager = SymbolManager::with_config(config);
122    for lib_info in libinfo_map.into_values() {
123        symbol_manager.add_known_lib(lib_info);
124    }
125    let symbol_manager = Arc::new(symbol_manager);
126    let new_service = make_service_fn(move |_conn| {
127        let symbol_manager = symbol_manager.clone();
128        let profile_filename = profile_filename.map(PathBuf::from);
129        let template_values = template_values.clone();
130        let path_prefix = path_prefix.clone();
131        async {
132            Ok::<_, Infallible>(service_fn(move |req| {
133                symbolication_service(
134                    req,
135                    template_values.clone(),
136                    symbol_manager.clone(),
137                    profile_filename.clone(),
138                    path_prefix.clone(),
139                )
140            }))
141        }
142    });
143
144    let server = builder.serve(new_service);
145
146    eprintln!("Local server listening at {}", server_origin);
147    if !open_in_browser {
148        if let Some(profiler_url) = &profiler_url {
149            eprintln!("  Open the profiler at {}", profiler_url);
150        }
151    }
152    eprintln!("Press Ctrl+C to stop.");
153
154    if open_in_browser {
155        if let Some(profiler_url) = &profiler_url {
156            let _ = webbrowser::open(profiler_url);
157        }
158    }
159
160    // Run this server for... forever!
161    if let Err(e) = server.await {
162        eprintln!("server error: {}", e);
163    }
164}
165
166fn parse_libinfo_map_from_profile(
167    reader: impl std::io::Read,
168) -> Result<HashMap<(String, DebugId), LibraryInfo>, std::io::Error> {
169    let profile: ProfileJsonProcess = serde_json::from_reader(reader)?;
170    let mut libinfo_map = HashMap::new();
171    add_to_libinfo_map_recursive(&profile, &mut libinfo_map);
172    Ok(libinfo_map)
173}
174
175#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)]
176#[serde(rename_all = "camelCase")]
177struct ProfileJsonProcess {
178    #[serde(default)]
179    pub libs: Vec<ProfileJsonLib>,
180    #[serde(default)]
181    pub threads: Vec<ProfileJsonThread>,
182    #[serde(default)]
183    pub processes: Vec<ProfileJsonProcess>,
184}
185
186#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)]
187#[serde(rename_all = "camelCase")]
188struct ProfileJsonThread {
189    #[serde(default)]
190    pub libs: Vec<ProfileJsonLib>,
191}
192
193#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)]
194#[serde(rename_all = "camelCase")]
195struct ProfileJsonLib {
196    pub debug_name: Option<String>,
197    pub debug_path: Option<String>,
198    pub name: Option<String>,
199    pub path: Option<String>,
200    pub breakpad_id: Option<String>,
201    pub code_id: Option<String>,
202}
203
204// Returns a base32 string for 24 random bytes.
205fn generate_token() -> String {
206    let mut bytes = [0u8; 24];
207    rand::thread_rng().fill_bytes(&mut bytes);
208    nix_base32::to_nix_base32(&bytes)
209}
210
211fn make_builder_at_port(port_selection: PortSelection) -> (Builder<AddrIncoming>, SocketAddr) {
212    match port_selection {
213        PortSelection::OnePort(port) => {
214            let addr = SocketAddr::from(([127, 0, 0, 1], port));
215            match Server::try_bind(&addr) {
216                Ok(builder) => (builder, addr),
217                Err(e) => {
218                    eprintln!("Could not bind to port {}: {}", port, e);
219                    std::process::exit(1)
220                }
221            }
222        }
223        PortSelection::TryMultiple(range) => {
224            let mut error = None;
225            for port in range.clone() {
226                let addr = SocketAddr::from(([127, 0, 0, 1], port));
227                match Server::try_bind(&addr) {
228                    Ok(builder) => return (builder, addr),
229                    Err(e) => {
230                        error.get_or_insert(e);
231                    }
232                }
233            }
234            match error {
235                Some(error) => {
236                    eprintln!(
237                        "Could not bind to any port in the range {:?}: {}",
238                        range, error,
239                    );
240                }
241                None => {
242                    eprintln!("Binding failed, port range empty? {:?}", range);
243                }
244            }
245            std::process::exit(1)
246        }
247    }
248}
249
250const TEMPLATE_WITH_PROFILE: &str = r#"
251<!DOCTYPE html>
252<html lang="en">
253<meta charset="utf-8">
254<title>Profiler Symbol Server</title>
255<body>
256
257<p>This is the profiler symbol server, running at <code>SERVER_URL</code>. You can:</p>
258<ul>
259    <li><a href="PROFILER_URL">Open the profile in the profiler UI</a></li>
260    <li><a download href="PROFILE_URL">Download the raw profile JSON</a></li>
261    <li>Obtain symbols by POSTing to <code>PATH_PREFIX/symbolicate/v5</code>, with the format specified by the <a href="https://tecken.readthedocs.io/en/latest/symbolication.html">Mozilla symbolication API documentation</a>.</li>
262    <li>Obtain source code by POSTing to <code>PATH_PREFIX/source/v1</code>, with the format specified in this <a href="https://github.com/mstange/profiler-get-symbols/issues/24#issuecomment-989985588">github comment</a>.</li>
263</ul>
264"#;
265
266const TEMPLATE_WITHOUT_PROFILE: &str = r#"
267<!DOCTYPE html>
268<html lang="en">
269<meta charset="utf-8">
270<title>Profiler Symbol Server</title>
271<body>
272
273<p>This is the profiler symbol server, running at <code>SERVER_URL</code>. You can:</p>
274<ul>
275    <li>Obtain symbols by POSTing to <code>PATH_PREFIX/symbolicate/v5</code>, with the format specified by the <a href="https://tecken.readthedocs.io/en/latest/symbolication.html">Mozilla symbolication API documentation</a>.</li>
276    <li>Obtain source code by POSTing to <code>PATH_PREFIX/source/v1</code>, with the format specified in this <a href="https://github.com/mstange/profiler-get-symbols/issues/24#issuecomment-989985588">github comment</a>.</li>
277</ul>
278"#;
279
280async fn symbolication_service(
281    req: Request<Body>,
282    template_values: Arc<HashMap<&'static str, String>>,
283    symbol_manager: Arc<SymbolManager>,
284    profile_filename: Option<PathBuf>,
285    path_prefix: String,
286) -> Result<Response<Body>, hyper::Error> {
287    let has_profile = profile_filename.is_some();
288    let method = req.method();
289    let path = req.uri().path();
290    let mut response = Response::new(Body::empty());
291
292    let path_without_prefix = match path.strip_prefix(&path_prefix) {
293        None => {
294            // The secret prefix was not part of the URL. Do not send CORS headers.
295            match (method, path) {
296                (&Method::GET, "/") => {
297                    response.headers_mut().insert(
298                        header::CONTENT_TYPE,
299                        header::HeaderValue::from_static("text/html"),
300                    );
301                    let template = match has_profile {
302                        true => TEMPLATE_WITH_PROFILE,
303                        false => TEMPLATE_WITHOUT_PROFILE,
304                    };
305                    *response.body_mut() =
306                        Body::from(substitute_template(template, &template_values));
307                }
308                _ => {
309                    *response.status_mut() = StatusCode::NOT_FOUND;
310                }
311            }
312            return Ok(response);
313        }
314        Some(path_without_prefix) => path_without_prefix,
315    };
316
317    // If we get here, then the secret prefix was part of the URL.
318    // This part is open to the public: we allow requests across origins.
319    // For background on CORS, see this document:
320    // https://w3c.github.io/webappsec-cors-for-developers/#cors
321    response.headers_mut().insert(
322        header::ACCESS_CONTROL_ALLOW_ORIGIN,
323        header::HeaderValue::from_static("*"),
324    );
325
326    match (method, path_without_prefix, profile_filename) {
327        (&Method::OPTIONS, _, _) => {
328            // https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS
329            *response.status_mut() = StatusCode::NO_CONTENT;
330            if req
331                .headers()
332                .contains_key(header::ACCESS_CONTROL_REQUEST_METHOD)
333            {
334                // This is a CORS preflight request.
335                // Reassure the client that we are CORS-aware and that it's free to request whatever.
336                response.headers_mut().insert(
337                    header::ACCESS_CONTROL_ALLOW_METHODS,
338                    header::HeaderValue::from_static("POST, GET, OPTIONS"),
339                );
340                response.headers_mut().insert(
341                    header::ACCESS_CONTROL_MAX_AGE,
342                    header::HeaderValue::from(86400),
343                );
344                if let Some(req_headers) = req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS)
345                {
346                    // All headers are fine.
347                    response
348                        .headers_mut()
349                        .insert(header::ACCESS_CONTROL_ALLOW_HEADERS, req_headers.clone());
350                }
351            } else {
352                // This is a regular OPTIONS request. Just send an Allow header with the allowed methods.
353                response.headers_mut().insert(
354                    header::ALLOW,
355                    header::HeaderValue::from_static("POST, GET, OPTIONS"),
356                );
357            }
358        }
359        (&Method::GET, "/profile.json", Some(profile_filename)) => {
360            if profile_filename.extension() == Some(OsStr::new("gz")) {
361                response.headers_mut().insert(
362                    header::CONTENT_ENCODING,
363                    header::HeaderValue::from_static("gzip"),
364                );
365            }
366            response.headers_mut().insert(
367                header::CONTENT_TYPE,
368                header::HeaderValue::from_static("application/json; charset=UTF-8"),
369            );
370            let (mut sender, body) = Body::channel();
371            *response.body_mut() = body;
372
373            // Stream the file out to the response body, asynchronously, after this function has returned.
374            tokio::spawn(async move {
375                let mut file = tokio::fs::File::open(&profile_filename)
376                    .await
377                    .expect("couldn't open profile file");
378                let mut contents = vec![0; 1024 * 1024];
379                loop {
380                    let data_len = file
381                        .read(&mut contents)
382                        .await
383                        .expect("couldn't read profile file");
384                    if data_len == 0 {
385                        break;
386                    }
387                    sender
388                        .send_data(Bytes::copy_from_slice(&contents[..data_len]))
389                        .await
390                        .expect("couldn't send data");
391                }
392            });
393        }
394        (&Method::POST, path, _) => {
395            response.headers_mut().insert(
396                header::CONTENT_TYPE,
397                header::HeaderValue::from_static("application/json"),
398            );
399            let path = path.to_string();
400            // Await the full body to be concatenated into a single `Bytes`...
401            let full_body = hyper::body::to_bytes(req.into_body()).await?;
402            let full_body = String::from_utf8(full_body.to_vec()).expect("invalid utf-8");
403            let response_json = symbol_manager.query_json_api(&path, &full_body).await;
404
405            *response.body_mut() = response_json.into();
406        }
407        _ => {
408            *response.status_mut() = StatusCode::NOT_FOUND;
409        }
410    };
411
412    Ok(response)
413}
414
415fn substitute_template(template: &str, template_values: &HashMap<&'static str, String>) -> String {
416    let mut s = template.to_string();
417    for (key, value) in template_values {
418        s = s.replace(key, value);
419    }
420    s
421}
422
423fn add_libs_to_libinfo_map(
424    libs: &[ProfileJsonLib],
425    libinfo_map: &mut HashMap<(String, DebugId), LibraryInfo>,
426) {
427    for lib in libs {
428        if let Some(lib_info) = libinfo_map_entry_for_lib(lib) {
429            libinfo_map.insert((lib_info.debug_name.clone(), lib_info.debug_id), lib_info);
430        }
431    }
432}
433
434fn libinfo_map_entry_for_lib(lib: &ProfileJsonLib) -> Option<LibraryInfo> {
435    let debug_name = lib.debug_name.clone()?;
436    let breakpad_id = lib.breakpad_id.as_ref()?;
437    let debug_path = lib.debug_path.clone();
438    let name = lib.name.clone();
439    let path = lib.path.clone();
440    let debug_id = DebugId::from_breakpad(breakpad_id).ok()?;
441    let code_id = lib
442        .code_id
443        .as_deref()
444        .and_then(|ci| CodeId::from_str(ci).ok());
445    let lib_info = LibraryInfo {
446        debug_id,
447        debug_name,
448        debug_path,
449        name,
450        code_id,
451        path,
452    };
453    Some(lib_info)
454}
455
456fn add_to_libinfo_map_recursive(
457    profile: &ProfileJsonProcess,
458    libinfo_map: &mut HashMap<(String, DebugId), LibraryInfo>,
459) {
460    add_libs_to_libinfo_map(&profile.libs, libinfo_map);
461    for thread in &profile.threads {
462        add_libs_to_libinfo_map(&thread.libs, libinfo_map);
463    }
464    for process in &profile.processes {
465        add_to_libinfo_map_recursive(process, libinfo_map);
466    }
467}
468
469#[cfg(test)]
470mod test {
471    use crate::{ProfileJsonLib, ProfileJsonProcess};
472
473    #[test]
474    fn deserialize_profile_json() {
475        let p: ProfileJsonProcess = serde_json::from_str("{}").unwrap();
476        assert!(p.libs.is_empty());
477        assert!(p.threads.is_empty());
478        assert!(p.processes.is_empty());
479
480        let p: ProfileJsonProcess = serde_json::from_str("{\"unknown_field\":[1, 2, 3]}").unwrap();
481        assert!(p.libs.is_empty());
482        assert!(p.threads.is_empty());
483        assert!(p.processes.is_empty());
484
485        let p: ProfileJsonProcess =
486            serde_json::from_str("{\"threads\":[{\"libs\":[{}]}]}").unwrap();
487        assert!(p.libs.is_empty());
488        assert_eq!(p.threads.len(), 1);
489        assert_eq!(p.threads[0].libs.len(), 1);
490        assert_eq!(p.threads[0].libs[0], ProfileJsonLib::default());
491        assert!(p.processes.is_empty());
492    }
493}