001/**
002 * Copyright (C) 2006-2024 Talend Inc. - www.talend.com
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.talend.sdk.component.runtime.manager.service.http.configurer;
017
018import static java.util.Collections.emptyMap;
019import static java.util.Locale.ROOT;
020import static java.util.Optional.ofNullable;
021import static java.util.stream.Collectors.joining;
022import static java.util.stream.Collectors.toMap;
023
024import java.io.UnsupportedEncodingException;
025import java.net.MalformedURLException;
026import java.net.URL;
027import java.net.URLEncoder;
028import java.nio.charset.StandardCharsets;
029import java.security.InvalidKeyException;
030import java.security.MessageDigest;
031import java.security.NoSuchAlgorithmException;
032import java.security.Signature;
033import java.security.SignatureException;
034import java.util.Base64;
035import java.util.Map;
036import java.util.Objects;
037import java.util.TreeMap;
038import java.util.UUID;
039import java.util.concurrent.TimeUnit;
040import java.util.stream.Stream;
041
042import javax.crypto.Mac;
043import javax.crypto.spec.SecretKeySpec;
044
045import org.talend.sdk.component.api.service.http.Configurer;
046import org.talend.sdk.component.api.service.http.configurer.oauth1.OAuth1;
047
048public class OAuth1ProviderImpl implements OAuth1.OAuth1Provider {
049
050    @Override
051    public Map<String, String> buildParameters(final String method, final String url, final byte[] payload,
052            final OAuth1.Configuration oauth1Config) {
053        final String algorithm = ofNullable(oauth1Config.getAlgorithm()).orElse("HMAC-SHA1");
054        final Map<String, String> values = new TreeMap<>();
055        values.put("oauth_consumer_key", oauth1Config.getConsumerKey());
056        values.put("oauth_nonce", ofNullable(oauth1Config.getNonce()).orElseGet(this::newNonce));
057        values.put("oauth_signature_method", algorithm);
058        values
059                .put("oauth_timestamp", ofNullable(oauth1Config.getTimestamp())
060                        .map(String::valueOf)
061                        .orElseGet(() -> Long.toString(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()))));
062        values.put("oauth_version", "1.0");
063        ofNullable(oauth1Config.getToken()).ifPresent(token -> values.put("oauth_token", token));
064        ofNullable(oauth1Config.getPayloadHashAlgorithm())
065                .ifPresent(algo -> values
066                        .put("oauth_body_hash", hash(algo, ofNullable(payload).orElseGet(() -> new byte[0]))));
067        ofNullable(oauth1Config.getOauthParameters()).ifPresent(values::putAll);
068        values.entrySet().forEach(e -> e.setValue(encode(e.getValue())));
069        values.putAll(extractQuery(url));
070
071        final String signature = sign(algorithm, signingString(values, method, url), oauth1Config);
072        values.put("oauth_signature", signature);
073        return values;
074    }
075
076    private Map<String, String> extractQuery(final String url) {
077        if (url.contains("?")) {
078            final String query = url.substring(url.indexOf('?') + 1);
079            if (!query.isEmpty()) {
080                return Stream.of(query.split("&")).map(kv -> {
081                    final int sep = kv.indexOf("=");
082                    if (sep > 0) {
083                        return new String[] { kv.substring(0, sep), kv.substring(sep + 1) };
084                    }
085                    return new String[] { kv, "" };
086                }).collect(toMap(pair -> pair[0], pair -> pair[1]));
087            }
088        }
089        return emptyMap();
090    }
091
092    @Override
093    public Configurer newConfigurer() {
094        return (connection, configuration) -> {
095
096            final OAuth1.Configuration oauth1Config = Stream
097                    .of(configuration.configuration())
098                    .filter(OAuth1.Configuration.class::isInstance)
099                    .findFirst()
100                    .map(OAuth1.Configuration.class::cast)
101                    .orElseThrow(() -> new IllegalArgumentException("No OAuth1.Configuration @ConfigurerOption set"));
102
103            final Map<String, String> values =
104                    buildParameters(connection.getMethod(), connection.getUrl(), connection.getPayload(), oauth1Config);
105
106            final String authorization = ofNullable(oauth1Config.getHeaderPrefix()).orElse("OAuth ") + values
107                    .entrySet()
108                    .stream()
109                    .filter(e -> e.getKey().startsWith("oauth_"))
110                    .map(e -> e.getKey() + "=\"" + e.getValue() + "\"")
111                    .collect(joining(", "));
112            connection.withHeader(ofNullable(oauth1Config.getHeader()).orElse("Authorization"), authorization);
113        };
114    }
115
116    private String hash(final String algo, final byte[] payload) {
117        if ("plain".equalsIgnoreCase(algo)) {
118            return Base64.getEncoder().encodeToString(payload);
119        }
120        try {
121            final MessageDigest digest = MessageDigest.getInstance(algo);
122            return Base64.getEncoder().encodeToString(digest.digest(payload));
123        } catch (final NoSuchAlgorithmException e) {
124            throw new IllegalArgumentException("Invalid hashing computation using algorithm: " + algo, e);
125        }
126    }
127
128    private String sign(final String algorithm, final String signingString, final OAuth1.Configuration configuration) {
129        if (algorithm.toLowerCase(ROOT).contains("hmac")) {
130            final byte[] signingKey = ofNullable(configuration.getSigningHmacKey())
131                    .orElseGet(() -> Stream
132                            .of(configuration.getConsumerSecret(), configuration.getTokenSecret())
133                            .filter(Objects::nonNull)
134                            .map(this::encode)
135                            .collect(joining("&"))
136                            .getBytes(StandardCharsets.UTF_8));
137            try {
138                final SecretKeySpec key = new SecretKeySpec(signingKey, algorithm);
139                final Mac mac = Mac.getInstance(key.getAlgorithm().replace("-", ""));
140                mac.init(key);
141                return encode(Base64
142                        .getEncoder()
143                        .encodeToString(mac.doFinal(signingString.getBytes(StandardCharsets.UTF_8))));
144            } catch (final InvalidKeyException | NoSuchAlgorithmException e) {
145                throw new IllegalStateException(e);
146            }
147        } else {
148            try {
149                final Signature signature = Signature.getInstance(algorithm.replace("-", ""));
150                signature.initSign(configuration.getSigningSignatureKey());
151                signature.update(signingString.getBytes(StandardCharsets.UTF_8));
152                return encode(Base64.getEncoder().encodeToString(signature.sign()));
153            } catch (final SignatureException | InvalidKeyException | NoSuchAlgorithmException e) {
154                throw new IllegalArgumentException(e);
155            }
156        }
157    }
158
159    private String signingString(final Map<String, String> values, final String method, final String url) {
160        return method.toUpperCase(ROOT) + "&" + encode(prepareUrl(url)) + "&"
161                + encode(values
162                        .entrySet()
163                        .stream()
164                        .map(e -> String.format("%s=%s", e.getKey(), e.getValue()))
165                        .collect(joining("&")));
166    }
167
168    private String prepareUrl(final String url) {
169        try {
170            final URL parsed = new URL(url);
171            return parsed.getProtocol() + "://" + parsed.getHost()
172                    + (shouldSkipPort(parsed) ? "" : ":" + parsed.getPort()) + stripQuery(parsed.getFile());
173        } catch (final MalformedURLException e) { // very unlikely
174            return stripQuery(url);
175        }
176    }
177
178    private boolean shouldSkipPort(final URL parsed) {
179        return parsed.getPort() == -1 || (parsed.getPort() == 80 && "http".equals(parsed.getProtocol()))
180                || (parsed.getPort() == 443 && "https".equals(parsed.getProtocol()));
181    }
182
183    private String stripQuery(final String str) {
184        if (str == null) {
185            return "";
186        }
187        if (str.contains("?")) {
188            return str.substring(0, str.indexOf('?'));
189        }
190        return str;
191    }
192
193    private String encode(final String value) {
194        try {
195            return URLEncoder.encode(value, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
196        } catch (final UnsupportedEncodingException uee) {
197            throw new IllegalStateException(uee.getMessage(), uee);
198        }
199    }
200
201    private String newNonce() {
202        return UUID.randomUUID().toString().replace("-", "");
203    }
204}