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

how to have websocket, http2/3, router on the same script? #983

Closed
ouvaa opened this issue Mar 11, 2024 · 5 comments
Closed

how to have websocket, http2/3, router on the same script? #983

ouvaa opened this issue Mar 11, 2024 · 5 comments

Comments

@ouvaa
Copy link

ouvaa commented Mar 11, 2024

is it possible for you to show a working example of :
websocket, http2/3 and router on the same script?
after 1 whole day doing rust, this is all i have, i have no idea how to add websocket to /ws and how to do recommended "routing".

i also dunno how to do static file like the m/a.css routing

pls help. thx:

//! A Http server returns Hello World String as Response from multiple services.
//!
//! About http/3 discovery:
//! as of writing this comment browsers that support http/3(latest firefox releases for example)
//! can redirect user visiting http/2 service of this example to http/3.
//! visit https://0.0.0.0:443/ after start the example and try to refresh the page until you see
//! "Hello World from Http/3!".

use std::thread;
use std::{convert::Infallible, fs, io, sync::Arc};

use openssl::ssl::{AlpnError, SslAcceptor, SslFiletype, SslMethod};
use quinn::ServerConfig;
use rustls::{Certificate, PrivateKey};
use xitca_http::{
    h1, h2, h3,
    http::{const_header_value::TEXT_HTML_UTF8,const_header_value::TEXT_UTF8, header::CONTENT_TYPE, Request, RequestExt, Response, Version},
    //util::middleware::{Logger, SocketConfig},
    HttpServiceBuilder, ResponseBody,
};

use xitca_service::{fn_service, ServiceExt};
use sailfish::TemplateOnce;

#[derive(TemplateOnce)]
#[template(path = "include.stpl")]
struct IncludeTemplate {
    title: String,
    name: String,
}


fn main() -> io::Result<()> {
    tracing_subscriber::fmt().with_max_level(tracing::Level::INFO).init();

    /*
    let service = Router::new()
        .insert("/hello", get(fn_service(handler)))
        .enclosed(HttpServiceBuilder::new());
    */

    // construct http2 openssl config.
    let acceptor = h2_config()?;

    // construct http3 quic server config
    let config = h3_config()?;

    // construct server
    xitca_server::Builder::new()
        // bind to a http/1 service.
        .bind(
            "http/1",
            "0.0.0.0:80",
            fn_service(handler_h1_redirect)
        //        .enclosed(Logger::new())
                .enclosed(HttpServiceBuilder::h1())
                //.enclosed(SocketConfig::new()),
        )?
        // bind to a http/2 service.
        // *. http/1 and http/2 both use tcp listener so it should be using a separate port.
        .bind(
            "http/2",
            "0.0.0.0:443",
            fn_service(handler_h2).enclosed(HttpServiceBuilder::h2().openssl(acceptor)),
        )?
        // bind to a http/3 service.
        // *. note the service name must be unique.
        //
        // Bind to same service with different bind_xxx API is allowed for reusing one service
        // on multiple socket addresses and protocols.
        .bind_h3(
            "http/3",
            "0.0.0.0:443",
            config,
            fn_service(handler_h3).enclosed(HttpServiceBuilder::h3()),
        )?
        .build()
        .wait()
}


async fn handler_h1_redirect(req: Request<RequestExt<h1::RequestBody>>) -> Result<Response<ResponseBody>, Infallible> {
    // Extract the host from the incoming request.
    // This assumes that the "Host" header is present in the request.
    let host = req.headers().get("Host").and_then(|v| v.to_str().ok()).unwrap_or("");

    // Construct the target HTTPS URL by appending the host to the "https://" scheme.
    let https_url = format!("https://{}{}", host, req.uri());

    Ok(Response::builder()
        .status(301) // or 307 for temporary redirection
        .header("Location", https_url) // Dynamically constructed target HTTPS URL
        .body("Redirecting to HTTPS".into())
        .unwrap())
}

/*
async fn handler(req: Request<RequestExt<RequestBody>>) -> Result<Response<ResponseBody>, Infallible> {
    let remote_addr = req.body().socket_addr();
    Ok(Response::new(ResponseBody::empty()))
}
*/

async fn handler_h2(
    req: Request<RequestExt<h2::RequestBody>>,
) -> Result<Response<ResponseBody>, Box<dyn std::error::Error>> {


    let thread_id = format!("{:?}", thread::current().id());
    let ctx = IncludeTemplate {
        title: "Website".to_owned(),
        //name: "Hanako".to_owned(),
        name: thread_id,
    };
//    println!("{}", ctx.render_once().unwrap());

    //match (req.method(), req.uri().path()) {
     //   (&Method::GET, "/") => Ok(Response::new(Full::new(Bytes::from(ctx.render_once().unwrap())))),
      //  (&Method::GET, "/m/a.css") => Ok(Response::new(Full::new(Bytes::from(ctx.render_once().unwrap())))),


    let remote_addr = format!("{} {}",ctx.render_once().unwrap(),req.body().socket_addr().to_string());


    let res = Response::builder()
        .status(200)
        // possible redirect browser to http/3 service.
        .header("Alt-Svc", "h3=\":443\"; ma=86400")
        .header(CONTENT_TYPE, TEXT_HTML_UTF8)
        .body(remote_addr.into())?;
    Ok(res)
}

async fn handler_h3(
    _: Request<RequestExt<h3::RequestBody>>,
) -> Result<Response<ResponseBody>, Box<dyn std::error::Error>> {
    Response::builder()
        .status(200)
        .version(Version::HTTP_3)
        .header(CONTENT_TYPE, TEXT_UTF8)
        .body("Hello World!".into())
        .map_err(Into::into)
}

fn h2_config() -> io::Result<SslAcceptor> {
    // set up openssl and alpn protocol.
    let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
    builder.set_private_key_file("../cert/key.pem", SslFiletype::PEM)?;
    builder.set_certificate_chain_file("../cert/cert.pem")?;

    builder.set_alpn_select_callback(|_, protocols| {
        const H2: &[u8] = b"\x02h2";
        const H11: &[u8] = b"\x08http/1.1";

        if protocols.windows(3).any(|window| window == H2) {
            Ok(b"h2")
        } else if protocols.windows(9).any(|window| window == H11) {
            Ok(b"http/1.1")
        } else {
            Err(AlpnError::NOACK)
        }
    });

    builder.set_alpn_protos(b"\x08http/1.1\x02h2")?;

    Ok(builder.build())
}

fn h3_config() -> io::Result<ServerConfig> {
    let cert = fs::read("../cert/cert.pem")?;
    let key = fs::read("../cert/key.pem")?;

    let key = rustls_pemfile::pkcs8_private_keys(&mut &*key).unwrap().remove(0);
    let key = PrivateKey(key);

    let cert = rustls_pemfile::certs(&mut &*cert)
        .unwrap()
        .into_iter()
        .map(Certificate)
        .collect();

    let mut acceptor = rustls::ServerConfig::builder()
        .with_safe_defaults()
        .with_no_client_auth()
        .with_single_cert(cert, key)
        .unwrap();

    acceptor.alpn_protocols = vec![b"h3".to_vec()];

    Ok(ServerConfig::with_crypto(Arc::new(acceptor)))
}
@ouvaa
Copy link
Author

ouvaa commented Mar 11, 2024

@fakeshadow stuck here, how to do wt with /wt together on :443?

    // construct server
    xitca_server::Builder::new()
        // bind to a http/1 service.
        .bind(
            "http/1",
            "0.0.0.0:80",
            fn_service(handler_h1_redirect)
        //        .enclosed(Logger::new())
                .enclosed(HttpServiceBuilder::h1())
                //.enclosed(SocketConfig::new()),
        )?
        // bind to a http/2 service.
        // *. http/1 and http/2 both use tcp listener so it should be using a separate port.
        .bind(
            "http/2",
            "0.0.0.0:443",
            fn_service(handler_h2).enclosed(HttpServiceBuilder::h2().openssl(acceptor)),
        )?
        // bind to a http/3 service.
        // *. note the service name must be unique.
        //
        // Bind to same service with different bind_xxx API is allowed for reusing one service
        // on multiple socket addresses and protocols.

        .bind_h3("wt", "0.0.0.0:443", h3_config(), fn_service(handler)?

        .bind_h3(
            "http/3",
            "0.0.0.0:443",
            config,
            fn_service(handler_h3).enclosed(HttpServiceBuilder::h3()),
        )?
        .build()
        .wait()

@fakeshadow
Copy link
Collaborator

[dependencies]
xitca-http = { version = "0.4", features = ["http2", "http3", "rustls"] }
xitca-server = { version = "0.2", features = ["http3"] }
xitca-web = { version = "0.4", features = ["websocket"] }

quinn = "0.10"
rustls = "0.23"

use std::sync::Arc;

use xitca_http::HttpServiceBuilder;
use xitca_web::{
    handler::{
        handler_service,
        websocket::{Message, WebSocket},
    },
    route::get,
    service::ServiceExt,
    App,
};

fn main() -> std::io::Result<()> {
    let service = App::new()
        .at("/", get(handler_service(handler)))
        .finish()
        .enclosed(HttpServiceBuilder::new().rustls(rustls_config()));

    xitca_server::Builder::new()
        .bind_all("xitca-web", "localhost:8080", quinn_config(), service)?
        .build()
        .wait()
}

async fn handler(mut ws: WebSocket) -> WebSocket {
    ws.on_msg(|tx, msg| {
        Box::pin(async move {
            if let Message::Text(txt) = msg {
                println!("Got text message: {txt}");
                tx.text(format!("Echo: {txt}")).await.unwrap();
            }
        })
    });
    ws
}

fn rustls_config() -> Arc<rustls::ServerConfig> {
    // your rustls config goes here
    todo!()
}

fn quinn_config() -> quinn::ServerConfig {
    // your quinn config goes here
    todo!()
}

If you take a closer look at the previous issue you would know the answer is already there for you. This crate is not a hand held coding session.

@fakeshadow
Copy link
Collaborator

fakeshadow commented Mar 11, 2024

The most fatal mistake is like I previously pointed out. You are using an example that show casing handling multiple http services as your template for everything. The example is not for your use case and you should look else where (And I pointed it out in previous issue you just don't want to follow it)

@ouvaa
Copy link
Author

ouvaa commented Mar 11, 2024

@fakeshadow you are right in everything. i'm just newbie rustlang learner. i really like xitca performance etc but it's really difficult to use. please take a look at :

https://salvo.rs/book/

hope you can do something along this line in future. I think I have to go with salvo coz the learning curve is too steep for me at this stage for xitca. I've been trying hard to make it happen for the past 2 days and hope you can go along the lines of salvo so in future will port my code back to xitca for the performance.

thanks a lot. xitca is great but i'm just too dense to use it for the moment. sorry to trouble you so much but will definitely support this project.

@ouvaa
Copy link
Author

ouvaa commented Mar 12, 2024

@fakeshadow

needed the performance, back to using xitca. can you help out this part and add to your example?

cant get this part to work. pls help do a routing where /ws is websocket and /plaintext is hello world.

  1. will run this on production and give feedback.
  2. will help contribute the online documentation.
  3. will also contribute back a working real world app using xitca
use std::sync::Arc;

use rustls::{Certificate, PrivateKey, ServerConfig};
use quinn::{ServerConfig, ServerConfigBuilder};
use std::fs::File;
use std::io::{BufReader, Read};

use xitca_http::HttpServiceBuilder;
use xitca_web::{
    handler::{
        handler_service,
        websocket::{Message, WebSocket},
    },
    route::get,
    service::ServiceExt,
    App,
};

fn main() -> std::io::Result<()> {
    let service = App::new()
        .at("/ws", get(handler_service(handler)))
        .at("/plaintext", get(handler_service(helloworld)))
        .finish()
        .enclosed(HttpServiceBuilder::new().rustls(rustls_config()));

    xitca_server::Builder::new()
        .bind_all("xitca-web", "0.0.0.0:8080", quinn_config(), service)?
        .build()
        .wait()
}

async fn handler(mut ws: WebSocket) -> WebSocket {
    ws.on_msg(|tx, msg| {
        Box::pin(async move {
            if let Message::Text(txt) = msg {
                println!("Got text message: {txt}");
                tx.text(format!("Echo: {txt}")).await.unwrap();
            }
        })
    });
    ws
}

fn rustls_config() -> Arc<rustls::ServerConfig> {

    // Load the server's certificate and private key.
    // These files are usually generated using a tool like openssl and must be present in your project directory or specified path.
    let cert_file = &mut BufReader::new(File::open("../cert/cert.pem").unwrap());
    let key_file = &mut BufReader::new(File::open("../cert/key.pem").unwrap());

    let cert_chain = rustls::internal::pemfile::certs(cert_file).unwrap();
    let mut keys = rustls::internal::pemfile::rsa_private_keys(key_file).unwrap();

    // Construct the server configuration with the certificate and private key.
    let config = ServerConfig::builder()
        .with_safe_defaults()
        .with_no_client_auth()
        .with_single_cert(cert_chain, PrivateKey(keys.remove(0))).unwrap();

    Arc::new(config)
}


fn quinn_config() -> quinn::ServerConfig {

    // Load the server's certificate and private key for QUIC.
    let cert_chain = quinn::CertificateChain::from_pem(&mut BufReader::new(File::open("../cert/cert.pem").unwrap())).unwrap();
    let priv_key = quinn::PrivateKey::from_pem(&mut BufReader::new(File::open("../cert/key.pem").unwrap())).unwrap();

    let mut crypto = rustls::ServerConfig::builder()
        .with_safe_defaults()
        .with_no_client_auth()
        .with_single_cert(cert_chain.into_rustls(), priv_key.into_rustls()).unwrap();

    // Configure ALPN to use HTTP/3.
    crypto.alpn_protocols = vec![b"h3".to_vec()];

    // Convert the rustls config into a quinn config.
    let mut config = ServerConfig::default();
    config.crypto = Arc::new(crypto);

    config
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants