Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PathResourceResolver can't resolve a GoogleStorageResource due to no Google Storage UrlStreamHandler #210

Closed
jhordies opened this issue Jan 19, 2021 · 6 comments · Fixed by #341

Comments

@jhordies
Copy link

jhordies commented Jan 19, 2021

When trying to use a Google Storage Resource as the spring static resource location the PathResourceResolver calls the method isResourceUnderLocation that calls the GoogleStorageResource.toURL() that throws a MalformedURLException because no URL stream handler is registered for the gs protocol (as stated by the documentation)

Sample
Add the following dependecy to your project:

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-gcp-starter-storage</artifactId>
		</dependency>

Set the following property in you project:
spring.resources.static-locations=gs:https://[YOUR_GCS_BUCKET]/

If this is a public bucket (allUsers with Storage read permission) and no credentials are needed, use the following config class:

@Configuration
public class BucketResourceConfig {

    @Bean
    public Storage storage() throws IOException {
        return StorageOptions.newBuilder()
                .setHeaderProvider(
                        new UserAgentHeaderProvider(GcpStorageAutoConfiguration.class))
                .build().getService();
    }

    @Bean
    public CredentialsProvider googleCredentials() throws Exception {
        return new NoCredentialsProvider();
    }
}

Assuming the root of the bucket contains a index.html
Call your server with http:https://host:port/index.html
Put a breakpoint in PathResourceResolver.isResourceUnderLocation(...)
and see it fail to call resource.getURL() even though resource.getInputStream() would return the index.html

@ttomsu
Copy link

ttomsu commented Jan 19, 2021

Can you provide more details of your environment, specifically which version of Spring Cloud GCP, which Spring Cloud version, and which Spring Boot version you're using?

@ttomsu
Copy link

ttomsu commented Jan 19, 2021

Could you also point me to what documentation you're referring?

@jhordies
Copy link
Author

jhordies commented Jan 19, 2021

Hi,

Could you also point me to what documentation you're referring?

I meant the javadoc of GoogleStorageResource

	/**
	 * Since the gs: protocol will normally not have a URL stream handler registered,
	 * this method will always throw a {@link java.net.MalformedURLException}.
	 * @return the URL for the GCS resource, if a URL stream handler is registered for the gs protocol.
	 */
	@Override
	public URL getURL() throws IOException {
		return getURI().toURL();
	}

Can you provide more details of your environment, specifically which version of Spring Cloud GCP, which Spring Cloud version, and which Spring Boot version you're using?

       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-gcp-dependencies</artifactId>
       <version>1.2.6.RELEASE</version>

      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <version>2.3.4.RELEASE</version>

Thanks

@ttomsu
Copy link

ttomsu commented Jan 22, 2021

And to what documentation are you looking at and following?

@jhordies
Copy link
Author

jhordies commented Jan 22, 2021

And to what documentation are you looking at and following?

None in particular,
I just read
https://cloud.spring.io/spring-cloud-gcp/multi/multi__spring_resources.html
https://cloud.spring.io/spring-cloud-static/spring-cloud-gcp/1.2.0.RELEASE/reference/html/

And as the GoogleStorageResource should be of type Spring Resources (abstraction for a number of low-level resources, such as file system files, classpath files, servlet context-relative files, etc.)
I expected to use it as a a spring.resources.static-locations without any customization in Spring.

I made it work by overriding checkResource/isReourceUnderLocation from the PathResourceResolver as follows to treat the GoogleStorageResource specifically by returning resource.exists() and so avoid calling resource.getURL().getPath() that would trigger the exception :

    private class SinglePageAppResourceResolver extends PathResourceResolver
    {
        @Override
        protected boolean checkResource(Resource resource, Resource location) throws IOException {
            if (isResourceUnderLocation(resource, location)) {
                return true;
            }
            Resource[] allowedLocations = getAllowedLocations();
            if (allowedLocations != null) {
                for (Resource current : allowedLocations) {
                    if (isResourceUnderLocation(resource, current)) {
                        return true;
                    }
                }
            }
            return false;
        }

        private boolean isResourceUnderLocation(Resource resource, Resource location) throws IOException {
            if (resource.getClass() != location.getClass()) {
                return false;
            }

            String resourcePath;
            String locationPath;

            if (resource instanceof UrlResource) {
                resourcePath = resource.getURL().toExternalForm();
                locationPath = StringUtils.cleanPath(location.getURL().toString());
            }
            else if (resource instanceof ClassPathResource) {
                resourcePath = ((ClassPathResource) resource).getPath();
                locationPath = StringUtils.cleanPath(((ClassPathResource) location).getPath());
            }
            else if (resource instanceof ServletContextResource) {
                resourcePath = ((ServletContextResource) resource).getPath();
                locationPath = StringUtils.cleanPath(((ServletContextResource) location).getPath());
            }
            else if(resource instanceof GoogleStorageResource){
                return resource.exists();
            }
            else {
                resourcePath = resource.getURL().getPath();
                locationPath = StringUtils.cleanPath(location.getURL().getPath());
            }

            if (locationPath.equals(resourcePath)) {
                return true;
            }
            locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/");
            return (resourcePath.startsWith(locationPath) && !isInvalidEncodedPath(resourcePath));
        }

        private boolean isInvalidEncodedPath(String resourcePath) {
            if (resourcePath.contains("%")) {
                // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars...
                try {
                    String decodedPath = URLDecoder.decode(resourcePath, "UTF-8");
                    if (decodedPath.contains("../") || decodedPath.contains("..\\")) {
                        logger.warn("Resolved resource path contains encoded \"../\" or \"..\\\": " + resourcePath);
                        return true;
                    }
                }
                catch (IllegalArgumentException ex) {
                    // May not be possible to decode...
                }
                catch (UnsupportedEncodingException ex) {
                    // Should never happen...
                }
            }
            return false;
        }
    }

The use it like this:

@Configuration
public class SinglePageAppWebMvcConfigurer extends WebMvcConfigurerAdapter
{
    @Autowired
    private ResourceProperties resourceProperties;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry)
    {
        registry.addResourceHandler("/**")
                .addResourceLocations(resourceProperties.getStaticLocations())
                .resourceChain(true)
                .addResolver(new SinglePageAppResourceResolver());
    }
}

@jhordies
Copy link
Author

jhordies commented Mar 3, 2021

Thanks for the fix.
Regards

prash-mi pushed a commit that referenced this issue Jun 20, 2023
Add the spring-data r2dbc dialect to the repo.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants