function http_router_for(session::ServerSession) router = HTTP.Router(default_404) security = session.options.security function create_serve_onefile(path) return request::HTTP.Request -> asset_response(normpath(path)) end HTTP.register!(router, "GET", "/", create_serve_onefile(project_relative_path(frontend_directory(), "index.html"))) HTTP.register!(router, "GET", "/edit", create_serve_onefile(project_relative_path(frontend_directory(), "editor.html"))) HTTP.register!(router, "GET", "/ping", r -> HTTP.Response(200, "OK!")) HTTP.register!(router, "GET", "/possible_binder_token_please", r -> session.binder_token === nothing ? HTTP.Response(200,"") : HTTP.Response(200, session.binder_token)) function try_launch_notebook_response( action::Function, path_or_url::AbstractString; as_redirect=true, title="", advice="", home_url="./", action_kwargs... ) try nb = action(session, path_or_url; action_kwargs...) notebook_response(nb; home_url, as_redirect) catch e if e isa SessionActions.NotebookIsRunningException notebook_response(e.notebook; home_url, as_redirect) else error_response(500, title, advice, sprint(showerror, e, stacktrace(catch_backtrace()))) end end end function serve_newfile(request::HTTP.Request) notebook_response(SessionActions.new(session); as_redirect=(request.method == "GET")) end HTTP.register!(router, "GET", "/new", serve_newfile) HTTP.register!(router, "POST", "/new", serve_newfile) # This is not in Dynamic.jl because of bookmarks, how HTML works, # real loading bars and the rest; Same for CustomLaunchEvent function serve_openfile(request::HTTP.Request) try uri = HTTP.URI(request.target) query = HTTP.queryparams(uri) as_sample = haskey(query, "as_sample") execution_allowed = haskey(query, "execution_allowed") if haskey(query, "path") path = tamepath(maybe_convert_path_to_wsl(query["path"])) if isfile(path) return try_launch_notebook_response( SessionActions.open, path; execution_allowed, as_redirect=(request.method == "GET"), as_sample, risky_file_source=nothing, title="Failed to load notebook", advice="The file $(htmlesc(path)) could not be loaded. Please report this error!", ) else return error_response(404, "Can't find a file here", "Please check whether $(htmlesc(path)) exists.") end elseif haskey(query, "url") url = query["url"] return try_launch_notebook_response( SessionActions.open_url, url; execution_allowed, as_redirect=(request.method == "GET"), as_sample, risky_file_source=url, title="Failed to load notebook", advice="The notebook from $(htmlesc(url)) could not be loaded. Please report this error!" ) else # You can ask Pluto to handle CustomLaunch events # and do some magic with how you open files. # You are responsible to keep this up to date. # See Events.jl for types and explanation # maybe_notebook_response = try_event_call(session, CustomLaunchEvent(query, request, try_launch_notebook_response)) isnothing(maybe_notebook_response) && return error("Empty request") return maybe_notebook_response end catch e return error_response(400, "Bad query", "Please report this error!", sprint(showerror, e, stacktrace(catch_backtrace()))) end end HTTP.register!(router, "GET", "/open", serve_openfile) HTTP.register!(router, "POST", "/open", serve_openfile) # normally shutdown is done through Dynamic.jl, with the exception of shutdowns made from the desktop app function serve_shutdown(request::HTTP.Request) notebook = notebook_from_uri(request) SessionActions.shutdown(session, notebook) return HTTP.Response(200) end HTTP.register!(router, "GET", "/shutdown", serve_shutdown) HTTP.register!(router, "POST", "/shutdown", serve_shutdown) # used in desktop app # looks like `/move?id=&newpath=`` function serve_move(request::HTTP.Request) uri = HTTP.URI(request.target) query = HTTP.queryparams(uri) notebook = notebook_from_uri(request) newpath = query["newpath"] try SessionActions.move(session, notebook, newpath) HTTP.Response(200, notebook.path) catch e error_response(400, "Bad query", "Please report this error!", sprint(showerror, e, stacktrace(catch_backtrace()))) end end HTTP.register!(router, "GET", "/move", serve_move) HTTP.register!(router, "POST", "/move", serve_move) function serve_notebooklist(request::HTTP.Request) return HTTP.Response(200, pack(Dict(k => v.path for (k, v) in session.notebooks))) end HTTP.register!(router, "GET", "/notebooklist", serve_notebooklist) function serve_sample(request::HTTP.Request) uri = HTTP.URI(request.target) sample_filename = split(HTTP.unescapeuri(uri.path), "sample/")[2] sample_path = project_relative_path("sample", sample_filename) try_launch_notebook_response( SessionActions.open, sample_path; as_redirect=(request.method == "GET"), home_url="../", as_sample=true, title="Failed to load sample", advice="Please report this error!" ) end HTTP.register!(router, "GET", "/sample/*", serve_sample) HTTP.register!(router, "POST","/sample/*", serve_sample) notebook_from_uri(request) = let uri = HTTP.URI(request.target) query = HTTP.queryparams(uri) id = UUID(query["id"]) session.notebooks[id] end function serve_notebookfile(request::HTTP.Request) try notebook = notebook_from_uri(request) response = HTTP.Response(200, sprint(save_notebook, notebook)) HTTP.setheader(response, "Content-Type" => "text/julia; charset=utf-8") HTTP.setheader(response, "Content-Disposition" => "inline; filename=\"$(basename(notebook.path))\"") response catch e return error_response(400, "Bad query", "Please report this error!", sprint(showerror, e, stacktrace(catch_backtrace()))) end end HTTP.register!(router, "GET", "/notebookfile", serve_notebookfile) function serve_statefile(request::HTTP.Request) try notebook = notebook_from_uri(request) response = HTTP.Response(200, Pluto.pack(Pluto.notebook_to_js(notebook))) HTTP.setheader(response, "Content-Type" => "application/octet-stream") HTTP.setheader(response, "Content-Disposition" => "attachment; filename=\"$(without_pluto_file_extension(basename(notebook.path))).plutostate\"") response catch e return error_response(400, "Bad query", "Please report this error!", sprint(showerror, e, stacktrace(catch_backtrace()))) end end HTTP.register!(router, "GET", "/statefile", serve_statefile) function serve_notebookexport(request::HTTP.Request) try notebook = notebook_from_uri(request) response = HTTP.Response(200, generate_html(notebook)) HTTP.setheader(response, "Content-Type" => "text/html; charset=utf-8") HTTP.setheader(response, "Content-Disposition" => "attachment; filename=\"$(without_pluto_file_extension(basename(notebook.path))).html\"") response catch e return error_response(400, "Bad query", "Please report this error!", sprint(showerror, e, stacktrace(catch_backtrace()))) end end HTTP.register!(router, "GET", "/notebookexport", serve_notebookexport) function serve_notebookupload(request::HTTP.Request) uri = HTTP.URI(request.target) query = HTTP.queryparams(uri) save_path = SessionActions.save_upload(request.body; filename_base=get(query, "name", nothing)) try_launch_notebook_response( SessionActions.open, save_path; as_redirect=false, as_sample=false, execution_allowed=haskey(query, "execution_allowed"), clear_frontmatter=haskey(query, "clear_frontmatter"), title="Failed to load notebook", advice="The contents could not be read as a Pluto notebook file. When copying contents from somewhere else, make sure that you copy the entire notebook file. You can also report this error!" ) end HTTP.register!(router, "POST", "/notebookupload", serve_notebookupload) function serve_asset(request::HTTP.Request) uri = HTTP.URI(request.target) filepath = project_relative_path(frontend_directory(), relpath(HTTP.unescapeuri(uri.path), "/")) asset_response(filepath; cacheable=should_cache(filepath)) end HTTP.register!(router, "GET", "/**", serve_asset) HTTP.register!(router, "GET", "/favicon.ico", create_serve_onefile(project_relative_path(frontend_directory(allow_bundled=false), "img", "favicon.ico"))) return scoped_router(session.options.server.base_url, router) end """ scoped_router(base_url::String, base_router::HTTP.Router)::HTTP.Router Returns a new `HTTP.Router` which delegates all requests to `base_router` but with requests trimmed so that they seem like they arrived at `/**` instead of `/\$base_url/**`. """ function scoped_router(base_url, base_router) base_url == "/" && return base_router @assert startswith(base_url, '/') "base_url \"$base_url\" should start with a '/'" @assert endswith(base_url, '/') "base_url \"$base_url\" should end with a '/'" @assert !occursin('*', base_url) "'*' not allowed in base_url \"$base_url\" " function handler(request) request.target = request.target[length(base_url):end] return base_router(request) end router = HTTP.Router(base_router._404, base_router._405) HTTP.register!(router, base_url * "**", handler) HTTP.register!(router, base_url, handler) return router end