Support proxying console in server side (#2535)

#### What type of PR is this?

/kind feature
/area core
/milestone 2.0

#### What this PR does / why we need it:

Support proxying console in server side. We just need to configure the properties as following:

```yaml
halo:
  console:
    proxy:
      endpoint: http://localhost:3000/
      enabled: true
```

Before starting Halo server, please start the console first at port 3000 by checkouting https://github.com/halo-dev/console/pull/638.

Now we can have a try to access console via <http://localhost:8090/console/>.

Please note that this feature should be only for development environment instead of production environment.

#### Todos

- [ ] Proxy WebSocket for hmr of Vite

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/2530

#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/2536/head
John Niang 2022-10-11 23:30:14 +08:00 committed by GitHub
parent af8860ffb6
commit dbaa087936
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 98 additions and 0 deletions

View File

@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.time.Instant;
import java.util.List;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -28,6 +29,7 @@ import org.springframework.web.reactive.resource.PathResourceResolver;
import org.springframework.web.reactive.result.view.ViewResolutionResultHandler;
import org.springframework.web.reactive.result.view.ViewResolver;
import reactor.core.publisher.Mono;
import run.halo.app.console.ConsoleProxyFilter;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.core.extension.endpoint.CustomEndpointsBuilder;
import run.halo.app.infra.properties.HaloProperties;
@ -123,4 +125,10 @@ public class WebFluxConfig implements WebFluxConfigurer {
.addResolver(new PathResourceResolver());
}
@ConditionalOnProperty(name = "halo.console.proxy.enabled", havingValue = "true")
@Bean
ConsoleProxyFilter consoleProxyFilter() {
return new ConsoleProxyFilter(haloProp);
}
}

View File

@ -0,0 +1,72 @@
package run.halo.app.console;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import run.halo.app.infra.properties.ConsoleProperties.ProxyProperties;
import run.halo.app.infra.properties.HaloProperties;
@Slf4j
public class ConsoleProxyFilter implements WebFilter {
private final ProxyProperties proxyProperties;
private final ServerWebExchangeMatcher consoleMatcher;
private final WebClient webClient;
public ConsoleProxyFilter(HaloProperties haloProperties) {
this.proxyProperties = haloProperties.getConsole().getProxy();
var consoleMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/console/**");
consoleMatcher = new AndServerWebExchangeMatcher(consoleMatcher,
new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML));
this.consoleMatcher = consoleMatcher;
this.webClient = WebClient.create(proxyProperties.getEndpoint().toString());
log.info("Initialized ConsoleProxyFilter to proxy console");
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return consoleMatcher.matches(exchange)
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
.map(matchResult -> {
var request = exchange.getRequest();
return UriComponentsBuilder.fromUriString(
request.getPath().pathWithinApplication().value())
.queryParams(request.getQueryParams())
.build()
.toUriString();
})
.doOnNext(uri -> {
if (log.isDebugEnabled()) {
log.debug("Proxy {} to {}", uri, proxyProperties.getEndpoint());
}
})
.flatMap(uri -> webClient.get()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(exchange.getRequest().getHeaders()))
.exchangeToMono(clientResponse -> {
var response = exchange.getResponse();
// set headers
response.getHeaders().putAll(clientResponse.headers().asHttpHeaders());
// set cookies
response.getCookies().putAll(clientResponse.cookies());
// set status code
response.setStatusCode(clientResponse.statusCode());
var body = clientResponse.body(BodyExtractors.toDataBuffers());
return exchange.getResponse().writeAndFlushWith(Mono.just(body));
}));
}
}

View File

@ -1,5 +1,7 @@
package run.halo.app.infra.properties;
import jakarta.validation.Valid;
import java.net.URI;
import lombok.Data;
@Data
@ -7,4 +9,20 @@ public class ConsoleProperties {
private String location = "classpath:/console/";
@Valid
private ProxyProperties proxy = new ProxyProperties();
@Data
public static class ProxyProperties {
/**
* Console endpoint in development environment to be proxied. e.g.: http://localhost:8090/
*/
private URI endpoint;
/**
* Indicates if the proxy behaviour is enabled. Default is false
*/
private boolean enabled = false;
}
}