diff --git a/manyfold/README.md b/manyfold/README.md index 07545a1..96a7654 100644 --- a/manyfold/README.md +++ b/manyfold/README.md @@ -18,6 +18,18 @@ $ docker run -t xrforge docker.io/coderofsalvation/xrforge:latest -v ./mnt:/mnt > To scan all (mounted) libraries run this once: `$ docker exec xrforge /manyfold/cli/manyfold.sh scan_libraries` +# ports + +By default the following services are running in the docker: + +| port | HTTP PATH | info | +|-------|----------------------------------------------| +| 3214 | / | [Manyfold](https://manyfold.app) | +[ 3214 | /view | [janusweb](https://github.com/meetecho/janus-gateway) the XR viewer | +| 5566 | | [janus-server](https://github.com/janusvr/janus-server) for chat + syncing avatar positions | +| 5577 | / | [cors-anywhere](https://github.com/Rob--W/cors-anywhere) for a deep immersive browsing via janusweb | +| *6379 | | Manyfold REDIS-server (* = never expose this port) | + # Build & Run the container-image > **NOTE**: [nix](https://nixos.org) is used to promote reproducability-over-repeatability @@ -29,7 +41,7 @@ $ docker load < $(nix-build nix/docker.nix) $ docker run $(manyfold/cli/manyfold run) # generates a dockercmd with sane env-flags [xrforge] podman detected..starting OCI container -+ /run/current-system/sw/bin/podman run -p 8790:3214 -p 8791:3215 --name xrforge -e SECRET_KEY_BASE=lkjwljlkwejrlkjek34k234l -e DATABASE_ADAPTER=sqlite3 -e SUDO_RUN_UNSAFELY=enabled -e MULTIUSER=enabled -e FEDERATION=enabled -e THEME=vapor -e HOMEPAGE=/models -e FEDERATE_DRIVE_CACHE=5s -v ./xrfragment/assets:/mnt/assets/xrfragment/#1 --cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse xrforge ++ /usr/bin/podman run -p 8080:3214 -p 8791:3215 --name xrforge -e SECRET_KEY_BASE=lkjwljlkwejrlkjek34k234l -e DATABASE_ADAPTER=sqlite3 -e SUDO_RUN_UNSAFELY=enabled -e MULTIUSER=enabled -e FEDERATION=enabled -e THEME=vapor -e HOMEPAGE=/models -e FEDERATE_DRIVE_CACHE=5s -v ./xrfragment/assets:/mnt/assets/xrfragment/#1 --cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse xrforge [xrforge] booting... [xrforge] applying filesystem overlay sending incremental file list @@ -66,9 +78,11 @@ $ docker run -t xrforge docker.io/coderofsalvation/xrforge:latest -v ./config:/c | `NO_PACKAGEALL` | `` | don't package all experiences every hour to /usr/src/app/public/experiences.zip | | `RCLONE_REMOTE` | `` | specify **single** rclone remote name (without semicolon) to mount (default: mount all rclone remotes)| | `UPLOAD_PATH` | `/mnt/experiences`| specify default library where user-files are uploaded (regular dir or mounted rclone path) | -| `FEDERATE_DRIVE_HOST` | `http://localhost:3215` | host adress which other hosts can use to access the federate drive | +| `FEDERATE_SERVERS` | see info | allowed servers to share remote content with | +| | | `"https://janusxr.org","https://web.janusxr.org", "https://vesta.janusxr.org", "https://xrfragment.org", "https://isvery.ninja", "https://xrhf.isvery.ninja", "https://xrforge.isvery.ninja"` | +| `FEDERATE_DRIVE_HOST` | `http://localhost:8081` | host adress which other hosts can use to access the federate drive | | `FEDERATE_DRIVE_PATH` | `/mnt` | serve path over HTTP (so other instances can add it as a remote). Specify `0` to disable | -| `FEDERATE_DRIVE_PORT` | `3215` | specify default library where user-files are uploaded (regular dir or mounted rclone path) | +| `FEDERATE_DRIVE_PORT` | `8081` | specify port | | `FEDERATE_DRIVE_USER` | `` | specify HTTP AUTH credentials (`user` e.g.) for restricted sharing | | `FEDERATE_DRIVE_PW` | `` | specify HTTP AUTH credentials (`pass` e.g.) for restricted sharing | | `FEDERATE_DRIVE_CACHE`| `1m0s` | specify interval to re-check all models/directories | @@ -94,9 +108,33 @@ It's also possible to enforce a default sqlite3 db via the `-v ./manyfold.sql:/m The server-image will boot `manyfold/cli/manyfold.sh boot` and check for directory `/manyfold` (in the container). When found, it uses the files in there instead (`/manyfold/usr/src/app/public/404.html` instead of `/usr/src/app/public/404.html` e.g.). -# Federated libraries +# Federation -Besides ActivitPub, XRForge allows federating manyfold libraries too, which allows manyfold libraries to: +Federation is possible on many levels: + +1. Outbound: cors-anywhere + +By default [cors-anywhere](https://github.com/Rob--W/cors-anywhere), so XRForge can source remote content from anywhere. +> It is running inside the docker on port 5577, and can be disabled by env-var `SERVER_CORS=''` + +1. Inbound: restrict via env-var `FEDERATE_SERVERS` + +This way you can only allow trusted servers to use your content (iframes e.g.): + +| usecase | env-var value | +|--------------------|---------------| +| every server | `FEDERATE_SERVERS='"*"'` (default) | +| only this instance | `FEDERATE_SERVERS=':self'` | +| trusted servers | `FEDERATE_SERVERS='"https://janusxr.org", https://web.janusxr.org", "https://vesta.janusxr.org", "https://xrfragment.org", "https://isvery.ninja", "https://xrhf.isvery.ninja", "https://xrforge.isvery.ninja"' | + +* ActivityPub: each author and experiences can be followed + +Next experiences and authors, an '@' address is shown.
+You can enter these in mastodon to follow updates. + +* Federating network-drives via `FEDERATE_DRIVE_HOST` + +Network-drives: * be mounted as a network drive on their desktop-machine (3D editor export-to-library) * scale horizontally across instances: @@ -128,7 +166,7 @@ To enable rclone to mount **readonly** network drives (=remotes), the container The quickest way is: 1. create directory `./manyfold/root/.config` outside of the container -2. add `-v ./manyfold/root/.config:/root/.config --cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse` to the docker cmd +2. add `-v ./manyfold/root/.config:/root/.config --cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse -p 8081:8081 -e FEDERATE_DRIVE_PORT=8081 -e FEDERATE_DRIVE_HOST=http://localhost:8081` to the docker cmd 3. now federate XRForge libraries by running `docker exec -it xrforge rclone config create myhttp http url=https://xrforgeinstanceB.com user=myuser pass=$(rclone obscure mypassword)` in a running container 4. profit! diff --git a/manyfold/cli/manyfold.sh b/manyfold/cli/manyfold.sh index 17c67b2..8ab16e2 100755 --- a/manyfold/cli/manyfold.sh +++ b/manyfold/cli/manyfold.sh @@ -1,20 +1,26 @@ #!/bin/sh oci=$(which podman || which docker) -test -n "$APPNAME" || export APPNAME=XRForge -test -n "$TAGLINE" || export TAGLINE="Publish single-file XR experiences to immersive networks" -test -n "$UPLOAD_PATH" || export UPLOAD_PATH=/mnt/experiences -test -n "$THEME" || export THEME=slate -test -n "$HOMEPAGE" || export HOMEPAGE=/models -test -n "$GODOT_VERSION" || export GODOT_VERSION=4.4.1-stable -test -n "$IMPORT_INSTANCES" || export IMPORT_INSTANCES=1 -test -n "$SERVER_CORS" || export SERVER_CORS=http://localhost:5577 -test -n "$SERVER_JANUS" || export SERVER_JANUS=http://localhost:5566 -test -n "$HTTPS_ONLY" || export HTTPS_ONLY=disabled +test -n "$APPNAME" || export APPNAME=XRForge +test -n "$TAGLINE" || export TAGLINE="Publish single-file XR experiences to immersive networks" +test -n "$UPLOAD_PATH" || export UPLOAD_PATH=/mnt/experiences +test -n "$THEME" || export THEME=slate +test -n "$HOMEPAGE" || export HOMEPAGE=/models +test -n "$GODOT_VERSION" || export GODOT_VERSION=4.4.1-stable +test -n "$IMPORT_INSTANCES" || export IMPORT_INSTANCES=1 +test -n "$SERVER_CORS" || export SERVER_CORS=http://localhost:5577 +test -n "$SERVER_JANUS" || export SERVER_JANUS=http://localhost:5566 +test -n "$HTTPS_ONLY" || unsafe=1 && export HTTPS_ONLY=disabled +test -n "$SECRET_KEY_BASE" || unsafe=1 && export SECRET_KEY_BASE=j1gf2cj3gfcjhf2j34298kjk2j3h4k +test -n "$SUDO_RUN_UNSAFELY" || unsafe=1 && export SUDO_RUN_UNSAFELY=enabled +test -n "$DATABASE_ADAPTER" || export DATABASE_ADAPTER=sqlite3 +test -n "$MULTIUSER" || export MULTIUSER=enabled +test -n "$FEDERATE_SERVERS" || export FEDERATE_SERVERS='"*"' +test -n "$PUBLIC_HOSTNAME" || export PUBLIC_HOSTNAME=localhost + test -n "$CADDY" && { export HTTPS_ONLY=enabled export SERVER_CORS=https://$CADDY:5577 export SERVER_JANUS=https://$CADDY:5566 - export PUBLIC_HOSTNAME=localhost } db=/config/manyfold.sqlite3 @@ -48,6 +54,7 @@ run(){ -e SECRET_KEY_BASE=lkjwljlkwejrlkjek34k234l \ -e DATABASE_ADAPTER=sqlite3 \ -e FEDERATE_DRIVE_HOST=http://localhost:8081 \ + -e FEDERATE_DRIVE_PORT=8081 \ -e PUBLIC_HOSTNAME=$PUBLIC_HOSTNAME \ -e SERVER_JANUS=$SERVER_JANUS \ -e SERVER_CORS=$SERVER_CORS \ @@ -248,14 +255,14 @@ rename_app(){ sed -i 's|File(s) uploaded succesfully|File(s) uploaded succesfully. The experience is now being (re)generated, please be patient and check back later|g' /usr/src/app/config/locales/model_files/en.yml } -allow_csp(){ - test -n "$FEDERATE_DRIVE_HOST" || return 0 - # allow iframes + other federated servers - servers='"https://web.janusxr.org", "https://vesta.janusxr.org"' - sed -i "s|:self,|:self, $servers, ENV['FEDERATE_DRIVE_HOST'], ENV['SERVER_CORS'], ENV['SERVER_JANUS'],|g" /usr/src/app/app/controllers/application_controller.rb - sed -i "s|content_security_policy.connect_src(\*origins)|content_security_policy.connect_src(\*origins);content_security_policy.connect_src( $servers, ENV['FEDERATE_DRIVE_HOST'], ENV['SERVER_CORS'], ENV['SERVER_JANUS'])|g" /usr/src/app/app/controllers/application_controller.rb - # content_security_policy.clear_directives() - # content_security_policy.default_src '*' +federate_servers(){ + # allow iframes + other federated servers by adjusting manyfold's strict CSP policies + local servers="$FEDERATE_SERVERS" + test -n "$FEDERATE_DRIVE_HOST" && servers="${servers}, \"$FEDERATE_DRIVE_HOST\"" + test -n "$SERVER_CORS" && servers="${servers}, \"$SERVER_CORS\"" + test -n "$SERVER_JANUS" && servers="${servers}, \"$SERVER_JANUS\"" + sed -i "s|:self,|:self, $servers,|g" /usr/src/app/app/controllers/application_controller.rb + sed -i "s|content_security_policy.connect_src(\*origins)|content_security_policy.connect_src(\*origins);content_security_policy.connect_src( $servers )|g" /usr/src/app/app/controllers/application_controller.rb } start_syslog(){ @@ -293,9 +300,14 @@ force_public(){ import_assets(){ test -n "$NO_ASSETS" && return 0 # nothing to do here - # mount because rclone does support symlinks outside of the served folder - mkdir /mnt/templates && mount --bind /nix/store/*-xrfragments/xrf/templates /mnt/templates - mkdir /mnt/assets && mount --bind /nix/store/*-xrfragments/xrf/assets /mnt/assets + if test -n "$FEDERATE_DRIVE_HOST"; then + # mount because rclone does support symlinks outside of the served folder + mkdir /mnt/templates && mount --bind /nix/store/*-xrfragments/xrf/templates /mnt/templates + mkdir /mnt/assets && mount --bind /nix/store/*-xrfragments/xrf/assets /mnt/assets + else + ln -s /nix/store/*-xrfragments/xrf/templates /mnt/templates + ln -s /nix/store/*-xrfragments/xrf/assets /mnt/assets + fi add_lib_to_db /mnt/assets add_lib_to_db /mnt/templates } @@ -348,7 +360,7 @@ boot(){ test -z "$NO_OVERLAYFS" && overlayfs start_syslog rename_app - allow_csp + federate_servers set_homepage start_hook_daemon mount_rclone @@ -371,9 +383,10 @@ is_inside_container(){ usage(){ echocolor "Usage:" manyfold.sh "" echocolor "Cmds:" - echocolor " " "run [-d] " "# runs a OCI container (needs podman/docker)" + echocolor " " "run [-d] " "# prints OCI container cmd (needs podman/docker)" exit 0 } +test "$unsafe" = 1 && echocolor "[WARNING]" "default env-vars SECRET_KEY_BASE or SUDO_RUN_UNSAFELY are used. Please check: https://codeberg.org/coderofsalvation/xrforge/src/branch/master/manyfold" && echo test -n "$1" && "$@" test -n "$1" || usage diff --git a/manyfold/usr/src/app/app/controllers/application_controller.rb b/manyfold/usr/src/app/app/controllers/application_controller.rb deleted file mode 100644 index 703dc80..0000000 --- a/manyfold/usr/src/app/app/controllers/application_controller.rb +++ /dev/null @@ -1,167 +0,0 @@ -class ApplicationController < ActionController::Base - include Pundit::Authorization - include BetterContentSecurityPolicy::HasContentSecurityPolicy - after_action :verify_authorized, except: :index, unless: -> { respond_to?(:fasp_client_controller?) } - after_action :verify_policy_scoped, only: :index, unless: -> { respond_to?(:fasp_client_controller?) } - after_action :set_content_security_policy_header, if: -> { request.format.html? } - - before_action :authenticate_user!, unless: -> { SiteSettings.multiuser_enabled? || has_signed_id? } - around_action :switch_locale, if: -> { request.format.html? } - before_action :check_for_first_use - before_action :show_security_alerts - before_action :check_scan_status - before_action :remember_ordering - before_action :restore_failed_search - - protect_from_forgery with: :null_session, if: :is_api_request? - - rescue_from ScopedSearch::QueryNotSupported, with: -> { - flash[:alert] = t("application.search_error") - flash[:query] = params[:q] - redirect_back_or_to root_path - } - - unless Rails.env.test? - rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized - end - - def index - raise NotImplementedError - end - - def authenticate_admin_user! - authenticate_user! - render plain: "401 Unauthorized", status: :unauthorized unless current_user.is_administrator? - end - - def check_for_first_use - authenticate_user! if User.count == 0 # rubocop:disable Pundit/UsePolicyScope - redirect_to(edit_user_registration_path) if current_user&.reset_password_token == "first_use" - end - - def check_scan_status - @scan_in_progress = Sidekiq::Queue.new("scan").size > 0 - end - - def remember_ordering - session["order"] ||= "name" - session["order"] = params["order"] if params["order"] - end - - private - - def restore_failed_search - @query ||= flash[:query] - end - - def is_api_request? - request.format.manyfold_api_v0? - end - - def has_signed_id? - params[:id] && ApplicationRecord.signed_id_verifier.valid_message?(params[:id]) - end - - def img_src - host = begin - SiteSettings.site_icon ? URI.parse(SiteSettings.site_icon).host : nil - rescue - nil - end - [ - :self, "https://web.janusxr.org", "https://vesta.janusxr.org", ENV['FEDERATE_DRIVE_HOST'], ENV['SERVER_CORS'], ENV['SERVER_JANUS'], - :data, - host, - "https://cdn.jsdelivr.net", - "https://raw.githubusercontent.com", - SiteSettings.federation_enabled? ? :https : nil - ].compact - end - - def frame_src - [ - :self, "https://web.janusxr.org", "https://vesta.janusxr.org", ENV['FEDERATE_DRIVE_HOST'], ENV['SERVER_CORS'], ENV['SERVER_JANUS'], - SiteSettings.federation_enabled? ? :https : nil - ].compact - end - - def configure_content_security_policy - return if Rails.env.test? - - # Standard security policy - content_security_policy.default_src :self - content_security_policy.connect_src :self - content_security_policy.frame_ancestors :self - content_security_policy.frame_src(*frame_src) - content_security_policy.font_src :self, "https://web.janusxr.org", "https://vesta.janusxr.org", ENV['FEDERATE_DRIVE_HOST'], ENV['SERVER_CORS'], ENV['SERVER_JANUS'], "https://cdn.jsdelivr.net", "https://fonts.gstatic.com" - content_security_policy.img_src(*img_src) - content_security_policy.object_src :none - content_security_policy.script_src :self - content_security_policy.style_src :self - content_security_policy.style_src_attr :unsafe_inline - content_security_policy.style_src_elem :self, "https://web.janusxr.org", "https://vesta.janusxr.org", ENV['FEDERATE_DRIVE_HOST'], ENV['SERVER_CORS'], ENV['SERVER_JANUS'], "nonce-#{content_security_policy_nonce}", "https://fonts.googleapis.com" - # Add library origins - origins = Library.all.filter_map(&:storage_origin) # rubocop:disable Pundit/UsePolicyScope - content_security_policy.img_src(*origins) - content_security_policy.connect_src(*origins);content_security_policy.connect_src( "https://web.janusxr.org", "https://vesta.janusxr.org", ENV['FEDERATE_DRIVE_HOST'], ENV['SERVER_CORS'], ENV['SERVER_JANUS']) - # If we're using Scout DevTrace in local development, we need to allow a load - # of inline stuff, so we need to add that and NOT add the nonce - if Rails.env.development? && ENV.fetch("SCOUT_DEV_TRACE", false) === "true" - scout_csp = [:unsafe_inline, "https://apm.scoutapp.com", "https://scoutapm.com"] - content_security_policy.img_src(*scout_csp) - content_security_policy.script_src(*scout_csp) - content_security_policy.style_src(*scout_csp) - content_security_policy.connect_src(*scout_csp) - content_security_policy.frame_src(*scout_csp) - else - content_security_policy.script_src "nonce-#{content_security_policy_nonce}" - end - end - - def switch_locale(&action) - locale = current_user&.interface_language || request.env["rack.locale"] - I18n.with_locale(locale.presence, &action) - end - - def show_security_alerts - return unless current_user&.is_administrator? - return if ENV.fetch("SUDO_RUN_UNSAFELY", nil) === "enabled" - flash.now[:alert] = t("security.running_as_root_html") if Process.uid == 0 - end - - def random_delay - # Not sure how secure this is; it's used to help with timing attacks on login ID lookups - # by adding a random 0-2 second delay into the response. There is probably a better way. - sleep Random.new.rand(2.0) - end - - def user_not_authorized - if current_user - raise ActiveRecord::RecordNotFound - else - redirect_to new_session_path(:user) - end - end - - def set_indexable(content) - arr = Array(content) - @indexing_directives = [ - ("noindex" unless arr.map(&:indexable?).all?), - ("noai noimageai" unless arr.map(&:ai_indexable?).all?) - ].compact.join(" ") - response.headers["X-Robots-Tag"] = @indexing_directives if @indexing_directives.presence - end - - def send_file_content(attachment, disposition: :attachment, derivative: nil) - head :not_found and return if attachment.nil? - # Check if we can send a direct URL - redirect_to(attachment.url, allow_other_host: true) if /https?:\/\//.match?(attachment.url) - # Otherwise provide a direct download - status, headers, body = attachment.to_rack_response(disposition: disposition) - self.status = status - self.headers.merge!(headers) - self.response_body = body - rescue Errno::ENOENT - head :internal_server_error - end -end diff --git a/manyfold/usr/src/app/app/views/application/_navbar.html.erb b/manyfold/usr/src/app/app/views/application/_navbar.html.erb index cc5f3d4..1a4ee97 100644 --- a/manyfold/usr/src/app/app/views/application/_navbar.html.erb +++ b/manyfold/usr/src/app/app/views/application/_navbar.html.erb @@ -30,6 +30,12 @@ <% end %> <% end %> +