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.HttpUtils;
048import org.ametys.core.util.JSONUtils;
049import org.ametys.runtime.config.Config;
050import org.ametys.runtime.plugin.component.AbstractLogEnabled;
051
052/**
053 * Manager to request Pégase token when needed.
054 */
055public class PegaseTokenManager extends AbstractLogEnabled implements Component, Serviceable, Initializable
056{
057    /** Role */
058    public static final String ROLE = PegaseTokenManager.class.getName();
059
060    /** The JSON utils */
061    protected JSONUtils _jsonUtils;
062    
063    /* Token */
064    private String _token;
065    private Instant _expirationDate;
066    
067    /* Configuration */
068    private boolean _isActive;
069    private String _username;
070    private String _password;
071    private String _authUrl;
072
073    public void service(ServiceManager manager) throws ServiceException
074    {
075        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
076    }
077    
078    public void initialize() throws Exception
079    {
080        _isActive = Config.getInstance().getValue("pegase.activate", true, false);
081        if (_isActive)
082        {
083            /* Authentication configuration */
084            _authUrl = HttpUtils.sanitize(Config.getInstance().getValue("pegase.auth.url"));
085            _username = Config.getInstance().getValue("pegase.api.username");
086            _password = Config.getInstance().getValue("pegase.api.password");
087        }
088    }
089    /**
090     * Get the token to log to Pégase API
091     * @throws IOException if an error occurs
092     * @return a valid token
093     */
094    public synchronized String getToken() throws IOException
095    {
096        if (!_isActive)
097        {
098            throw new UnsupportedOperationException("Pégase is not active in the configuration, you cannot request a token.");
099        }
100        
101        if (_expirationDate == null || Instant.now().isAfter(_expirationDate))
102        {
103            CloseableHttpClient httpClient = _getHttpClient();
104    
105            List<NameValuePair> urlParams = new ArrayList<>();
106            urlParams.add(new BasicNameValuePair("username", _username));
107            urlParams.add(new BasicNameValuePair("password", _password));
108            urlParams.add(new BasicNameValuePair("token", "true"));
109            
110            // Prepare a request object
111            HttpPost postRequest = new HttpPost(_authUrl);
112            
113            // HTTP parameters
114            postRequest.setEntity(new UrlEncodedFormEntity(urlParams, "UTF-8"));
115            postRequest.setHeader("Content-Type", "application/x-www-form-urlencoded");
116            
117            // Execute the request
118            _token = _executeHttpRequest(httpClient, postRequest);
119            _expirationDate = _extractExpirationDate();
120        }
121        
122        return _token;
123    }
124    
125    /**
126     * Extract the expiration date from the token.
127     * @return the expiration dat of the current token
128     */
129    protected Instant _extractExpirationDate()
130    {
131        String[] splitToken = _token.split("\\.");
132        
133        if (splitToken.length < 2)
134        {
135            getLogger().error("Invalid token format, cannot get the expiration date. The token will be reset at each API call.");
136            return null;
137        }
138        
139        String playload = splitToken[1];
140        Decoder decoder = Base64.getUrlDecoder();
141        byte[] decodedToken = decoder.decode(playload);
142        
143        String decodedTokenString = new String(decodedToken, StandardCharsets.UTF_8);
144        Map<String, Object> map = _jsonUtils.convertJsonToMap(decodedTokenString);
145        Long expirationDateInSeconds = Long.parseLong(map.get("exp").toString());
146        
147        return Instant.ofEpochSecond(expirationDateInSeconds);
148    }
149    
150    private CloseableHttpClient _getHttpClient()
151    {
152        RequestConfig requestConfig = RequestConfig.custom().build();
153        return HttpClientBuilder.create()
154                .setDefaultRequestConfig(requestConfig)
155                .useSystemProperties()
156                .build();
157    }
158    
159    private String _executeHttpRequest(CloseableHttpClient httpClient, HttpUriRequest httpRequest) throws IOException
160    {
161        httpRequest.setHeader("accept", "application/json");
162        
163        // Execute the request
164        try (CloseableHttpResponse httpResponse = httpClient.execute(httpRequest))
165        {
166            StatusLine statusLine = httpResponse.getStatusLine();
167            if (statusLine.getStatusCode() / 100 != 2)
168            {
169                throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
170            }
171            
172            HttpEntity entity = httpResponse.getEntity();
173            if (entity == null)
174            {
175                throw new IOException("The response entity is empty.");
176            }
177            
178            try (InputStream is = entity.getContent())
179            {
180                return IOUtils.toString(is, StandardCharsets.UTF_8);
181            }
182        }
183    }
184}