001/*
002 *  Copyright 2023 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.captchetat.captcha;
017
018import java.io.UnsupportedEncodingException;
019import java.nio.charset.StandardCharsets;
020import java.time.Duration;
021import java.util.ArrayList;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025
026import org.apache.avalon.framework.activity.Initializable;
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.commons.lang3.BooleanUtils;
032import org.apache.commons.lang3.tuple.Pair;
033import org.apache.http.HttpEntity;
034import org.apache.http.NameValuePair;
035import org.apache.http.client.config.RequestConfig;
036import org.apache.http.client.entity.UrlEncodedFormEntity;
037import org.apache.http.client.methods.CloseableHttpResponse;
038import org.apache.http.client.methods.HttpPost;
039import org.apache.http.entity.ContentType;
040import org.apache.http.entity.StringEntity;
041import org.apache.http.impl.client.CloseableHttpClient;
042import org.apache.http.impl.client.HttpClientBuilder;
043import org.apache.http.message.BasicNameValuePair;
044import org.apache.http.util.EntityUtils;
045
046import org.ametys.core.cache.AbstractCacheManager;
047import org.ametys.core.cache.Cache;
048import org.ametys.core.util.JSONUtils;
049import org.ametys.runtime.config.Config;
050import org.ametys.runtime.i18n.I18nizableText;
051import org.ametys.runtime.plugin.component.AbstractLogEnabled;
052
053
054/**
055 * Captcha implementation with images
056 */
057public class CaptchEtatHelper extends AbstractLogEnabled implements Component, Initializable, Serviceable
058{
059
060    /** Role */
061    public static final String ROLE = CaptchEtatHelper.class.getName();
062    
063    /** id of the token cache */
064    public static final String CAPCHETAT_TOKEN_CACHE = CaptchetatReader.class.getName();
065
066    
067    private static final String CAPTCHA_CLIENT_ID = "captchetat.client_id";
068    private static final String CAPTCHA_CLIENT_SECRET = "captchetat.client_secret";
069    private static final String CAPTCHA_ENDPOINT = "captchetat.endpoint";
070    
071    private static final Map<String, String> CAPTCHA_ENDPOINT_AUTH = Map.of(
072            "production", "https://oauth.piste.gouv.fr/api/oauth/token",
073            "sandbox", "https://sandbox-oauth.piste.gouv.fr/api/oauth/token"
074    );
075    private static final Map<String, String> CAPTCHA_ENDPOINT_PISTE = Map.of(
076            "production", "https://api.piste.gouv.fr/piste/captcha/",
077            "sandbox", "https://sandbox-api.piste.gouv.fr/piste/captcha/"
078    );
079    
080    private AbstractCacheManager _cacheManager;
081    private CloseableHttpClient _httpClient;
082    private JSONUtils _jsonUtils;
083    
084    @Override
085    public void service(ServiceManager smanager) throws ServiceException
086    {
087        _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE);
088        _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE);
089    }
090
091    /**
092     * Initialize
093     */
094    @Override
095    public void initialize()
096    {
097        // A little bit less than an hour, so the token is still valid after user submit the captcha
098        Duration duration = Duration.ofMinutes(50);
099        if (!_cacheManager.hasCache(CAPCHETAT_TOKEN_CACHE))
100        {            
101            _cacheManager.createMemoryCache(CAPCHETAT_TOKEN_CACHE, 
102                    new I18nizableText("plugin.captchetat", "PLUGINS_CAPTCHETAT_CACHE_TOKEN_LABEL"),
103                    new I18nizableText("plugin.captchetat", "PLUGINS_CAPTCHETAT_CACHE_TOKEN_DESCRIPTION"),
104                    true,
105                    duration);
106        }
107
108        RequestConfig config = RequestConfig.custom()
109                .setConnectTimeout(20000)
110                .setConnectionRequestTimeout(20000)
111                .setSocketTimeout(20000).build();
112        
113        _httpClient = HttpClientBuilder.create()
114            .setDefaultRequestConfig(config)
115            .disableRedirectHandling()
116            .useSystemProperties()
117            .build();
118        
119        
120    }
121
122    private Cache<Pair<String, String>, String> _getCache()
123    {
124        return _cacheManager.get(CAPCHETAT_TOKEN_CACHE);
125    }
126    
127    private String _getToken(Pair<String, String> key)
128    {
129        return _getCache().get(key, this::_computeToken);
130    }
131
132    private String _computeToken(Pair<String, String> key)
133    {
134        
135        String url = CAPTCHA_ENDPOINT_AUTH.get(Config.getInstance().getValue(CAPTCHA_ENDPOINT));
136        HttpPost httpPost = new HttpPost(url);
137        List<NameValuePair> pairs = new ArrayList<>();
138        pairs.add(new BasicNameValuePair("grant_type", "client_credentials"));
139        pairs.add(new BasicNameValuePair("client_id", key.getLeft()));
140        pairs.add(new BasicNameValuePair("client_secret", key.getRight()));
141        pairs.add(new BasicNameValuePair("scope", "piste.captchetat"));
142
143        try
144        {
145            httpPost.setEntity(new UrlEncodedFormEntity(pairs));
146        }
147        catch (UnsupportedEncodingException e)
148        {
149            throw new RuntimeException("Error while building request, can't compute token", e);
150        }
151        
152        try (CloseableHttpResponse response = _httpClient.execute(httpPost))
153        {
154            switch (response.getStatusLine().getStatusCode())
155            {
156                case 200: 
157                    break;
158                
159                case 403: 
160                    throw new IllegalStateException("The CMS back-office refused the connection");
161                    
162                case 500: 
163                default:
164                    throw new IllegalStateException("The captchEtat token generator server returned an error");
165            }
166            HttpEntity entity = response.getEntity();
167            String responseString = EntityUtils.toString(entity, "UTF-8");
168            Map<String, Object> result = _jsonUtils.convertJsonToMap(responseString);
169            return (String) result.get("access_token");
170        }
171        catch (Exception e)
172        {
173            throw new RuntimeException("Error during request, can't compute token", e);
174        }
175    }
176
177    /**
178     * get Token
179     * @return token
180     */
181    public String getToken()
182    {
183        return _getToken(Pair.of(Config.getInstance().getValue(CAPTCHA_CLIENT_ID), Config.getInstance().getValue(CAPTCHA_CLIENT_SECRET)));
184    }
185
186    /**
187     * get Token
188     * @return token
189     */
190    public String getEndpoint()
191    {
192        return CAPTCHA_ENDPOINT_PISTE.get(Config.getInstance().getValue(CAPTCHA_ENDPOINT));
193    }
194    
195    /**
196     * Check if the captcha is correct
197     * @param key the key
198     * @param value the value
199     * @return true if correct
200     */
201    public boolean checkAndInvalidateCaptcha(String key, String value)
202    {
203        String url = getEndpoint() + "valider-captcha";
204        HttpPost httpPost = new HttpPost(url);
205        List<NameValuePair> pairs = new ArrayList<>();
206        pairs.add(new BasicNameValuePair("id", key));
207        pairs.add(new BasicNameValuePair("code", value));
208        String token = getToken();
209        httpPost.addHeader("Authorization", "Bearer " + token);
210        httpPost.addHeader("accept", "application/json");
211        httpPost.addHeader("Content-Type", "application/json");
212        
213        Map<String, Object> parameters = new HashMap<>();
214        parameters.put("id", key);
215        parameters.put("code", value);
216        String json = _jsonUtils.convertObjectToJson(parameters);
217        HttpEntity jsonEntity = new StringEntity(json, ContentType.create("application/json", StandardCharsets.UTF_8));
218        
219        httpPost.setEntity(jsonEntity);
220        
221        try (CloseableHttpResponse response = _httpClient.execute(httpPost))
222        {
223            switch (response.getStatusLine().getStatusCode())
224            {
225                case 200: 
226                    break;
227                case 403: 
228                    throw new IllegalStateException("The CMS back-office refused the connection");
229                case 500: 
230                default:
231                    throw new IllegalStateException("The captchEtat verification server returned an error");
232            }
233            HttpEntity entity = response.getEntity();
234            String responseString = EntityUtils.toString(entity, "UTF-8");
235            return BooleanUtils.toBoolean(responseString);
236        }
237        catch (Exception e)
238        {
239            getLogger().error("Error during request, can't verify captcha", e);
240        }
241        
242        return false;
243    }
244    
245}