0

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.

Puteri
  • 3,348
  • 4
  • 12
  • 27
Roshan Upreti
  • 1,782
  • 3
  • 21
  • 34
  • Is there any reason why you are building the URL manually with vanilla Java code instead of using the cloud Storage libraries? It could be part of the reason why it's not working. – Ralemos Jun 24 '20 at 14:57
  • @ralemos I tried using the cloud storage libraries too, as explained here: `https://stackoverflow.com/q/62521295/4131849`, but doesn't seem to work. I don't know why google likes to complicate stuff. Can you maybe help me with that? – Roshan Upreti Jun 25 '20 at 04:56

1 Answers1

1

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.

Ralemos
  • 5,571
  • 2
  • 9
  • 18
  • thanks a lot for pointing me to the right direction. I managed to get the upload working with a single method now. But, the one issue that still persists is the link invalidation. I still would like the link to be invalid after an object is uploaded, but I'm still able to use the same link many times after the first upload, within the valid period of course. – Roshan Upreti Jun 27 '20 at 11:42
  • Glad to know that part of the issue is solved. I did some research and the only thing I could find is this [community question](https://stackoverflow.com/questions/24042783) were the most upvoted answer says that this is currently not possible, although it is an old post (5+ years) but since there is nothing mentioning that on the documentation 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. I will add this information to the answer to make it more complete. – Ralemos Jun 29 '20 at 12:22