opcua_core/comms/
url.rs

1// OPCUA for Rust
2// SPDX-License-Identifier: MPL-2.0
3// Copyright (C) 2017-2022 Adam Lock
4
5//! Provides functions for parsing Urls from strings.
6
7use url::Url;
8
9use opcua_types::{constants::DEFAULT_OPC_UA_SERVER_PORT, status_code::StatusCode};
10
11pub const OPC_TCP_SCHEME: &str = "opc.tcp";
12
13/// Creates a `Url` from the input string, supplying a default port if necessary.
14fn opc_url_from_str(s: &str) -> Result<Url, ()> {
15    Url::parse(s)
16        .map(|mut url| {
17            if url.port().is_none() {
18                // If no port is supplied, then treat it as the default port 4840
19                let _ = url.set_port(Some(DEFAULT_OPC_UA_SERVER_PORT));
20            }
21            url
22        })
23        .map_err(|err| {
24            error!("Cannot parse url \"{}\", error = {:?}", s, err);
25        })
26}
27
28/// Replace the hostname in the supplied url and return a new url
29pub fn url_with_replaced_hostname(url: &str, hostname: &str) -> Result<String, ()> {
30    let mut url = opc_url_from_str(url)?;
31    let _ = url.set_host(Some(hostname));
32    Ok(url.into_string())
33}
34
35/// Test if the two urls match except for the hostname. Can be used by a server whose endpoint doesn't
36/// exactly match the incoming connection, e.g. 127.0.0.1 vs localhost.
37pub fn url_matches_except_host(url1: &str, url2: &str) -> bool {
38    if let Ok(mut url1) = opc_url_from_str(url1) {
39        if let Ok(mut url2) = opc_url_from_str(url2) {
40            // Both hostnames are set to xxxx so the comparison should come out as the same url
41            // if they actually match one another.
42            if url1.set_host(Some("xxxx")).is_ok() && url2.set_host(Some("xxxx")).is_ok() {
43                return url1 == url2;
44            }
45        } else {
46            error!("Cannot parse url \"{}\"", url2);
47        }
48    } else {
49        error!("Cannot parse url \"{}\"", url1);
50    }
51    false
52}
53
54/// Takes an endpoint url and strips off the path and args to leave just the protocol, host & port.
55pub fn server_url_from_endpoint_url(endpoint_url: &str) -> std::result::Result<String, ()> {
56    opc_url_from_str(endpoint_url).map(|mut url| {
57        url.set_query(None);
58        if let Some(port) = url.port() {
59            // If the port is the default, strip it so the url string omits it.
60            if port == DEFAULT_OPC_UA_SERVER_PORT {
61                let _ = url.set_port(None);
62            }
63        }
64        url.into_string()
65    })
66}
67
68pub fn is_valid_opc_ua_url(url: &str) -> bool {
69    is_opc_ua_binary_url(url)
70}
71
72pub fn is_opc_ua_binary_url(url: &str) -> bool {
73    if let Ok(url) = opc_url_from_str(url) {
74        url.scheme() == OPC_TCP_SCHEME
75    } else {
76        false
77    }
78}
79
80pub fn hostname_from_url(url: &str) -> Result<String, ()> {
81    // Validate and split out the endpoint we have
82    if let Ok(url) = Url::parse(url) {
83        if let Some(host) = url.host_str() {
84            Ok(host.to_string())
85        } else {
86            Err(())
87        }
88    } else {
89        Err(())
90    }
91}
92
93pub fn hostname_port_from_url(url: &str, default_port: u16) -> Result<(String, u16), StatusCode> {
94    // Validate and split out the endpoint we have
95    let url = Url::parse(url).map_err(|_| StatusCode::BadTcpEndpointUrlInvalid)?;
96
97    if url.scheme() != OPC_TCP_SCHEME || !url.has_host() {
98        Err(StatusCode::BadTcpEndpointUrlInvalid)
99    } else {
100        let host = url.host_str().unwrap();
101        let port = url.port().unwrap_or(default_port);
102        Ok((host.to_string(), port))
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn url_scheme() {
112        assert!(is_opc_ua_binary_url("opc.tcp://foo/xyz"));
113        assert!(is_opc_ua_binary_url(
114            "opc.tcp://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/xyz"
115        ));
116        assert!(!is_opc_ua_binary_url("http://foo/xyz"));
117    }
118
119    #[test]
120    fn url_matches_test() {
121        assert!(url_matches_except_host(
122            "opc.tcp://localhost/xyz",
123            "opc.tcp://127.0.0.1/xyz"
124        ));
125        assert!(!url_matches_except_host(
126            "opc.tcp://localhost/xyz",
127            "opc.tcp://127.0.0.1/abc"
128        ));
129    }
130
131    #[test]
132    fn server_url_from_endpoint_url_test() {
133        assert_eq!(
134            "opc.tcp://localhost/",
135            server_url_from_endpoint_url("opc.tcp://localhost").unwrap()
136        );
137        assert_eq!(
138            "opc.tcp://localhost/",
139            server_url_from_endpoint_url("opc.tcp://localhost:4840").unwrap()
140        );
141        assert_eq!(
142            "opc.tcp://localhost:4841/",
143            server_url_from_endpoint_url("opc.tcp://localhost:4841").unwrap()
144        );
145        assert_eq!(
146            "opc.tcp://localhost/xyz/abc",
147            server_url_from_endpoint_url("opc.tcp://localhost/xyz/abc?1").unwrap()
148        );
149        assert_eq!(
150            "opc.tcp://localhost:999/xyz/abc",
151            server_url_from_endpoint_url("opc.tcp://localhost:999/xyz/abc?1").unwrap()
152        );
153    }
154
155    #[test]
156    fn url_with_replaced_hostname_test() {
157        assert_eq!(
158            url_with_replaced_hostname("opc.tcp://foo:123/x", "foo").unwrap(),
159            "opc.tcp://foo:123/x"
160        );
161        assert_eq!(
162            url_with_replaced_hostname("opc.tcp://foo:123/x", "bar").unwrap(),
163            "opc.tcp://bar:123/x"
164        );
165        assert_eq!(
166            url_with_replaced_hostname("opc.tcp://localhost:123/x", "127.0.0.1").unwrap(),
167            "opc.tcp://127.0.0.1:123/x"
168        );
169    }
170}