mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-29 17:04:04 +09:00
Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0141eae018 | ||
|
|
c49c41a0d7 | ||
|
|
49f06d0b22 | ||
|
|
ed5a172871 | ||
|
|
8d3c2bbd2b | ||
|
|
03fa883f98 | ||
|
|
8e0aa7d636 | ||
|
|
efaae642b0 | ||
|
|
28525bec5d | ||
|
|
52ccd24efa | ||
|
|
c189ddea41 | ||
|
|
7ffb043cf3 | ||
|
|
17f81d4fa0 | ||
|
|
905165b7e2 | ||
|
|
ce62fd0814 | ||
|
|
47cdbb2a0f | ||
|
|
6ec04cbc28 | ||
|
|
e74d192aec | ||
|
|
67ca61da1f | ||
|
|
0febc9f415 | ||
|
|
4a75954080 | ||
|
|
69c94b6594 | ||
|
|
ba11a0a67f | ||
|
|
8c7f49e003 | ||
|
|
3fada3d997 | ||
|
|
dde94b06ed | ||
|
|
1852adb06c | ||
|
|
e983e3f79f | ||
|
|
53e36a16ea | ||
|
|
a05292f548 |
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
<% if (session_token = `op signin --account basecamp --raw`.strip) != "" %># Generated by mrsk envify
|
<% if (session_token = `op signin --account 23QPQDKZC5BKBIIG7UGT5GR5RM --raw`.strip) != "" %># Generated by mrsk envify
|
||||||
MRSK_REGISTRY_PASSWORD=<%= `op read "op://Deploy/Docker Hub MRSK/password" -n --session #{session_token}` %>
|
MRSK_REGISTRY_PASSWORD=<%= `op read "op://Deploy/Docker Hub MRSK/password" -n --session #{session_token}` %>
|
||||||
RAILS_MASTER_KEY=<%= `op read "op://Development/Chat-in-a-box/RAILS_MASTER_KEY" -n --session #{session_token}` %>
|
RAILS_MASTER_KEY=<%= `op read "op://Development/Chat-in-a-box/RAILS_MASTER_KEY" -n --session #{session_token}` %>
|
||||||
<% else raise ArgumentError, "Session token missing" end %>
|
<% else raise ArgumentError, "Session token missing" end %>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/"
|
||||||
|
groups:
|
||||||
|
github-actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
cooldown:
|
||||||
|
default-days: 7
|
||||||
|
|
||||||
|
- package-ecosystem: bundler
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
cooldown:
|
||||||
|
semver-major-days: 7
|
||||||
|
semver-minor-days: 3
|
||||||
|
semver-patch-days: 2
|
||||||
|
default-days: 7
|
||||||
|
|
||||||
|
- package-ecosystem: docker
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
cooldown:
|
||||||
|
semver-major-days: 7
|
||||||
|
semver-minor-days: 3
|
||||||
|
semver-patch-days: 2
|
||||||
|
default-days: 7
|
||||||
+47
-10
@@ -6,16 +6,22 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
scan:
|
scan:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||||
with:
|
with:
|
||||||
ruby-version: .ruby-version
|
ruby-version: .ruby-version
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
@@ -25,12 +31,16 @@ jobs:
|
|||||||
|
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||||
with:
|
with:
|
||||||
ruby-version: .ruby-version
|
ruby-version: .ruby-version
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
@@ -38,11 +48,32 @@ jobs:
|
|||||||
- name: Lint code for consistent style
|
- name: Lint code for consistent style
|
||||||
run: bin/rubocop
|
run: bin/rubocop
|
||||||
|
|
||||||
|
lint-actions:
|
||||||
|
name: GitHub Actions audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Run actionlint
|
||||||
|
uses: rhysd/actionlint@393031adb9afb225ee52ae2ccd7a5af5525e03e8 # v1.7.11
|
||||||
|
|
||||||
|
- name: Run zizmor
|
||||||
|
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||||
|
with:
|
||||||
|
advanced-security: false
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis # zizmor: ignore[unpinned-images] -- version tag is fine for service containers
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
@@ -51,10 +82,12 @@ jobs:
|
|||||||
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg
|
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||||
env:
|
env:
|
||||||
REDIS_URL: redis://localhost:6379/0
|
REDIS_URL: redis://localhost:6379/0
|
||||||
with:
|
with:
|
||||||
@@ -66,9 +99,11 @@ jobs:
|
|||||||
|
|
||||||
test_system:
|
test_system:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis # zizmor: ignore[unpinned-images] -- version tag is fine for service containers
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
@@ -77,10 +112,12 @@ jobs:
|
|||||||
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg
|
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||||
env:
|
env:
|
||||||
REDIS_URL: redis://localhost:6379/0
|
REDIS_URL: redis://localhost:6379/0
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ concurrency:
|
|||||||
group: publish-${{ github.workflow }}-${{ github.ref }}
|
group: publish-${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
id-token: write
|
|
||||||
attestations: write
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE_DESCRIPTION: Campfire is a web-based chat application with multiple rooms, direct messages, file attachments with previews, search, web push notifications, @mentions, and bot integrations. Single-tenant; production-ready image with web app, background jobs, caching, file serving, and SSL.
|
IMAGE_DESCRIPTION: Campfire is a web-based chat application with multiple rooms, direct messages, file attachments with previews, search, web push notifications, @mentions, and bot integrations. Single-tenant; production-ready image with web app, background jobs, caching, file serving, and SSL.
|
||||||
@@ -27,6 +23,11 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build and push image (${{ matrix.arch }})
|
name: Build and push image (${{ matrix.arch }})
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -43,13 +44,15 @@ jobs:
|
|||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.11.1
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@v3.5.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -66,7 +69,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract Docker metadata (tags, labels) with arch suffix
|
- name: Extract Docker metadata (tags, labels) with arch suffix
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5.8.0
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
with:
|
with:
|
||||||
images: ${{ steps.vars.outputs.canonical }}
|
images: ${{ steps.vars.outputs.canonical }}
|
||||||
tags: |
|
tags: |
|
||||||
@@ -84,7 +87,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push (${{ matrix.platform }})
|
- name: Build and push (${{ matrix.platform }})
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v6.18.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
@@ -102,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Attest image provenance (per-arch)
|
- name: Attest image provenance (per-arch)
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: actions/attest-build-provenance@v3.0.0
|
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ steps.vars.outputs.canonical }}
|
subject-name: ${{ steps.vars.outputs.canonical }}
|
||||||
subject-digest: ${{ steps.build.outputs.digest }}
|
subject-digest: ${{ steps.build.outputs.digest }}
|
||||||
@@ -113,16 +116,20 @@ jobs:
|
|||||||
needs: build
|
needs: build
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
id-token: write
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Docker Buildx (for imagetools)
|
- name: Set up Docker Buildx (for imagetools)
|
||||||
uses: docker/setup-buildx-action@v3.11.1
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@v3.5.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -139,7 +146,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Compute base tags (no suffix)
|
- name: Compute base tags (no suffix)
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5.8.0
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
with:
|
with:
|
||||||
images: ${{ steps.vars.outputs.canonical }}
|
images: ${{ steps.vars.outputs.canonical }}
|
||||||
tags: |
|
tags: |
|
||||||
@@ -157,9 +164,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Create multi-arch manifests
|
- name: Create multi-arch manifests
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
TAGS: ${{ steps.meta.outputs.tags }}
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
tags="${{ steps.meta.outputs.tags }}"
|
tags="$TAGS"
|
||||||
echo "Creating manifests for tags:"
|
echo "Creating manifests for tags:"
|
||||||
printf '%s\n' "$tags"
|
printf '%s\n' "$tags"
|
||||||
while IFS= read -r tag; do
|
while IFS= read -r tag; do
|
||||||
@@ -185,13 +194,15 @@ jobs:
|
|||||||
done <<< "$tags"
|
done <<< "$tags"
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.9.2
|
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||||
|
|
||||||
- name: Cosign sign all tags (keyless OIDC)
|
- name: Cosign sign all tags (keyless OIDC)
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
TAGS: ${{ steps.meta.outputs.tags }}
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
tags="${{ steps.meta.outputs.tags }}"
|
tags="$TAGS"
|
||||||
printf '%s\n' "$tags"
|
printf '%s\n' "$tags"
|
||||||
while IFS= read -r tag; do
|
while IFS= read -r tag; do
|
||||||
[ -z "$tag" ] && continue
|
[ -z "$tag" ] && continue
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ FROM base AS build
|
|||||||
|
|
||||||
# Install packages need to build gems
|
# Install packages need to build gems
|
||||||
RUN apt-get update -qq && \
|
RUN apt-get update -qq && \
|
||||||
apt-get install -y build-essential git pkg-config libyaml-dev && \
|
apt-get install -y build-essential git pkg-config libyaml-dev libssl-dev && \
|
||||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||||
|
|
||||||
# Install application gems
|
# Install application gems
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ gem "sqlite3"
|
|||||||
gem "redis", "~> 5.4"
|
gem "redis", "~> 5.4"
|
||||||
|
|
||||||
# Deployment
|
# Deployment
|
||||||
gem "puma", "~> 6.6"
|
gem "puma", "~> 7.2", ">= 7.2.1"
|
||||||
|
|
||||||
# Jobs
|
# Jobs
|
||||||
gem "resque", "~> 2.7.0"
|
gem "resque", "~> 2.7.0"
|
||||||
@@ -48,6 +48,7 @@ gem "thruster"
|
|||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem "debug"
|
gem "debug"
|
||||||
gem "rubocop-rails-omakase", require: false
|
gem "rubocop-rails-omakase", require: false
|
||||||
|
gem "bundler-audit", require: false
|
||||||
gem "faker", require: false
|
gem "faker", require: false
|
||||||
gem "brakeman", require: false
|
gem "brakeman", require: false
|
||||||
end
|
end
|
||||||
|
|||||||
+31
-27
@@ -128,18 +128,21 @@ GIT
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
action_text-trix (2.1.15)
|
action_text-trix (2.1.19)
|
||||||
railties
|
railties
|
||||||
addressable (2.8.7)
|
addressable (2.9.0)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
ast (2.4.3)
|
ast (2.4.3)
|
||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.22)
|
||||||
benchmark (0.5.0)
|
benchmark (0.5.0)
|
||||||
bigdecimal (3.3.1)
|
bigdecimal (3.3.1)
|
||||||
brakeman (7.1.1)
|
brakeman (8.0.4)
|
||||||
racc
|
racc
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
|
bundler-audit (0.9.3)
|
||||||
|
bundler (>= 1.2.0)
|
||||||
|
thor (~> 1.0)
|
||||||
capybara (3.40.0)
|
capybara (3.40.0)
|
||||||
addressable
|
addressable
|
||||||
matrix
|
matrix
|
||||||
@@ -161,7 +164,7 @@ GEM
|
|||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
drb (2.2.3)
|
drb (2.2.3)
|
||||||
erb (6.0.0)
|
erb (6.0.4)
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
faker (3.5.2)
|
faker (3.5.2)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
@@ -189,7 +192,7 @@ GEM
|
|||||||
actionview (>= 7.0.0)
|
actionview (>= 7.0.0)
|
||||||
activesupport (>= 7.0.0)
|
activesupport (>= 7.0.0)
|
||||||
json (2.13.2)
|
json (2.13.2)
|
||||||
jwt (3.1.2)
|
jwt (3.2.0)
|
||||||
base64
|
base64
|
||||||
kredis (1.8.0)
|
kredis (1.8.0)
|
||||||
activemodel (>= 6.0.0)
|
activemodel (>= 6.0.0)
|
||||||
@@ -221,7 +224,7 @@ GEM
|
|||||||
ruby2_keywords (~> 0.0.1)
|
ruby2_keywords (~> 0.0.1)
|
||||||
net-http-persistent (4.0.6)
|
net-http-persistent (4.0.6)
|
||||||
connection_pool (~> 2.2, >= 2.2.4)
|
connection_pool (~> 2.2, >= 2.2.4)
|
||||||
net-imap (0.5.12)
|
net-imap (0.6.4)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
@@ -231,13 +234,13 @@ GEM
|
|||||||
net-smtp (0.5.1)
|
net-smtp (0.5.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.5)
|
nio4r (2.7.5)
|
||||||
nokogiri (1.18.10-aarch64-linux-gnu)
|
nokogiri (1.19.3-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.10-arm64-darwin)
|
nokogiri (1.19.3-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.10-x86_64-darwin)
|
nokogiri (1.19.3-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.10-x86_64-linux-gnu)
|
nokogiri (1.19.3-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
openssl (3.3.0)
|
openssl (3.3.0)
|
||||||
ostruct (0.6.3)
|
ostruct (0.6.3)
|
||||||
@@ -256,15 +259,15 @@ GEM
|
|||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (6.0.2)
|
public_suffix (6.0.2)
|
||||||
puma (6.6.1)
|
puma (7.2.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.2.4)
|
rack (3.2.6)
|
||||||
rack-protection (4.1.1)
|
rack-protection (4.2.1)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
logger (>= 1.6.0)
|
logger (>= 1.6.0)
|
||||||
rack (>= 3.0.0, < 4)
|
rack (>= 3.0.0, < 4)
|
||||||
rack-session (2.1.1)
|
rack-session (2.1.2)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.2.0)
|
rack-test (2.2.0)
|
||||||
@@ -305,12 +308,12 @@ GEM
|
|||||||
resque-pool (0.7.1)
|
resque-pool (0.7.1)
|
||||||
rake (>= 10.0, < 14.0)
|
rake (>= 10.0, < 14.0)
|
||||||
resque (>= 1.22, < 3)
|
resque (>= 1.22, < 3)
|
||||||
rexml (3.4.1)
|
rexml (3.4.4)
|
||||||
rqrcode (3.1.0)
|
rqrcode (3.2.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rqrcode_core (~> 2.0)
|
rqrcode_core (~> 2.0)
|
||||||
rqrcode_core (2.0.0)
|
rqrcode_core (2.1.0)
|
||||||
rubocop (1.80.0)
|
rubocop (1.80.2)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
lint_roller (~> 1.1.0)
|
lint_roller (~> 1.1.0)
|
||||||
@@ -357,11 +360,11 @@ GEM
|
|||||||
sentry-ruby (5.26.0)
|
sentry-ruby (5.26.0)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sinatra (4.1.1)
|
sinatra (4.2.1)
|
||||||
logger (>= 1.6.0)
|
logger (>= 1.6.0)
|
||||||
mustermann (~> 3.0)
|
mustermann (~> 3.0)
|
||||||
rack (>= 3.0.0, < 4)
|
rack (>= 3.0.0, < 4)
|
||||||
rack-protection (= 4.1.1)
|
rack-protection (= 4.2.1)
|
||||||
rack-session (>= 2.0.0, < 3)
|
rack-session (>= 2.0.0, < 3)
|
||||||
tilt (~> 2.0)
|
tilt (~> 2.0)
|
||||||
sqlite3 (2.7.3-aarch64-linux-gnu)
|
sqlite3 (2.7.3-aarch64-linux-gnu)
|
||||||
@@ -381,9 +384,9 @@ GEM
|
|||||||
tsort (0.2.0)
|
tsort (0.2.0)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
unicode-display_width (3.1.5)
|
unicode-display_width (3.2.0)
|
||||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
unicode-emoji (~> 4.1)
|
||||||
unicode-emoji (4.0.4)
|
unicode-emoji (4.2.0)
|
||||||
uri (1.1.1)
|
uri (1.1.1)
|
||||||
useragent (0.16.11)
|
useragent (0.16.11)
|
||||||
web-push (3.0.2)
|
web-push (3.0.2)
|
||||||
@@ -412,6 +415,7 @@ DEPENDENCIES
|
|||||||
bcrypt
|
bcrypt
|
||||||
benchmark
|
benchmark
|
||||||
brakeman
|
brakeman
|
||||||
|
bundler-audit
|
||||||
capybara
|
capybara
|
||||||
debug
|
debug
|
||||||
faker
|
faker
|
||||||
@@ -425,7 +429,7 @@ DEPENDENCIES
|
|||||||
ostruct
|
ostruct
|
||||||
platform_agent
|
platform_agent
|
||||||
propshaft!
|
propshaft!
|
||||||
puma (~> 6.6)
|
puma (~> 7.2, >= 7.2.1)
|
||||||
rails!
|
rails!
|
||||||
rails_autolink
|
rails_autolink
|
||||||
redis (~> 5.4)
|
redis (~> 5.4)
|
||||||
@@ -444,4 +448,4 @@ DEPENDENCIES
|
|||||||
webmock
|
webmock
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.9
|
4.0.13
|
||||||
@@ -67,17 +67,27 @@ module Authentication
|
|||||||
authenticated_as session
|
authenticated_as session
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def terminate_current_session
|
||||||
|
Current.session&.destroy!
|
||||||
|
reset_session
|
||||||
|
remove_authentication_cookie
|
||||||
|
end
|
||||||
|
|
||||||
def authenticated_as(session)
|
def authenticated_as(session)
|
||||||
Current.user = session.user
|
Current.session = session
|
||||||
set_authenticated_by(:session)
|
set_authenticated_by(:session)
|
||||||
cookies.signed.permanent[:session_token] = { value: session.token, httponly: true, same_site: :lax }
|
set_authentication_cookie(session)
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_authenticating_url
|
def post_authenticating_url
|
||||||
session.delete(:return_to_after_authenticating) || root_url
|
session.delete(:return_to_after_authenticating) || root_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_authentication
|
def set_authentication_cookie(session)
|
||||||
|
cookies.signed.permanent[:session_token] = { value: session.token, httponly: true, same_site: :lax }
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_authentication_cookie
|
||||||
cookies.delete(:session_token)
|
cookies.delete(:session_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class SessionsController < ApplicationController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
remove_push_subscription
|
remove_push_subscription
|
||||||
reset_authentication
|
terminate_current_session
|
||||||
redirect_to root_url
|
redirect_to root_url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class Users::SidebarsController < ApplicationController
|
|||||||
|
|
||||||
def find_direct_placeholder_users
|
def find_direct_placeholder_users
|
||||||
exclude_user_ids = user_ids_already_in_direct_rooms_with_current_user.including(Current.user.id)
|
exclude_user_ids = user_ids_already_in_direct_rooms_with_current_user.including(Current.user.id)
|
||||||
User.active.where.not(id: exclude_user_ids).order(:created_at).limit(DIRECT_PLACEHOLDERS - exclude_user_ids.count)
|
User.active.where.not(id: exclude_user_ids).order(:created_at).limit([ DIRECT_PLACEHOLDERS - exclude_user_ids.count, 0 ].max)
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_ids_already_in_direct_rooms_with_current_user
|
def user_ids_already_in_direct_rooms_with_current_user
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
class Current < ActiveSupport::CurrentAttributes
|
class Current < ActiveSupport::CurrentAttributes
|
||||||
attribute :user, :request
|
attribute :session, :user, :request
|
||||||
|
|
||||||
delegate :host, :protocol, to: :request, prefix: true, allow_nil: true
|
delegate :host, :protocol, to: :request, prefix: true, allow_nil: true
|
||||||
|
|
||||||
|
def session=(value)
|
||||||
|
super(value)
|
||||||
|
|
||||||
|
if value.present?
|
||||||
|
self.user = session.user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def account
|
def account
|
||||||
Account.first
|
Account.first
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -60,7 +60,10 @@ echo
|
|||||||
if command -v brew &>/dev/null; then
|
if command -v brew &>/dev/null; then
|
||||||
step "Installing packages" brew install sqlite ffmpeg mise
|
step "Installing packages" brew install sqlite ffmpeg mise
|
||||||
elif command -v pacman &>/dev/null; then
|
elif command -v pacman &>/dev/null; then
|
||||||
step "Installing packages" sudo pacman -S --noconfirm --needed sqlite ffmpeg mise
|
packages=(sqlite ffmpeg mise)
|
||||||
|
if ! pacman -Q "${packages[@]}" >/dev/null 2>&1; then
|
||||||
|
step "Installing packages" sudo pacman -S --noconfirm --needed "${packages[@]}"
|
||||||
|
fi
|
||||||
elif command -v apt &>/dev/null; then
|
elif command -v apt &>/dev/null; then
|
||||||
step "Installing packages" sudo apt-get install --no-install-recommends -y libsqlite3-0 ffmpeg
|
step "Installing packages" sudo apt-get install --no-install-recommends -y libsqlite3-0 ffmpeg
|
||||||
fi
|
fi
|
||||||
@@ -100,6 +103,20 @@ if ! redis_running; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Install GitHub Actions linting tools
|
||||||
|
for tool in actionlint shellcheck zizmor; do
|
||||||
|
if ! command -v "$tool" &> /dev/null; then
|
||||||
|
if command -v brew &> /dev/null; then
|
||||||
|
step "Installing $tool" brew install "$tool"
|
||||||
|
elif command -v pacman &> /dev/null; then
|
||||||
|
step "Installing $tool" sudo pacman -S --noconfirm "$tool"
|
||||||
|
else
|
||||||
|
echo "Error: install $tool manually" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
step "Cleaning up logs and tempfiles" rails log:clear tmp:clear
|
step "Cleaning up logs and tempfiles" rails log:clear tmp:clear
|
||||||
|
|
||||||
step "Restarting services" rails restart
|
step "Restarting services" rails restart
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ CI.run do
|
|||||||
step "Setup", "bin/setup --skip-server"
|
step "Setup", "bin/setup --skip-server"
|
||||||
|
|
||||||
step "Style: Ruby", "bin/rubocop"
|
step "Style: Ruby", "bin/rubocop"
|
||||||
|
step "Style: GitHub Actions (actionlint)", "actionlint"
|
||||||
|
step "Style: GitHub Actions (zizmor)", "zizmor ."
|
||||||
|
|
||||||
step "Security: Gem audit", "bin/bundler-audit"
|
step "Security: Gem audit", "bin/bundler-audit"
|
||||||
step "Security: Importmap vulnerability audit", "bin/importmap audit"
|
step "Security: Importmap vulnerability audit", "bin/importmap audit"
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
|||||||
pin "@hotwired/turbo-rails", to: "turbo.js"
|
pin "@hotwired/turbo-rails", to: "turbo.js"
|
||||||
pin "@rails/actioncable", to: "actioncable.esm.js"
|
pin "@rails/actioncable", to: "actioncable.esm.js"
|
||||||
pin "@rails/request.js", to: "@rails--request.js" # @0.0.8
|
pin "@rails/request.js", to: "@rails--request.js" # @0.0.8
|
||||||
pin "trix", to: "trix.esm.min.js" # @2.0.10
|
pin "trix", to: "trix.esm.min.js" # @2.1.19
|
||||||
pin "@rails/actiontext", to: "actiontext.js"
|
pin "@rails/actiontext", to: "actiontext.js"
|
||||||
pin "highlight.js", to: "highlight.js/core.js"
|
pin "highlight.js", to: "highlight.js/core.js"
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,30 @@ ActiveSupport.on_load(:active_storage_blob) do
|
|||||||
ActiveStorage::DiskController.after_action only: :show do
|
ActiveStorage::DiskController.after_action only: :show do
|
||||||
response.set_header("Cache-Control", "max-age=3600, public")
|
response.set_header("Cache-Control", "max-age=3600, public")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Gate the ActiveStorage write path behind app authentication. These endpoints
|
||||||
|
# ship unauthenticated by Rails default; Campfire never uses direct upload for
|
||||||
|
# legit attachments (those go through MessagesController#create, and Trix file
|
||||||
|
# drops are disabled in the composer). Requiring a session on the write actions
|
||||||
|
# blocks anonymous blob writes and disk-fill while leaving blob serving public.
|
||||||
|
#
|
||||||
|
# ActiveStorage controllers live in ActiveStorage::Engine, so they see the
|
||||||
|
# engine's url helpers, not the main app's. Include the application helpers
|
||||||
|
# first so Authentication#request_authentication can redirect to new_session_url.
|
||||||
|
ActiveStorage::DirectUploadsController.include Rails.application.routes.url_helpers
|
||||||
|
ActiveStorage::DirectUploadsController.include Authentication # only action: #create
|
||||||
|
|
||||||
|
ActiveStorage::DiskController.include Rails.application.routes.url_helpers
|
||||||
|
ActiveStorage::DiskController.include Authentication
|
||||||
|
|
||||||
|
# Blob serving (#show) stays public so signed-token attachment URLs keep
|
||||||
|
# resolving for unauthenticated and bot clients alike.
|
||||||
|
ActiveStorage::DiskController.allow_unauthenticated_access only: :show
|
||||||
|
ActiveStorage::DiskController.allow_bot_access only: :show
|
||||||
|
|
||||||
|
# Including Authentication re-adds protect_from_forgery, but Active Storage's
|
||||||
|
# direct-upload service PUT (#update) carries only signed service headers and
|
||||||
|
# no authenticity token. Re-exempt it from CSRF so authenticated uploads can
|
||||||
|
# still store bytes; the signed URL token and the session check remain.
|
||||||
|
ActiveStorage::DiskController.skip_forgery_protection only: :update
|
||||||
end
|
end
|
||||||
@@ -16,7 +16,7 @@ module RestrictedHTTP
|
|||||||
|
|
||||||
def private_ip?(ip)
|
def private_ip?(ip)
|
||||||
IPAddr.new(ip).then do |ipaddr|
|
IPAddr.new(ip).then do |ipaddr|
|
||||||
ipaddr.private? || ipaddr.loopback? || ipaddr.link_local? || ipaddr.ipv4_mapped? || LOCAL_IP.include?(ipaddr)
|
ipaddr.private? || ipaddr.loopback? || ipaddr.link_local? || ipaddr.ipv4_mapped? || ipaddr.ipv4_compat? || LOCAL_IP.include?(ipaddr)
|
||||||
end
|
end
|
||||||
rescue IPAddr::InvalidAddressError
|
rescue IPAddr::InvalidAddressError
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class ActiveStorage::DirectUploadsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
setup do
|
||||||
|
host! "once.campfire.test"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create requires authentication" do
|
||||||
|
assert_no_difference -> { ActiveStorage::Blob.count } do
|
||||||
|
post rails_direct_uploads_url, params: { blob: blob_params }
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to new_session_url
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create succeeds for an authenticated session" do
|
||||||
|
sign_in :david
|
||||||
|
|
||||||
|
assert_difference -> { ActiveStorage::Blob.count }, 1 do
|
||||||
|
post rails_direct_uploads_url, params: { blob: blob_params }
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "disk show stays reachable without authentication" do
|
||||||
|
blob = ActiveStorage::Blob.create_and_upload! \
|
||||||
|
io: StringIO.new("hello"), filename: "hello.txt", content_type: "text/plain"
|
||||||
|
|
||||||
|
ActiveStorage::Current.set(url_options: { host: "once.campfire.test", protocol: "http" }) do
|
||||||
|
get blob.url
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "disk update requires authentication" do
|
||||||
|
_blob, url = direct_upload_url_for("hello")
|
||||||
|
|
||||||
|
put url, params: "hello", headers: { "Content-Type" => "text/plain" }
|
||||||
|
|
||||||
|
assert_redirected_to new_session_url
|
||||||
|
end
|
||||||
|
|
||||||
|
test "disk update stores bytes for an authenticated session without a CSRF token" do
|
||||||
|
sign_in :david
|
||||||
|
blob, url = direct_upload_url_for("hello")
|
||||||
|
|
||||||
|
# Forgery protection is off in test by default; turn it on so this proves the
|
||||||
|
# signed-token service PUT stays CSRF-exempt even when the concern re-arms it.
|
||||||
|
with_forgery_protection do
|
||||||
|
put url, params: "hello", headers: { "Content-Type" => "text/plain" }
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :no_content
|
||||||
|
assert_equal "hello", blob.download
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def blob_params
|
||||||
|
content = "hello"
|
||||||
|
{ filename: "hello.txt", byte_size: content.bytesize, content_type: "text/plain", \
|
||||||
|
checksum: Digest::MD5.base64digest(content) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def direct_upload_url_for(content)
|
||||||
|
blob = ActiveStorage::Blob.create_before_direct_upload! \
|
||||||
|
filename: "hello.txt", byte_size: content.bytesize, \
|
||||||
|
checksum: Digest::MD5.base64digest(content), content_type: "text/plain"
|
||||||
|
url = ActiveStorage::Current.set(url_options: { host: "once.campfire.test", protocol: "http" }) do
|
||||||
|
blob.service_url_for_direct_upload
|
||||||
|
end
|
||||||
|
[ blob, url ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_forgery_protection
|
||||||
|
original = ActionController::Base.allow_forgery_protection
|
||||||
|
ActionController::Base.allow_forgery_protection = true
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
ActionController::Base.allow_forgery_protection = original
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -28,7 +28,9 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "create with valid credentials" do
|
test "create with valid credentials" do
|
||||||
post session_url, params: { email_address: "david@37signals.com", password: "secret123456" }
|
assert_difference -> { Session.count }, +1 do
|
||||||
|
post session_url, params: { email_address: "david@37signals.com", password: "secret123456" }
|
||||||
|
end
|
||||||
|
|
||||||
assert_redirected_to root_url
|
assert_redirected_to root_url
|
||||||
assert parsed_cookies.signed[:session_token]
|
assert parsed_cookies.signed[:session_token]
|
||||||
@@ -43,11 +45,15 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
test "destroy" do
|
test "destroy" do
|
||||||
sign_in :david
|
sign_in :david
|
||||||
|
session = users(:david).sessions.last
|
||||||
|
|
||||||
delete session_url
|
assert_difference -> { Session.count }, -1 do
|
||||||
|
delete session_url
|
||||||
|
end
|
||||||
|
|
||||||
assert_redirected_to root_url
|
assert_redirected_to root_url
|
||||||
assert_not cookies[:session_token].present?
|
assert_not cookies[:session_token].present?
|
||||||
|
assert_nil Session.find_by(id: session.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "destroy removes the push subscription for the device" do
|
test "destroy removes the push subscription for the device" do
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
require "test_helper"
|
||||||
|
require "restricted_http/private_network_guard"
|
||||||
|
|
||||||
|
class RestrictedHTTP::PrivateNetworkGuardTest < ActiveSupport::TestCase
|
||||||
|
test "private_ip? returns true for 'This' network (RFC1700)" do
|
||||||
|
assert_private_ip "0.0.0.0"
|
||||||
|
assert_private_ip "0.255.255.255"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for loopback addresses" do
|
||||||
|
assert_private_ip "127.0.0.0"
|
||||||
|
assert_private_ip "127.0.0.1"
|
||||||
|
assert_private_ip "127.255.255.255"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for RFC1918 private addresses" do
|
||||||
|
assert_private_ip "10.0.0.0"
|
||||||
|
assert_private_ip "10.255.255.255"
|
||||||
|
assert_private_ip "172.16.0.0"
|
||||||
|
assert_private_ip "172.31.255.255"
|
||||||
|
assert_private_ip "192.168.0.0"
|
||||||
|
assert_private_ip "192.168.255.255"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for link-local addresses" do
|
||||||
|
assert_private_ip "169.254.0.1"
|
||||||
|
assert_private_ip "169.254.169.254" # AWS IMDS
|
||||||
|
assert_private_ip "169.254.255.255"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns false for public addresses" do
|
||||||
|
assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("93.184.216.34")
|
||||||
|
assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("8.8.8.8")
|
||||||
|
end
|
||||||
|
|
||||||
|
# IPv6 address format tests (SSRF bypass prevention)
|
||||||
|
|
||||||
|
test "private_ip? returns true for IPv4-mapped IPv6 addresses with private IPs" do
|
||||||
|
assert_private_ip "::ffff:192.168.1.1"
|
||||||
|
assert_private_ip "::ffff:10.0.0.1"
|
||||||
|
assert_private_ip "::ffff:172.16.0.1"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for IPv4-mapped IPv6 addresses with link-local IPs" do
|
||||||
|
assert_private_ip "::ffff:169.254.169.254" # AWS metadata via mapped format
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for IPv4-mapped IPv6 addresses even with public IPs" do
|
||||||
|
# Block all ipv4_mapped? since DNS never returns this format legitimately
|
||||||
|
assert_private_ip "::ffff:93.184.216.34"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for IPv4-compatible IPv6 addresses with private IPs" do
|
||||||
|
assert_private_ip "::192.168.1.1"
|
||||||
|
assert_private_ip "::10.0.0.1"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for IPv4-compatible IPv6 addresses with link-local IPs" do
|
||||||
|
assert_private_ip "::169.254.169.254" # AWS metadata via compat format - the reported bypass
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for IPv4-compatible IPv6 addresses even with public IPs" do
|
||||||
|
# Block all ipv4_compat? since DNS never returns this format legitimately
|
||||||
|
assert_private_ip "::93.184.216.34"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "private_ip? returns true for invalid addresses" do
|
||||||
|
assert RestrictedHTTP::PrivateNetworkGuard.private_ip?("not-an-ip")
|
||||||
|
assert RestrictedHTTP::PrivateNetworkGuard.private_ip?("")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "resolve raises Violation for private hostname" do
|
||||||
|
Resolv.stubs(:getaddress).returns("192.168.1.1")
|
||||||
|
assert_raises RestrictedHTTP::Violation do
|
||||||
|
RestrictedHTTP::PrivateNetworkGuard.resolve("private.example.com")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "resolve returns IP for public hostname" do
|
||||||
|
Resolv.stubs(:getaddress).returns("93.184.216.34")
|
||||||
|
assert_equal "93.184.216.34", RestrictedHTTP::PrivateNetworkGuard.resolve("example.com")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def assert_private_ip(address)
|
||||||
|
assert RestrictedHTTP::PrivateNetworkGuard.private_ip?(address),
|
||||||
|
"Expected #{address} to be classified as private"
|
||||||
|
end
|
||||||
|
end
|
||||||
Vendored
+4
-3
@@ -1,6 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
Trix 2.0.10
|
Trix 2.1.19
|
||||||
Copyright © 2024 37signals, LLC
|
Copyright © 2026 37signals, LLC
|
||||||
*/
|
*/
|
||||||
var t="2.0.10";const e="[data-trix-attachment]",i={preview:{presentation:"gallery",caption:{name:!0,size:!0}},file:{caption:{size:!0}}},n={default:{tagName:"div",parse:!1},quote:{tagName:"blockquote",nestable:!0},heading1:{tagName:"h1",terminal:!0,breakOnReturn:!0,group:!1},code:{tagName:"pre",terminal:!0,text:{plaintext:!0}},bulletList:{tagName:"ul",parse:!1},bullet:{tagName:"li",listAttribute:"bulletList",group:!1,nestable:!0,test(t){return r(t.parentNode)===n[this.listAttribute].tagName}},numberList:{tagName:"ol",parse:!1},number:{tagName:"li",listAttribute:"numberList",group:!1,nestable:!0,test(t){return r(t.parentNode)===n[this.listAttribute].tagName}},attachmentGallery:{tagName:"div",exclusive:!0,terminal:!0,parse:!1,group:!1}},r=t=>{var e;return null==t||null===(e=t.tagName)||void 0===e?void 0:e.toLowerCase()},o=navigator.userAgent.match(/android\s([0-9]+.*Chrome)/i),s=o&&parseInt(o[1]);var a={composesExistingText:/Android.*Chrome/.test(navigator.userAgent),recentAndroid:s&&s>12,samsungAndroid:s&&navigator.userAgent.match(/Android.*SM-/),forcesObjectResizing:/Trident.*rv:11/.test(navigator.userAgent),supportsInputEvents:"undefined"!=typeof InputEvent&&["data","getTargetRanges","inputType"].every((t=>t in InputEvent.prototype))},l={attachFiles:"Attach Files",bold:"Bold",bullets:"Bullets",byte:"Byte",bytes:"Bytes",captionPlaceholder:"Add a caption…",code:"Code",heading1:"Heading",indent:"Increase Level",italic:"Italic",link:"Link",numbers:"Numbers",outdent:"Decrease Level",quote:"Quote",redo:"Redo",remove:"Remove",strike:"Strikethrough",undo:"Undo",unlink:"Unlink",url:"URL",urlPlaceholder:"Enter a URL…",GB:"GB",KB:"KB",MB:"MB",PB:"PB",TB:"TB"};const c=[l.bytes,l.KB,l.MB,l.GB,l.TB,l.PB];var h={prefix:"IEC",precision:2,formatter(t){switch(t){case 0:return"0 ".concat(l.bytes);case 1:return"1 ".concat(l.byte);default:let e;"SI"===this.prefix?e=1e3:"IEC"===this.prefix&&(e=1024);const i=Math.floor(Math.log(t)/Math.log(e)),n=(t/Math.pow(e,i)).toFixed(this.precision).replace(/0*$/,"").replace(/\.$/,"");return"".concat(n," ").concat(c[i])}}};const u="\ufeff",d=" ",g=function(t){for(const e in t){const i=t[e];this[e]=i}return this},m=document.documentElement,p=m.matches,f=function(t){let{onElement:e,matchingSelector:i,withCallback:n,inPhase:r,preventDefault:o,times:s}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const a=e||m,l=i,c="capturing"===r,h=function(t){null!=s&&0==--s&&h.destroy();const e=A(t.target,{matchingSelector:l});null!=e&&(null==n||n.call(e,t,e),o&&t.preventDefault())};return h.destroy=()=>a.removeEventListener(t,h,c),a.addEventListener(t,h,c),h},b=function(t){let{onElement:e,bubbles:i,cancelable:n,attributes:r}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const o=null!=e?e:m;i=!1!==i,n=!1!==n;const s=document.createEvent("Events");return s.initEvent(t,i,n),null!=r&&g.call(s,r),o.dispatchEvent(s)},v=function(t,e){if(1===(null==t?void 0:t.nodeType))return p.call(t,e)},A=function(t){let{matchingSelector:e,untilNode:i}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};for(;t&&t.nodeType!==Node.ELEMENT_NODE;)t=t.parentNode;if(null!=t){if(null==e)return t;if(t.closest&&null==i)return t.closest(e);for(;t&&t!==i;){if(v(t,e))return t;t=t.parentNode}}},x=t=>document.activeElement!==t&&y(t,document.activeElement),y=function(t,e){if(t&&e)for(;e;){if(e===t)return!0;e=e.parentNode}},C=function(t){var e;if(null===(e=t)||void 0===e||!e.parentNode)return;let i=0;for(t=t.previousSibling;t;)i++,t=t.previousSibling;return i},R=t=>{var e;return null==t||null===(e=t.parentNode)||void 0===e?void 0:e.removeChild(t)},S=function(t){let{onlyNodesOfType:e,usingFilter:i,expandEntityReferences:n}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const r=(()=>{switch(e){case"element":return NodeFilter.SHOW_ELEMENT;case"text":return NodeFilter.SHOW_TEXT;case"comment":return NodeFilter.SHOW_COMMENT;default:return NodeFilter.SHOW_ALL}})();return document.createTreeWalker(t,r,null!=i?i:null,!0===n)},E=t=>{var e;return null==t||null===(e=t.tagName)||void 0===e?void 0:e.toLowerCase()},k=function(t){let e,i,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};"object"==typeof t?(n=t,t=n.tagName):n={attributes:n};const r=document.createElement(t);if(null!=n.editable&&(null==n.attributes&&(n.attributes={}),n.attributes.contenteditable=n.editable),n.attributes)for(e in n.attributes)i=n.attributes[e],r.setAttribute(e,i);if(n.style)for(e in n.style)i=n.style[e],r.style[e]=i;if(n.data)for(e in n.data)i=n.data[e],r.dataset[e]=i;return n.className&&n.className.split(" ").forEach((t=>{r.classList.add(t)})),n.textContent&&(r.textContent=n.textContent),n.childNodes&&[].concat(n.childNodes).forEach((t=>{r.appendChild(t)})),r};let L;const D=function(){if(null!=L)return L;L=[];for(const t in n){const e=n[t];e.tagName&&L.push(e.tagName)}return L},w=t=>B(null==t?void 0:t.firstChild),T=function(t){let{strict:e}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{strict:!0};return e?B(t):B(t)Line truncated
|
var t="2.1.19";const e="[data-trix-attachment]",i={preview:{presentation:"gallery",caption:{name:!0,size:!0}},file:{caption:{size:!0}}},n={default:{tagName:"div",parse:!1},quote:{tagName:"blockquote",nestable:!0},heading1:{tagName:"h1",terminal:!0,breakOnReturn:!0,group:!1},code:{tagName:"pre",terminal:!0,htmlAttributes:["language"],text:{plaintext:!0}},bulletList:{tagName:"ul",parse:!1},bullet:{tagName:"li",listAttribute:"bulletList",group:!1,nestable:!0,test(t){return r(t.parentNode)===n[this.listAttribute].tagName}},numberList:{tagName:"ol",parse:!1},number:{tagName:"li",listAttribute:"numberList",group:!1,nestable:!0,test(t){return r(t.parentNode)===n[this.listAttribute].tagName}},attachmentGallery:{tagName:"div",exclusive:!0,terminal:!0,parse:!1,group:!1}},r=t=>{var e;return null==t||null===(e=t.tagName)||void 0===e?void 0:e.toLowerCase()},o=navigator.userAgent.match(/android\s([0-9]+.*Chrome)/i),s=o&&parseInt(o[1]);var a={composesExistingText:/Android.*Chrome/.test(navigator.userAgent),recentAndroid:s&&s>12,samsungAndroid:s&&navigator.userAgent.match(/Android.*SM-/),forcesObjectResizing:/Trident.*rv:11/.test(navigator.userAgent),supportsInputEvents:"undefined"!=typeof InputEvent&&["data","getTargetRanges","inputType"].every((t=>t in InputEvent.prototype))},l={ADD_ATTR:["language"],SAFE_FOR_XML:!1,RETURN_DOM:!0},c={attachFiles:"Attach Files",bold:"Bold",bullets:"Bullets",byte:"Byte",bytes:"Bytes",captionPlaceholder:"Add a caption…",code:"Code",heading1:"Heading",indent:"Increase Level",italic:"Italic",link:"Link",numbers:"Numbers",outdent:"Decrease Level",quote:"Quote",redo:"Redo",remove:"Remove",strike:"Strikethrough",undo:"Undo",unlink:"Unlink",url:"URL",urlPlaceholder:"Enter a URL…",GB:"GB",KB:"KB",MB:"MB",PB:"PB",TB:"TB"};const u=[c.bytes,c.KB,c.MB,c.GB,c.TB,c.PB];var h={prefix:"IEC",precision:2,formatter(t){switch(t){case 0:return"0 ".concat(c.bytes);case 1:return"1 ".concat(c.byte);default:let e;"SI"===this.prefix?e=1e3:"IEC"===this.prefix&&(e=1024);const i=Math.floor(Math.log(t)/Math.log(e)),n=(t/Math.pow(e,i)).toFixed(this.precision).replace(/0*$/,"").replace(/\.$/,"");return"".concat(n," ").concat(u[i])}}};const d="\ufeff",g=" ",m=function(t){for(const e in t){const i=t[e];this[e]=i}return this},p=document.documentElement,f=p.matches,b=function(t){let{onElement:e,matchingSelector:i,withCallback:n,inPhase:r,preventDefault:o,times:s}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const a=e||p,l=i,c="capturing"===r,u=function(t){null!=s&&0==--s&&u.destroy();const e=x(t.target,{matchingSelector:l});null!=e&&(null==n||n.call(e,t,e),o&&t.preventDefault())};return u.destroy=()=>a.removeEventListener(t,u,c),a.addEventListener(t,u,c),u},v=function(t){let{bubbles:e,cancelable:i,attributes:n}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};e=!1!==e,i=!1!==i;const r=document.createEvent("Events");return r.initEvent(t,e,i),null!=n&&m.call(r,n),r},A=function(t){let{onElement:e,bubbles:i,cancelable:n,attributes:r}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const o=null!=e?e:p,s=v(t,{bubbles:i,cancelable:n,attributes:r});return o.dispatchEvent(s)},y=function(t,e){if(1===(null==t?void 0:t.nodeType))return f.call(t,e)},x=function(t){let{matchingSelector:e,untilNode:i}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};for(;t&&t.nodeType!==Node.ELEMENT_NODE;)t=t.parentNode;if(null!=t){if(null==e)return t;if(t.closest&&null==i)return t.closest(e);for(;t&&t!==i;){if(y(t,e))return t;t=t.parentNode}}},C=t=>document.activeElement!==t&&E(t,document.activeElement),E=function(t,e){if(t&&e)for(;e;){if(e===t)return!0;e=e.parentNode}},S=function(t){var e;if(null===(e=t)||void 0===e||!e.parentNode)return;let i=0;for(t=t.previousSibling;t;)i++,t=t.previousSibling;return i},R=t=>{var e;return null==t||null===(e=t.parentNode)||void 0===e?void 0:e.removeChild(t)},T=function(t){let{onlyNodesOfType:e,usingFilter:i,expandEntityReferences:n}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const r=(()=>{switch(e){case"element":return NodeFilter.SHOW_ELEMENT;case"text":return NodeFilter.SHOW_TEXT;case"comment":return NodeFilter.SHOW_COMMENT;default:return NodeFilter.SHOW_ALL}})();return document.createTreeWalker(t,r,null!=i?i:null,!0===n)},k=t=>{var e;return null==t||null===(e=t.tagName)||void 0===e?void 0:e.toLowerCase()},w=function(t){let e,i,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};"object"==typeof t?(n=t,t=n.tagName):n={attributes:n};const r=document.createElement(t);if(null!=n.editable&&(null==n.attributes&&(n.attributes={}),n.attributes.contenteditable=n.editable),n.attributes)for(e in n.attributes)i=n.attributes[e],r.setAttribute(e,i);if(n.style)for(e in n.style)i=n.style[e],r.style[e]=i;if(n.data)for(e in n.data)i=n.data[e],r.dataset[e]=i;return n.className&&n.className.split(" ").forEach((t=>{r.classList.add(t)})),n.textContent&&(r.textContent=n.textContent),n.childNodes&&[].concat(n.childNodes).forEach((t=>{r.appendChild(t)})),r};let D;const L=funLine truncated
|
||||||
|
/*! @license DOMPurify 3.4.2 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.2/LICENSE */const{entries:oe,setPrototypeOf:se,isFrozen:ae,getPrototypeOf:le,getOwnPropertyDescriptor:ce}=Object;let{freeze:ue,seal:he,create:de}=Object,{apply:ge,construct:me}="undefined"!=typeof Reflect&&Reflect;ue||(ue=function(t){return t}),he||(he=function(t){return t}),ge||(ge=function(t,e){for(var i=arguments.length,n=new Array(i>2?i-2:0),r=2;r<i;r++)n[r-2]=arguments[r];return t.apply(e,n)}),me||(me=function(t){for(var e=arguments.length,i=new Array(e>1?e-1:0),n=1;n<e;n++)i[n-1]=arguments[n];return new t(...i)});const pe=Me(Array.prototype.forEach),fe=Me(Array.prototype.lastIndexOf),be=Me(Array.prototype.pop),ve=Me(Array.prototype.push),Ae=Me(Array.prototype.splice),ye=Array.isArray,xe=Me(String.prototype.toLowerCase),Ce=Me(String.prototype.toString),Ee=Me(String.prototype.match),Se=Me(String.prototype.replace),Re=Me(String.prototype.indexOf),Te=Me(String.prototype.trim),ke=Me(Number.prototype.toString),we=Me(Boolean.prototype.toString),De="undefined"==typeof BigInt?null:Me(BigInt.prototype.toString),Le="undefined"==typeof Symbol?null:Me(Symbol.prototype.toString),Ne=Me(Object.prototype.hasOwnProperty),Ie=Me(Object.prototype.toString),Oe=Me(RegExp.prototype.test),Fe=(Pe=TypeError,function(){for(var t=arguments.length,e=new Array(t),i=0;i<t;i++)e[i]=arguments[i];return me(Pe,e)});var Pe;function Me(t){return function(e){e instanceof RegExp&&(e.lastIndex=0);for(var i=arguments.length,n=new Array(i>1?i-1:0),r=1;r<i;r++)n[r-1]=arguments[r];return ge(t,e,n)}}function Be(t,e){let i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:xe;if(se&&se(t,null),!ye(e))return t;let n=e.length;for(;n--;){let r=e[n];if("string"==typeof r){const t=i(r);t!==r&&(ae(e)||(e[n]=t),r=t)}t[r]=!0}return t}function _e(t){for(let e=0;e<t.length;e++){Ne(t,e)||(t[e]=null)}return t}function je(t){const e=de(null);for(const[i,n]of oe(t)){Ne(t,i)&&(ye(n)?e[i]=_e(n):n&&"object"==typeof n&&n.constructor===Object?e[i]=je(n):e[i]=n)}return e}function We(t,e){for(;null!==t;){const i=ce(t,e);if(i){if(i.get)return Me(i.get);if("function"==typeof i.value)return Me(i.value)}t=le(t)}return function(){return null}}const Ue=ue(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),Ve=ue(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),ze=ue(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),He=ue(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),qe=ue(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),Je=ue(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),Ke=ue(["#text"]),Ge=ue(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","contLine truncated
|
||||||
//# sourceMappingURL=trix.esm.min.js.map
|
//# sourceMappingURL=trix.esm.min.js.map
|
||||||
Reference in new issue
Block a user