/*
 * Copyright 2017 The Netty Project
 *
 * The Netty Project 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 io.netty.example.ocsp;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.HttpsURLConnection;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.BERTags;
import org.bouncycastle.asn1.DERTaggedObject;
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.x509.extension.X509ExtensionUtil;

import io.netty.util.CharsetUtil;

public final class OcspUtils {
    /**
     * The OID for OCSP responder URLs.
     *
     * http://www.alvestrand.no/objectid/1.3.6.1.5.5.7.48.1.html
     */
    private static final ASN1ObjectIdentifier OCSP_RESPONDER_OID
        = new ASN1ObjectIdentifier("1.3.6.1.5.5.7.48.1").intern();

    private static final String OCSP_REQUEST_TYPE = "application/ocsp-request";

    private static final String OCSP_RESPONSE_TYPE = "application/ocsp-response";

    private OcspUtils() {
    }

    /**
     * Returns the OCSP responder {@link URI} or {@code null} if it doesn't have one.
     */
    public static URI ocspUri(X509Certificate certificate) throws IOException {
        byte[] value = certificate.getExtensionValue(Extension.authorityInfoAccess.getId());
        if (value == null) {
            return null;
        }

        ASN1Primitive authorityInfoAccess = X509ExtensionUtil.fromExtensionValue(value);
        if (!(authorityInfoAccess instanceof DLSequence)) {
            return null;
        }

        DLSequence aiaSequence = (DLSequence) authorityInfoAccess;
        DERTaggedObject taggedObject = findObject(aiaSequence, OCSP_RESPONDER_OID, DERTaggedObject.class);
        if (taggedObject == null) {
            return null;
        }

        if (taggedObject.getTagNo() != BERTags.OBJECT_IDENTIFIER) {
            return null;
        }

        byte[] encoded = taggedObject.getEncoded();
        int length = (int) encoded[1] & 0xFF;
        String uri = new String(encoded, 2, length, CharsetUtil.UTF_8);
        return URI.create(uri);
    }

    private static <T> T findObject(DLSequence sequence, ASN1ObjectIdentifier oid, Class<T> type) {
        for (ASN1Encodable element : sequence) {
            if (!(element instanceof DLSequence)) {
                continue;
            }

            DLSequence subSequence = (DLSequence) element;
            if (subSequence.size() != 2) {
                continue;
            }

            ASN1Encodable key = subSequence.getObjectAt(0);
            ASN1Encodable value = subSequence.getObjectAt(1);

            if (key.equals(oid) && type.isInstance(value)) {
                return type.cast(value);
            }
        }

        return null;
    }

    /**
     * TODO: This is a very crude and non-scalable HTTP client to fetch the OCSP response from the
     * CA's OCSP responder server. It's meant to demonstrate the basic building blocks on how to
     * interact with the responder server and you should consider using Netty's HTTP client instead.
     */
    public static OCSPResp request(URI uri, OCSPReq request, long timeout, TimeUnit unit) throws IOException {
        byte[] encoded = request.getEncoded();

        URL url = uri.toURL();
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        try {
            connection.setConnectTimeout((int) unit.toMillis(timeout));
            connection.setReadTimeout((int) unit.toMillis(timeout));
            connection.setDoOutput(true);
            connection.setDoInput(true);
            connection.setRequestMethod("POST");
            connection.setRequestProperty("host", uri.getHost());
            connection.setRequestProperty("content-type", OCSP_REQUEST_TYPE);
            connection.setRequestProperty("accept", OCSP_RESPONSE_TYPE);
            connection.setRequestProperty("content-length", String.valueOf(encoded.length));

            OutputStream out = connection.getOutputStream();
            try {
                out.write(encoded);
                out.flush();

                InputStream in = connection.getInputStream();
                try {
                    int code = connection.getResponseCode();
                    if (code != HttpsURLConnection.HTTP_OK) {
                        throw new IOException("Unexpected status-code=" + code);
                    }

                    String contentType = connection.getContentType();
                    if (!contentType.equalsIgnoreCase(OCSP_RESPONSE_TYPE)) {
                        throw new IOException("Unexpected content-type=" + contentType);
                    }

                    int contentLength = connection.getContentLength();
                    if (contentLength == -1) {
                        // Probably a terrible idea!
                        contentLength = Integer.MAX_VALUE;
                    }

                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    try {
                        byte[] buffer = new byte[8192];
                        int length = -1;

                        while ((length = in.read(buffer)) != -1) {
                            baos.write(buffer, 0, length);

                            if (baos.size() >= contentLength) {
                                break;
                            }
                        }
                    } finally {
                        baos.close();
                    }
                    return new OCSPResp(baos.toByteArray());
                } finally {
                    in.close();
                }
            } finally {
                out.close();
            }
        } finally {
            connection.disconnect();
        }
    }
}
