1use std::any;
8use std::convert::TryInto;
9use std::fmt::{self, Debug};
10
11use async_trait::async_trait;
12use bytes::Bytes;
13use graphql_client::{GraphQLQuery, QueryBody, Response};
14use http::{HeaderMap, HeaderName, Response as HttpResponse};
15use itertools::Itertools;
16use log::{debug, info};
17use reqwest::blocking::Client;
18use reqwest::{Certificate, Client as AsyncClient};
19use serde::de::DeserializeOwned;
20use serde::Deserialize;
21use thiserror::Error;
22use url::Url;
23
24#[cfg(any(feature = "client_der", feature = "client_pem"))]
25use reqwest::Identity as TlsIdentity;
26
27use crate::api;
28use crate::auth::{Auth, AuthError};
29
30const DEFAULT_USER_AGENT: &str = concat!("rust-gitlab/v", env!("CARGO_PKG_VERSION"));
31
32#[derive(Debug, Clone)]
34pub struct CertificateBundle {
35 certs: Vec<Certificate>,
36}
37
38impl CertificateBundle {
39 #[cfg(feature = "client_der")]
41 pub fn der(bytes: &[u8]) -> Result<Self, reqwest::Error> {
42 let cert = Certificate::from_der(bytes)?;
43
44 Ok(Self {
45 certs: vec![cert],
46 })
47 }
48
49 #[cfg(feature = "client_pem")]
51 pub fn pem(bytes: &[u8]) -> Result<Self, reqwest::Error> {
52 let cert = Certificate::from_pem(bytes)?;
53
54 Ok(Self {
55 certs: vec![cert],
56 })
57 }
58
59 #[cfg(feature = "client_pem")]
61 pub fn pem_bundle(bytes: &[u8]) -> Result<Self, reqwest::Error> {
62 let certs = Certificate::from_pem_bundle(bytes)?;
63
64 Ok(Self {
65 certs,
66 })
67 }
68}
69
70#[derive(Debug, Error)]
71#[non_exhaustive]
72pub enum GitlabError {
73 #[error("failed to parse url: {}", source)]
74 UrlParse {
75 #[from]
76 source: url::ParseError,
77 },
78 #[error("error setting auth header: {}", source)]
79 AuthError {
80 #[from]
81 source: AuthError,
82 },
83 #[error("communication with gitlab: {}", source)]
84 Communication {
85 #[from]
86 source: reqwest::Error,
87 },
88 #[error("gitlab HTTP error: {}", status)]
89 Http { status: reqwest::StatusCode },
90 #[allow(clippy::upper_case_acronyms)]
91 #[error("graphql error: [\"{}\"]", message.iter().format("\", \""))]
92 GraphQL { message: Vec<graphql_client::Error> },
93 #[error("no response from gitlab")]
94 NoResponse {},
95 #[error("could not parse {} data from JSON: {}", typename, source)]
96 DataType {
97 #[source]
98 source: serde_json::Error,
99 typename: &'static str,
100 },
101 #[error("api error: {}", source)]
102 Api {
103 #[from]
104 source: api::ApiError<RestError>,
105 },
106}
107
108impl GitlabError {
109 fn http(status: reqwest::StatusCode) -> Self {
110 GitlabError::Http {
111 status,
112 }
113 }
114
115 fn graphql(message: Vec<graphql_client::Error>) -> Self {
116 GitlabError::GraphQL {
117 message,
118 }
119 }
120
121 fn no_response() -> Self {
122 GitlabError::NoResponse {}
123 }
124
125 fn data_type<T>(source: serde_json::Error) -> Self {
126 GitlabError::DataType {
127 source,
128 typename: any::type_name::<T>(),
129 }
130 }
131}
132
133type GitlabResult<T> = Result<T, GitlabError>;
134
135#[derive(Clone)]
139enum ClientCert {
140 None,
141 #[cfg(feature = "client_der")]
142 Der(Vec<u8>, String),
143 #[cfg(feature = "client_pem")]
144 Pem(Vec<u8>),
145}
146
147#[derive(Clone)]
151pub struct Gitlab {
152 client: Client,
154 rest_url: Url,
156 graphql_url: Url,
158 auth: Auth,
160}
161
162impl Debug for Gitlab {
163 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
164 f.debug_struct("Gitlab")
165 .field("rest_url", &self.rest_url)
166 .field("graphql_url", &self.graphql_url)
167 .finish()
168 }
169}
170
171#[derive(Debug, Clone)]
174enum CertPolicy {
175 Default,
176 Insecure,
179 CustomRoots(CertificateBundle),
181}
182
183fn log_request_id(headers: &HeaderMap) {
184 const REQUEST_ID_HEADER: HeaderName = HeaderName::from_static("x-request-id");
185
186 if let Some(req_id) = headers.get(REQUEST_ID_HEADER) {
187 debug!(
188 target: "gitlab",
189 "{}: {}",
190 REQUEST_ID_HEADER.as_str(),
191 req_id.to_str().unwrap_or("<INVALID>"),
192 );
193 }
194}
195
196impl Gitlab {
197 pub fn new<H, T>(host: H, token: T) -> GitlabResult<Self>
202 where
203 H: AsRef<str>,
204 T: Into<String>,
205 {
206 Self::new_impl(
207 "https",
208 host.as_ref(),
209 Auth::Token(token.into()),
210 CertPolicy::Default,
211 ClientCert::None,
212 DEFAULT_USER_AGENT,
213 )
214 }
215
216 pub fn new_insecure<H, T>(host: H, token: T) -> GitlabResult<Self>
221 where
222 H: AsRef<str>,
223 T: Into<String>,
224 {
225 Self::new_impl(
226 "http",
227 host.as_ref(),
228 Auth::Token(token.into()),
229 CertPolicy::Insecure,
230 ClientCert::None,
231 DEFAULT_USER_AGENT,
232 )
233 }
234
235 pub fn new_custom_root_certs<H, T>(
240 host: H,
241 token: T,
242 root_certificate: CertificateBundle,
243 ) -> GitlabResult<Self>
244 where
245 H: AsRef<str>,
246 T: Into<String>,
247 {
248 Self::new_impl(
249 "https",
250 host.as_ref(),
251 Auth::Token(token.into()),
252 CertPolicy::CustomRoots(root_certificate),
253 ClientCert::None,
254 DEFAULT_USER_AGENT,
255 )
256 }
257
258 pub fn new_job_token<H, T>(host: H, token: T) -> GitlabResult<Self>
263 where
264 H: AsRef<str>,
265 T: Into<String>,
266 {
267 Self::new_impl(
268 "https",
269 host.as_ref(),
270 Auth::JobToken(token.into()),
271 CertPolicy::Default,
272 ClientCert::None,
273 DEFAULT_USER_AGENT,
274 )
275 }
276
277 pub fn new_job_token_insecure<H, T>(host: H, token: T) -> GitlabResult<Self>
282 where
283 H: AsRef<str>,
284 T: Into<String>,
285 {
286 Self::new_impl(
287 "http",
288 host.as_ref(),
289 Auth::JobToken(token.into()),
290 CertPolicy::Insecure,
291 ClientCert::None,
292 DEFAULT_USER_AGENT,
293 )
294 }
295
296 pub fn with_oauth2<H, T>(host: H, token: T) -> GitlabResult<Self>
301 where
302 H: AsRef<str>,
303 T: Into<String>,
304 {
305 Self::new_impl(
306 "https",
307 host.as_ref(),
308 Auth::OAuth2(token.into()),
309 CertPolicy::Default,
310 ClientCert::None,
311 DEFAULT_USER_AGENT,
312 )
313 }
314
315 pub fn with_oauth2_insecure<H, T>(host: H, token: T) -> GitlabResult<Self>
320 where
321 H: AsRef<str>,
322 T: Into<String>,
323 {
324 Self::new_impl(
325 "http",
326 host.as_ref(),
327 Auth::OAuth2(token.into()),
328 CertPolicy::Default,
329 ClientCert::None,
330 DEFAULT_USER_AGENT,
331 )
332 }
333
334 fn new_impl(
336 protocol: &str,
337 host: &str,
338 auth: Auth,
339 cert_validation: CertPolicy,
340 identity: ClientCert,
341 user_agent: &str,
342 ) -> GitlabResult<Self> {
343 let rest_url = Url::parse(&format!("{protocol}://{host}/api/v4/"))?;
344 let graphql_url = Url::parse(&format!("{protocol}://{host}/api/graphql"))?;
345
346 let client_builder = Client::builder().user_agent(user_agent);
347
348 let client_builder = match cert_validation {
349 CertPolicy::Insecure => client_builder.tls_danger_accept_invalid_certs(true),
350 CertPolicy::Default => {
351 match identity {
352 ClientCert::None => client_builder,
353 #[cfg(feature = "client_der")]
354 ClientCert::Der(der, password) => {
355 let id = TlsIdentity::from_pkcs12_der(&der, &password)?;
356 client_builder.identity(id)
357 },
358 #[cfg(feature = "client_pem")]
359 ClientCert::Pem(pem) => {
360 let id = TlsIdentity::from_pem(&pem)?;
361 client_builder.identity(id)
362 },
363 }
364 },
365 CertPolicy::CustomRoots(root_certs) => client_builder.tls_certs_merge(root_certs.certs),
366 };
367
368 let client = client_builder.build()?;
369
370 let api = Gitlab {
371 client,
372 rest_url,
373 graphql_url,
374 auth,
375 };
376
377 api.auth.check_connection(&api)?;
379
380 Ok(api)
381 }
382
383 pub fn builder<H, T>(host: H, token: T) -> GitlabBuilder
385 where
386 H: Into<String>,
387 T: Into<String>,
388 {
389 GitlabBuilder::new(host, token)
390 }
391
392 pub fn graphql<Q>(&self, query: &QueryBody<Q::Variables>) -> GitlabResult<Q::ResponseData>
394 where
395 Q: GraphQLQuery,
396 Q::Variables: Debug,
397 for<'d> Q::ResponseData: Deserialize<'d>,
398 {
399 info!(
400 target: "gitlab",
401 "sending GraphQL query '{}' {:?}",
402 query.operation_name,
403 query.variables,
404 );
405 let req = self.client.post(self.graphql_url.clone()).json(query);
406 let rsp: Response<Q::ResponseData> = self.send(req)?;
407
408 if let Some(errs) = rsp.errors {
409 return Err(GitlabError::graphql(errs));
410 }
411 rsp.data.ok_or_else(GitlabError::no_response)
412 }
413
414 fn send<T>(&self, req: reqwest::blocking::RequestBuilder) -> GitlabResult<T>
416 where
417 T: DeserializeOwned,
418 {
419 let auth_headers = {
420 let mut headers = HeaderMap::default();
421 self.auth.set_header(&mut headers)?;
422 headers
423 };
424 let rsp = req.headers(auth_headers).send()?;
425
426 log_request_id(rsp.headers());
427
428 let status = rsp.status();
429 if status.is_server_error() {
430 return Err(GitlabError::http(status));
431 }
432
433 serde_json::from_reader::<_, T>(rsp).map_err(GitlabError::data_type::<T>)
434 }
435
436 fn rest_auth(
438 &self,
439 mut request: http::request::Builder,
440 body: Vec<u8>,
441 auth: &Auth,
442 ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
443 let call = || -> Result<_, RestError> {
444 auth.set_header(request.headers_mut().unwrap())?;
445 let http_request = request.body(body)?;
446 let request = http_request.try_into()?;
447 let rsp = self.client.execute(request)?;
448
449 log_request_id(rsp.headers());
450
451 let mut http_rsp = HttpResponse::builder()
452 .status(rsp.status())
453 .version(rsp.version());
454 let headers = http_rsp.headers_mut().unwrap();
455 for (key, value) in rsp.headers() {
456 headers.insert(key, value.clone());
457 }
458 Ok(http_rsp.body(rsp.bytes()?)?)
459 };
460 call().map_err(api::ApiError::client)
461 }
462}
463
464#[derive(Debug, Error)]
465#[non_exhaustive]
466pub enum RestError {
467 #[error("error setting auth header: {}", source)]
468 AuthError {
469 #[from]
470 source: AuthError,
471 },
472 #[error("communication with gitlab: {}", source)]
473 Communication {
474 #[from]
475 source: reqwest::Error,
476 },
477 #[error("`http` error: {}", source)]
478 Http {
479 #[from]
480 source: http::Error,
481 },
482}
483
484impl api::RestClient for Gitlab {
485 type Error = RestError;
486
487 fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
488 debug!(target: "gitlab", "REST api call {endpoint}");
489 Ok(self.rest_url.join(endpoint)?)
490 }
491}
492
493impl api::Client for Gitlab {
494 fn rest(
495 &self,
496 request: http::request::Builder,
497 body: Vec<u8>,
498 ) -> Result<HttpResponse<Bytes>, api::ApiError<Self::Error>> {
499 self.rest_auth(request, body, &self.auth)
500 }
501}
502
503pub struct GitlabBuilder {
504 protocol: &'static str,
505 host: String,
506 token: Auth,
507 cert_validation: CertPolicy,
508 identity: ClientCert,
509 user_agent: String,
510}
511
512impl GitlabBuilder {
513 pub fn new<H, T>(host: H, token: T) -> Self
515 where
516 H: Into<String>,
517 T: Into<String>,
518 {
519 Self {
520 protocol: "https",
521 host: host.into(),
522 token: Auth::Token(token.into()),
523 cert_validation: CertPolicy::Default,
524 identity: ClientCert::None,
525 user_agent: DEFAULT_USER_AGENT.to_string(),
526 }
527 }
528
529 pub fn new_unauthenticated<H>(host: H) -> Self
531 where
532 H: Into<String>,
533 {
534 Self {
535 protocol: "https",
536 host: host.into(),
537 token: Auth::None,
538 cert_validation: CertPolicy::Default,
539 identity: ClientCert::None,
540 user_agent: DEFAULT_USER_AGENT.to_string(),
541 }
542 }
543
544 pub fn new_with_job_token<H, T>(host: H, token: T) -> Self
546 where
547 H: Into<String>,
548 T: Into<String>,
549 {
550 Self {
551 protocol: "https",
552 host: host.into(),
553 token: Auth::JobToken(token.into()),
554 cert_validation: CertPolicy::Default,
555 identity: ClientCert::None,
556 user_agent: DEFAULT_USER_AGENT.to_string(),
557 }
558 }
559
560 pub fn insecure(&mut self) -> &mut Self {
562 self.protocol = "http";
563 self
564 }
565
566 pub fn cert_insecure(&mut self) -> &mut Self {
567 self.cert_validation = CertPolicy::Insecure;
568 self
569 }
570
571 pub fn oauth2_token(&mut self) -> &mut Self {
573 if let Auth::Token(token) = self.token.clone() {
574 self.token = Auth::OAuth2(token);
575 }
576 self
577 }
578
579 pub fn job_token(&mut self) -> &mut Self {
581 if let Auth::Token(token) = self.token.clone() {
582 self.token = Auth::JobToken(token);
583 }
584 self
585 }
586
587 #[cfg(any(doc, feature = "client_der"))]
590 pub fn client_identity_from_der(&mut self, der: &[u8], password: &str) -> &mut Self {
591 self.identity = ClientCert::Der(der.into(), password.into());
592 self
593 }
594
595 #[cfg(any(doc, feature = "client_pem"))]
598 pub fn client_identity_from_pem(&mut self, pem: &[u8]) -> &mut Self {
599 self.identity = ClientCert::Pem(pem.into());
600 self
601 }
602
603 pub fn user_agent<U>(&mut self, user_agent: U) -> &mut Self
605 where
606 U: Into<String>,
607 {
608 self.user_agent = user_agent.into();
609 self
610 }
611
612 pub fn build(&self) -> GitlabResult<Gitlab> {
613 Gitlab::new_impl(
614 self.protocol,
615 &self.host,
616 self.token.clone(),
617 self.cert_validation.clone(),
618 self.identity.clone(),
619 &self.user_agent,
620 )
621 }
622
623 pub async fn build_async(&self) -> GitlabResult<AsyncGitlab> {
624 AsyncGitlab::new_impl(
625 self.protocol,
626 &self.host,
627 self.token.clone(),
628 self.cert_validation.clone(),
629 self.identity.clone(),
630 &self.user_agent,
631 )
632 .await
633 }
634}
635
636#[derive(Clone)]
640pub struct AsyncGitlab {
641 client: reqwest::Client,
643 instance_url: Url,
645 rest_url: Url,
647 graphql_url: Url,
649 auth: Auth,
651}
652
653impl Debug for AsyncGitlab {
654 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
655 f.debug_struct("AsyncGitlab")
656 .field("instance_url", &self.instance_url)
657 .field("rest_url", &self.rest_url)
658 .field("graphql_url", &self.graphql_url)
659 .finish()
660 }
661}
662
663#[async_trait]
664impl api::RestClient for AsyncGitlab {
665 type Error = RestError;
666
667 fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
668 debug!(target: "gitlab", "REST api call {endpoint}");
669 Ok(self.rest_url.join(endpoint)?)
670 }
671
672 fn instance_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
673 debug!(target: "gitlab", "instance api call {endpoint}");
674 Ok(self.instance_url.join(endpoint)?)
675 }
676}
677
678#[async_trait]
679impl api::AsyncClient for AsyncGitlab {
680 async fn rest_async(
681 &self,
682 request: http::request::Builder,
683 body: Vec<u8>,
684 ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
685 self.rest_async_auth(request, body, &self.auth).await
686 }
687}
688
689impl AsyncGitlab {
690 async fn new_impl(
692 protocol: &str,
693 host: &str,
694 auth: Auth,
695 cert_validation: CertPolicy,
696 identity: ClientCert,
697 user_agent: &str,
698 ) -> GitlabResult<Self> {
699 let instance_url = Url::parse(&format!("{protocol}://{host}/"))?;
700 let rest_url = Url::parse(&format!("{protocol}://{host}/api/v4/"))?;
701 let graphql_url = Url::parse(&format!("{protocol}://{host}/api/graphql"))?;
702
703 let client_builder = AsyncClient::builder().user_agent(user_agent);
704
705 let client_builder = match cert_validation {
706 CertPolicy::Insecure => client_builder.tls_danger_accept_invalid_certs(true),
707 CertPolicy::Default => {
708 match identity {
709 ClientCert::None => client_builder,
710 #[cfg(feature = "client_der")]
711 ClientCert::Der(der, password) => {
712 let id = TlsIdentity::from_pkcs12_der(&der, &password)?;
713 client_builder.identity(id)
714 },
715 #[cfg(feature = "client_pem")]
716 ClientCert::Pem(pem) => {
717 let id = TlsIdentity::from_pem(&pem)?;
718 client_builder.identity(id)
719 },
720 }
721 },
722 CertPolicy::CustomRoots(root_certs) => client_builder.tls_certs_merge(root_certs.certs),
723 };
724
725 let client = client_builder.build()?;
726
727 let api = AsyncGitlab {
728 client,
729 instance_url,
730 rest_url,
731 graphql_url,
732 auth,
733 };
734
735 api.auth.check_connection_async(&api).await?;
737
738 Ok(api)
739 }
740
741 pub async fn graphql<Q>(&self, query: &QueryBody<Q::Variables>) -> GitlabResult<Q::ResponseData>
743 where
744 Q: GraphQLQuery,
745 Q::Variables: Debug,
746 for<'d> Q::ResponseData: Deserialize<'d>,
747 {
748 info!(
749 target: "gitlab",
750 "sending GraphQL query '{}' {:?}",
751 query.operation_name,
752 query.variables,
753 );
754 let req = self.client.post(self.graphql_url.clone()).json(query);
755 let rsp: Response<Q::ResponseData> = self.send(req).await?;
756
757 if let Some(errs) = rsp.errors {
758 return Err(GitlabError::graphql(errs));
759 }
760 rsp.data.ok_or_else(GitlabError::no_response)
761 }
762
763 async fn send<T>(&self, req: reqwest::RequestBuilder) -> GitlabResult<T>
765 where
766 T: DeserializeOwned,
767 {
768 let auth_headers = {
769 let mut headers = HeaderMap::default();
770 self.auth.set_header(&mut headers)?;
771 headers
772 };
773 let rsp = req.headers(auth_headers).send().await?;
774 let status = rsp.status();
775 if status.is_server_error() {
776 return Err(GitlabError::http(status));
777 }
778
779 serde_json::from_slice::<T>(&rsp.bytes().await?).map_err(GitlabError::data_type::<T>)
780 }
781
782 async fn rest_async_auth(
784 &self,
785 mut request: http::request::Builder,
786 body: Vec<u8>,
787 auth: &Auth,
788 ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
789 use futures_util::TryFutureExt;
790 let call = || {
791 async {
792 auth.set_header(request.headers_mut().unwrap())?;
793 let http_request = request.body(body)?;
794 let request = http_request.try_into()?;
795 let rsp = self.client.execute(request).await?;
796
797 log_request_id(rsp.headers());
798
799 let mut http_rsp = HttpResponse::builder()
800 .status(rsp.status())
801 .version(rsp.version());
802 let headers = http_rsp.headers_mut().unwrap();
803 for (key, value) in rsp.headers() {
804 headers.insert(key, value.clone());
805 }
806 Ok(http_rsp.body(rsp.bytes().await?)?)
807 }
808 };
809 call().map_err(api::ApiError::client).await
810 }
811}
812
813#[derive(Clone)]
814pub struct ImpersonationClient<'a, T> {
815 auth: Auth,
816 client: &'a T,
817}
818
819impl<'a, C> ImpersonationClient<'a, C> {
820 pub fn new<T>(client: &'a C, token: T) -> Self
822 where
823 T: Into<String>,
824 {
825 Self {
826 auth: Auth::Token(token.into()),
827 client,
828 }
829 }
830
831 pub fn oauth2_token(&mut self) -> &mut Self {
833 if let Auth::Token(auth) = self.auth.clone() {
834 self.auth = Auth::OAuth2(auth);
835 }
836 self
837 }
838}
839
840impl<C> api::RestClient for ImpersonationClient<'_, C>
841where
842 C: api::RestClient,
843{
844 type Error = C::Error;
845
846 fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
847 self.client.rest_endpoint(endpoint)
848 }
849
850 fn instance_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
851 self.client.instance_endpoint(endpoint)
852 }
853}
854
855impl api::Client for ImpersonationClient<'_, Gitlab> {
856 fn rest(
857 &self,
858 request: http::request::Builder,
859 body: Vec<u8>,
860 ) -> Result<HttpResponse<Bytes>, api::ApiError<Self::Error>> {
861 self.client.rest_auth(request, body, &self.auth)
862 }
863}
864
865#[allow(clippy::needless_lifetimes)] #[async_trait]
867impl<'a> api::AsyncClient for ImpersonationClient<'a, AsyncGitlab> {
868 async fn rest_async(
869 &self,
870 request: http::request::Builder,
871 body: Vec<u8>,
872 ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
873 self.client.rest_async_auth(request, body, &self.auth).await
874 }
875}