#pingora #web-server #http #host-name #virtual-hosts

virtual-hosts-module

A module handling configuration per virtual host with Pingora

1 unstable release

0.2.0 May 12, 2024

#403 in HTTP server

Download history 60/week @ 2024-05-06 36/week @ 2024-05-13

96 downloads per month

Apache-2.0

40KB
522 lines

Virtual Hosts Module for Pingora

This module simplifies dealing with virtual hosts. It wraps any handler implementing module_utils::RequestFilter and its configuration, allowing to supply a different configuration for that handler for each virtual host and subdirectories of that host. For example, if Static Files Module is the wrapped handler, the configuration file might look like this:

vhosts:
    localhost:8000:
        aliases:
            - 127.0.0.1:8000
            - "[::1]:8000"
        root: ./local-debug-root
    example.com:
        aliases:
            - www.example.com
        default: true
        root: ./production-root
        subdirs:
            /metrics
                root: ./metrics
            /test:
                strip_prefix: true
                root: ./local-debug-root
                redirect_prefix: /test

A virtual host configuration adds three configuration settings to the configuration of the wrapped handler:

  • aliases lists additional host names that should share the same configuration.
  • default can be set to true to indicate that this configuration should apply to all host names not listed explicitly.
  • subdirs maps subdirectories to their respective configuration. The configuration is that of the wrapped handler with the added strip_prefix setting. If true, this setting will remove the subdirectory path from the URI before the request is passed on to the handler.

If no default host entry is present and a request is made for an unknown host name, this handler will leave the request unhandled. Otherwise the handling is delegated to the wrapped handler.

When selecting a subdirectory configuration, longer matching paths are preferred. Matching always happens against full file names, meaning that URI /test/abc matches the subdirectory /test whereas the URI /test_abc doesn’t. If no matching path is found, the host configuration will be used.

Note: When the strip_prefix option is used, the subsequent handlers will receive a URI which doesn’t match the actual URI of the request. This might result in wrong links or redirects. When using Static Files Module you can set redirect_prefix setting like in the example above to compensate. Upstream responses might have to be corrected via Pingora’s upstream_response_filter.

Code example

Usually, the virtual hosts configuration will be read from a configuration file and used to instantiate the corresponding handler. This is how it would be done:

use pingora_core::server::configuration::{Opt, ServerConf};
use module_utils::{FromYaml, merge_conf};
use static_files_module::{StaticFilesConf, StaticFilesHandler};
use structopt::StructOpt;
use virtual_hosts_module::{VirtualHostsConf, VirtualHostsHandler};

// Combine Pingora server configuration with virtual hosts wrapping static files configuration.
#[merge_conf]
struct Conf {
    server: ServerConf,
    virtual_hosts: VirtualHostsConf<StaticFilesConf>,
}

// Read command line options and configuration file.
let opt = Opt::from_args();
let conf = opt
    .conf
    .as_ref()
    .and_then(|path| Conf::load_from_yaml(path).ok())
    .unwrap_or_else(Conf::default);

// Create handler from configuration
let handler: VirtualHostsHandler<StaticFilesHandler> = conf.virtual_hosts.try_into().unwrap();

You can then use that handler in your server implementation:

use async_trait::async_trait;
use pingora_core::upstreams::peer::HttpPeer;
use pingora_core::Error;
use pingora_proxy::{ProxyHttp, Session};
use module_utils::RequestFilter;
use static_files_module::StaticFilesHandler;
use virtual_hosts_module::VirtualHostsHandler;

pub struct MyServer {
    handler: VirtualHostsHandler<StaticFilesHandler>,
}

#[async_trait]
impl ProxyHttp for MyServer {
    type CTX = <VirtualHostsHandler<StaticFilesHandler> as RequestFilter>::CTX;
    fn new_ctx(&self) -> Self::CTX {
        VirtualHostsHandler::<StaticFilesHandler>::new_ctx()
    }

    async fn request_filter(
        &self,
        session: &mut Session,
        ctx: &mut Self::CTX,
    ) -> Result<bool, Box<Error>> {
        self.handler.handle(session, ctx).await
    }

    async fn upstream_peer(
        &self,
        _session: &mut Session,
        _ctx: &mut Self::CTX,
    ) -> Result<Box<HttpPeer>, Box<Error>> {
        // Virtual hosts handler didn't handle the request, meaning no matching virtual host in
        // configuration. Delegate to upstream peer.
        Ok(Box::new(HttpPeer::new(
            "example.com:443",
            true,
            "example.com".to_owned(),
        )))
    }
}

For complete and more comprehensive code see virtual-hosts example in the repository.

Dependencies

~39–55MB
~1M SLoC