Roshan Upreti
Roshan Upreti

Reputation: 2052

How to make Google cloud storage Upload URL work just once with HTTP PUT and expire in a certain time

I'm using the following two JAVA methods to create a signed URL to upload to Google Cloud Storage, using HTTP PUT. The first method is supposed to generate the actual upload URL using POST, while the second one is supposed to generate the URL using serviceAccountCredentials, to be used(to POST) by the first one.

First Method

 public String getUploadLink(String bucketName, String uuid, String objectName, String mimeType)
        throws IOException, GenericAttachmentException {
    if (!bucketExists(bucketName)) {
        createBucket(bucketName);
    }
    URL myURL = new URL(getSignedUrlToPost(bucketName, uuid, objectName, mimeType));
    HttpURLConnection myURLConnection = (HttpURLConnection) myURL.openConnection();
    myURLConnection.setRequestMethod("POST");
    myURLConnection.setRequestProperty("Content-Type", mimeType);
    myURLConnection.setRequestProperty("x-goog-resumable", "start");
    // Send POST request
    myURLConnection.setDoOutput(true);
    DataOutputStream wr = new DataOutputStream(myURLConnection.getOutputStream());
    wr.flush();
    wr.close();
    int responseCode = myURLConnection.getResponseCode();
    if (responseCode != 201) {
        throw new GenericAttachmentException(500,
                "Error generating signed URL",
                "Something went wrong while attempting to generate the URL.");
    }
    return myURLConnection.getHeaderField("Location");
}

Second Method

private String getSignedUrlToPost(String bucketName, String uuid, String objectName,
                                  String mimeType) throws GenericAttachmentException {
    try {
        String verb = "POST";
        long now = System.currentTimeMillis();
        /* Expire in a minute. */
        long expiryTimeInSeconds = (now + 60 * 1000L) / 1000;
        String canonicalizedExtensionHeaders = "x-goog-resumable:start";
        byte[] sr = serviceAccountCredentials.sign(
                (verb + "\n\n" + mimeType + "\n" + expiryTimeInSeconds + "\n" + canonicalizedExtensionHeaders
                        +
                        "\n" + "/" + bucketName + "/" + uuid + "/" + objectName).getBytes());
        String urlSignature = new String(Base64.encodeBase64(sr));
        return "https://storage.googleapis.com/" + bucketName + "/" + uuid + "/" + objectName +
                "?GoogleAccessId=" + serviceAccountEmail +
                "&Expires=" + expiryTimeInSeconds +
                "&Signature=" + URLEncoder.encode(urlSignature, "UTF-8");
    } catch (UnsupportedEncodingException e) {
        throw new GenericAttachmentException(500,
                "Something went wrong while encoding the URL.",
                e.getMessage());
    }
}

This gives me an upload URL, as follows:

https://storage.googleapis.com/bucket-name/7c9a5bd6-ece2-497d-b485-a9c53e27f253/a.pdf?GoogleAccessId=storage-dev@project-name-xxxxxx.iam.gserviceaccount.com&Expires=1592883655&Signature=IlTGvwGNN8VYrPE9qzSW0AIAwqMvbNoZ34TQ4nr4Po5vwZx78or9iiqBhO0jqoeoX6BYP%2BHGkWPIKMUijB%2FZ0L6Z%2BtnaZZkIJ581YQ3JK8BEHWqWyf0V07RwAN0TGAyld7h1JntWmGDyXKtjmy6Skt1C0GocJZA2x9GMxo94OD9kpFbjBucixgQDE%2BEtCzDUXWkymATls690pyLftXhAI0CVWg%2FPlcAe2Q%2F9M%2F68s5eWVSXa0%2BXIVQQ%2FucgXO8RbEDeu%2BWjrL3TcYQFTFd8Q%2BvcwKkpjbmKGpmMnYuTc7HSKrRWLLGxixsLBSjKdQDK4Tu14%2F0ROJVJo4Gv%2FX4oknQ%3D%3D&upload_id=AAANsUlwcmdpeCuME5YbeSpnfw5eQw_Sb65xl7t59b6GcNkNE0PUfe44tUDXHfobXRo-EBGI6X-I5zPqXyPBm4paSyBGyzZCWw

Issues:

  1. I'm able to upload an object, using the above URL, but the link doesn't expire after one minute.
  2. The link doesn't invalidate itself after the initial upload is done, meaning, when I use the same link to upload, for example, xyz.jpg using HTTP PUT, it returns 200 OK, even though it doesn't actually upload xyz.jpg and replace the original file.

Shouldn't the upload link invalidate itself once it is used to upload something to the server? Am i missing something here? Could really use some help.

Upvotes: 0

Views: 713

Answers (1)

Rafael Lemos
Rafael Lemos

Reputation: 5829

Google Cloud's Libraries for creating a signed URL are actually a lot simpler than building it "manually", as you can see on the sample code example in this official documentation. Following that, your getSignedUrlToPost function could look like this:

private String getSignedUrlToPost(String bucketName, String objectName, String mimeType) 
        throws StorageException {
    try {
        Storage storage = StorageOptions.newBuilder().setProjectId("YOUR_PROJECT_ID")
                                        .build().getService();

        // Define Resource
        BlobInfo blobInfo = BlobInfo.newBuilder(BlobId.of(bucketName, objectName))
                                    .build();

        // Generate Signed URL
        Map<String, String> extensionHeaders = new HashMap<>();
        extensionHeaders.put("Content-Type", mimeType);

        //setting it to expire 10 minutes
        return storage.signUrl(
                    blobInfo,
                    10,
                    TimeUnit.MINUTES,
                    Storage.SignUrlOption.httpMethod(HttpMethod.POST),
                    Storage.SignUrlOption.withExtHeaders(extensionHeaders),
                    Storage.SignUrlOption.withV4Signature());
                    
    } catch (StorageException e) {
        throw new StorageException(500,
                "Something went wrong while encoding the URL: "
                e.getMessage());
    }
}

I find it always best to use the official libraries as they often simplify your code and it's less error prone since you don't have to figure out what needs to be done behind the courtains.


EDIT

For the expiration of the URL after a single use, I did some research and the only thing I could find is this community question were the most upvoted answer says that this is currently not possible.

Although it is an old post (5+ years), there is no mention of that being changed in the documentation, so I assume that this is still valid. I would suggest that, in order to workaround this issue, you make the URL have a short time expiration limit and adjust it to your app's needs.

NOTE: You can find more details on the options for the signedURLs for storage in the in the javadoc for that class.

Upvotes: 1

Related Questions