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}