Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jclouds.chef.filters; import static com.google.common.base.Charsets.UTF_8; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.hash.Hashing.sha1; import static com.google.common.io.BaseEncoding.base64; import java.io.IOException; import java.security.PrivateKey; import java.util.NoSuchElementException; import javax.annotation.Resource; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; import javax.inject.Singleton; import org.jclouds.Constants; import org.jclouds.crypto.Crypto; import org.jclouds.date.TimeStamp; import org.jclouds.domain.Credentials; import org.jclouds.http.HttpException; import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpRequestFilter; import org.jclouds.http.HttpUtils; import org.jclouds.http.internal.SignatureWire; import org.jclouds.io.ByteStreams2; import org.jclouds.io.Payload; import org.jclouds.io.Payloads; import org.jclouds.io.payloads.MultipartForm; import org.jclouds.io.payloads.Part; import org.jclouds.io.payloads.RSAEncryptingPayload; import org.jclouds.logging.Logger; import org.jclouds.util.Strings2; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.base.Supplier; import com.google.common.base.Throwables; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; import com.google.common.io.ByteSource; /** * Ported from mixlib-authentication in order to sign Chef requests. * * @see <a href= "https://github.com/chef/mixlib-authentication" /> */ @Singleton public class SignedHeaderAuth implements HttpRequestFilter { public static final String SIGNING_DESCRIPTION = "version=1.0"; private final SignatureWire signatureWire; private final Supplier<Credentials> creds; private final Supplier<PrivateKey> supplyKey; private final Provider<String> timeStampProvider; private final String emptyStringHash; private final HttpUtils utils; private final Crypto crypto; @Resource @Named(Constants.LOGGER_SIGNATURE) Logger signatureLog = Logger.NULL; @Inject public SignedHeaderAuth(SignatureWire signatureWire, @org.jclouds.location.Provider Supplier<Credentials> creds, Supplier<PrivateKey> supplyKey, @TimeStamp Provider<String> timeStampProvider, HttpUtils utils, Crypto crypto) { this.signatureWire = checkNotNull(signatureWire, "signatureWire"); this.creds = checkNotNull(creds, "creds"); this.supplyKey = checkNotNull(supplyKey, "supplyKey"); this.timeStampProvider = checkNotNull(timeStampProvider, "timeStampProvider"); this.emptyStringHash = hashBody(Payloads.newStringPayload("")); this.utils = checkNotNull(utils, "utils"); this.crypto = checkNotNull(crypto, "crypto"); } public HttpRequest filter(HttpRequest input) throws HttpException { HttpRequest request = input.toBuilder().endpoint(input.getEndpoint().toString().replace("%3F", "?")) .build(); String contentHash = hashBody(request.getPayload()); Multimap<String, String> headers = ArrayListMultimap.create(); headers.put("X-Ops-Content-Hash", contentHash); String timestamp = timeStampProvider.get(); String toSign = createStringToSign(request.getMethod(), hashPath(request.getEndpoint().getPath()), contentHash, timestamp); headers.put("X-Ops-Userid", creds.get().identity); headers.put("X-Ops-Sign", SIGNING_DESCRIPTION); request = calculateAndReplaceAuthorizationHeaders(request, toSign); headers.put("X-Ops-Timestamp", timestamp); utils.logRequest(signatureLog, request, "<<"); return request.toBuilder().replaceHeaders(headers).build(); } @VisibleForTesting HttpRequest calculateAndReplaceAuthorizationHeaders(HttpRequest request, String toSign) throws HttpException { String signature = sign(toSign); if (signatureWire.enabled()) signatureWire.input(Strings2.toInputStream(signature)); String[] signatureLines = Iterables.toArray(Splitter.fixedLength(60).split(signature), String.class); Multimap<String, String> headers = ArrayListMultimap.create(); for (int i = 0; i < signatureLines.length; i++) { headers.put("X-Ops-Authorization-" + (i + 1), signatureLines[i]); } return request.toBuilder().replaceHeaders(headers).build(); } public String createStringToSign(String request, String hashedPath, String contentHash, String timestamp) { return new StringBuilder().append("Method:").append(request).append("\n").append("Hashed Path:") .append(hashedPath).append("\n").append("X-Ops-Content-Hash:").append(contentHash).append("\n") .append("X-Ops-Timestamp:").append(timestamp).append("\n").append("X-Ops-UserId:") .append(creds.get().identity).toString(); } @VisibleForTesting String hashPath(String path) { try { return base64().encode(ByteSource.wrap(canonicalPath(path).getBytes(UTF_8)).hash(sha1()).asBytes()); } catch (Exception e) { Throwables.propagateIfPossible(e); throw new HttpException("error creating sigature for path: " + path, e); } } /** * Build the canonicalized path, which collapses multiple slashes (/) and * removes a trailing slash unless the path is only "/" */ @VisibleForTesting String canonicalPath(String path) { path = path.replaceAll("\\/+", "/"); return path.endsWith("/") && path.length() > 1 ? path.substring(0, path.length() - 1) : path; } @VisibleForTesting String hashBody(Payload payload) { if (payload == null) return emptyStringHash; payload = useTheFilePartIfForm(payload); checkArgument(payload != null, "payload was null"); checkArgument(payload.isRepeatable(), "payload must be repeatable: " + payload); try { return base64().encode(ByteStreams2.hashAndClose(payload.getInput(), sha1()).asBytes()); } catch (Exception e) { Throwables.propagateIfPossible(e); throw new HttpException("error creating sigature for payload: " + payload, e); } } private Payload useTheFilePartIfForm(Payload payload) { if (payload instanceof MultipartForm) { Iterable<? extends Part> parts = MultipartForm.class.cast(payload).getRawContent(); try { payload = Iterables.find(parts, new Predicate<Part>() { @Override public boolean apply(Part input) { return "file".equals(input.getName()); } }); } catch (NoSuchElementException e) { } } return payload; } public String sign(String toSign) { try { byte[] encrypted = ByteStreams2.toByteArrayAndClose( new RSAEncryptingPayload(crypto, Payloads.newStringPayload(toSign), supplyKey.get()) .openStream()); return base64().encode(encrypted); } catch (IOException e) { throw new HttpException("error signing request", e); } } }