Serve static content with webflux and multiple wildcards - spring-boot

I want to serve react app with routes from Spring Boot with WebFlux and functional routing.
I'm trying to implement following rules:
There are few /api endpoints that should be routed with functions.
Everything starting with the /static should serve static content.
Everything else should serve index.html
I've added following function:
#Bean
RouterFunction<ServerResponse> routerFunction(Handler1 handler1,
Handler2 handler2) {
HandlerFunction<ServerResponse> indexPage = (req) -> ServerResponse.ok().bodyValue(new ClassPathResource("public/index.html"));
return RouterFunctions.route()
.GET("/api/route1", handler1)
.POST("/api/route2", handler2)
.resources("/static/**", new ClassPathResource("/public/**"))
.GET("/**", indexPage)
.build();
}
API routes work fine, but when I try to get any static content I get index page.
Changing /** to / allows me get static content as well as index page by / route.
Based on the documentation route with static has should be checked first, but somehow it becomes overridden with the last wildcard route.
Router functions are evaluated in order: if the first route does not match, the second is evaluated, and so on. Therefore, it makes sense to declare more specific routes before general ones.
What am I missing?

Figured it out. ClassPathResource accepts path to location. No need for wildcards.
With GET("/*", indexPage) static resources were served not using resources("/static/**", new ClassPathResource("/public/**")), but with the default webflux configuration spring.webflux.static-path-pattern.
Correct router function declaration:
#Bean
RouterFunction<ServerResponse> routerFunction(Handler1 handler1,
Handler2 handler2) {
HandlerFunction<ServerResponse> indexPage = (req) -> ServerResponse.ok().bodyValue(new ClassPathResource("public/index.html"));
return RouterFunctions.route()
.GET("/api/route1", handler1)
.POST("/api/route2", handler2)
.resources("/static/**", new ClassPathResource("/public/"))
.GET("/**", indexPage)
.build();
}

Related

What is the best practice for serve HTML files in Quarkus

I don't want the extension of my HTML files to show up in the address bar like index.html, login.html. Instead, I want these files to be accessed with patterns like /HOMEPAGE /LOGIN
I don't hold these files under the resources/META-INF/resources directory because I also don't want these files to be accessed directly from the address bar by typing the file name.
I could not find a built-in solution in Quarkus to meet these needs. So I followed my own solution.
#GET
#Produces(MediaType.TEXT_HTML)
#Path("/LOGIN")
public String loginPage() throws IOException {
String fullPath = PATH + "login.html";
return Files.readString(Paths.get( fullPath ));
}
But I'm not sure if this is the right solution. Are there any best practices on Quarkus for the kind of needs I mentioned?
Using Quarkus Reactive Routes, you can create a route like:
#ApplicationScoped
public class StaticContentDeclarativeRoute {
#Route(path = "/homepage", methods = Route.HttpMethod.GET)
void indexContent(RoutingContext rc) {
StaticHandler.create(FileSystemAccess.RELATIVE, "content/index.html").handle(rc);
}
}
so that in this way at the URL /homepage you'll have the index.html page served.
Consider the path can also be absolute using FileSystemAccess.ROOT.

Renamed: CSRF with Angular and Spring ("Request method 'DELETE' not supported")

I am trying to call my controller's delete method:
Spring:
#RestController
#RequestMapping("/api")
#CrossOrigin
public class Controller
{
#DeleteMapping("thing/item/{name}")
public void deleteThing(#PathVariable String name)
{
System.out.println("CALL"); // Never called!!!
}
}
Angular 7:
deleteTemplate(name: string) {
const url = `host/api/thing/item/${name}`;
return this.http.delete(url);
}
I've found something about including options:
httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
deleteTemplate(name: string) {
const url = `${host}/api/thing/item/${name}`; // host is not relevant, same path with GET works.
return this.http.delete(url, this.httpOptions);
}
But I don't think that it's even needed since everything I am sending is in link itself (no json).
Every time its:
WARN 15280 --- [io-8443-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'DELETE' not supported]
How to fix this?
DELETE is sent with 405 error and preceeding OPTIONS (with 200 code). It reaches server and does this error.
EDIT
Above is a sample of code that is simplified and still doesn't work. I understand your comments, but host can be anything and doesn't matter here. Its localhost now and as mentioned - it works in sense of reaching endpoint.
To check if I am wrong I did this:
#Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
this.requestMappingHandlerMapping.getHandlerMethods()
.forEach((e, k) -> System.out.println(e + " OF " + k));
After startup, it printed everything I expected, including:
{GET /api/thing/item/{name}} OF public myPackage.Thing myPackage.Controller.getThing(java.lang.String)
{DELETE /api/thing/item/{name}} OF public void myPackage.Controller.deleteThing(java.lang.String)
Any ideas?
Goddamn. Turns out problem was in CSRF, mainly in hidden Angular documentation and misleading Spring filter logs.
It took me about 10h to make it work, but only in production (where Angular is on same host/port with Spring). I will try to make it work in dev, but that requires basically rewriting whole module supplied by Angular that is bound to its defaults.
Now as to the problem:
Everything starts with Spring Security when we want to use CSRF (and we do). Apparently CsrfFilter literally explodes if it can't match CSRF tokens it expects and falls back to /error. This all happens without ANY specific log message, but simple Request method 'DELETE' not supported, which from my question we know IT IS present.
This is not only for DELETE, but all action requests (non-GET/HEAD).
This all points to "How to setup CSRF with Angular?". Well, here we go:
https://angular.io/guide/http#security-xsrf-protection
And it WORKS by DEFAULT. Based on Cookie-Header mechanism for CSRF.
But wait, there's more!
Spring needs to bo configured for Cookie-Header mechanism. Happily we got:
#Override
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().csrfTokenRepository(this.csrfRepo());
}
private CookieCsrfTokenRepository csrfRepo()
{
CookieCsrfTokenRepository repo = new CookieCsrfTokenRepository();
repo.setCookieHttpOnly(false);
return repo;
}
Which is built-in in spring and actually uses same cookie/header names as ones used by Angular. But what's up with repo.setCookieHttpOnly(false);? Well... another poor documented thingy. You just NEED to set it to false, otherwise Angular's side will not work.
Again - this only works for production builds, because Angular doesn't support external links. More at: Angular 6 does not add X-XSRF-TOKEN header to http request
So yeah... to make it work in dev localhost with 2 servers I'll need 1st to recreate https://angular.io/api/common/http/HttpClientXsrfModule mechanism to also intercept non-default requests.
EDIT
Based on JB Nizet comment I cooked up setup that works for both dev and production. Indeed proxy is awesome. With CSRF and SSL enabled (in my case), you also need to enable SSL on Angular CLI, otherwise Angular will not be able to use CSRF of SSL-enabled proxy backend and thus not work.
"serve": {
"options": {
"proxyConfig": "proxy.conf.json",
"sslKey": "localhost.key",
"sslCert": "localhost.crt",
"ssl": true
}
}
Note that Cert and Key will be auto-generated if not found :)
For Spring backend you don't need same cert. I use separate JKS there.
Cheers!
Add a slash / at the beginning of your path.
#DeleteMapping("/thing/item/{name}")

Serve static file for all sub-routes

I build an MVC Core application with single-page-clients.
I have configured some routes for /api/... which works well. Additionally I want to serve static files for some routes. e.g.:
For all sub-routes of /Home/ I want to receive /Home/index.html
For all sub-routes of /App/ I want to receive /App/index.html
I added app.UseStaticFiles() to Configure() so I can access /Home/index.html but it does not work for any other sub-route.
What is missing?
I changed my routing-system to attribute routing. Among others I added a HomeController:
[Route("")]
public class HomeController : Controller
{
[Route("")]
public IActionResult Index()
{
return View(); // The Home-page
}
[Route("Error")]
public IActionResult Error()
{
// show an error page
return Content(Activity.Current?.Id?.ToString() ?? HttpContext.TraceIdentifier.ToString());
}
[Route("{client}/{*tail}")]
[Produces("text/html")]
public IActionResult ClientApp(string client, string tail)
{
// show a client app
try
{
return new ContentResult()
{
Content = System.IO.File.ReadAllText($"./wwwroot/{client}/index.html"),
ContentType = "text/html"
};
}
catch
{
return RedirectToAction("/Error");
}
}
}
My client apps have had an index.html file inside its own folder (client routing-part) inside of wwwroot. When a request tries to access /something/... the route of ClientApp matches with something as the client-app folder name and the index.html is sent to the client. There is no redirect and the url stays the same.
It causes no problem with static files if you add UseStaticFiles before AddMvc in Startup:
app.UseStaticFiles();
app.UseMvc();
Tested in ASP.NET MVC Core 2.0.

Angular2: unable to navigate to url using location.go(url)

I am trying to navigate to a specific url using location.go service from typescript function. It changes the url in the browser but the component of the url is not reflected in the screen. It stays on the login (actual) screen - say for eg:
constructor(location: Location, public _userdetails: userdetails){
this.location = location;
}
login(){
if (this.username && this.password){
this._userdetails.username = this.username;
this.location.go('/home');
}
else{
console.log('Cannot be blank');
}
}
Is there a compile method or a refresh method I am missing?
Yes, for redirecting from one route to another you should not be using location.go, it generally used to get normalized url on browser.
Location docs has strictly mentioned below note.
Note: it's better to use Router service to trigger route changes. Use
Location only if you need to interact with or create normalized URLs
outside of routing.
Rather you should use Router API's navigate method, which will take ['routeName'] as you do it while creating href using [routerLink] directive.
If you wanted redirect via URL only then you could use navigateByUrl which takes URL as string like router.navigateByUrl(url)
import {
ROUTER_DIRECTIVES,
RouteConfig,
ROUTER_PROVIDERS,
Location, //note: in newer angular2 versions Location has been moved from router to common package
Router
} from 'angular2/router';
constructor(location: Location,
public _userdetails: userdetails,
public _router: Router){
this.location = location;
}
login(){
if (this.username && this.password){
this._userdetails.username = this.username;
//I assumed your `/home` route name is `Home`
this._router.navigate(['Home']); //this will navigate to Home state.
//below way is to navigate by URL
//this.router.navigateByUrl('/home')
}
else{
console.log('Cannot be blank');
}
}
Update
In further study, I found that, when you call navigate, basically it does accept routeName inside array, and if parameters are there then should get pass with it like ['routeName', {id: 1, name: 'Test'}].
Below is API of navigate function of Router.
Router.prototype.navigate = function(linkParams) {
var instruction = this.generate(linkParams); //get instruction by look up RouteRegistry
return this.navigateByInstruction(instruction, false);
};
When linkParams passed to generate function, it does return out all the Instruction's required to navigate on other compoent. Here Instruction means ComponentInstruction's which have all information about Routing, basically what ever we register inside #RouteConfig, will get added to RouteRegistry(like on what path which Component should assign to to router-outlet).
Now retrieved Instruction's gets passed to navigateByInstruction method, which is responsible to load Component and will make various thing available to component like urlParams & parent & child component instruction, if they are there.
Instruction (in console)
auxInstruction: Object //<- parent instructions
child: null //<- child instructions
component: ComponentInstruction //<-current component object
specificity: 10000
urlParams: Array[0] <-- parameter passed for the route
urlPath: "about" //<-- current url path
Note: Whole answer is based on older router version, when Angular 2 was in beta release.

Restricting auto Help Page contents when using Attribute Routing in Web API 2

I'm currently implementing a Web API using Web API 2's attribute routing (http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2). I am also using the Help Pages module in order to automatically generate documentation from XML comments (http://www.asp.net/web-api/overview/creating-web-apis/creating-api-help-pages).
For this API I am providing support for optional return format extensions, so that every API method has a pair of routes defined on it like so:
[HttpGet]
[Route("Path/Foo")]
[Route("Path/Foo.{ext}")]
public HttpResponseMessage DoFoo()
{
// Some API function.
}
This allows a user to hit any of these and get a result:
www.example.com/api/Controller/Path/Foo
www.example.com/api/Controller/Path/Foo.json
www.example.com/api/Controller/Path/Foo.xml
My issue is that when Help Pages uses MapHttpAttributeRoutes() to generate documentation, it is picking up both routes for each method. So right now I see help for:
api/Controller/Foo
api/Controller/Foo.{ext}
But I want to only see:
api/Controller/Foo.{ext}
I would prefer to hide the non-extension route on each method, so that every method only shows a single Help Page entry.
Has anyone else tried something similar? Is there a work around that I am missing?
My question would be is that, would consumers of your api figure out easily that the {ext} is optional?...personally, I would prefer the default behavior...but anyways following are some workarounds that I can think of:
A quick and dirty workaround. Split the DoFoo into 2 actions like DoFoo() and DoFooWithExt maybe. Notice that I am using an attribute called ApiExplorerSettings, which is for HelpPage purposes. Example below:
[HttpGet]
[Route("Path/Foo")]
[ApiExplorerSettings(IgnoreApi=true)]
public HttpResponseMessage DoFoo()
{
return DoFooHelper();
}
[HttpGet]
[Route("Path/Foo.{ext}")]
public HttpResponseMessage DoFooWithExt()
{
return DoFooHelper();
}
private HttpResponseMessage DoFooHelper()
{
//do something
}
Create a custom ApiExplorer (which HelpPage feature uses internally) and check for specific routes like the following and can decide whether to show the action or not for that particular route.
// update the config with this custom implementation
config.Services.Replace(typeof(IApiExplorer), new CustomApiExplorer(config));
public class CustomApiExplorer : ApiExplorer
{
public CustomApiExplorer(HttpConfiguration config) : base(config)
{
}
public override bool ShouldExploreAction(string actionVariableValue, HttpActionDescriptor actionDescriptor, IHttpRoute route)
{
if (route.RouteTemplate.EndsWith("Path/Foo", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return base.ShouldExploreAction(actionVariableValue, actionDescriptor, route);
}
}
Get list of all ApiDescription from the default ApiExplorer and then filter out the descriptions which you do not like. Example:
Configuration.Services.GetApiExplorer().ApiDescriptions.Where((apiDesc) => !apiDesc.RelativePath.EndsWith("Path/Foo", StringComparison.OrdinalIgnoreCase))

Resources