1212import java .io .IOException ;
1313import java .io .InputStream ;
1414import java .io .Serializable ;
15+ import java .net .InetAddress ;
16+ import java .net .URI ;
17+ import java .net .URISyntaxException ;
18+ import java .net .UnknownHostException ;
1519import java .nio .CharBuffer ;
1620import java .nio .charset .Charset ;
1721import java .nio .charset .StandardCharsets ;
4347 */
4448public final class RESTClientUtility {
4549
50+ public static final String AUTHORIZATION = "Authorization" ;
51+ public static final String BEARER = "Bearer " ;
52+
4653 /** Private empty constructor */
4754 private RESTClientUtility () {
4855 // do nothing
@@ -51,10 +58,10 @@ private RESTClientUtility() {
5158 public static <T extends Serializable > T sendPostRequestWithoutBodyHttp5 (Class <T > elementClass , String url ,
5259 String resource , AccessToken accessToken ) throws GenericException {
5360 try (final CloseableHttpClient httpclient = HttpClients .createDefault ()) {
54- HttpPost httpPost = new HttpPost (url + resource );
55- httpPost .addHeader ("Authorization" , "Bearer " + accessToken .getToken ());
56- httpPost .addHeader ("content-type" , "application/json" );
57- httpPost .addHeader ("Accept" , "application/json" );
61+ HttpPost httpPost = new HttpPost (buildSafeUri ( url , resource ) );
62+ httpPost .addHeader (AUTHORIZATION , BEARER + accessToken .getToken ());
63+ httpPost .addHeader ("content-type" , RodaConstants . MEDIA_TYPE_APPLICATION_JSON );
64+ httpPost .addHeader ("Accept" , RodaConstants . MEDIA_TYPE_APPLICATION_JSON );
5865
5966 Result result = httpclient .execute (httpPost , response -> new Result (response .getCode (),
6067 EntityUtils .toString (response .getEntity (), Charset .defaultCharset ())));
@@ -76,10 +83,10 @@ public static <T extends Serializable> T sendPostRequestWithoutBodyHttp5(Class<T
7683 public static <T extends Serializable > T sendPostRequestHttpClient5 (Object element , Class <T > elementClass , String url ,
7784 String resource , AccessToken accessToken ) throws GenericException {
7885 try (final CloseableHttpClient httpclient = HttpClients .createDefault ()) {
79- HttpPost httpPost = new HttpPost (url + resource );
80- httpPost .addHeader ("Authorization" , "Bearer " + accessToken .getToken ());
81- httpPost .addHeader ("content-type" , "application/json" );
82- httpPost .addHeader ("Accept" , "application/json" );
86+ HttpPost httpPost = new HttpPost (buildSafeUri ( url , resource ) );
87+ httpPost .addHeader (AUTHORIZATION , BEARER + accessToken .getToken ());
88+ httpPost .addHeader ("content-type" , RodaConstants . MEDIA_TYPE_APPLICATION_JSON );
89+ httpPost .addHeader ("Accept" , RodaConstants . MEDIA_TYPE_APPLICATION_JSON );
8390
8491 httpPost .setEntity (new StringEntity (JsonUtils .getJsonFromObject (element )));
8592
@@ -103,8 +110,8 @@ public static <T extends Serializable> T sendPostRequestHttpClient5(Object eleme
103110 public static int sendPostRequestWithCompressedFileHttp5 (String url , String resource , Path path ,
104111 AccessToken accessToken ) throws RODAException {
105112 try (final CloseableHttpClient httpclient = HttpClients .createDefault ()) {
106- HttpPost httpPost = new HttpPost (url + resource );
107- httpPost .addHeader ("Authorization" , "Bearer " + accessToken .getToken ());
113+ HttpPost httpPost = new HttpPost (buildSafeUri ( url , resource ) );
114+ httpPost .addHeader (AUTHORIZATION , BEARER + accessToken .getToken ());
108115
109116 InputStream inputStream = Files .newInputStream (path .toFile ().toPath ());
110117 MultipartEntityBuilder builder = MultipartEntityBuilder .create ();
@@ -124,14 +131,14 @@ public static int sendPostRequestWithCompressedFileHttp5(String url, String reso
124131 public static int sendPostRequestWithFileHttp5 (String url , String resource , String username , SecureString password ,
125132 Path file ) throws RODAException {
126133 try (final CloseableHttpClient httpclient = HttpClients .createDefault ()) {
127- HttpPost httpPost = new HttpPost (url + resource );
134+ HttpPost httpPost = new HttpPost (buildSafeUri ( url , resource ) );
128135 try (
129136 SecureString basicAuth = new SecureString (
130137 ArrayUtils .addAll ((username + ":" ).toCharArray (), password .getChars ()));
131138 SecureString basicAuthToken = new SecureString (
132139 Base64 .encode (StandardCharsets .UTF_8 .encode (CharBuffer .wrap (basicAuth )).array ()))) {
133140
134- httpPost .setHeader ("Authorization" , "Basic " + basicAuthToken );
141+ httpPost .setHeader (AUTHORIZATION , "Basic " + basicAuthToken );
135142 File fileToUpload = new File (FilenameUtils .normalize (file .toString ()));
136143 InputStream inputStream = new FileInputStream (fileToUpload );
137144 MultipartEntityBuilder builder = MultipartEntityBuilder .create ();
@@ -148,6 +155,48 @@ public static int sendPostRequestWithFileHttp5(String url, String resource, Stri
148155 }
149156 }
150157
158+ private static URI buildSafeUri (String baseUrl , String resource ) throws GenericException {
159+ if (baseUrl == null ) {
160+ throw new GenericException ("Base URL must not be null" );
161+ }
162+ try {
163+ URI base = new URI (baseUrl .trim ());
164+ String scheme = base .getScheme ();
165+ if (!"http" .equalsIgnoreCase (scheme ) && !"https" .equalsIgnoreCase (scheme )) {
166+ throw new GenericException ("Unsupported URL scheme for central instance: " + scheme );
167+ }
168+ if (base .getHost () == null ) {
169+ throw new GenericException ("Central instance URL must include a host" );
170+ }
171+ if (base .getUserInfo () != null ) {
172+ throw new GenericException ("User info is not allowed in central instance URL" );
173+ }
174+
175+ // Optional but safer: prevent requests to loopback or private addresses
176+ try {
177+ InetAddress addr = InetAddress .getByName (base .getHost ());
178+ if (addr .isAnyLocalAddress () || addr .isLoopbackAddress () || addr .isSiteLocalAddress ()) {
179+ throw new GenericException ("Central instance URL points to a disallowed address" );
180+ }
181+ } catch (UnknownHostException e ) {
182+ throw new GenericException ("Cannot resolve central instance host" , e );
183+ }
184+
185+ String basePath = base .getPath () == null ? "" : base .getPath ();
186+ String resPath = resource == null ? "" : resource ;
187+ // Ensure exactly one slash between base path and resource
188+ if (!basePath .endsWith ("/" ) && !resPath .startsWith ("/" )) {
189+ resPath = RodaConstants .API_SEP + resPath ;
190+ } else if (basePath .endsWith ("/" ) && resPath .startsWith ("/" )) {
191+ resPath = resPath .substring (1 );
192+ }
193+ return new URI (base .getScheme (), base .getUserInfo (), base .getHost (), base .getPort (), basePath + resPath , null ,
194+ null );
195+ } catch (URISyntaxException e ) {
196+ throw new GenericException ("Invalid central instance URL" , e );
197+ }
198+ }
199+
151200 private record Result (int statusCode , String content ) {
152201 }
153202}
0 commit comments