diff --git a/manyfold/README.md b/manyfold/README.md index 9df4171..2f480f1 100644 --- a/manyfold/README.md +++ b/manyfold/README.md @@ -24,7 +24,7 @@ $ docker run -t xrforge docker.io/coderofsalvation/xrforge:latest -v ./mnt:/mnt $ git clone --recurse-submodules --depth 1 https://codeberg.org/coderofsalvation/xrforge.git $ cd xrforge $ docker load < $(nix-build nix/docker.nix) -$ manyfold/cli/manyfold run +$ 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 @@ -36,11 +36,11 @@ sending incremental file list > NOTE: if you don't want the default XR Fragments asset-library, omit `--recurse-submodules` in the git cmd -To preserve your uploaded models, add the `experiences` as a docker volume-flag, for example: +To preserve your uploaded models and db, add these docker volume-flags, for example: ``` -$ mkdir experiences -$ manyfold/cli/manyfold run -v ./experiences:/mnt/experiences +$ mkdir experiences config +$ docker run -t xrforge docker.io/coderofsalvation/xrforge:latest -v ./config:/config -v ./experiences:/mnt/experiences ``` diff --git a/manyfold/cli/manyfold.sh b/manyfold/cli/manyfold.sh index 7ccee3b..bf61e20 100755 --- a/manyfold/cli/manyfold.sh +++ b/manyfold/cli/manyfold.sh @@ -6,6 +6,7 @@ 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 "$CORS_PROXY" || export CORS_PROXY=1 db=/config/manyfold.sqlite3 # utility funcs @@ -19,14 +20,15 @@ create_config(){ } run(){ - test -x ${oci} || { echo "warning: not running manyfold OCI container [install podman/docker/etc]"; exit; } - echocolor "[$APPNAME]" "$(basename ${oci}) detected..starting OCI container" - ${oci} rm -f xrforge - #-e NO_OVERLAYFS=true \ - #-e NO_DEFAULTDB=true \ - #-e PUBLIC_HOSTNAME=localhost \ - #-e PUBLIC_PORT=80 \ - debug ${oci} run "$@" -p 8790:3214 -p 8791:3215 --name xrforge \ + { + test -x ${oci} || { echo "warning: not running manyfold OCI container [install podman/docker/etc]"; exit; } + ${oci} rm -f xrforge + } 1>&2 + #-e NO_OVERLAYFS=true \ + #-e NO_DEFAULTDB=true \ + #-e PUBLIC_HOSTNAME=localhost \ + #-e PUBLIC_PORT=80 \ + echo ${oci} run "$@" -p 8790:3214 -p 8791:3215 --name xrforge \ -e SECRET_KEY_BASE=lkjwljlkwejrlkjek34k234l \ -e DATABASE_ADAPTER=sqlite3 \ -e FEDERATE_DRIVE_HOST=http://localhost:8791 \ @@ -124,7 +126,8 @@ set_upload_path(){ test -d $UPLOAD_PATH || mkdir $UPLOAD_PATH add_lib_to_db $UPLOAD_PATH id=$(sqlite3 $db "select id from libraries where path = '$UPLOAD_PATH';") - sqlite3 $db "UPDATE settings set value = $id where var = 'default_library';" + sqlite3 $db "INSERT INTO settings VALUES(6,'default_library',replace('--- $id\n','\n',char(10); + INSERT INTO sqlite_sequence VALUES('settings',6);" } mount_dir(){ @@ -192,8 +195,7 @@ set_homepage(){ rename_app(){ echocolor "[$APPNAME]" "renaming manyfold to $APPNAME" sed -i 's/title: Manyfold/title: '$APPNAME'/g' /usr/src/app/config/locales/*.yml - sed -i 's|powered_by_html:.*|powered_by_html: Radically opensource-powered by Manyfold, XR Fragments and NIX|g' /usr/src/app/config/locales/*.yml - + sed -i 's|powered_by_html:.*|powered_by_html: Radically opensource-powered by Manyfold, XR Fragments, JanusWeb and NIX|g' /usr/src/app/config/locales/*.yml sed -i 's|Models|Experiences|g' /usr/src/app/config/locales/*.yml /usr/src/app/config/locales/*/*.yml sed -i 's|Model|Experience|g' /usr/src/app/config/locales/*.yml /usr/src/app/config/locales/*/*.yml } @@ -208,7 +210,8 @@ start_syslog(){ scan_libraries(){ cd /usr/src/app echocolor "scanning libraries" - bin/manyfold libraries scan + test -f ${db}.startupscan || bin/manyfold libraries scan + touch ${db}.startupscan # only do once } scan_experience(){ @@ -250,7 +253,7 @@ init_database(){ set_admin set_global site_name "'$APPNAME'" set_global site_tagline "'$TAGLINE'" - set_global model_tags_auto_tag_new "replace('--- \"build \"\\n','\\n',char(10))" + set_global model_tags_auto_tag_new "replace('--- \"\"\\n','\\n',char(10))" set_global model_path_template "replace('--- \"{creator}/{modelId} \"\\n','\\n',char(10))" #set_global about "$ABOUT" mount_rclone diff --git a/manyfold/usr/src/app/controllers/cors_proxy.rb b/manyfold/usr/src/app/controllers/cors_proxy.rb new file mode 100644 index 0000000..735159d --- /dev/null +++ b/manyfold/usr/src/app/controllers/cors_proxy.rb @@ -0,0 +1,101 @@ +require 'net/http' +require 'uri' + +# Defines the controller logic for the CORS proxy endpoint. +# It handles all HTTP methods and forwards the request to the target URL +# provided as a path segment (e.g., /cors/https://example.com/data?q=1). +class CorsProxyController < ApplicationController + # Skip CSRF protection for this action, as it's intended to handle external requests + # that might not have a valid CSRF token. + skip_before_action :verify_authenticity_token + + # The main proxy action. + def proxy + # Assuming the route is configured as: match '/cors/*target_url_segment', ... + target_url_segment = params[:target_url_segment] + + # Reconstruct the full target URL. The globbing parameter captures the base path + # (e.g., "https://example.com/data"), and we re-attach the raw query string + # (e.g., "?q=1¶m=2") that the Rails router parsed separately. + query_string = request.query_string.present? ? "?#{request.query_string}" : "" + target_url = target_url_segment.to_s + query_string + + # 1. Input Validation + unless target_url.present? + return render json: { error: 'Target URL is required as a path segment after /cors/.' }, status: :bad_request + end + + begin + uri = URI.parse(target_url) + # Security check: Only allow standard HTTP/HTTPS schemes. + unless ['http', 'https'].include?(uri.scheme&.downcase) + return render json: { error: 'Invalid URL scheme. Only http and https are supported.' }, status: :bad_request + end + rescue URI::InvalidURIError + return render json: { error: 'Invalid URL format.' }, status: :bad_request + end + + # 2. Determine the appropriate Net::HTTP request class based on the incoming method + request_class = case request.method + when 'GET' then Net::HTTP::Get + when 'POST' then Net::HTTP::Post + when 'PUT' then Net::HTTP::Put + when 'DELETE' then Net::HTTP::Delete + when 'PATCH' then Net::HTTP::Patch + when 'HEAD' then Net::HTTP::Head + else + return render json: { error: "Unsupported HTTP method: #{request.method}" }, status: :method_not_allowed + end + + # 3. Initialize the outgoing request + # Use uri.request_uri which includes the path and query parameters of the target + outgoing_request = request_class.new(uri.request_uri) + + # 4. Copy relevant headers from the incoming request to the outgoing request + # We explicitly skip certain headers that are managed by the HTTP client or should not be proxied. + request.headers.each do |key, value| + header_key = key.sub(/^HTTP_/, '').underscore.dasherize.split('-').map(&:capitalize).join('-') + + # Skip internal/sensitive/managed headers + next if ['Host', 'Content-Length', 'Connection', 'Transfer-Encoding', 'X-Request-Id', 'X-Forwarded-For'].include?(header_key) + next if header_key.start_with?('X-') # Generally skip custom Rails internal headers + + # Pass all other headers (including Authorization, Content-Type, etc.) + outgoing_request[header_key] = value + end + + # 5. Handle request body for methods that carry a payload + if request.body.present? && !['GET', 'HEAD'].include?(request.method) + request.body.rewind # Ensure we read from the beginning of the stream + outgoing_request.body = request.body.read + end + + # 6. Execute the request + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = (uri.scheme == 'https') + + # Set reasonable timeouts + http.read_timeout = 15 + http.open_timeout = 5 + + target_response = http.request(outgoing_request) + + # 7. Proxy the response back to the client + # Copy headers from the target response to the client response + target_response.each_header do |key, value| + # Skip internal headers but ensure CORS headers (Access-Control-*) are passed + next if ['Transfer-Encoding', 'Connection', 'Content-Length'].include?(key.downcase) + response.headers[key] = value + end + + # Set status and send back the body and content type + render plain: target_response.body, + status: target_response.code.to_i, + content_type: target_response['Content-Type'] || 'text/plain' + + rescue StandardError => e + # Catch network errors (DNS failures, timeouts, etc.) + Rails.logger.error "CORS Proxy Error: #{e.class}: #{e.message}" + render json: { error: "Proxy request failed due to a server error: #{e.message}" }, status: :internal_server_error + end +end