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.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) => {
366 let mut client_builder = client_builder;
367 for cert in root_certs.certs {
368 client_builder = client_builder.add_root_certificate(cert)
369 }
370 client_builder
371 },
372 };
373
374 let client = client_builder.build()?;
375
376 let api = Gitlab {
377 client,
378 rest_url,
379 graphql_url,
380 auth,
381 };
382
383 api.auth.check_connection(&api)?;
385
386 Ok(api)
387 }
388
389 pub fn builder<H, T>(host: H, token: T) -> GitlabBuilder
391 where
392 H: Into<String>,
393 T: Into<String>,
394 {
395 GitlabBuilder::new(host, token)
396 }
397
398 pub fn graphql<Q>(&self, query: &QueryBody<Q::Variables>) -> GitlabResult<Q::ResponseData>
400 where
401 Q: GraphQLQuery,
402 Q::Variables: Debug,
403 for<'d> Q::ResponseData: Deserialize<'d>,
404 {
405 info!(
406 target: "gitlab",
407 "sending GraphQL query '{}' {:?}",
408 query.operation_name,
409 query.variables,
410 );
411 let req = self.client.post(self.graphql_url.clone()).json(query);
412 let rsp: Response<Q::ResponseData> = self.send(req)?;
413
414 if let Some(errs) = rsp.errors {
415 return Err(GitlabError::graphql(errs));
416 }
417 rsp.data.ok_or_else(GitlabError::no_response)
418 }
419
420 fn send<T>(&self, req: reqwest::blocking::RequestBuilder) -> GitlabResult<T>
422 where
423 T: DeserializeOwned,
424 {
425 let auth_headers = {
426 let mut headers = HeaderMap::default();
427 self.auth.set_header(&mut headers)?;
428 headers
429 };
430 let rsp = req.headers(auth_headers).send()?;
431
432 log_request_id(rsp.headers());
433
434 let status = rsp.status();
435 if status.is_server_error() {
436 return Err(GitlabError::http(status));
437 }
438
439 serde_json::from_reader::<_, T>(rsp).map_err(GitlabError::data_type::<T>)
440 }
441
442 fn rest_auth(
444 &self,
445 mut request: http::request::Builder,
446 body: Vec<u8>,
447 auth: &Auth,
448 ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
449 let call = || -> Result<_, RestError> {
450 auth.set_header(request.headers_mut().unwrap())?;
451 let http_request = request.body(body)?;
452 let request = http_request.try_into()?;
453 let rsp = self.client.execute(request)?;
454
455 log_request_id(rsp.headers());
456
457 let mut http_rsp = HttpResponse::builder()
458 .status(rsp.status())
459 .version(rsp.version());
460 let headers = http_rsp.headers_mut().unwrap();
461 for (key, value) in rsp.headers() {
462 headers.insert(key, value.clone());
463 }
464 Ok(http_rsp.body(rsp.bytes()?)?)
465 };
466 call().map_err(api::ApiError::client)
467 }
468}
469
470#[derive(Debug, Error)]
471#[non_exhaustive]
472pub enum RestError {
473 #[error("error setting auth header: {}", source)]
474 AuthError {
475 #[from]
476 source: AuthError,
477 },
478 #[error("communication with gitlab: {}", source)]
479 Communication {
480 #[from]
481 source: reqwest::Error,
482 },
483 #[error("`http` error: {}", source)]
484 Http {
485 #[from]
486 source: http::Error,
487 },
488}
489
490impl api::RestClient for Gitlab {
491 type Error = RestError;
492
493 fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
494 debug!(target: "gitlab", "REST api call {endpoint}");
495 Ok(self.rest_url.join(endpoint)?)
496 }
497}
498
499impl api::Client for Gitlab {
500 fn rest(
501 &self,
502 request: http::request::Builder,
503 body: Vec<u8>,
504 ) -> Result<HttpResponse<Bytes>, api::ApiError<Self::Error>> {
505 self.rest_auth(request, body, &self.auth)
506 }
507}
508
509pub struct GitlabBuilder {
510 protocol: &'static str,
511 host: String,
512 token: Auth,
513 cert_validation: CertPolicy,
514 identity: ClientCert,
515 user_agent: String,
516}
517
518impl GitlabBuilder {
519 pub fn new<H, T>(host: H, token: T) -> Self
521 where
522 H: Into<String>,
523 T: Into<String>,
524 {
525 Self {
526 protocol: "https",
527 host: host.into(),
528 token: Auth::Token(token.into()),
529 cert_validation: CertPolicy::Default,
530 identity: ClientCert::None,
531 user_agent: DEFAULT_USER_AGENT.to_string(),
532 }
533 }
534
535 pub fn new_unauthenticated<H>(host: H) -> Self
537 where
538 H: Into<String>,
539 {
540 Self {
541 protocol: "https",
542 host: host.into(),
543 token: Auth::None,
544 cert_validation: CertPolicy::Default,
545 identity: ClientCert::None,
546 user_agent: DEFAULT_USER_AGENT.to_string(),
547 }
548 }
549
550 pub fn new_with_job_token<H, T>(host: H, token: T) -> Self
552 where
553 H: Into<String>,
554 T: Into<String>,
555 {
556 Self {
557 protocol: "https",
558 host: host.into(),
559 token: Auth::JobToken(token.into()),
560 cert_validation: CertPolicy::Default,
561 identity: ClientCert::None,
562 user_agent: DEFAULT_USER_AGENT.to_string(),
563 }
564 }
565
566 pub fn insecure(&mut self) -> &mut Self {
568 self.protocol = "http";
569 self
570 }
571
572 pub fn cert_insecure(&mut self) -> &mut Self {
573 self.cert_validation = CertPolicy::Insecure;
574 self
575 }
576
577 pub fn oauth2_token(&mut self) -> &mut Self {
579 if let Auth::Token(token) = self.token.clone() {
580 self.token = Auth::OAuth2(token);
581 }
582 self
583 }
584
585 pub fn job_token(&mut self) -> &mut Self {
587 if let Auth::Token(token) = self.token.clone() {
588 self.token = Auth::JobToken(token);
589 }
590 self
591 }
592
593 #[cfg(any(doc, feature = "client_der"))]
596 pub fn client_identity_from_der(&mut self, der: &[u8], password: &str) -> &mut Self {
597 self.identity = ClientCert::Der(der.into(), password.into());
598 self
599 }
600
601 #[cfg(any(doc, feature = "client_pem"))]
604 pub fn client_identity_from_pem(&mut self, pem: &[u8]) -> &mut Self {
605 self.identity = ClientCert::Pem(pem.into());
606 self
607 }
608
609 pub fn user_agent<U>(&mut self, user_agent: U) -> &mut Self
611 where
612 U: Into<String>,
613 {
614 self.user_agent = user_agent.into();
615 self
616 }
617
618 pub fn build(&self) -> GitlabResult<Gitlab> {
619 Gitlab::new_impl(
620 self.protocol,
621 &self.host,
622 self.token.clone(),
623 self.cert_validation.clone(),
624 self.identity.clone(),
625 &self.user_agent,
626 )
627 }
628
629 pub async fn build_async(&self) -> GitlabResult<AsyncGitlab> {
630 AsyncGitlab::new_impl(
631 self.protocol,
632 &self.host,
633 self.token.clone(),
634 self.cert_validation.clone(),
635 self.identity.clone(),
636 &self.user_agent,
637 )
638 .await
639 }
640}
641
642#[derive(Clone)]
646pub struct AsyncGitlab {
647 client: reqwest::Client,
649 instance_url: Url,
651 rest_url: Url,
653 graphql_url: Url,
655 auth: Auth,
657}
658
659impl Debug for AsyncGitlab {
660 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
661 f.debug_struct("AsyncGitlab")
662 .field("instance_url", &self.instance_url)
663 .field("rest_url", &self.rest_url)
664 .field("graphql_url", &self.graphql_url)
665 .finish()
666 }
667}
668
669#[async_trait]
670impl api::RestClient for AsyncGitlab {
671 type Error = RestError;
672
673 fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
674 debug!(target: "gitlab", "REST api call {endpoint}");
675 Ok(self.rest_url.join(endpoint)?)
676 }
677
678 fn instance_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
679 debug!(target: "gitlab", "instance api call {endpoint}");
680 Ok(self.instance_url.join(endpoint)?)
681 }
682}
683
684#[async_trait]
685impl api::AsyncClient for AsyncGitlab {
686 async fn rest_async(
687 &self,
688 request: http::request::Builder,
689 body: Vec<u8>,
690 ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
691 self.rest_async_auth(request, body, &self.auth).await
692 }
693}
694
695impl AsyncGitlab {
696 async fn new_impl(
698 protocol: &str,
699 host: &str,
700 auth: Auth,
701 cert_validation: CertPolicy,
702 identity: ClientCert,
703 user_agent: &str,
704 ) -> GitlabResult<Self> {
705 let instance_url = Url::parse(&format!("{protocol}://{host}/"))?;
706 let rest_url = Url::parse(&format!("{protocol}://{host}/api/v4/"))?;
707 let graphql_url = Url::parse(&format!("{protocol}://{host}/api/graphql"))?;
708
709 let client_builder = AsyncClient::builder().user_agent(user_agent);
710
711 let client_builder = match cert_validation {
712 CertPolicy::Insecure => client_builder.danger_accept_invalid_certs(true),
713 CertPolicy::Default => {
714 match identity {
715 ClientCert::None => client_builder,
716 #[cfg(feature = "client_der")]
717 ClientCert::Der(der, password) => {
718 let id = TlsIdentity::from_pkcs12_der(&der, &password)?;
719 client_builder.identity(id)
720 },
721 #[cfg(feature = "client_pem")]
722 ClientCert::Pem(pem) => {
723 let id = TlsIdentity::from_pem(&pem)?;
724 client_builder.identity(id)
725 },
726 }
727 },
728 CertPolicy::CustomRoots(root_certs) => {
729 let mut client_builder = client_builder;
730 for cert in root_certs.certs {
731 client_builder = client_builder.add_root_certificate(cert)
732 }
733 client_builder
734 },
735 };
736
737 let client = client_builder.build()?;
738
739 let api = AsyncGitlab {
740 client,
741 instance_url,
742 rest_url,
743 graphql_url,
744 auth,
745 };
746
747 api.auth.check_connection_async(&api).await?;
749
750 Ok(api)
751 }
752
753 pub async fn graphql<Q>(&self, query: &QueryBody<Q::Variables>) -> GitlabResult<Q::ResponseData>
755 where
756 Q: GraphQLQuery,
757 Q::Variables: Debug,
758 for<'d> Q::ResponseData: Deserialize<'d>,
759 {
760 info!(
761 target: "gitlab",
762 "sending GraphQL query '{}' {:?}",
763 query.operation_name,
764 query.variables,
765 );
766 let req = self.client.post(self.graphql_url.clone()).json(query);
767 let rsp: Response<Q::ResponseData> = self.send(req).await?;
768
769 if let Some(errs) = rsp.errors {
770 return Err(GitlabError::graphql(errs));
771 }
772 rsp.data.ok_or_else(GitlabError::no_response)
773 }
774
775 async fn send<T>(&self, req: reqwest::RequestBuilder) -> GitlabResult<T>
777 where
778 T: DeserializeOwned,
779 {
780 let auth_headers = {
781 let mut headers = HeaderMap::default();
782 self.auth.set_header(&mut headers)?;
783 headers
784 };
785 let rsp = req.headers(auth_headers).send().await?;
786 let status = rsp.status();
787 if status.is_server_error() {
788 return Err(GitlabError::http(status));
789 }
790
791 serde_json::from_slice::<T>(&rsp.bytes().await?).map_err(GitlabError::data_type::<T>)
792 }
793
794 async fn rest_async_auth(
796 &self,
797 mut request: http::request::Builder,
798 body: Vec<u8>,
799 auth: &Auth,
800 ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
801 use futures_util::TryFutureExt;
802 let call = || {
803 async {
804 auth.set_header(request.headers_mut().unwrap())?;
805 let http_request = request.body(body)?;
806 let request = http_request.try_into()?;
807 let rsp = self.client.execute(request).await?;
808
809 log_request_id(rsp.headers());
810
811 let mut http_rsp = HttpResponse::builder()
812 .status(rsp.status())
813 .version(rsp.version());
814 let headers = http_rsp.headers_mut().unwrap();
815 for (key, value) in rsp.headers() {
816 headers.insert(key, value.clone());
817 }
818 Ok(http_rsp.body(rsp.bytes().await?)?)
819 }
820 };
821 call().map_err(api::ApiError::client).await
822 }
823}
824
825#[derive(Clone)]
826pub struct ImpersonationClient<'a, T> {
827 auth: Auth,
828 client: &'a T,
829}
830
831impl<'a, C> ImpersonationClient<'a, C> {
832 pub fn new<T>(client: &'a C, token: T) -> Self
834 where
835 T: Into<String>,
836 {
837 Self {
838 auth: Auth::Token(token.into()),
839 client,
840 }
841 }
842
843 pub fn oauth2_token(&mut self) -> &mut Self {
845 if let Auth::Token(auth) = self.auth.clone() {
846 self.auth = Auth::OAuth2(auth);
847 }
848 self
849 }
850}
851
852impl<C> api::RestClient for ImpersonationClient<'_, C>
853where
854 C: api::RestClient,
855{
856 type Error = C::Error;
857
858 fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
859 self.client.rest_endpoint(endpoint)
860 }
861
862 fn instance_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
863 self.client.instance_endpoint(endpoint)
864 }
865}
866
867impl api::Client for ImpersonationClient<'_, Gitlab> {
868 fn rest(
869 &self,
870 request: http::request::Builder,
871 body: Vec<u8>,
872 ) -> Result<HttpResponse<Bytes>, api::ApiError<Self::Error>> {
873 self.client.rest_auth(request, body, &self.auth)
874 }
875}
876
877#[allow(clippy::needless_lifetimes)] #[async_trait]
879impl<'a> api::AsyncClient for ImpersonationClient<'a, AsyncGitlab> {
880 async fn rest_async(
881 &self,
882 request: http::request::Builder,
883 body: Vec<u8>,
884 ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
885 self.client.rest_async_auth(request, body, &self.auth).await
886 }
887}