001/* 002 * Copyright 2022 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.odfsync.pegase.ws; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.nio.charset.StandardCharsets; 021import java.time.Instant; 022import java.util.ArrayList; 023import java.util.Base64; 024import java.util.Base64.Decoder; 025import java.util.List; 026import java.util.Map; 027 028import org.apache.avalon.framework.activity.Initializable; 029import org.apache.avalon.framework.component.Component; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.commons.io.IOUtils; 034import org.apache.http.HttpEntity; 035import org.apache.http.NameValuePair; 036import org.apache.http.StatusLine; 037import org.apache.http.client.HttpResponseException; 038import org.apache.http.client.config.RequestConfig; 039import org.apache.http.client.entity.UrlEncodedFormEntity; 040import org.apache.http.client.methods.CloseableHttpResponse; 041import org.apache.http.client.methods.HttpPost; 042import org.apache.http.client.methods.HttpUriRequest; 043import org.apache.http.impl.client.CloseableHttpClient; 044import org.apache.http.impl.client.HttpClientBuilder; 045import org.apache.http.message.BasicNameValuePair; 046 047import org.ametys.core.util.JSONUtils; 048import org.ametys.runtime.config.Config; 049import org.ametys.runtime.plugin.component.AbstractLogEnabled; 050 051/** 052 * Manager to request Pégase token when needed. 053 */ 054public class PegaseTokenManager extends AbstractLogEnabled implements Component, Serviceable, Initializable 055{ 056 /** Role */ 057 public static final String ROLE = PegaseTokenManager.class.getName(); 058 059 /** The JSON utils */ 060 protected JSONUtils _jsonUtils; 061 062 /* Token */ 063 private String _token; 064 private Instant _expirationDate; 065 066 /* Configuration */ 067 private boolean _isActive; 068 private String _username; 069 private String _password; 070 private String _authUrl; 071 072 public void service(ServiceManager manager) throws ServiceException 073 { 074 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 075 } 076 077 public void initialize() throws Exception 078 { 079 _isActive = Config.getInstance().getValue("pegase.activate", true, false); 080 if (_isActive) 081 { 082 /* Authentication configuration */ 083 _authUrl = Config.getInstance().getValue("pegase.auth.url"); 084 _username = Config.getInstance().getValue("pegase.api.username"); 085 _password = Config.getInstance().getValue("pegase.api.password"); 086 } 087 } 088 /** 089 * Get the token to log to Pégase API 090 * @throws IOException if an error occurs 091 * @return a valid token 092 */ 093 public synchronized String getToken() throws IOException 094 { 095 if (!_isActive) 096 { 097 throw new UnsupportedOperationException("Pégase is not active in the configuration, you cannot request a token."); 098 } 099 100 if (_expirationDate == null || Instant.now().isAfter(_expirationDate)) 101 { 102 CloseableHttpClient httpClient = _getHttpClient(); 103 104 List<NameValuePair> urlParams = new ArrayList<>(); 105 urlParams.add(new BasicNameValuePair("username", _username)); 106 urlParams.add(new BasicNameValuePair("password", _password)); 107 urlParams.add(new BasicNameValuePair("token", "true")); 108 109 // Prepare a request object 110 HttpPost postRequest = new HttpPost(_authUrl); 111 112 // HTTP parameters 113 postRequest.setEntity(new UrlEncodedFormEntity(urlParams, "UTF-8")); 114 postRequest.setHeader("Content-Type", "application/x-www-form-urlencoded"); 115 116 // Execute the request 117 _token = _executeHttpRequest(httpClient, postRequest); 118 _expirationDate = _extractExpirationDate(); 119 } 120 121 return _token; 122 } 123 124 /** 125 * Extract the expiration date from the token. 126 * @return the expiration dat of the current token 127 */ 128 protected Instant _extractExpirationDate() 129 { 130 String[] splitToken = _token.split("\\."); 131 132 if (splitToken.length < 2) 133 { 134 getLogger().error("Invalid token format, cannot get the expiration date. The token will be reset at each API call."); 135 return null; 136 } 137 138 String playload = splitToken[1]; 139 Decoder decoder = Base64.getUrlDecoder(); 140 byte[] decodedToken = decoder.decode(playload); 141 142 String decodedTokenString = new String(decodedToken, StandardCharsets.UTF_8); 143 Map<String, Object> map = _jsonUtils.convertJsonToMap(decodedTokenString); 144 Long expirationDateInSeconds = Long.parseLong(map.get("exp").toString()); 145 146 return Instant.ofEpochSecond(expirationDateInSeconds); 147 } 148 149 private CloseableHttpClient _getHttpClient() 150 { 151 RequestConfig requestConfig = RequestConfig.custom().build(); 152 return HttpClientBuilder.create() 153 .setDefaultRequestConfig(requestConfig) 154 .useSystemProperties() 155 .build(); 156 } 157 158 private String _executeHttpRequest(CloseableHttpClient httpClient, HttpUriRequest httpRequest) throws IOException 159 { 160 httpRequest.setHeader("accept", "application/json"); 161 162 // Execute the request 163 try (CloseableHttpResponse httpResponse = httpClient.execute(httpRequest)) 164 { 165 StatusLine statusLine = httpResponse.getStatusLine(); 166 if (statusLine.getStatusCode() / 100 != 2) 167 { 168 throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase()); 169 } 170 171 HttpEntity entity = httpResponse.getEntity(); 172 if (entity == null) 173 { 174 throw new IOException("The response entity is empty."); 175 } 176 177 try (InputStream is = entity.getContent()) 178 { 179 return IOUtils.toString(is, StandardCharsets.UTF_8); 180 } 181 } 182 } 183}