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

Static file serving #239

Closed
luser opened this issue Mar 22, 2017 · 25 comments
Closed

Static file serving #239

luser opened this issue Mar 22, 2017 · 25 comments
Assignees
Labels
question A question (converts to discussion) request Request for new functionality

Comments

@luser
Copy link

luser commented Mar 22, 2017

First off: let me just say that Rocket is very nice and has been a pleasure to use, kudos!

I looked around the docs and couldn't find any support for serving a set of static files from a directory. This is something that I've seen supported in every web framework I've ever used. As an arbitrary example, here's how it works in Flask. This is a pretty common pattern to serve things like CSS files and JS scripts, so I think it would be useful to have. Obviously one can serve individual files by just having a handler that returns a File, but this seems like generally useful functionality. Ideally one would simply be able to do something like rocket::ignite().mount("/static", StaticFiles(some_path).

@haheute
Copy link

haheute commented Mar 22, 2017

I am not sure if this helps.. Have you seen the the static_files example ?

@SergioBenitez SergioBenitez added the question A question (converts to discussion) label Mar 23, 2017
@SergioBenitez
Copy link
Member

Indeed, this is covered in the static_files example.

@luser
Copy link
Author

luser commented Mar 23, 2017

I had not seen that, thanks! I did eventually find the bit about segments parameters which has that same code as an example, but I still think a helper that simplified this extremely common use case would be nice. It took me a while to find that part of the documentation because I was looking around for "static file serving" and I did not expect to find it under "Dynamic Segments". :)

@SergioBenitez
Copy link
Member

@luser Perhaps something in contrib that you can simply mount. So it would look something like:

use rocket_contrib::StaticFiles;

rocket::ignite().mount("/path_to_serve_on", StaticFiles::from("/path_to_serve_from"));

This would presumably serve files from the /path_to_serve_from directory at the path /path_to_serve_on.

I think something like this would be nice, though I'm not sure if it's actually possible to do this in a clean way at the moment. I'll look into it.

@SergioBenitez SergioBenitez added the request Request for new functionality label Mar 23, 2017
@luser
Copy link
Author

luser commented Mar 24, 2017

That sounds great!

@theduke
Copy link

theduke commented Apr 9, 2017

I think the way to do it right now is sufficiently simple, just showing how in the guide under "Serving Static Files" or something alike would suffice.

@lnicola
Copy link
Contributor

lnicola commented Apr 13, 2017

I think it would be best to offer something that supports HTTP caching.

@marti1125
Copy link

I have a question I am using bootstrap only (/css/bootstrap.min.css, /css/bootstrap-theme.min.css)
It shows errors in console (GET /css/bootstrap-theme.min.css.map:) ??
any reason about this =?
captura de pantalla 2017-04-14 a las 11 21 45

@SergioBenitez
Copy link
Member

@marti1125 Your browser is sending a request for the source map files (*.map) but they don't exist in the directory. Presumably you have developer tools open.

@marti1125
Copy link

@SergioBenitez yep it is =D

@quadrupleslap
Copy link
Contributor

Without actually running it, it looks like the static files example would be vulnerable to a path traversal attack. Is there anything preventing this?

@SergioBenitez
Copy link
Member

SergioBenitez commented Aug 26, 2017

@quadrupleslap See the docs here and here.

@Ekranos
Copy link

Ekranos commented Apr 12, 2018

Since the provided example was not enough for, since it didn't even support the Last-Modified header, I wrote my own Fairing for rocket.

Maybe we could build upon this, stabilize it somewhere in the future and get it in contrib then.

https://crates.io/crates/rocket_static_fs

Sadly docs.rs doesn't build the docs cause of an outdated rustc version. But basically the example from the docs shares your apps src folder under /src/:

#![feature(plugin)]
#![plugin(rocket_codegen)]

extern crate rocket;
extern crate rocket_static_fs;

#[get("/")]
fn index() -> &'static str {
    "Hellos, world!"
}

fn main() {
    rocket::ignite()
        .attach(rocket_static_fs::StaticFileServer::new("src", "/src/").unwrap())
        .mount("/", routes![index])
        .launch();
}

@quadrupleslap
Copy link
Contributor

quadrupleslap commented Apr 13, 2018

I just remembered that tilpner/includedir is another solution to this, if you don't mind pulling your entire website into your executable.

Edit: it's definitely not a comprehensive solution, but it's an interesting place to start.

@Ekranos
Copy link

Ekranos commented Apr 13, 2018

@quadrupleslap Including everything is the binary is cool, but this also doesn't handle things like Not-Modified. My small project is aimed to provide a pretty solid solution for static file serving with different backends and support for caching, ranges, compression, directory listing. It's not there yet, but since I need these features, it won't be long until it has these properly implemented.

@Boscop
Copy link

Boscop commented May 18, 2018

@SergioBenitez Is it still planned to allow paths starting with a single dot as described in #560 (comment) ?

The plan in my previous comment covers your use-case as well by relaxing the constraints on PathBuf. I'll close this out and track in #239.

Doing StaticFiles::from("/path_to_serve_from").allow_hidden() would not work for my use case:

#![feature(plugin)]
#![plugin(rocket_codegen)]
extern crate rocket;
extern crate rocket_contrib;

use std::path::{Path, PathBuf};
use std::fs;
use rocket::response::{NamedFile, Responder};
use rocket::response::content::Html;
use rocket::{Request, Response};
use rocket::http::Status;
use rocket::config::{Config, Environment};

fn list_dir<P: AsRef<Path>>(dir: P) -> String {
    let dir = dir.as_ref();
    let rows = fs::read_dir(&dir).ok().map_or_else(|| "error reading dir".to_string(), |x| x.filter_map(|e| e.ok()).map(|entry| {
        let abs_path = entry.path();
        let rel_path = abs_path.strip_prefix(dir).unwrap();
        let path_str = rel_path.to_string_lossy();
        let url = path_str.to_string() + if abs_path.is_dir() { "/" } else { "" };
        format!(r#"<li><a href="{}">{}</a>"#, url, url)
    }).collect::<String>());
    let dir = dir.to_string_lossy();
    let dir = if dir == "." { "/".into() } else { dir.replace('\\', "/") };
    format!(r#"
        <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><html>
        <title>Directory listing for {}</title>
        <body>
        <h2>Directory listing for {}</h2>
        <hr>
        <ul>
        {}
        </ul>
        <hr>
        </body>
        </html>
    "#, dir, dir, rows)
}

#[get("/")]
fn index() -> Html<String> {
    Html(list_dir("."))
}

enum PathResp { File(PathBuf), Dir(PathBuf) }

impl Responder<'static> for PathResp {
    fn respond_to(self, req: &Request) -> Result<Response<'static>, Status> {
        match self {
            PathResp::File(path) => NamedFile::open(path).ok().respond_to(req),
            PathResp::Dir(path) => Html(list_dir(&path)).respond_to(req)
        }
    }
}

#[get("/<path..>")]
fn path(path: PathBuf) -> PathResp {
    if path.is_dir() { PathResp::Dir(path) } else { PathResp::File(path) }
}

fn main() {
    let config = Config::build(Environment::Staging)
        .address("0.0.0.0").port(8000).finalize().unwrap();
    rocket::custom(config, true).mount("/", routes![index, path]).launch();
}

(The UnsafePBuf workaround would work but I'd prefer if the FromSegments impl for PathBuf allowed paths starting with a single dot.)

@Ekranos
Copy link

Ekranos commented May 27, 2018

@Boscop In case you just need a simplistic, yet functional static file server for rocket, you may want to take a look at https://crates.io/crates/rocket_static_fs

@liufuyang
Copy link

liufuyang commented Aug 3, 2018

@SergioBenitez I tried to use the static_files example to setup a static site while supporting other api endpoints as well. However I get this collisions error when loading it up with cargo run:

  Mounting '/':
    => GET /
    => GET /<file..>
    => GET /api/hello
    => GET /api/test
    => POST /api/upload
    => GET /api/upload/<id>
Error: GET /api/upload/<id> and GET /<file..> collide!
Error: Rocket failed to launch due to routing collisions.

So basically I have setup the static serving on / like

#[get("/")]
fn index() -> io::Result<NamedFile> {
    NamedFile::open("dist/index.html")
}

// allow html to reference any file with path /static under folder "static"
#[get("/<file..>")]
fn files(file: PathBuf) -> Option<NamedFile> {
    NamedFile::open(Path::new("dist/").join(file)).ok()
}

Then added some api endpoint like

#[get("/api/upload/<id>")]
fn retrieve(id: PasteID) -> Option<File> {
    let filename = format!("tmp/{id}", id = id);
    File::open(&filename).ok()
}

BTW, the static html and js files are build/generated by a vue project, which as a single index.html and it reference all the built js and css directly via /js/.... or /css/..... So if I mount the static file endpoint not on root / then the js frontend app won't work normally.

How can I solve this? 🤔 Thank you.

@liufuyang
Copy link

Oh I just looked carefully at #723 again it seems I can add rank to solve it. #[get("/<file..>", rank = 10)]

Is this the recommended way?

@SergioBenitez
Copy link
Member

With ec130f9, you can now do the following:

use rocket_contrib::static_files::StaticFiles;

fn main() {
    rocket::ignite()
        .mount("/path_to_serve_on", StaticFiles::from("/path_to_serve_from"))
        .launch();
}

@liufuyang
Copy link

This sound awesome, will give a try :D Thank you @SergioBenitez 💯

@liufuyang
Copy link

liufuyang commented Sep 7, 2018

@SergioBenitez Hey Sergio again, if I would like to make some user authentication, so that for example if a user has not logged in and want to visit /, then the user will be redirected to some login page (so that all the static files cannot be accessed), I cannot use your recommended way above, right?

I guess I will have to use something like:

#[get("/")]
fn index(_user: User) -> io::Result<NamedFile> {
    NamedFile::open("dist/index.html")
}

// allow html to reference any file with path /static under folder "static"
#[get("/<file..>", rank = 10)]   // use rank here to allow other api endpoint available as well
fn files(file: PathBuf, _user: User) -> Option<NamedFile> {
    NamedFile::open(Path::new("dist/").join(file)).ok()
}

with User setup like in this session example: https://github.com/SergioBenitez/Rocket/blob/master/examples/session/src/main.rs

Does my thoughts make any sense to you? Thanks in advance :)

@SergioBenitez SergioBenitez self-assigned this Sep 10, 2018
@SergioBenitez
Copy link
Member

@liufuyang I'm not sure I've understood what you're saying. I think you're saying that you want:

  • Authorized users to be able to access static files.
  • Unauthorized users to be denied access to static files.

If that's the case, then, just as would be the case in any other web framework with a pluggable static-files server, you're likely better off handling this yourself. In Rocket, the way you've illustrated looks about right.

@liufuyang
Copy link

@SergioBenitez Thank you.
Basically I am using a single Rocket server to host a group of API endpoints and a set of static html plus js code. And those html + js code will make a UI on the client side that does request on the APIs my Rocket server is hosting. Now I need to add some authentication mechanism with oauth2. So that's why I am asking about it :)

@SamTV12345
Copy link

What happened to the last request? Was handling range header ever implemented?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question A question (converts to discussion) request Request for new functionality
Projects
None yet
Development

No branches or pull requests