Compare commits
2 Commits
develop
...
gek/transc
Author | SHA1 | Date |
---|---|---|
|
24cf47660f | 3 years ago |
|
e0bafff490 | 3 years ago |
1753 changed files with 26015 additions and 115874 deletions
@ -1,65 +1,57 @@
@@ -1,65 +1,57 @@
|
||||
# Recreate this file via |
||||
# find static -type d -print0 | xargs -0 -I {} echo "{}/* linguist-vendored" | xclip -selection clipboard |
||||
webroot/js/web_modules/* linguist-vendored |
||||
webroot/js/web_modules/@joeattardi/* linguist-vendored |
||||
webroot/js/web_modules/@justinribeiro/* linguist-vendored |
||||
webroot/js/web_modules/@videojs/http-streaming/dist/* linguist-vendored |
||||
webroot/js/web_modules/@videojs/themes/fantasy/* linguist-vendored |
||||
webroot/js/web_modules/common/* linguist-vendored |
||||
webroot/js/web_modules/markjs/dist/* linguist-vendored |
||||
webroot/js/web_modules/tailwindcss/dist/* linguist-vendored |
||||
webroot/js/web_modules/videojs/* linguist-vendored |
||||
webroot/js/web_modules/micromodal/dist/* linguist-vendored |
||||
static/* linguist-vendored |
||||
static/admin/* linguist-vendored |
||||
docs/api/* linguist-documentation |
||||
static/* linguist-vendored |
||||
static/web/* linguist-vendored |
||||
static/web/admin/* linguist-vendored |
||||
static/web/admin/federation/* linguist-vendored |
||||
static/web/admin/federation/actions/* linguist-vendored |
||||
static/web/admin/federation/followers/* linguist-vendored |
||||
static/web/admin/logs/* linguist-vendored |
||||
static/web/admin/config-social-items/* linguist-vendored |
||||
static/web/admin/config/* linguist-vendored |
||||
static/web/admin/config/general/* linguist-vendored |
||||
static/web/admin/config/server/* linguist-vendored |
||||
static/web/admin/config-chat/* linguist-vendored |
||||
static/web/admin/config-federation/* linguist-vendored |
||||
static/web/admin/viewer-info/* linguist-vendored |
||||
static/web/admin/access-tokens/* linguist-vendored |
||||
static/web/admin/actions/* linguist-vendored |
||||
static/web/admin/help/* linguist-vendored |
||||
static/web/admin/webhooks/* linguist-vendored |
||||
static/web/admin/chat/* linguist-vendored |
||||
static/web/admin/chat/messages/* linguist-vendored |
||||
static/web/admin/chat/users/* linguist-vendored |
||||
static/web/admin/chat/emojis/* linguist-vendored |
||||
static/web/admin/upgrade/* linguist-vendored |
||||
static/web/admin/config-notify/* linguist-vendored |
||||
static/web/admin/hardware-info/* linguist-vendored |
||||
static/web/admin/config-video/* linguist-vendored |
||||
static/web/admin/stream-health/* linguist-vendored |
||||
static/web/404/* linguist-vendored |
||||
static/web/_next/* linguist-vendored |
||||
static/web/_next/static/* linguist-vendored |
||||
static/web/_next/static/l-3emuM7cUz2zU2fzzpRq/* linguist-vendored |
||||
static/web/_next/static/media/* linguist-vendored |
||||
static/web/_next/static/chunks/* linguist-vendored |
||||
static/web/_next/static/chunks/pages/* linguist-vendored |
||||
static/web/_next/static/chunks/pages/admin/* linguist-vendored |
||||
static/web/_next/static/chunks/pages/admin/federation/* linguist-vendored |
||||
static/web/_next/static/chunks/pages/admin/config/* linguist-vendored |
||||
static/web/_next/static/chunks/pages/admin/chat/* linguist-vendored |
||||
static/web/_next/static/chunks/pages/embed/* linguist-vendored |
||||
static/web/_next/static/chunks/pages/embed/chat/* linguist-vendored |
||||
static/web/_next/static/css/* linguist-vendored |
||||
static/web/_next/static/OQyHVua-s5F40yEopTtjx/* linguist-vendored |
||||
static/web/_next/OQyHVua-s5F40yEopTtjx/* linguist-vendored |
||||
static/web/embed/* linguist-vendored |
||||
static/web/embed/chat/* linguist-vendored |
||||
static/web/embed/chat/readonly/* linguist-vendored |
||||
static/web/embed/chat/readwrite/* linguist-vendored |
||||
static/web/embed/video/* linguist-vendored |
||||
static/web/fonts/* linguist-vendored |
||||
static/web/fonts/inter/* linguist-vendored |
||||
static/web/styles/* linguist-vendored |
||||
static/web/styles/admin/* linguist-vendored |
||||
static/web/img/* linguist-vendored |
||||
static/web/img/favicon/* linguist-vendored |
||||
static/web/img/platformlogos/* linguist-vendored |
||||
static/img/* linguist-vendored |
||||
static/img/emoji/* linguist-vendored |
||||
static/img/emoji/dog/* linguist-vendored |
||||
static/img/emoji/conigliolo96/* linguist-vendored |
||||
static/img/emoji/mutant/* linguist-vendored |
||||
static/img/emoji/blob/* linguist-vendored |
||||
static/admin/* linguist-vendored |
||||
static/admin/logs/* linguist-vendored |
||||
static/admin/config-social-items/* linguist-vendored |
||||
static/admin/offline-notice/* linguist-vendored |
||||
static/admin/config-chat/* linguist-vendored |
||||
static/admin/404/* linguist-vendored |
||||
static/admin/_next/* linguist-vendored |
||||
static/admin/_next/static/* linguist-vendored |
||||
static/admin/_next/static/chunks/* linguist-vendored |
||||
static/admin/_next/static/chunks/pages/* linguist-vendored |
||||
static/admin/_next/static/chunks/pages/chat/* linguist-vendored |
||||
static/admin/_next/static/b1nOF3ZgELnezD8dvvt2B/* linguist-vendored |
||||
static/admin/_next/static/css/* linguist-vendored |
||||
static/admin/_next/quK9VwW_avTP773Ot9m2x/* linguist-vendored |
||||
static/admin/viewer-info/* linguist-vendored |
||||
static/admin/access-tokens/* linguist-vendored |
||||
static/admin/config-storage/* linguist-vendored |
||||
static/admin/config-public-details/* linguist-vendored |
||||
static/admin/config-server-details/* linguist-vendored |
||||
static/admin/actions/* linguist-vendored |
||||
static/admin/help/* linguist-vendored |
||||
static/admin/webhooks/* linguist-vendored |
||||
static/admin/chat/* linguist-vendored |
||||
static/admin/chat/messages/* linguist-vendored |
||||
static/admin/chat/users/* linguist-vendored |
||||
static/admin/upgrade/* linguist-vendored |
||||
static/admin/hardware-info/* linguist-vendored |
||||
static/admin/config-video/* linguist-vendored |
||||
webroot/js/web_modules/* linguist-vendored |
||||
webroot/js/web_modules/micromodal/* linguist-vendored |
||||
webroot/js/web_modules/micromodal/dist/* linguist-vendored |
||||
webroot/js/web_modules/common/* linguist-vendored |
||||
webroot/js/web_modules/@videojs/* linguist-vendored |
||||
webroot/js/web_modules/@videojs/http-streaming/* linguist-vendored |
||||
webroot/js/web_modules/@videojs/http-streaming/dist/* linguist-vendored |
||||
webroot/js/web_modules/@videojs/themes/* linguist-vendored |
||||
webroot/js/web_modules/@videojs/themes/fantasy/* linguist-vendored |
||||
webroot/js/web_modules/markjs/* linguist-vendored |
||||
webroot/js/web_modules/markjs/dist/* linguist-vendored |
||||
webroot/js/web_modules/@joeattardi/* linguist-vendored |
||||
webroot/js/web_modules/tailwindcss/* linguist-vendored |
||||
webroot/js/web_modules/tailwindcss/dist/* linguist-vendored |
||||
webroot/js/web_modules/videojs/* linguist-vendored |
||||
|
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
--- |
||||
name: Bug report or feature request |
||||
about: Having problems or have ideas? We'd love to know what you think and help you out. |
||||
--- |
@ -1,15 +0,0 @@
@@ -1,15 +0,0 @@
|
||||
name: Bug report or feature request |
||||
description: Submit a bug you encountered or share an idea you have for the project. |
||||
body: |
||||
- type: markdown |
||||
attributes: |
||||
value: | |
||||
Thanks for helping by reporting issues and sharing ideas you might have! |
||||
While no idea is a bad idea, some might make more sense for Owncast than others. |
||||
Take a look at the [Owncast product definition](https://github.com/owncast/owncast/blob/develop/docs/product-definition.md) to see what our focus is and how your requests might align. |
||||
|
||||
- type: textarea |
||||
id: issue-body |
||||
attributes: |
||||
label: Share your bug report, feature request, or comment. |
||||
description: Please include as much detail as possible. |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
|
||||
Please include a summary of the change and which issue number is fixed, including relevant motivation and context. Feel free to mark this as a Draft or WIP and write up some details later. |
||||
|
||||
# Description |
||||
|
||||
Fixes # (issue) |
||||
|
||||
--- |
||||
|
||||
Some things you might want to mention: |
||||
|
||||
1. Why are you making the change? |
||||
2. Explain how it works and decisions you made. |
||||
3. If you're fixing something, what was wrong? How should we stop from having this issue happen again? |
||||
4. If this is a new feature or addition to functionality, why should it be added? What are the use cases? Who was asking for this functionality? |
||||
|
||||
If this is an unsolicited change or have no issue associated please do your best to detail the motivations behind this PR. |
@ -1,19 +0,0 @@
@@ -1,19 +0,0 @@
|
||||
|
||||
Please include a summary of the change and which issue number is fixed, including relevant motivation and context. Feel free to mark this as a Draft or WIP and write up some details later. |
||||
|
||||
If there is no issue filed for this particular change it's highly recommended you file one. While creating this PR means you probably already did the work, in the future make sure an issue is filed beforehand so changes, fixes and features can be discussed ahead of time. |
||||
|
||||
# Description |
||||
|
||||
Fixes # (issue) |
||||
|
||||
--- |
||||
|
||||
Some things you might want to mention: |
||||
|
||||
1. Why are you making the change? |
||||
2. Explain how it works and decisions you made. |
||||
3. If you're fixing something, what was wrong? How should we stop from having this issue happen again? |
||||
4. If this is a new feature or addition to functionality, why should it be added? What are the use cases? Who was asking for this functionality? |
||||
|
||||
If this is an unsolicited change or have no issue associated please do your best to detail the motivations behind this PR, and think about filing an issue to discuss changes ahead of time in the future. |
@ -1,4 +0,0 @@
@@ -1,4 +0,0 @@
|
||||
name: Javascript config |
||||
|
||||
paths-ignore: |
||||
- static/web |
@ -1,20 +0,0 @@
@@ -1,20 +0,0 @@
|
||||
name: Lint |
||||
|
||||
on: |
||||
push: |
||||
paths: |
||||
- '.github/workflows/*' |
||||
pull_request: |
||||
paths: |
||||
- '.github/workflows/*' |
||||
|
||||
jobs: |
||||
actionlint: |
||||
name: GitHub actions |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v4 |
||||
|
||||
- uses: docker://rhysd/actionlint:latest |
||||
with: |
||||
args: -shellcheck= -color |
@ -1,42 +0,0 @@
@@ -1,42 +0,0 @@
|
||||
name: Add comment on good first issues |
||||
on: |
||||
issues: |
||||
types: |
||||
- labeled |
||||
jobs: |
||||
add-comment: |
||||
if: github.event.label.name == 'good first issue' || github.event.label.name == 'help wanted' || github.event.label.name == 'hacktoberfest' |
||||
runs-on: ubuntu-latest |
||||
permissions: |
||||
issues: write |
||||
steps: |
||||
- name: Add comment |
||||
uses: peter-evans/create-or-update-comment@0f44b017d10caeea6a4c1b410ba0521ad8a02815 |
||||
with: |
||||
issue-number: ${{ github.event.issue.number }} |
||||
body: | |
||||
## Good First Issue |
||||
|
||||
This item was marked as a good first issue because of the following: |
||||
|
||||
- It's self contained as a single feature or change. |
||||
- Is clear when it's complete. |
||||
- You do not need deep knowledge of Owncast to accomplish it. |
||||
|
||||
|
||||
### Next Steps |
||||
|
||||
1. Comment on this issue before starting work so it can be assigned to you. Also, this issue may have been filed with limited detail or changes may have occurred that are worth sharing with you before you start work. |
||||
2. Drop by our [community chat](https://owncast.rocket.chat/) if you'd like to be involved in more real-time discussion around Owncast to talk about this change. |
||||
3. Follow the project's getting started tips to make sure you can [build and run the project from source](https://owncast.online/development). |
||||
|
||||
### Notes |
||||
|
||||
- Development takes place on the `develop` branch. |
||||
- We use Storybook for testing and developing React components. `npm run storybook`. A hosted version [is available for viewing](https://owncast.online/components). |
||||
- If you need to install the Go programming language to run the Owncast backend it's simple from [here](https://go.dev/dl/). |
||||
- Active contributors get an Owncast t-shirt! Ask about it if you feel like you've been contributing and haven't yet been given one. |
||||
|
||||
### New to Git? |
||||
|
||||
If you're brand new to Git you may want a short primer about the Fork -> Commit -> Pull Request workflow that enables changes to get made collaboratively using git. Visit the [First Contributions](https://github.com/firstcontributions/first-contributions) project to learn step-by-step how to commit a change to a Git repository such as this one. |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
name: Automated browser tests |
||||
on: [push, pull_request] |
||||
jobs: |
||||
browser: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
- uses: actions/setup-go@v3 |
||||
with: |
||||
stable: 'false' |
||||
go-version: '1.17.2' |
||||
|
||||
- name: Run browser tests |
||||
run: cd test/automated/browser && ./run.sh |
||||
|
||||
- uses: actions/upload-artifact@v3 |
||||
with: |
||||
name: screenshots-${{ github.run_id }} |
||||
path: test/automated/browser/screenshots/*.png |
@ -1,54 +0,0 @@
@@ -1,54 +0,0 @@
|
||||
name: Browser Tests |
||||
|
||||
on: |
||||
push: |
||||
paths: |
||||
- 'web/**' |
||||
- 'test/automated/browser/**' |
||||
pull_request: |
||||
paths: |
||||
- 'web/**' |
||||
- 'test/automated/browser/**' |
||||
|
||||
jobs: |
||||
cypress-run: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- id: skip_check |
||||
uses: fkirc/skip-duplicate-actions@v5 |
||||
with: |
||||
concurrent_skipping: 'same_content_newer' |
||||
|
||||
- name: Checkout |
||||
uses: actions/checkout@v4 |
||||
|
||||
- uses: actions/setup-node@v4 |
||||
with: |
||||
node-version: 18.9.0 |
||||
|
||||
- name: Cache node modules |
||||
uses: actions/cache@v3 |
||||
env: |
||||
cache-name: cache-node-modules-browser-tests |
||||
with: |
||||
path: ~/.npm |
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('test/automated/browser/package-lock.json') }} |
||||
restore-keys: | |
||||
${{ runner.os }}-build-${{ env.cache-name }}- |
||||
${{ runner.os }}-build- |
||||
${{ runner.os }}- |
||||
|
||||
- uses: actions/setup-go@v5 |
||||
with: |
||||
go-version: '1.21' |
||||
cache: true |
||||
|
||||
- name: Install Google Chrome |
||||
run: sudo apt-get update && sudo apt-get install google-chrome-stable |
||||
|
||||
- name: Run Browser tests |
||||
uses: nick-fields/retry@v2 |
||||
with: |
||||
timeout_minutes: 20 |
||||
max_attempts: 3 |
||||
command: cd test/automated/browser && ./run.sh |
@ -1,54 +0,0 @@
@@ -1,54 +0,0 @@
|
||||
name: Build and Deploy Components+Style Guide |
||||
on: |
||||
push: |
||||
branches: |
||||
- develop |
||||
paths: ['web/stories/**', 'web/components/**', 'web/.storybook/**'] # Trigger the action only when files change in the folders defined here |
||||
|
||||
jobs: |
||||
build-and-deploy: |
||||
runs-on: ubuntu-latest |
||||
if: github.repository == 'owncast/owncast' |
||||
|
||||
steps: |
||||
- name: Checkout |
||||
uses: actions/checkout@v4 |
||||
|
||||
- name: Cache node modules |
||||
uses: actions/cache@v3 |
||||
env: |
||||
cache-name: cache-node-modules-bundle-web-app |
||||
with: |
||||
path: ~/.npm |
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('web/package-lock.json') }} |
||||
restore-keys: | |
||||
${{ runner.os }}-build-${{ env.cache-name }}- |
||||
${{ runner.os }}-build- |
||||
${{ runner.os }}- |
||||
|
||||
- name: Install and Build |
||||
run: | # Install npm packages and build the Storybook files |
||||
cd web |
||||
npm install --include-dev --force |
||||
cd .storybook/tools |
||||
./generate-stories.sh |
||||
cd - |
||||
npm run build-storybook -- -o ../docs/components |
||||
|
||||
- name: Commit changes |
||||
uses: EndBug/add-and-commit@v9 |
||||
with: |
||||
author_name: Owncast |
||||
author_email: owncast@owncast.online |
||||
message: 'Commit updated Storybook stories' |
||||
add: '*.stories.*' |
||||
pull: '--rebase --autostash' |
||||
env: |
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
||||
|
||||
- name: Dispatch event to web site |
||||
uses: peter-evans/repository-dispatch@v2 |
||||
with: |
||||
token: ${{ secrets.BUNDLE_STORYBOOK_OWNCAST_ONLINE }} |
||||
repository: owncast/owncast.github.io |
||||
event-type: bundle-components-library |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
name: Bundle admin (owncast/owncast-admin) |
||||
on: |
||||
repository_dispatch: |
||||
types: [bundle-admin-event] |
||||
jobs: |
||||
bundle: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- name: Bundle admin |
||||
uses: actions/checkout@v3 |
||||
- run: build/admin/bundleAdmin.sh |
||||
|
||||
- name: Commit changes |
||||
uses: EndBug/add-and-commit@v9 |
||||
with: |
||||
author_name: Owncast |
||||
author_email: owncast@owncast.online |
||||
message: "Update admin to ${{ github.event.client_payload.sha }}" |
||||
add: "static/admin" |
||||
env: |
||||
GITHUB_TOKEN: ${{ secrets.GH_CR_PAT }} |
@ -1,51 +0,0 @@
@@ -1,51 +0,0 @@
|
||||
# .github/workflows/chromatic.yml |
||||
|
||||
# Workflow name |
||||
name: 'Chromatic' |
||||
|
||||
on: |
||||
push: |
||||
paths: |
||||
- web/** |
||||
pull_request_target: |
||||
paths: |
||||
- web/** |
||||
|
||||
# List of jobs |
||||
jobs: |
||||
chromatic-deployment: |
||||
# Operating System |
||||
runs-on: ubuntu-latest |
||||
if: github.repository == 'owncast/owncast' |
||||
|
||||
defaults: |
||||
run: |
||||
working-directory: ./web |
||||
|
||||
steps: |
||||
- id: skip_check |
||||
uses: fkirc/skip-duplicate-actions@v5 |
||||
with: |
||||
concurrent_skipping: 'same_content_newer' |
||||
- name: Check out code |
||||
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }} |
||||
uses: actions/checkout@v4 |
||||
with: |
||||
# Make sure the actual branch is checked out when running on pull requests |
||||
ref: ${{ github.event.pull_request.head.ref }} |
||||
repository: ${{ github.event.pull_request.head.repo.full_name }} |
||||
fetch-depth: 0 |
||||
|
||||
- name: Install dependencies |
||||
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }} |
||||
run: npm install |
||||
|
||||
- name: Publish to Chromatic |
||||
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }} |
||||
|
||||
uses: chromaui/action@v1 |
||||
# Chromatic GitHub Action options |
||||
with: |
||||
workingDir: web |
||||
projectToken: f47410569b62 |
||||
onlyChanged: true |
@ -1,28 +0,0 @@
@@ -1,28 +0,0 @@
|
||||
name: Lint |
||||
|
||||
on: |
||||
push: |
||||
branches: |
||||
- develop |
||||
paths: |
||||
- 'Dockerfile' |
||||
pull_request: |
||||
branches: |
||||
- develop |
||||
paths: |
||||
- 'Dockerfile' |
||||
|
||||
jobs: |
||||
trivy: |
||||
name: Dockerfile |
||||
runs-on: ubuntu-latest |
||||
container: |
||||
image: aquasec/trivy |
||||
steps: |
||||
- uses: actions/checkout@v4 |
||||
|
||||
- name: Check critical issues |
||||
run: trivy config --exit-code 1 --severity "HIGH,CRITICAL" ./Dockerfile |
||||
|
||||
- name: Check non-critical issues |
||||
run: trivy config --severity "LOW,MEDIUM" ./Dockerfile |
@ -1,56 +0,0 @@
@@ -1,56 +0,0 @@
|
||||
# See https://docs.earthly.dev/ci-integration/vendor-specific-guides/gh-actions-integration |
||||
# for details. |
||||
|
||||
name: Build development container |
||||
|
||||
on: |
||||
schedule: |
||||
- cron: '0 2 * * *' |
||||
push: |
||||
branches: |
||||
- develop |
||||
pull_request: |
||||
branches: |
||||
- develop |
||||
|
||||
jobs: |
||||
Earthly: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- name: Set up Earthly |
||||
uses: earthly/actions-setup@v1 |
||||
with: |
||||
version: 'latest' # or pin to an specific version, e.g. "v0.6.10" |
||||
|
||||
- name: Log Earthly version |
||||
run: earthly --version |
||||
|
||||
- name: Authenticate to GitHub Container Registry |
||||
if: ${{ github.event_name == 'schedule' && env.GH_CR_PAT != null }} |
||||
env: |
||||
GH_CR_PAT: ${{ secrets.GH_CR_PAT }} |
||||
run: echo "${{ secrets.GH_CR_PAT }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin |
||||
|
||||
- name: Set up QEMU |
||||
uses: docker/setup-qemu-action@v3 |
||||
with: |
||||
image: tonistiigi/binfmt:latest |
||||
platforms: all |
||||
|
||||
- name: Checkout repo |
||||
uses: actions/checkout@v4 |
||||
with: |
||||
fetch-depth: 0 |
||||
|
||||
- name: Build and push |
||||
if: ${{ github.event_name == 'schedule' && env.GH_CR_PAT != null }} |
||||
env: |
||||
GH_CR_PAT: ${{ secrets.GH_CR_PAT }} |
||||
EARTHLY_BUILD_TAG: 'nightly' |
||||
EARTHLY_BUILD_BRANCH: 'develop' |
||||
EARTHLY_PUSH: true |
||||
uses: nick-fields/retry@v2 |
||||
with: |
||||
timeout_minutes: 20 |
||||
max_attempts: 3 |
||||
command: ./build/develop/container.sh |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
name: Build nightly docker |
||||
|
||||
on: |
||||
workflow_dispatch: |
||||
schedule: |
||||
- cron: "0 2 * * *" |
||||
|
||||
jobs: |
||||
Docker: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
|
||||
- name: Log into GitHub Container Registry |
||||
env: |
||||
GH_CR_PAT: ${{ secrets.GH_CR_PAT }} |
||||
run: echo "${{ secrets.GH_CR_PAT }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin |
||||
if: env.GH_CR_PAT != null |
||||
|
||||
- uses: actions/checkout@v3 |
||||
- name: Setup and run |
||||
env: |
||||
GH_CR_PAT: ${{ secrets.GH_CR_PAT }} |
||||
run: cd build/release && ./docker-nightly.sh |
||||
if: env.GH_CR_PAT != null |
@ -1,38 +0,0 @@
@@ -1,38 +0,0 @@
|
||||
name: Lint |
||||
on: |
||||
push: |
||||
paths-ignore: |
||||
- 'web/**' |
||||
pull_request: |
||||
paths-ignore: |
||||
- 'web/**' |
||||
|
||||
permissions: |
||||
contents: read |
||||
|
||||
jobs: |
||||
golangci: |
||||
name: Go linter |
||||
if: ${{ github.actor != 'dependabot[bot]' }} |
||||
runs-on: ubuntu-latest |
||||
|
||||
steps: |
||||
- id: skip_check |
||||
uses: fkirc/skip-duplicate-actions@v5 |
||||
with: |
||||
concurrent_skipping: 'same_content_newer' |
||||
|
||||
- uses: actions/checkout@v4 |
||||
with: |
||||
fetch-depth: 0 |
||||
|
||||
- uses: actions/setup-go@v5 |
||||
with: |
||||
go-version: '1.21' |
||||
cache: true |
||||
- uses: actions/checkout@v4 |
||||
- name: golangci-lint |
||||
uses: golangci/golangci-lint-action@v3 |
||||
with: |
||||
only-new-issues: true |
||||
args: --timeout=3m |
@ -1,68 +0,0 @@
@@ -1,68 +0,0 @@
|
||||
name: Go Tests |
||||
|
||||
on: |
||||
push: |
||||
paths-ignore: |
||||
- 'web/**' |
||||
pull_request: |
||||
paths-ignore: |
||||
- 'web/**' |
||||
|
||||
jobs: |
||||
test: |
||||
strategy: |
||||
matrix: |
||||
go-version: [1.20.x, 1.21.x] |
||||
os: [ubuntu-latest, macos-latest, windows-latest] |
||||
runs-on: ${{ matrix.os }} |
||||
steps: |
||||
- uses: actions/checkout@v4 |
||||
|
||||
- uses: actions/cache@v3 |
||||
with: |
||||
path: | |
||||
~/.cache/go-build |
||||
~/go/pkg/mod |
||||
key: go-test-${{ github.sha }} |
||||
restore-keys: | |
||||
go-test- |
||||
|
||||
- name: Install go |
||||
uses: actions/setup-go@v5 |
||||
with: |
||||
go-version: '^1' |
||||
cache: true |
||||
|
||||
- name: Run tests |
||||
run: go test ./... |
||||
|
||||
test-bsds: |
||||
runs-on: macos-latest |
||||
strategy: |
||||
matrix: |
||||
os: |
||||
- name: freebsd |
||||
version: 12.2 |
||||
- name: openbsd |
||||
version: 6.8 |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v4 |
||||
|
||||
- uses: actions/cache@v3 |
||||
with: |
||||
path: | |
||||
~/.cache/go-build |
||||
~/go/pkg/mod |
||||
key: go-test-${{ github.sha }} |
||||
restore-keys: | |
||||
go-test- |
||||
|
||||
- name: Install go |
||||
uses: actions/setup-go@v5 |
||||
with: |
||||
go-version: '^1' |
||||
cache: true |
||||
|
||||
- name: Run tests |
||||
run: go test ./... |
@ -1,185 +0,0 @@
@@ -1,185 +0,0 @@
|
||||
name: Javascript |
||||
|
||||
# This action works with pull requests and pushes |
||||
on: |
||||
push: |
||||
paths: |
||||
- web/** |
||||
- '!**.md' |
||||
|
||||
pull_request: |
||||
paths: |
||||
- web/** |
||||
- '!**.md' |
||||
|
||||
jobs: |
||||
formatting: |
||||
name: Code formatting |
||||
runs-on: ubuntu-latest |
||||
defaults: |
||||
run: |
||||
working-directory: ./web |
||||
|
||||
steps: |
||||
- id: skip_check |
||||
uses: fkirc/skip-duplicate-actions@v5 |
||||
with: |
||||
concurrent_skipping: 'same_content_newer' |
||||
cancel_others: 'true' |
||||
skip_after_successful_duplicate: 'true' |
||||
|
||||
- name: Checkout |
||||
uses: actions/checkout@v4 |
||||
with: |
||||
# Make sure the actual branch is checked out when running on pull requests |
||||
ref: ${{ github.event.pull_request.head.ref }} |
||||
repository: ${{ github.event.pull_request.head.repo.full_name }} |
||||
fetch-depth: 0 |
||||
persist-credentials: true |
||||
|
||||
- name: Get changed files |
||||
id: changed-files-yaml |
||||
uses: tj-actions/changed-files@v41 |
||||
with: |
||||
path: 'web' |
||||
files_ignore: | |
||||
static/** |
||||
web/next.config.js |
||||
files_yaml: | |
||||
src: |
||||
- '**/*.{js,ts,tsx,jsx,css,md}' |
||||
|
||||
- name: Cache node modules |
||||
uses: actions/cache@v3 |
||||
env: |
||||
cache-name: cache-node-modules-bundle-web-app |
||||
with: |
||||
path: ~/.npm |
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('web/package-lock.json') }} |
||||
restore-keys: | |
||||
${{ runner.os }}-build-${{ env.cache-name }}- |
||||
${{ runner.os }}-build- |
||||
${{ runner.os }}- |
||||
|
||||
- name: Install Dependencies |
||||
run: npm install |
||||
|
||||
- name: Lint |
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' |
||||
run: npx eslint --fix ${{ steps.changed-files-yaml.outputs.src_all_changed_files }} |
||||
|
||||
- name: Prettier |
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' |
||||
run: npx prettier --write ${{ steps.changed-files-yaml.outputs.src_all_changed_files }} |
||||
|
||||
- name: Commit changes |
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' |
||||
uses: EndBug/add-and-commit@v9 |
||||
with: |
||||
author_name: Owncast |
||||
author_email: owncast@owncast.online |
||||
message: 'Javascript formatting autofixes' |
||||
add: ${{ steps.changed-files-yaml.outputs.src_all_changed_files }} |
||||
pull: '--rebase --autostash' |
||||
|
||||
unused-code: |
||||
name: Test for unused code |
||||
runs-on: ubuntu-latest |
||||
defaults: |
||||
run: |
||||
working-directory: ./web |
||||
|
||||
steps: |
||||
- id: skip_check |
||||
uses: fkirc/skip-duplicate-actions@v5 |
||||
with: |
||||
concurrent_skipping: 'same_content_newer' |
||||
cancel_others: 'true' |
||||
skip_after_successful_duplicate: 'true' |
||||
|
||||
- name: Checkout |
||||
uses: actions/checkout@v4 |
||||
with: |
||||
# Make sure the actual branch is checked out when running on pull requests |
||||
ref: ${{ github.event.pull_request.head.ref }} |
||||
repository: ${{ github.event.pull_request.head.repo.full_name }} |
||||
fetch-depth: 0 |
||||
|
||||
- name: Cache node modules |
||||
uses: actions/cache@v3 |
||||
env: |
||||
cache-name: cache-node-modules-bundle-web-app |
||||
with: |
||||
path: ~/.npm |
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('web/package-lock.json') }} |
||||
restore-keys: | |
||||
${{ runner.os }}-build-${{ env.cache-name }}- |
||||
${{ runner.os }}-build- |
||||
${{ runner.os }}- |
||||
|
||||
- name: Install Dependencies |
||||
run: npm install |
||||
|
||||
- name: Check for unused JS code and dependencies |
||||
run: npx knip --include dependencies,files,exports |
||||
|
||||
# After any formatting and linting is complete we can run the build |
||||
# and bundle step. This both will verify that the build is successful as |
||||
# well as commiting the updated static files into the repository for use. |
||||
web-bundle: |
||||
name: Build and bundle web project |
||||
runs-on: ubuntu-latest |
||||
if: github.repository == 'owncast/owncast' |
||||
needs: [formatting, unused-code] |
||||
steps: |
||||
- id: skip_check |
||||
uses: fkirc/skip-duplicate-actions@v5 |
||||
with: |
||||
concurrent_skipping: 'same_content_newer' |
||||
cancel_others: 'true' |
||||
skip_after_successful_duplicate: 'true' |
||||
|
||||
- name: Cache node modules |
||||
uses: actions/cache@v3 |
||||
env: |
||||
cache-name: cache-node-modules-bundle-web-app |
||||
with: |
||||
path: ~/.npm |
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('web/package-lock.json') }} |
||||
restore-keys: | |
||||
${{ runner.os }}-build-${{ env.cache-name }}- |
||||
${{ runner.os }}-build- |
||||
${{ runner.os }}- |
||||
|
||||
- name: Checkout |
||||
uses: actions/checkout@v4 |
||||
with: |
||||
# Make sure the actual branch is checked out when running on pull requests |
||||
ref: ${{ github.event.pull_request.head.ref }} |
||||
repository: ${{ github.event.pull_request.head.repo.full_name }} |
||||
fetch-depth: 0 |
||||
|
||||
- name: Bundle web app (next.js build) |
||||
run: build/web/bundleWeb.sh |
||||
|
||||
- name: Rebase |
||||
if: ${{ github.ref == 'refs/heads/develop' }} |
||||
run: | |
||||
git add static/web |
||||
git pull --rebase --autostash |
||||
|
||||
# Only commit built web project files on develop. |
||||
- name: Commit changes |
||||
if: ${{ github.ref == 'refs/heads/develop' }} |
||||
uses: EndBug/add-and-commit@v9 |
||||
with: |
||||
message: 'Bundle embedded web app' |
||||
add: 'static/web' |
||||
author_name: Owncast |
||||
author_email: owncast@owncast.online |
||||
|
||||
- name: Push changes |
||||
if: ${{ github.ref == 'refs/heads/develop' }} |
||||
run: | |
||||
git pull --rebase --autostash |
||||
git push |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
name: Format Javascript |
||||
|
||||
# This action works with pull requests and pushes |
||||
on: |
||||
push: |
||||
branches: |
||||
- develop |
||||
|
||||
jobs: |
||||
prettier: |
||||
runs-on: ubuntu-latest |
||||
if: ${{ github.actor != 'dependabot[bot]' }} |
||||
|
||||
steps: |
||||
- name: Checkout |
||||
uses: actions/checkout@v3 |
||||
with: |
||||
# Make sure the actual branch is checked out when running on pull requests |
||||
ref: ${{ github.head_ref }} |
||||
fetch-depth: 0 |
||||
|
||||
- name: Prettify code |
||||
uses: creyD/prettier_action@v4.2 |
||||
with: |
||||
# This part is also where you can pass other options, for example: |
||||
prettier_options: --write webroot/**/*.{js,md} |
||||
only_changed: true |
||||
env: |
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
name: javascript-packages |
||||
on: |
||||
push: |
||||
paths: |
||||
- build/javascript/package.json |
||||
|
||||
jobs: |
||||
run: |
||||
if: ${{ github.actor != 'dependabot[bot]' }} |
||||
name: npm run build |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- name: Checkout repo |
||||
uses: actions/checkout@v3 |
||||
with: |
||||
# Make sure the actual branch is checked out when running on pull requests |
||||
ref: ${{ github.head_ref }} |
||||
|
||||
- name: Build dependencies |
||||
uses: actions/setup-node@v3 |
||||
with: |
||||
node-version: '12' |
||||
- run: cd build/javascript && npm run build |
||||
|
||||
- name: Commit changes |
||||
uses: EndBug/add-and-commit@v9 |
||||
with: |
||||
author_name: Owncast |
||||
author_email: owncast@owncast.online |
||||
message: "Commit updated Javascript packages" |
||||
add: "build/javascript/package* webroot/js/web_modules" |
||||
env: |
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
@ -1,45 +0,0 @@
@@ -1,45 +0,0 @@
|
||||
name: Javascript Tests |
||||
|
||||
on: |
||||
push: |
||||
paths: |
||||
- 'web/**' |
||||
pull_request: |
||||
paths: |
||||
- 'web/**' |
||||
|
||||
jobs: |
||||
jest-run: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- id: skip_check |
||||
uses: fkirc/skip-duplicate-actions@v5 |
||||
with: |
||||
concurrent_skipping: 'same_content_newer' |
||||
|
||||
- name: Checkout |
||||
uses: actions/checkout@v4 |
||||
|
||||
- uses: actions/setup-node@v4 |
||||
with: |
||||
node-version: 18.9.0 |
||||
|
||||
- name: Cache node modules |
||||
uses: actions/cache@v3 |
||||
env: |
||||
cache-name: cache-node-modules-javascript-tests |
||||
with: |
||||
path: ~/.npm |
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('web/package-lock.json') }} |
||||
restore-keys: | |
||||
${{ runner.os }}-build-${{ env.cache-name }}- |
||||
${{ runner.os }}-build- |
||||
${{ runner.os }}- |
||||
|
||||
- name: Install Dependencies |
||||
working-directory: ./web |
||||
run: npm install |
||||
|
||||
- name: Run tests |
||||
working-directory: ./web |
||||
run: npm test |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
name: lint |
||||
on: |
||||
push: |
||||
pull_request: |
||||
|
||||
permissions: |
||||
contents: read |
||||
|
||||
jobs: |
||||
golangci: |
||||
name: Go linter |
||||
if: ${{ github.actor != 'dependabot[bot]' }} |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
with: |
||||
fetch-depth: 0 |
||||
|
||||
- uses: actions/setup-go@v3 |
||||
- uses: actions/checkout@v3 |
||||
- name: golangci-lint |
||||
uses: golangci/golangci-lint-action@v2 |
||||
with: |
||||
only-new-issues: true |
@ -1,58 +0,0 @@
@@ -1,58 +0,0 @@
|
||||
name: Take nightly screenshots |
||||
|
||||
on: |
||||
schedule: |
||||
- cron: '0 4 * * *' |
||||
|
||||
env: |
||||
BROWSERSTACK_KEY: ${{ secrets.BROWSERSTACK_KEY }} |
||||
BROWSERSTACK_PASSWORD: ${{ secrets.BROWSERSTACK_PASSWORD }} |
||||
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} |
||||
TEST_URL: http://localhost:8080 |
||||
|
||||
jobs: |
||||
Screenshots: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v4 |
||||
- uses: actions/setup-go@v5 |
||||
with: |
||||
go-version: '1.21' |
||||
cache: true |
||||
|
||||
- name: Cache node modules |
||||
uses: actions/cache@v3 |
||||
env: |
||||
cache-name: cache-node-modules-screenshots |
||||
with: |
||||
path: ~/.npm |
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('test/automated/screenshots/package-lock.json') }} |
||||
restore-keys: | |
||||
${{ runner.os }}-build-${{ env.cache-name }}- |
||||
${{ runner.os }}-build- |
||||
${{ runner.os }}- |
||||
|
||||
- name: Automate screenshots |
||||
uses: nick-fields/retry@v2 |
||||
with: |
||||
timeout_minutes: 10 |
||||
max_attempts: 4 |
||||
command: cd test/automated/screenshots && ./run.sh |
||||
|
||||
- name: Commit changes |
||||
uses: EndBug/add-and-commit@v9 |
||||
with: |
||||
author_name: Owncast |
||||
author_email: owncast@owncast.online |
||||
message: 'Commit screenshots' |
||||
add: '*.png' |
||||
pull: '--rebase --autostash' |
||||
env: |
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
||||
|
||||
- name: Dispatch event to web site |
||||
uses: peter-evans/repository-dispatch@v2 |
||||
with: |
||||
token: ${{ secrets.BUNDLE_STORYBOOK_OWNCAST_ONLINE }} |
||||
repository: owncast/owncast.github.io |
||||
event-type: bundle-components-library |
@ -1,30 +0,0 @@
@@ -1,30 +0,0 @@
|
||||
name: Lint |
||||
|
||||
on: |
||||
push: |
||||
branches: |
||||
- develop |
||||
paths: |
||||
- '**.sh' |
||||
pull_request: |
||||
branches: |
||||
- develop |
||||
paths: |
||||
- '**.sh' |
||||
|
||||
jobs: |
||||
shellcheck: |
||||
runs-on: ubuntu-latest |
||||
env: |
||||
LANG: C.UTF-8 |
||||
container: |
||||
image: docker.io/ubuntu:24.04 |
||||
steps: |
||||
- uses: actions/checkout@v4 |
||||
|
||||
- name: Install shellcheck |
||||
run: apt update && apt install -y shellcheck bash && shellcheck --version |
||||
|
||||
- name: Check shell scripts |
||||
run: shopt -s globstar && ls **/*.sh && shellcheck -x -P "SCRIPTDIR" --severity=info **/*.sh |
||||
shell: bash |
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
name: Tests |
||||
|
||||
on: [push, pull_request] |
||||
jobs: |
||||
test: |
||||
strategy: |
||||
matrix: |
||||
go-version: [1.16.x, 1.17.x] |
||||
os: [ubuntu-latest, macos-latest, windows-latest] |
||||
runs-on: ${{ matrix.os }} |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
|
||||
- name: Install go |
||||
uses: actions/setup-go@v3 |
||||
with: |
||||
go-version: "^1" |
||||
|
||||
- name: Run tests |
||||
run: go test ./... |
||||
|
||||
test-bsds: |
||||
runs-on: macos-10.15 |
||||
strategy: |
||||
matrix: |
||||
os: |
||||
- name: freebsd |
||||
version: 12.2 |
||||
- name: openbsd |
||||
version: 6.8 |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
|
||||
- name: Install go |
||||
uses: actions/setup-go@v3 |
||||
with: |
||||
go-version: "^1" |
||||
|
||||
- name: Run tests |
||||
run: go test ./... |
||||
|
@ -1,5 +0,0 @@
@@ -1,5 +0,0 @@
|
||||
# Automatic workspace preparation for gitpod instances |
||||
|
||||
tasks: |
||||
- init: sudo apt-get install ffmpeg -y && go get && go build ./... && go test ./... |
||||
command: go run . |
@ -1,4 +1,3 @@
@@ -1,4 +1,3 @@
|
||||
# Ignore artifacts: |
||||
build/javascript |
||||
webroot/js/web_modules |
||||
static/ |
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
{ |
||||
"cSpell.words": [ |
||||
"Debugln", |
||||
"Errorln", |
||||
"Fediverse", |
||||
"Ffmpeg", |
||||
"ffmpegpath", |
||||
"ffmpg", |
||||
"geoip", |
||||
"gosec", |
||||
"mattn", |
||||
"Mbps", |
||||
"nolint", |
||||
"Owncast", |
||||
"ppid", |
||||
"preact", |
||||
"RTMP", |
||||
"rtmpserverport", |
||||
"sqlite", |
||||
"Tracef", |
||||
"Traceln", |
||||
"upgrader", |
||||
"Upgrader", |
||||
"videojs", |
||||
"Warnf", |
||||
"Warnln" |
||||
] |
||||
} |
@ -1,168 +0,0 @@
@@ -1,168 +0,0 @@
|
||||
VERSION --new-platform 0.6 |
||||
|
||||
FROM --platform=linux/amd64 alpine:3.15.5 |
||||
ARG version=develop |
||||
|
||||
WORKDIR /build |
||||
|
||||
build-all: |
||||
BUILD --platform=linux/amd64 --platform=linux/386 --platform=linux/arm64 --platform=linux/arm/v7 --platform=darwin/amd64 --platform=darwin/arm64 +build |
||||
|
||||
package-all: |
||||
BUILD --platform=linux/amd64 --platform=linux/386 --platform=linux/arm64 --platform=linux/arm/v7 --platform=darwin/amd64 --platform=darwin/arm64 +package |
||||
|
||||
docker-all: |
||||
BUILD --platform=linux/amd64 --platform=linux/386 --platform=linux/arm64 --platform=linux/arm/v7 +docker |
||||
|
||||
crosscompiler: |
||||
# This image is missing a few platforms, so we'll add them locally |
||||
FROM --platform=linux/amd64 bdwyertech/go-crosscompile |
||||
RUN apk add --update --no-cache tar gzip upx >> /dev/null |
||||
RUN curl -sfL "https://owncast-infra.nyc3.cdn.digitaloceanspaces.com/build/armv7l-linux-musleabihf-cross.tgz" | tar zxf - -C /usr/ --strip-components=1 |
||||
RUN curl -sfL "https://owncast-infra.nyc3.cdn.digitaloceanspaces.com/build/i686-linux-musl-cross.tgz" | tar zxf - -C /usr/ --strip-components=1 |
||||
RUN curl -sfL "https://owncast-infra.nyc3.cdn.digitaloceanspaces.com/build/x86_64-linux-musl-cross.tgz" | tar zxf - -C /usr/ --strip-components=1 |
||||
|
||||
code: |
||||
FROM --platform=linux/amd64 +crosscompiler |
||||
COPY . /build |
||||
|
||||
build: |
||||
ARG EARTHLY_GIT_HASH # provided by Earthly |
||||
ARG TARGETPLATFORM # provided by Earthly |
||||
ARG TARGETOS # provided by Earthly |
||||
ARG TARGETARCH # provided by Earthly |
||||
ARG GOOS=$TARGETOS |
||||
ARG GOARCH=$TARGETARCH |
||||
|
||||
FROM --platform=linux/amd64 +code |
||||
|
||||
RUN echo "Finding CC configuration for $TARGETPLATFORM" |
||||
IF [ "$TARGETPLATFORM" = "linux/amd64" ] |
||||
ARG NAME=linux-64bit |
||||
ARG CC=x86_64-linux-musl-gcc |
||||
ARG CXX=x86_64-linux-musl-g++ |
||||
ELSE IF [ "$TARGETPLATFORM" = "linux/386" ] |
||||
ARG NAME=linux-32bit |
||||
ARG CC=i686-linux-musl-gcc |
||||
ARG CXX=i686-linux-musl-g++ |
||||
ELSE IF [ "$TARGETPLATFORM" = "linux/arm64" ] |
||||
ARG NAME=linux-arm64 |
||||
ARG CC=aarch64-linux-musl-gcc |
||||
ARG CXX=aarch64-linux-musl-g++ |
||||
ELSE IF [ "$TARGETPLATFORM" = "linux/arm/v7" ] |
||||
ARG NAME=linux-arm7 |
||||
ARG CC=armv7l-linux-musleabihf-gcc |
||||
ARG CXX=armv7l-linux-musleabihf-g++ |
||||
ARG GOARM=7 |
||||
ELSE IF [ "$TARGETPLATFORM" = "darwin/amd64" ] |
||||
ARG NAME=macOS-64bit |
||||
ARG CC=o64-clang |
||||
ARG CXX=o64-clang++ |
||||
ELSE IF [ "$TARGETPLATFORM" = "darwin/arm64" ] |
||||
ARG NAME=macOS-arm64 |
||||
ARG CC=o64-clang |
||||
ARG CXX=o64-clang++ |
||||
ELSE |
||||
RUN echo "Failed to find CC configuration for $TARGETPLATFORM" |
||||
ARG --required CC |
||||
ARG --required CXX |
||||
END |
||||
|
||||
ENV CGO_ENABLED=1 |
||||
ENV GOOS=$GOOS |
||||
ENV GOARCH=$GOARCH |
||||
ENV GOARM=$GOARM |
||||
ENV CC=$CC |
||||
ENV CXX=$CXX |
||||
|
||||
WORKDIR /build |
||||
# MacOSX disallows static executables, so we omit the static flag on this platform |
||||
RUN go build -a -installsuffix cgo -ldflags "$([ "$GOOS"z != darwinz ] && echo "-linkmode external -extldflags -static ") -s -w -X github.com/owncast/owncast/config.GitCommit=$EARTHLY_GIT_HASH -X github.com/owncast/owncast/config.VersionNumber=$version -X github.com/owncast/owncast/config.BuildPlatform=$NAME" -tags sqlite_omit_load_extension -o owncast main.go |
||||
|
||||
# Decrease the size of the shipped binary. But only for non-Apple platforms. |
||||
# See https://github.com/upx/upx/issues/612 |
||||
IF [ "$GOOS" != "darwin" ] |
||||
RUN upx --best --lzma owncast |
||||
# Test the binary |
||||
RUN upx -t owncast |
||||
END |
||||
|
||||
SAVE ARTIFACT owncast owncast |
||||
|
||||
package: |
||||
RUN apk add --update --no-cache zip >> /dev/null |
||||
|
||||
ARG TARGETPLATFORM # provided by Earthly |
||||
IF [ "$TARGETPLATFORM" = "linux/amd64" ] |
||||
ARG NAME=linux-64bit |
||||
ELSE IF [ "$TARGETPLATFORM" = "linux/386" ] |
||||
ARG NAME=linux-32bit |
||||
ELSE IF [ "$TARGETPLATFORM" = "linux/arm64" ] |
||||
ARG NAME=linux-arm64 |
||||
ELSE IF [ "$TARGETPLATFORM" = "linux/arm/v7" ] |
||||
ARG NAME=linux-arm7 |
||||
ELSE IF [ "$TARGETPLATFORM" = "darwin/amd64" ] |
||||
ARG NAME=macOS-64bit |
||||
ELSE IF [ "$TARGETPLATFORM" = "darwin/arm64" ] |
||||
ARG NAME=macOS-arm64 |
||||
ELSE |
||||
ARG NAME=custom |
||||
END |
||||
|
||||
COPY (+build/owncast --platform $TARGETPLATFORM) /build/dist/owncast |
||||
ENV ZIPNAME owncast-$version-$NAME.zip |
||||
RUN cd /build/dist && zip -r -q -8 /build/dist/owncast.zip . |
||||
SAVE ARTIFACT --keep-ts /build/dist/owncast.zip owncast.zip AS LOCAL dist/$ZIPNAME |
||||
|
||||
docker: |
||||
# Multiple image names can be tagged at once. They should all be passed |
||||
# in as space separated strings using the full account/repo:tag format. |
||||
# https://github.com/earthly/earthly/blob/aea38448fa9c0064b1b70d61be717ae740689fb9/docs/earthfile/earthfile.md#assigning-multiple-image-names |
||||
ARG TARGETPLATFORM |
||||
FROM --platform=$TARGETPLATFORM alpine:3.15.5 |
||||
RUN apk update && apk add --no-cache ffmpeg ffmpeg-libs ca-certificates unzip && update-ca-certificates |
||||
RUN addgroup -g 101 -S owncast && adduser -u 101 -S owncast -G owncast |
||||
WORKDIR /app |
||||
COPY --platform=$TARGETPLATFORM +package/owncast.zip /app |
||||
RUN unzip -x owncast.zip && mkdir data |
||||
|
||||
# temporarily disable until we figure out how to move forward |
||||
# RUN chown -R owncast:owncast /app |
||||
# USER owncast |
||||
|
||||
ENTRYPOINT ["/app/owncast"] |
||||
EXPOSE 8080 1935 |
||||
|
||||
ARG images=ghcr.io/owncast/owncast:testing |
||||
RUN echo "Saving images: ${images}" |
||||
|
||||
# Tag this image with the list of names |
||||
# passed along. |
||||
FOR --no-cache i IN ${images} |
||||
SAVE IMAGE --push "${i}" |
||||
END |
||||
|
||||
dockerfile: |
||||
FROM DOCKERFILE -f Dockerfile . |
||||
|
||||
unit-tests: |
||||
FROM --platform=linux/amd64 bdwyertech/go-crosscompile |
||||
COPY . /build |
||||
WORKDIR /build |
||||
RUN go test ./... |
||||
|
||||
api-tests: |
||||
FROM --platform=linux/amd64 bdwyertech/go-crosscompile |
||||
RUN apk add npm font-noto && fc-cache -f |
||||
COPY . /build |
||||
WORKDIR /build/test/automated/api |
||||
RUN npm install |
||||
RUN ./run.sh |
||||
|
||||
hls-tests: |
||||
FROM --platform=linux/amd64 bdwyertech/go-crosscompile |
||||
RUN apk add npm font-noto && fc-cache -f |
||||
COPY . /build |
||||
WORKDIR /build/test/automated/hls |
||||
RUN npm install |
||||
RUN ./run.sh |
@ -1,107 +0,0 @@
@@ -1,107 +0,0 @@
|
||||
package persistence |
||||
|
||||
import ( |
||||
"os" |
||||
"testing" |
||||
|
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/models" |
||||
"github.com/owncast/owncast/utils" |
||||
) |
||||
|
||||
func TestMain(m *testing.M) { |
||||
setup() |
||||
code := m.Run() |
||||
os.Exit(code) |
||||
} |
||||
|
||||
var followers = []models.Follower{} |
||||
|
||||
func setup() { |
||||
data.SetupPersistence(":memory:") |
||||
_datastore = data.GetDatastore() |
||||
createFederationFollowersTable() |
||||
|
||||
number := 100 |
||||
for i := 0; i < number; i++ { |
||||
u := createFakeFollower() |
||||
createFollow(u.ActorIRI, u.Inbox, "https://fake.fediverse.server/some/request", u.Name, u.Username, u.Image, nil, true) |
||||
followers = append(followers, u) |
||||
} |
||||
} |
||||
|
||||
func TestQueryFollowers(t *testing.T) { |
||||
f, total, err := GetFederationFollowers(10, 0) |
||||
if err != nil { |
||||
t.Errorf("Error querying followers: %s", err) |
||||
} |
||||
|
||||
if len(f) != 10 { |
||||
t.Errorf("Expected 10 followers, got %d", len(f)) |
||||
} |
||||
|
||||
if total != 100 { |
||||
t.Errorf("Expected 100 followers, got %d", total) |
||||
} |
||||
} |
||||
|
||||
func TestQueryFollowersWithOffset(t *testing.T) { |
||||
f, total, err := GetFederationFollowers(10, 10) |
||||
if err != nil { |
||||
t.Errorf("Error querying followers: %s", err) |
||||
} |
||||
|
||||
if len(f) != 10 { |
||||
t.Errorf("Expected 10 followers, got %d", len(f)) |
||||
} |
||||
|
||||
if total != 100 { |
||||
t.Errorf("Expected 100 followers, got %d", total) |
||||
} |
||||
} |
||||
|
||||
func TestQueryFollowersWithOffsetAndLimit(t *testing.T) { |
||||
f, total, err := GetFederationFollowers(10, 90) |
||||
if err != nil { |
||||
t.Errorf("Error querying followers: %s", err) |
||||
} |
||||
|
||||
if len(f) != 10 { |
||||
t.Errorf("Expected 10 followers, got %d", len(f)) |
||||
} |
||||
|
||||
if total != 100 { |
||||
t.Errorf("Expected 100 followers, got %d", total) |
||||
} |
||||
} |
||||
|
||||
func TestQueryFollowersWithPagination(t *testing.T) { |
||||
f, _, err := GetFederationFollowers(15, 10) |
||||
if err != nil { |
||||
t.Errorf("Error querying followers: %s", err) |
||||
} |
||||
|
||||
comparisonFollowers := followers[10:25] |
||||
if len(f) != len(comparisonFollowers) { |
||||
t.Errorf("Expected %d followers, got %d", len(comparisonFollowers), len(f)) |
||||
} |
||||
|
||||
for i, follower := range f { |
||||
if follower.ActorIRI != comparisonFollowers[i].ActorIRI { |
||||
t.Errorf("Expected %s, got %s", comparisonFollowers[i].ActorIRI, follower.ActorIRI) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func createFakeFollower() models.Follower { |
||||
user, _ := utils.GenerateRandomString(10) |
||||
|
||||
return models.Follower{ |
||||
ActorIRI: "https://freedom.eagle/user/" + user, |
||||
Inbox: "https://fake.fediverse.server/user/" + user + "/inbox", |
||||
Image: "https://fake.fediverse.server/user/" + user + "/avatar.png", |
||||
Name: user, |
||||
Username: user, |
||||
Timestamp: utils.NullTime{}, |
||||
} |
||||
} |
@ -1,61 +0,0 @@
@@ -1,61 +0,0 @@
|
||||
package webfinger |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"net/http" |
||||
"net/url" |
||||
"strings" |
||||
|
||||
"github.com/owncast/owncast/utils" |
||||
) |
||||
|
||||
// GetWebfingerLinks will return webfinger data for an account.
|
||||
func GetWebfingerLinks(account string) ([]map[string]interface{}, error) { |
||||
type webfingerResponse struct { |
||||
Links []map[string]interface{} `json:"links"` |
||||
} |
||||
|
||||
account = strings.TrimLeft(account, "@") // remove any leading @
|
||||
accountComponents := strings.Split(account, "@") |
||||
fediverseServer := accountComponents[1] |
||||
|
||||
// Reject any requests to our internal network or loopback.
|
||||
if utils.IsHostnameInternal(fediverseServer) { |
||||
return nil, errors.New("unable to use provided host as a valid fediverse server") |
||||
} |
||||
|
||||
// HTTPS is required.
|
||||
requestURL, err := url.Parse("https://" + fediverseServer) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("unable to parse fediverse server host %s", fediverseServer) |
||||
} |
||||
|
||||
requestURL.Path = "/.well-known/webfinger" |
||||
query := requestURL.Query() |
||||
query.Add("resource", fmt.Sprintf("acct:%s", account)) |
||||
requestURL.RawQuery = query.Encode() |
||||
|
||||
// Do not support redirects.
|
||||
client := &http.Client{ |
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error { |
||||
return http.ErrUseLastResponse |
||||
}, |
||||
} |
||||
|
||||
response, err := client.Get(requestURL.String()) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
defer response.Body.Close() |
||||
|
||||
var links webfingerResponse |
||||
decoder := json.NewDecoder(response.Body) |
||||
if err := decoder.Decode(&links); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return links.Links, nil |
||||
} |
@ -1,11 +0,0 @@
@@ -1,11 +0,0 @@
|
||||
package auth |
||||
|
||||
// Type represents a form of authentication.
|
||||
type Type string |
||||
|
||||
// The different auth types we support.
|
||||
const ( |
||||
// IndieAuth https://indieauth.spec.indieweb.org/.
|
||||
IndieAuth Type = "indieauth" |
||||
Fediverse Type = "fediverse" |
||||
) |
@ -1,115 +0,0 @@
@@ -1,115 +0,0 @@
|
||||
package fediverse |
||||
|
||||
import ( |
||||
"crypto/rand" |
||||
"errors" |
||||
"io" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// OTPRegistration represents a single OTP request.
|
||||
type OTPRegistration struct { |
||||
Timestamp time.Time |
||||
UserID string |
||||
UserDisplayName string |
||||
Code string |
||||
Account string |
||||
} |
||||
|
||||
// Key by access token to limit one OTP request for a person
|
||||
// to be active at a time.
|
||||
var ( |
||||
pendingAuthRequests = make(map[string]OTPRegistration) |
||||
lock = sync.Mutex{} |
||||
) |
||||
|
||||
const ( |
||||
registrationTimeout = time.Minute * 10 |
||||
maxPendingRequests = 1000 |
||||
) |
||||
|
||||
func init() { |
||||
go setupExpiredRequestPruner() |
||||
} |
||||
|
||||
// Clear out any pending requests that have been pending for greater than
|
||||
// the specified timeout value.
|
||||
func setupExpiredRequestPruner() { |
||||
pruneExpiredRequestsTimer := time.NewTicker(registrationTimeout) |
||||
|
||||
for range pruneExpiredRequestsTimer.C { |
||||
lock.Lock() |
||||
log.Debugln("Pruning expired OTP requests.") |
||||
for k, v := range pendingAuthRequests { |
||||
if time.Since(v.Timestamp) > registrationTimeout { |
||||
delete(pendingAuthRequests, k) |
||||
} |
||||
} |
||||
lock.Unlock() |
||||
} |
||||
} |
||||
|
||||
// RegisterFediverseOTP will start the OTP flow for a user, creating a new
|
||||
// code and returning it to be sent to a destination.
|
||||
func RegisterFediverseOTP(accessToken, userID, userDisplayName, account string) (OTPRegistration, bool, error) { |
||||
request, requestExists := pendingAuthRequests[accessToken] |
||||
|
||||
// If a request is already registered and has not expired then return that
|
||||
// existing request.
|
||||
if requestExists && time.Since(request.Timestamp) < registrationTimeout { |
||||
return request, false, nil |
||||
} |
||||
|
||||
lock.Lock() |
||||
defer lock.Unlock() |
||||
|
||||
if len(pendingAuthRequests)+1 > maxPendingRequests { |
||||
return request, false, errors.New("Please try again later. Too many pending requests.") |
||||
} |
||||
|
||||
code, _ := createCode() |
||||
r := OTPRegistration{ |
||||
Code: code, |
||||
UserID: userID, |
||||
UserDisplayName: userDisplayName, |
||||
Account: strings.ToLower(account), |
||||
Timestamp: time.Now(), |
||||
} |
||||
pendingAuthRequests[accessToken] = r |
||||
|
||||
return r, true, nil |
||||
} |
||||
|
||||
// ValidateFediverseOTP will verify a OTP code for a auth request.
|
||||
func ValidateFediverseOTP(accessToken, code string) (bool, *OTPRegistration) { |
||||
request, ok := pendingAuthRequests[accessToken] |
||||
|
||||
if !ok || request.Code != code || time.Since(request.Timestamp) > registrationTimeout { |
||||
return false, nil |
||||
} |
||||
|
||||
lock.Lock() |
||||
defer lock.Unlock() |
||||
|
||||
delete(pendingAuthRequests, accessToken) |
||||
return true, &request |
||||
} |
||||
|
||||
func createCode() (string, error) { |
||||
table := [...]byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'} |
||||
|
||||
digits := 6 |
||||
b := make([]byte, digits) |
||||
n, err := io.ReadAtLeast(rand.Reader, b, digits) |
||||
if n != digits { |
||||
return "", err |
||||
} |
||||
for i := 0; i < len(b); i++ { |
||||
b[i] = table[int(b[i])%len(table)] |
||||
} |
||||
return string(b), nil |
||||
} |
@ -1,111 +0,0 @@
@@ -1,111 +0,0 @@
|
||||
package fediverse |
||||
|
||||
import ( |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/owncast/owncast/utils" |
||||
) |
||||
|
||||
const ( |
||||
accessToken = "fake-access-token" |
||||
account = "blah" |
||||
userID = "fake-user-id" |
||||
userDisplayName = "fake-user-display-name" |
||||
) |
||||
|
||||
func TestOTPFlowValidation(t *testing.T) { |
||||
r, success, err := RegisterFediverseOTP(accessToken, userID, userDisplayName, account) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
if !success { |
||||
t.Error("Registration should be permitted.") |
||||
} |
||||
|
||||
if r.Code == "" { |
||||
t.Error("Code is empty") |
||||
} |
||||
|
||||
if r.Account != account { |
||||
t.Error("Account is not set correctly") |
||||
} |
||||
|
||||
if r.Timestamp.IsZero() { |
||||
t.Error("Timestamp is empty") |
||||
} |
||||
|
||||
valid, registration := ValidateFediverseOTP(accessToken, r.Code) |
||||
if !valid { |
||||
t.Error("Code is not valid") |
||||
} |
||||
|
||||
if registration.Account != account { |
||||
t.Error("Account is not set correctly") |
||||
} |
||||
|
||||
if registration.UserID != userID { |
||||
t.Error("UserID is not set correctly") |
||||
} |
||||
|
||||
if registration.UserDisplayName != userDisplayName { |
||||
t.Error("UserDisplayName is not set correctly") |
||||
} |
||||
} |
||||
|
||||
func TestSingleOTPFlowRequest(t *testing.T) { |
||||
r1, _, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, account) |
||||
r2, s2, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, account) |
||||
|
||||
if r1.Code != r2.Code { |
||||
t.Error("Only one registration should be permitted.") |
||||
} |
||||
|
||||
if s2 { |
||||
t.Error("Second registration should not be permitted.") |
||||
} |
||||
} |
||||
|
||||
func TestAccountCaseInsensitive(t *testing.T) { |
||||
account := "Account" |
||||
accessToken := "another-fake-access-token" |
||||
r1, _, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, account) |
||||
_, reg1 := ValidateFediverseOTP(accessToken, r1.Code) |
||||
|
||||
// Simulate second auth with account in different case
|
||||
r2, _, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, strings.ToUpper(account)) |
||||
_, reg2 := ValidateFediverseOTP(accessToken, r2.Code) |
||||
|
||||
if reg1.Account != reg2.Account { |
||||
t.Errorf("Account names should be case-insensitive: %s %s", reg1.Account, reg2.Account) |
||||
} |
||||
} |
||||
|
||||
func TestLimitGlobalPendingRequests(t *testing.T) { |
||||
for i := 0; i < maxPendingRequests-1; i++ { |
||||
at, _ := utils.GenerateRandomString(10) |
||||
uid, _ := utils.GenerateRandomString(10) |
||||
account, _ := utils.GenerateRandomString(10) |
||||
|
||||
_, success, error := RegisterFediverseOTP(at, uid, "userDisplayName", account) |
||||
if !success { |
||||
t.Error("Registration should be permitted.", i, " of ", len(pendingAuthRequests)) |
||||
} |
||||
if error != nil { |
||||
t.Error(error) |
||||
} |
||||
} |
||||
|
||||
// This one should fail
|
||||
at, _ := utils.GenerateRandomString(10) |
||||
uid, _ := utils.GenerateRandomString(10) |
||||
account, _ := utils.GenerateRandomString(10) |
||||
_, success, error := RegisterFediverseOTP(at, uid, "userDisplayName", account) |
||||
if success { |
||||
t.Error("Registration should not be permitted.") |
||||
} |
||||
if error == nil { |
||||
t.Error("Error should be returned.") |
||||
} |
||||
} |
@ -1,168 +0,0 @@
@@ -1,168 +0,0 @@
|
||||
package indieauth |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"net/url" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/utils" |
||||
"github.com/pkg/errors" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
var ( |
||||
pendingAuthRequests = make(map[string]*Request) |
||||
lock = sync.Mutex{} |
||||
) |
||||
|
||||
const registrationTimeout = time.Minute * 10 |
||||
|
||||
func init() { |
||||
go setupExpiredRequestPruner() |
||||
} |
||||
|
||||
// Clear out any pending requests that have been pending for greater than
|
||||
// the specified timeout value.
|
||||
func setupExpiredRequestPruner() { |
||||
pruneExpiredRequestsTimer := time.NewTicker(registrationTimeout) |
||||
|
||||
for range pruneExpiredRequestsTimer.C { |
||||
lock.Lock() |
||||
log.Debugln("Pruning expired IndieAuth requests.") |
||||
for k, v := range pendingAuthRequests { |
||||
if time.Since(v.Timestamp) > registrationTimeout { |
||||
delete(pendingAuthRequests, k) |
||||
} |
||||
} |
||||
lock.Unlock() |
||||
} |
||||
} |
||||
|
||||
// StartAuthFlow will begin the IndieAuth flow by generating an auth request.
|
||||
func StartAuthFlow(authHost, userID, accessToken, displayName string) (*url.URL, error) { |
||||
// Limit the number of pending requests
|
||||
if len(pendingAuthRequests) >= maxPendingRequests { |
||||
return nil, errors.New("Please try again later. Too many pending requests.") |
||||
} |
||||
|
||||
// Reject any requests to our internal network or loopback
|
||||
if utils.IsHostnameInternal(authHost) { |
||||
return nil, errors.New("unable to use provided host") |
||||
} |
||||
|
||||
// Santity check the server URL
|
||||
u, err := url.ParseRequestURI(authHost) |
||||
if err != nil { |
||||
return nil, errors.New("unable to parse server URL") |
||||
} |
||||
|
||||
// Limit to only secured connections
|
||||
if u.Scheme != "https" { |
||||
return nil, errors.New("only servers secured with https are supported") |
||||
} |
||||
|
||||
serverURL := data.GetServerURL() |
||||
if serverURL == "" { |
||||
return nil, errors.New("Owncast server URL must be set when using auth") |
||||
} |
||||
|
||||
r, err := createAuthRequest(authHost, userID, displayName, accessToken, serverURL) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to generate IndieAuth request") |
||||
} |
||||
|
||||
pendingAuthRequests[r.State] = r |
||||
|
||||
return r.Redirect, nil |
||||
} |
||||
|
||||
// HandleCallbackCode will handle the callback from the IndieAuth server
|
||||
// to continue the next step of the auth flow.
|
||||
func HandleCallbackCode(code, state string) (*Request, *Response, error) { |
||||
request, exists := pendingAuthRequests[state] |
||||
if !exists { |
||||
return nil, nil, errors.New("no auth requests pending") |
||||
} |
||||
|
||||
data := url.Values{} |
||||
data.Set("grant_type", "authorization_code") |
||||
data.Set("code", code) |
||||
data.Set("client_id", request.ClientID) |
||||
data.Set("redirect_uri", request.Callback.String()) |
||||
data.Set("code_verifier", request.CodeVerifier) |
||||
|
||||
// Do not support redirects.
|
||||
client := &http.Client{ |
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error { |
||||
return http.ErrUseLastResponse |
||||
}, |
||||
} |
||||
|
||||
r, err := http.NewRequest("POST", request.Endpoint.String(), strings.NewReader(data.Encode())) // URL-encoded payload
|
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
r.Header.Add("Content-Type", "application/x-www-form-urlencoded") |
||||
r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) |
||||
|
||||
res, err := client.Do(r) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer res.Body.Close() |
||||
body, err := io.ReadAll(res.Body) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
|
||||
var response Response |
||||
if err := json.Unmarshal(body, &response); err != nil { |
||||
return nil, nil, errors.Wrap(err, "unable to parse IndieAuth response: "+string(body)) |
||||
} |
||||
|
||||
if response.Error != "" || response.ErrorDescription != "" { |
||||
errorText := makeIndieAuthClientErrorText(response.Error) |
||||
log.Debugln("IndieAuth error:", response.Error, response.ErrorDescription) |
||||
return nil, nil, fmt.Errorf("IndieAuth error: %s - %s", errorText, response.ErrorDescription) |
||||
} |
||||
|
||||
// In case this IndieAuth server does not use OAuth error keys or has internal
|
||||
// issues resulting in unstructured errors.
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 { |
||||
log.Debugln("IndieAuth error. status code:", res.StatusCode, "body:", string(body)) |
||||
return nil, nil, errors.New("there was an error authenticating against IndieAuth server") |
||||
} |
||||
|
||||
// Trim any trailing slash so we can accurately compare the two "me" values
|
||||
meResponseVerifier := strings.TrimRight(response.Me, "/") |
||||
meRequestVerifier := strings.TrimRight(request.Me.String(), "/") |
||||
|
||||
// What we sent and what we got back must match
|
||||
if meRequestVerifier != meResponseVerifier { |
||||
return nil, nil, errors.New("indieauth response does not match the initial anticipated auth destination") |
||||
} |
||||
|
||||
return request, &response, nil |
||||
} |
||||
|
||||
// Error value should be from this list:
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||
func makeIndieAuthClientErrorText(err string) string { |
||||
switch err { |
||||
case "invalid_request", "invalid_client": |
||||
return "The authentication request was invalid. Please report this to the Owncast project." |
||||
case "invalid_grant", "unauthorized_client": |
||||
return "This authorization request is unauthorized." |
||||
case "unsupported_grant_type": |
||||
return "The authorization grant type is not supported by the authorization server." |
||||
default: |
||||
return err |
||||
} |
||||
} |
@ -1,126 +0,0 @@
@@ -1,126 +0,0 @@
|
||||
package indieauth |
||||
|
||||
import ( |
||||
"crypto/sha256" |
||||
"encoding/base64" |
||||
"fmt" |
||||
"net/http" |
||||
"net/url" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/andybalholm/cascadia" |
||||
"github.com/pkg/errors" |
||||
"golang.org/x/net/html" |
||||
) |
||||
|
||||
func createAuthRequest(authDestination, userID, displayName, accessToken, baseServer string) (*Request, error) { |
||||
authURL, err := url.Parse(authDestination) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to parse IndieAuth destination") |
||||
} |
||||
|
||||
authEndpointURL, err := getAuthEndpointFromURL(authURL.String()) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to get IndieAuth endpoint from destination URL") |
||||
} |
||||
|
||||
baseServerURL, err := url.Parse(baseServer) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to parse local owncast base server URL") |
||||
} |
||||
|
||||
callbackURL := *baseServerURL |
||||
callbackURL.Path = "/api/auth/indieauth/callback" |
||||
|
||||
codeVerifier := randString(50) |
||||
codeChallenge := createCodeChallenge(codeVerifier) |
||||
state := randString(20) |
||||
responseType := "code" |
||||
clientID := baseServerURL.String() // Our local URL
|
||||
codeChallengeMethod := "S256" |
||||
|
||||
redirect := *authEndpointURL |
||||
|
||||
q := authURL.Query() |
||||
q.Add("response_type", responseType) |
||||
q.Add("client_id", clientID) |
||||
q.Add("state", state) |
||||
q.Add("code_challenge_method", codeChallengeMethod) |
||||
q.Add("code_challenge", codeChallenge) |
||||
q.Add("me", authURL.String()) |
||||
q.Add("redirect_uri", callbackURL.String()) |
||||
redirect.RawQuery = q.Encode() |
||||
|
||||
return &Request{ |
||||
Me: authURL, |
||||
UserID: userID, |
||||
DisplayName: displayName, |
||||
CurrentAccessToken: accessToken, |
||||
Endpoint: authEndpointURL, |
||||
ClientID: baseServer, |
||||
CodeVerifier: codeVerifier, |
||||
CodeChallenge: codeChallenge, |
||||
State: state, |
||||
Redirect: &redirect, |
||||
Callback: &callbackURL, |
||||
Timestamp: time.Now(), |
||||
}, nil |
||||
} |
||||
|
||||
func getAuthEndpointFromURL(urlstring string) (*url.URL, error) { |
||||
htmlDocScrapeURL, err := url.Parse(urlstring) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to parse URL") |
||||
} |
||||
|
||||
if htmlDocScrapeURL.Scheme != "https" { |
||||
return nil, fmt.Errorf("url must be https") |
||||
} |
||||
|
||||
r, err := http.Get(htmlDocScrapeURL.String()) // nolint:gosec
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer r.Body.Close() |
||||
|
||||
scrapedHTMLDocument, err := html.Parse(r.Body) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to parse html at remote auth host") |
||||
} |
||||
authorizationEndpointTag := cascadia.MustCompile("link[rel=authorization_endpoint]").MatchAll(scrapedHTMLDocument) |
||||
if len(authorizationEndpointTag) == 0 { |
||||
return nil, fmt.Errorf("url does not support indieauth") |
||||
} |
||||
|
||||
for _, attr := range authorizationEndpointTag[len(authorizationEndpointTag)-1].Attr { |
||||
if attr.Key == "href" { |
||||
u, err := url.Parse(attr.Val) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to parse authorization endpoint") |
||||
} |
||||
|
||||
// If it is a relative URL we an fill in the missing components
|
||||
// by using the original URL we scraped, since it is the same host.
|
||||
if u.Scheme == "" { |
||||
u.Scheme = htmlDocScrapeURL.Scheme |
||||
} |
||||
|
||||
if u.Host == "" { |
||||
u.Host = htmlDocScrapeURL.Host |
||||
} |
||||
|
||||
return u, nil |
||||
} |
||||
} |
||||
|
||||
return nil, fmt.Errorf("unable to find href value for authorization_endpoint") |
||||
} |
||||
|
||||
func createCodeChallenge(codeVerifier string) string { |
||||
sha256hash := sha256.Sum256([]byte(codeVerifier)) |
||||
|
||||
encodedHashedCode := strings.TrimRight(base64.URLEncoding.EncodeToString(sha256hash[:]), "=") |
||||
|
||||
return encodedHashedCode |
||||
} |
@ -1,35 +0,0 @@
@@ -1,35 +0,0 @@
|
||||
package indieauth |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/owncast/owncast/utils" |
||||
) |
||||
|
||||
func TestLimitGlobalPendingRequests(t *testing.T) { |
||||
// Simulate 10 pending requests
|
||||
for i := 0; i < maxPendingRequests-1; i++ { |
||||
cid, _ := utils.GenerateRandomString(10) |
||||
redirectURL, _ := utils.GenerateRandomString(10) |
||||
cc, _ := utils.GenerateRandomString(10) |
||||
state, _ := utils.GenerateRandomString(10) |
||||
me, _ := utils.GenerateRandomString(10) |
||||
|
||||
_, err := StartServerAuth(cid, redirectURL, cc, state, me) |
||||
if err != nil { |
||||
t.Error("Registration should be permitted.", i, " of ", len(pendingAuthRequests), err) |
||||
} |
||||
} |
||||
|
||||
// This should throw an error
|
||||
cid, _ := utils.GenerateRandomString(10) |
||||
redirectURL, _ := utils.GenerateRandomString(10) |
||||
cc, _ := utils.GenerateRandomString(10) |
||||
state, _ := utils.GenerateRandomString(10) |
||||
me, _ := utils.GenerateRandomString(10) |
||||
|
||||
_, err := StartServerAuth(cid, redirectURL, cc, state, me) |
||||
if err == nil { |
||||
t.Error("Registration should not be permitted.") |
||||
} |
||||
} |
@ -1,34 +0,0 @@
@@ -1,34 +0,0 @@
|
||||
package indieauth |
||||
|
||||
import ( |
||||
"math/rand" |
||||
"time" |
||||
"unsafe" |
||||
) |
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" |
||||
const ( |
||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
) |
||||
|
||||
var src = rand.NewSource(time.Now().UnixNano()) |
||||
|
||||
func randString(n int) string { |
||||
b := make([]byte, n) |
||||
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
|
||||
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; { |
||||
if remain == 0 { |
||||
cache, remain = src.Int63(), letterIdxMax |
||||
} |
||||
if idx := int(cache & letterIdxMask); idx < len(letterBytes) { |
||||
b[i] = letterBytes[idx] |
||||
i-- |
||||
} |
||||
cache >>= letterIdxBits |
||||
remain-- |
||||
} |
||||
|
||||
return *(*string)(unsafe.Pointer(&b)) // nolint:gosec
|
||||
} |
@ -1,22 +0,0 @@
@@ -1,22 +0,0 @@
|
||||
package indieauth |
||||
|
||||
import ( |
||||
"net/url" |
||||
"time" |
||||
) |
||||
|
||||
// Request represents a single in-flight IndieAuth request.
|
||||
type Request struct { |
||||
Timestamp time.Time |
||||
Endpoint *url.URL |
||||
Redirect *url.URL // Outbound redirect URL to continue auth flow
|
||||
Callback *url.URL // Inbound URL to get auth flow results
|
||||
Me *url.URL |
||||
UserID string |
||||
DisplayName string |
||||
CurrentAccessToken string |
||||
ClientID string |
||||
CodeVerifier string |
||||
CodeChallenge string |
||||
State string |
||||
} |
@ -1,18 +0,0 @@
@@ -1,18 +0,0 @@
|
||||
package indieauth |
||||
|
||||
// Profile represents optional profile data that is returned
|
||||
// when completing the IndieAuth flow.
|
||||
type Profile struct { |
||||
Name string `json:"name"` |
||||
URL string `json:"url"` |
||||
Photo string `json:"photo"` |
||||
} |
||||
|
||||
// Response the response returned when completing
|
||||
// the IndieAuth flow.
|
||||
type Response struct { |
||||
Me string `json:"me,omitempty"` |
||||
Profile Profile `json:"profile,omitempty"` |
||||
Error string `json:"error,omitempty"` |
||||
ErrorDescription string `json:"error_description,omitempty"` |
||||
} |
@ -1,101 +0,0 @@
@@ -1,101 +0,0 @@
|
||||
package indieauth |
||||
|
||||
import ( |
||||
"fmt" |
||||
"time" |
||||
|
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/pkg/errors" |
||||
"github.com/teris-io/shortid" |
||||
) |
||||
|
||||
// ServerAuthRequest is n inbound request to authenticate against
|
||||
// this Owncast instance.
|
||||
type ServerAuthRequest struct { |
||||
Timestamp time.Time |
||||
ClientID string |
||||
RedirectURI string |
||||
CodeChallenge string |
||||
State string |
||||
Me string |
||||
Code string |
||||
} |
||||
|
||||
// ServerProfile represents basic user-provided data about this Owncast instance.
|
||||
type ServerProfile struct { |
||||
Name string `json:"name"` |
||||
URL string `json:"url"` |
||||
Photo string `json:"photo"` |
||||
} |
||||
|
||||
// ServerProfileResponse is returned when an auth flow requests the final
|
||||
// confirmation of the IndieAuth flow.
|
||||
type ServerProfileResponse struct { |
||||
Me string `json:"me,omitempty"` |
||||
Profile ServerProfile `json:"profile,omitempty"` |
||||
// Error keys need to match the OAuth spec.
|
||||
Error string `json:"error,omitempty"` |
||||
ErrorDescription string `json:"error_description,omitempty"` |
||||
} |
||||
|
||||
var pendingServerAuthRequests = map[string]ServerAuthRequest{} |
||||
|
||||
const maxPendingRequests = 100 |
||||
|
||||
// StartServerAuth will handle the authentication for the admin user of this
|
||||
// Owncast server. Initiated via a GET of the auth endpoint.
|
||||
// https://indieweb.org/authorization-endpoint
|
||||
func StartServerAuth(clientID, redirectURI, codeChallenge, state, me string) (*ServerAuthRequest, error) { |
||||
if len(pendingServerAuthRequests)+1 >= maxPendingRequests { |
||||
return nil, errors.New("Please try again later. Too many pending requests.") |
||||
} |
||||
|
||||
code := shortid.MustGenerate() |
||||
|
||||
r := ServerAuthRequest{ |
||||
ClientID: clientID, |
||||
RedirectURI: redirectURI, |
||||
CodeChallenge: codeChallenge, |
||||
State: state, |
||||
Me: me, |
||||
Code: code, |
||||
Timestamp: time.Now(), |
||||
} |
||||
|
||||
pendingServerAuthRequests[code] = r |
||||
|
||||
return &r, nil |
||||
} |
||||
|
||||
// CompleteServerAuth will verify that the values provided in the final step
|
||||
// of the IndieAuth flow are correct, and return some basic profile info.
|
||||
func CompleteServerAuth(code, redirectURI, clientID string, codeVerifier string) (*ServerProfileResponse, error) { |
||||
request, pending := pendingServerAuthRequests[code] |
||||
if !pending { |
||||
return nil, errors.New("no pending authentication request") |
||||
} |
||||
|
||||
if request.RedirectURI != redirectURI { |
||||
return nil, errors.New("redirect URI does not match") |
||||
} |
||||
|
||||
if request.ClientID != clientID { |
||||
return nil, errors.New("client ID does not match") |
||||
} |
||||
|
||||
codeChallengeFromRequest := createCodeChallenge(codeVerifier) |
||||
if request.CodeChallenge != codeChallengeFromRequest { |
||||
return nil, errors.New("code verifier is incorrect") |
||||
} |
||||
|
||||
response := ServerProfileResponse{ |
||||
Me: data.GetServerURL(), |
||||
Profile: ServerProfile{ |
||||
Name: data.GetServerName(), |
||||
URL: data.GetServerURL(), |
||||
Photo: fmt.Sprintf("%s/%s", data.GetServerURL(), data.GetLogoPath()), |
||||
}, |
||||
} |
||||
|
||||
return &response, nil |
||||
} |
@ -1,67 +0,0 @@
@@ -1,67 +0,0 @@
|
||||
package auth |
||||
|
||||
import ( |
||||
"context" |
||||
"strings" |
||||
|
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/core/user" |
||||
|
||||
"github.com/owncast/owncast/db" |
||||
) |
||||
|
||||
var _datastore *data.Datastore |
||||
|
||||
// Setup will initialize auth persistence.
|
||||
func Setup(db *data.Datastore) { |
||||
_datastore = db |
||||
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS auth ( |
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
||||
"user_id" TEXT NOT NULL, |
||||
"token" TEXT NOT NULL, |
||||
"type" TEXT NOT NULL, |
||||
"timestamp" DATE DEFAULT CURRENT_TIMESTAMP NOT NULL, |
||||
FOREIGN KEY(user_id) REFERENCES users(id) |
||||
);` |
||||
_datastore.MustExec(createTableSQL) |
||||
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_auth_token ON auth (token);`) |
||||
} |
||||
|
||||
// AddAuth will add an external authentication token and type for a user.
|
||||
func AddAuth(userID, authToken string, authType Type) error { |
||||
return _datastore.GetQueries().AddAuthForUser(context.Background(), db.AddAuthForUserParams{ |
||||
UserID: userID, |
||||
Token: authToken, |
||||
Type: string(authType), |
||||
}) |
||||
} |
||||
|
||||
// GetUserByAuth will return an existing user given auth details if a user
|
||||
// has previously authenticated with that method.
|
||||
func GetUserByAuth(authToken string, authType Type) *user.User { |
||||
u, err := _datastore.GetQueries().GetUserByAuth(context.Background(), db.GetUserByAuthParams{ |
||||
Token: authToken, |
||||
Type: string(authType), |
||||
}) |
||||
if err != nil { |
||||
return nil |
||||
} |
||||
|
||||
var scopes []string |
||||
if u.Scopes.Valid { |
||||
scopes = strings.Split(u.Scopes.String, ",") |
||||
} |
||||
|
||||
return &user.User{ |
||||
ID: u.ID, |
||||
DisplayName: u.DisplayName, |
||||
DisplayColor: int(u.DisplayColor), |
||||
CreatedAt: u.CreatedAt.Time, |
||||
DisabledAt: &u.DisabledAt.Time, |
||||
PreviousNames: strings.Split(u.PreviousNames.String, ","), |
||||
NameChangedAt: &u.NamechangedAt.Time, |
||||
AuthenticatedAt: &u.AuthenticatedAt.Time, |
||||
Scopes: scopes, |
||||
} |
||||
} |
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash |
||||
# shellcheck disable=SC2059 |
||||
|
||||
set -o errexit |
||||
set -o nounset |
||||
set -o pipefail |
||||
|
||||
INSTALL_TEMP_DIRECTORY="$(mktemp -d)" |
||||
PROJECT_SOURCE_DIR=$(pwd) |
||||
cd $INSTALL_TEMP_DIRECTORY |
||||
|
||||
shutdown () { |
||||
rm -rf "$INSTALL_TEMP_DIRECTORY" |
||||
} |
||||
trap shutdown INT TERM ABRT EXIT |
||||
|
||||
echo "Cloning owncast admin into $INSTALL_TEMP_DIRECTORY..." |
||||
git clone https://github.com/owncast/owncast-admin 2> /dev/null |
||||
cd owncast-admin |
||||
|
||||
echo "Installing npm modules for the owncast admin..." |
||||
npm --silent install 2> /dev/null |
||||
|
||||
echo "Building owncast admin..." |
||||
rm -rf .next |
||||
(node_modules/.bin/next build && node_modules/.bin/next export) | grep info |
||||
|
||||
echo "Copying admin to project directory..." |
||||
ADMIN_BUILD_DIR=$(pwd) |
||||
cd $PROJECT_SOURCE_DIR |
||||
mkdir -p admin 2> /dev/null |
||||
cd admin |
||||
|
||||
# Remove the old one |
||||
rm -rf $PROJECT_SOURCE_DIR/static/admin |
||||
|
||||
# Copy over the new one |
||||
mv ${ADMIN_BUILD_DIR}/out $PROJECT_SOURCE_DIR/static/admin |
||||
|
||||
shutdown |
||||
echo "Done." |
@ -1,24 +0,0 @@
@@ -1,24 +0,0 @@
|
||||
#!/bin/sh |
||||
set -e |
||||
|
||||
# Development container builder |
||||
# |
||||
# Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages |
||||
# env vars: |
||||
# $EARTHLY_BUILD_BRANCH: git branch to checkout |
||||
# $EARTHLY_BUILD_TAG: tag for container image |
||||
|
||||
EARTHLY_IMAGE_NAME="owncast" |
||||
BUILD_TAG=${EARTHLY_BUILD_TAG:-develop} |
||||
DATE=$(date +"%Y%m%d") |
||||
VERSION="${DATE}-${BUILD_TAG}" |
||||
|
||||
echo "Building container image ${EARTHLY_IMAGE_NAME}:${BUILD_TAG} ..." |
||||
|
||||
# Change to the root directory of the repository |
||||
cd "$(git rev-parse --show-toplevel)" || exit |
||||
if [ -n "${EARTHLY_BUILD_BRANCH}" ]; then |
||||
git checkout "${EARTHLY_BUILD_BRANCH}" || exit |
||||
fi |
||||
|
||||
earthly --ci +docker-all --images="ghcr.io/owncast/${EARTHLY_IMAGE_NAME}:${BUILD_TAG}" --version="${VERSION}" |
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
## Third party web dependencies |
||||
|
||||
Owncast's web frontend utilizes a few third party Javascript and CSS dependencies that we ship with the application. |
||||
|
||||
To add, remove, or update one of these components: |
||||
|
||||
1. Perform your `npm install/uninstall/etc`, or edit the `package.json` file to reflect the change you want to make. |
||||
2. Edit the `snowpack` `install` block of `package.json` to specify what files you want to add to the Owncast project. This can be an entire library (such as `preact`) or a single file (such as `video.js/dist/video.min.js`). These paths point to files that live in `node_modules`. |
||||
3. Run `npm run build`. This will download the requested module from NPM, package up the assets you specified, and then copy them to the Owncast web app in the `webroot/js/web_modules` directory. |
||||
4. Your new web dependency is now available for use in your web code. |
||||
|
||||
## VideoJS versions |
||||
|
||||
Currently Videojs version 7.8.3 and http-streaming version 2.2.0 are hardcoded because these are versions that have been found to work properly with our HLS stream. Other versions have had issues with things like discontinuities causing a loading spinner. |
||||
|
||||
So if you update videojs or vhs make sure you do an end-to-end test of a stream and make sure the "this stream is offline" ending video displays properly. |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
{ |
||||
"name": "owncast-dependencies", |
||||
"version": "1.0.0", |
||||
"description": "Javascript dependencies for Owncast web app", |
||||
"main": "index.js", |
||||
"dependencies": { |
||||
"@joeattardi/emoji-button": "^4.6.2", |
||||
"@videojs/themes": "^1.0.1", |
||||
"htm": "^3.1.0", |
||||
"mark.js": "^8.11.1", |
||||
"micromodal": "^0.4.10", |
||||
"preact": "10.6.6", |
||||
"tailwindcss": "^1.9.6", |
||||
"video.js": "7.17.0" |
||||
}, |
||||
"devDependencies": { |
||||
"cssnano": "5.1.0", |
||||
"postcss": "8.4.7", |
||||
"postcss-cli": "9.1.0" |
||||
}, |
||||
"snowpack": { |
||||
"install": [ |
||||
"@videojs/themes/fantasy/*", |
||||
"video.js/dist/video-js.min.css", |
||||
"video.js/dist/video.min.js", |
||||
"@joeattardi/emoji-button", |
||||
"htm", |
||||
"preact", |
||||
"preact/hooks", |
||||
"mark.js/dist/mark.es6.min.js", |
||||
"tailwindcss/dist/tailwind.min.css", |
||||
"micromodal/dist/micromodal.min.js" |
||||
] |
||||
}, |
||||
"scripts": { |
||||
"test": "echo \"Error: no test specified\" && exit 1", |
||||
"build": "npm install && npx snowpack@2.18.4 install && cp node_modules/video.js/dist/video-js.min.css web_modules/videojs && rm -rf ../../webroot/js/web_modules && cp -R web_modules ../../webroot/js" |
||||
}, |
||||
"author": "Owncast", |
||||
"license": "ISC" |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
module.exports = { |
||||
plugins: [ |
||||
require('cssnano')({ |
||||
preset: 'default', |
||||
}), |
||||
], |
||||
}; |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
module.exports = { |
||||
purge: { |
||||
enabled: true, |
||||
mode: 'layers', |
||||
content: ['../../webroot/js/**'], |
||||
}, |
||||
}; |
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
#!/bin/sh |
||||
|
||||
# Human readable names of binary distributions |
||||
DISTRO=(macOS-64bit linux-64bit linux-32bit linux-arm7 linux-arm64) |
||||
# Operating systems for the respective distributions |
||||
OS=(darwin linux linux linux linux) |
||||
# Architectures for the respective distributions |
||||
ARCH=(amd64 amd64 386 arm-7 arm64) |
||||
|
||||
# Version |
||||
VERSION=$1 |
||||
SHOULD_RELEASE=$2 |
||||
|
||||
# Build info |
||||
GIT_COMMIT=$(git rev-list -1 HEAD) |
||||
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) |
||||
|
||||
if [[ -z "${VERSION}" ]]; then |
||||
echo "Version must be specified when running build" |
||||
exit |
||||
fi |
||||
|
||||
BUILD_TEMP_DIRECTORY="$(mktemp -d)" |
||||
cd $BUILD_TEMP_DIRECTORY |
||||
|
||||
echo "Cloning owncast into $BUILD_TEMP_DIRECTORY..." |
||||
git clone https://github.com/owncast/owncast 2> /dev/null |
||||
cd owncast |
||||
|
||||
echo "Changing to branch: $GIT_BRANCH" |
||||
git checkout $GIT_BRANCH |
||||
|
||||
[[ -z "${VERSION}" ]] && VERSION='unknownver' || VERSION="${VERSION}" |
||||
|
||||
# Change to the root directory of the repository |
||||
cd $(git rev-parse --show-toplevel) |
||||
|
||||
echo "Cleaning working directories..." |
||||
rm -rf ./webroot/hls/* ./hls/* ./webroot/thumbnail.jpg |
||||
|
||||
echo "Creating version ${VERSION} from commit ${GIT_COMMIT}" |
||||
|
||||
# Create production build of Tailwind CSS |
||||
pushd build/javascript >> /dev/null |
||||
# Install the tailwind & postcss CLIs |
||||
npm install --quiet --no-progress |
||||
# Run the tailwind CLI and pipe it to postcss for minification. |
||||
# Save it to a temp directory that we will reference below. |
||||
NODE_ENV="production" ./node_modules/.bin/tailwind build | ./node_modules/.bin/postcss > "${TMPDIR}tailwind.min.css" |
||||
popd |
||||
|
||||
mkdir -p dist |
||||
|
||||
build() { |
||||
NAME=$1 |
||||
OS=$2 |
||||
ARCH=$3 |
||||
VERSION=$4 |
||||
GIT_COMMIT=$5 |
||||
|
||||
echo "Building ${NAME} (${OS}/${ARCH}) release from ${GIT_BRANCH} ${GIT_COMMIT}..." |
||||
|
||||
mkdir -p dist/${NAME} |
||||
mkdir -p dist/${NAME}/data |
||||
|
||||
cp -R webroot/ dist/${NAME}/webroot/ |
||||
|
||||
# Copy the production pruned+minified css to the build's directory. |
||||
cp "${TMPDIR}tailwind.min.css" ./dist/${NAME}/webroot/js/web_modules/tailwindcss/dist/tailwind.min.css |
||||
cp README.md dist/${NAME} |
||||
|
||||
pushd dist/${NAME} >> /dev/null |
||||
|
||||
CGO_ENABLED=1 ~/go/bin/xgo -go latest --branch ${GIT_BRANCH} -ldflags "-s -w -X github.com/owncast/owncast/config.GitCommit=${GIT_COMMIT} -X github.com/owncast/owncast/config.BuildVersion=${VERSION} -X github.com/owncast/owncast/config.BuildPlatform=${NAME}" -tags enable_updates -targets "${OS}/${ARCH}" github.com/owncast/owncast |
||||
mv owncast-*-${ARCH} owncast |
||||
|
||||
zip -r -q -8 ../owncast-$VERSION-$NAME.zip . |
||||
popd >> /dev/null |
||||
|
||||
rm -rf dist/${NAME}/ |
||||
} |
||||
|
||||
for i in "${!DISTRO[@]}"; do |
||||
build ${DISTRO[$i]} ${OS[$i]} ${ARCH[$i]} $VERSION $GIT_COMMIT |
||||
done |
||||
|
||||
echo "Build archives are available in $BUILD_TEMP_DIRECTORY/owncast/dist" |
||||
ls -alh "$BUILD_TEMP_DIRECTORY/owncast/dist" |
||||
|
||||
# Use the second argument "release" to create an actual release. |
||||
if [ "$SHOULD_RELEASE" != "release" ]; then |
||||
echo "Not uploading a release." |
||||
exit |
||||
fi |
||||
|
||||
# Create the tag |
||||
git tag -a "v${VERSION}" -m "Release build v${VERSION}" |
||||
|
||||
# On macOS open the Github page for new releases so they can be uploaded |
||||
if test -f "/usr/bin/open"; then |
||||
open "https://github.com/owncast/owncast/releases/new" |
||||
open dist |
||||
fi |
||||
|
||||
# Docker build |
||||
# Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages |
||||
DOCKER_IMAGE="owncast-${VERSION}" |
||||
echo "Building Docker image ${DOCKER_IMAGE}..." |
||||
|
||||
# Change to the root directory of the repository |
||||
cd $(git rev-parse --show-toplevel) |
||||
|
||||
# Docker build |
||||
docker build --build-arg NAME=docker --build-arg VERSION=${VERSION} --build-arg GIT_COMMIT=$GIT_COMMIT -t gabekangas/owncast:$VERSION -t gabekangas/owncast:latest -t owncast . |
||||
|
||||
# Dockerhub |
||||
# You must be authenticated via `docker login` with your Dockerhub credentials first. |
||||
docker push "gabekangas/owncast:${VERSION}" |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
# Docker build |
||||
# Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages |
||||
DOCKER_IMAGE="owncast" |
||||
DATE=$(date +"%Y%m%d") |
||||
VERSION="${DATE}-nightly" |
||||
GIT_COMMIT=$(git rev-list -1 HEAD) |
||||
|
||||
# Create production build of Tailwind CSS |
||||
pushd ../../build/javascript >> /dev/null |
||||
# Install the tailwind & postcss CLIs |
||||
npm install --quiet --no-progress |
||||
# Run the tailwind CLI and pipe it to postcss for minification. |
||||
# Save it to a temp directory that we will reference below. |
||||
NODE_ENV="production" ./node_modules/.bin/tailwind build | ./node_modules/.bin/postcss > "../../webroot/js/web_modules/tailwindcss/dist/tailwind.min.css" |
||||
popd |
||||
|
||||
echo "Building Docker image ${DOCKER_IMAGE}..." |
||||
|
||||
# Change to the root directory of the repository |
||||
cd $(git rev-parse --show-toplevel) |
||||
|
||||
# Docker build |
||||
docker build --build-arg NAME=docker --build-arg VERSION=${VERSION} --build-arg GIT_COMMIT=$GIT_COMMIT -t ghcr.io/owncast/${DOCKER_IMAGE}:nightly . |
||||
|
||||
# Dockerhub |
||||
# You must be authenticated via `docker login` with your Dockerhub credentials first. |
||||
# docker push gabekangas/owncast:nightly |
||||
|
||||
docker push ghcr.io/owncast/${DOCKER_IMAGE}:nightly |
@ -1,40 +0,0 @@
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env bash |
||||
# shellcheck disable=SC2059 |
||||
|
||||
set -o errexit |
||||
set -o nounset |
||||
set -o pipefail |
||||
|
||||
OFFLINE= |
||||
while [[ $# -gt 0 ]]; do |
||||
case $1 in |
||||
--offline) |
||||
OFFLINE=1 |
||||
;; |
||||
esac |
||||
shift |
||||
done |
||||
|
||||
# Change to the root directory of the repository |
||||
cd "$(git rev-parse --show-toplevel)" |
||||
|
||||
cd web |
||||
|
||||
if [ ! "$OFFLINE" ]; then |
||||
echo "Installing npm modules for the owncast web..." |
||||
npm --silent install 2>/dev/null |
||||
fi |
||||
|
||||
echo "Building owncast web..." |
||||
rm -rf .next |
||||
node_modules/.bin/next build | grep info |
||||
|
||||
echo "Copying web project to dist directory..." |
||||
|
||||
# Remove the old one |
||||
rm -rf ../static/web |
||||
|
||||
# Copy over the new one |
||||
mv ./out ../static/web |
||||
|
||||
echo "Done." |
@ -1,7 +0,0 @@
@@ -1,7 +0,0 @@
|
||||
# Contrib |
||||
|
||||
This directory contains unmaintained, unsupported, and unorganized contributions by the Owncast community. |
||||
|
||||
It is a place to put scripts, config files and examples that might be useful to share with others without expectation that they're an official part of the project. |
||||
|
||||
[Read Drew DeVault's description of the contrib directory](https://drewdevault.com/2020/06/06/Add-a-contrib-directory.html) for details and background. |
@ -1,19 +0,0 @@
@@ -1,19 +0,0 @@
|
||||
[Unit] |
||||
Description=Owncast Service |
||||
|
||||
[Service] |
||||
Type=simple |
||||
WorkingDirectory=[path to owncast directory] |
||||
ReadWritePaths=[path to owncast directory] |
||||
ExecStart=[path to owncast directory]/owncast |
||||
Restart=always |
||||
RestartSec=5 |
||||
User=[user to run owncast as] |
||||
Group=[group to run owncast as] |
||||
NoNewPrivileges=true |
||||
SecureBits=noroot |
||||
ProtectSystem=strict |
||||
ProtectHome=read-only |
||||
|
||||
[Install] |
||||
WantedBy=multi-user.target |
@ -1,82 +0,0 @@
@@ -1,82 +0,0 @@
|
||||
# Owncast on Windows |
||||
|
||||
> Note: Owncast currently **does not natively support the Windows Operating System**, however it is possible to run Owncast on Windows using the Windows Subsystem for Linux (WSL2). |
||||
|
||||
This document is a user-contributed document and the Owncast project does not actively maintain Windows support. Hopefully this can be helpful in pointing people in the right direction. |
||||
|
||||
This document list out the steps in detail to install and run Owncast in Windows using Windows Subsystem for Linux, specifically **WSL2**. |
||||
|
||||
Below are steps both for local development, contributing to the project and running it in production. |
||||
|
||||
--- |
||||
|
||||
## Required: Installing WSL2 in Windows |
||||
|
||||
There are lots of tutorials available online (videos and docs both) on how to install WSL2. |
||||
Here are the official documents from Microsoft -> [Install Linux on Windows with WSL](https://learn.microsoft.com/en-us/windows/wsl/setup/environment) |
||||
Some points to remember -> |
||||
|
||||
- Preferable method to install WSL2 is by using the `wsl --install `. If you are facing issues with this method you can look at - [Manual installation steps for older versions of WSL](https://learn.microsoft.com/en-us/windows/wsl/install-manual) |
||||
- Make sure you have enabled the Virtual Machine feature. (ignore if used wsl --install method) |
||||
- Make sure you have WSL2 |
||||
- Installed your Linux distribution of choice and make sure you installed the latest available version (Preferably Ubuntu) |
||||
|
||||
### Setting up WSL2 and the distribution of your choice |
||||
|
||||
After basic setup, you can look into setting WSL2 for development. Here is the link for a detailed document by Microsoft - [https://learn.microsoft.com/en-us/windows/wsl/setup/environment](https://learn.microsoft.com/en-us/windows/wsl/setup/environment) |
||||
|
||||
--- |
||||
|
||||
## Installing Owncast under WSL2 |
||||
|
||||
Once you're running WSL2 in Windows you can install Owncast the same way you would on any Linux distribution by following the [Quickstart](https://owncast.online/quickstart/) guide. |
||||
|
||||
## Contributing to Owncast by performing local development |
||||
|
||||
If you want to use your Windows machine to contribute to Owncast, you'll need to do so under WSL2 and make sure the following prerequisites are installed. |
||||
|
||||
### Make sure all the prerequisites are installed in WSL2 |
||||
|
||||
Here is the list for all the prerequisites required -> |
||||
|
||||
- C compiler, such as [GCC compiler](https://gcc.gnu.org/install/download.html) or a [Musl-compatible compiler](https://musl.libc.org/) |
||||
- npm (Node Package Manager) is installed as `sudo apt install npm`. |
||||
- Node.js is installed (LTS Version) `sudo apt install nodejs`. |
||||
- [ffmpeg](https://ffmpeg.org/download.html) |
||||
- Install the [Go toolchain](https://golang.org/dl/) (1.21 or above). |
||||
|
||||
### Read more |
||||
|
||||
Once your local development environment is setup, you can read more about how to contribute to Owncast [by reading the development document](https://owncast.online/development/). |
||||
|
||||
## Some possible issues you can face while setting up WSL2 |
||||
|
||||
### You have an older version of Nodejs installed in the WSL2 |
||||
|
||||
To solve this issue you can look at nvm. Here is one tutorial - [Node-Version-Manager](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-20-04#option-3-installing-node-using-the-node-version-manager). |
||||
|
||||
### The broadcasting Software failed to connect to the server |
||||
|
||||
This issue arises when you try to use `rtmp://localhost:1935/live` for example in OBS. |
||||
To solve this issue you need to find the correct IP address for the WSL2 you are running and use that instead of localhost. |
||||
You can use the below commands to find that -> |
||||
Note: you can use either of these, whichever works for you. |
||||
|
||||
- In WSL2 Terminal - |
||||
`ip addr show eth0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}'` |
||||
- In Windows Terminal - |
||||
`wsl -- ip -o -4 -json addr list eth0` |
||||
In this result look for "local": X.X.X.X |
||||
|
||||
After finding the IP address in your broadcasting software make the server point to |
||||
`rtmp://<your version of IP address>:1935/live` |
||||
|
||||
Example in OBS-Studio -> |
||||
 |
||||
|
||||
## More resources |
||||
|
||||
- [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/) |
||||
- [Owncast development documentation](https://owncast.online/development/) |
||||
- [Owncast quickstart guide](https://owncast.online/quickstart/) |
||||
- [Owncast README](https://github.com/owncast/owncast/blob/develop/README.md#building-from-source) |
@ -1,32 +0,0 @@
@@ -1,32 +0,0 @@
|
||||
# Run 'man hitch.conf' for a description of all options. |
||||
|
||||
frontend = { |
||||
host = "*" |
||||
port = "443" |
||||
} |
||||
backend = "[127.0.0.1]:8443" |
||||
workers = 4 # number of CPU cores |
||||
|
||||
daemon = on |
||||
|
||||
# We strongly recommend you create a separate non-privileged hitch |
||||
# user and group |
||||
user = "hitch" |
||||
group = "hitch" |
||||
|
||||
# Enable to let clients negotiate HTTP/2 with ALPN. (default off) |
||||
# alpn-protos = "h2, http/1.1" |
||||
|
||||
# run Varnish as backend over PROXY; varnishd -a :80 -a localhost:6086,PROXY .. |
||||
write-proxy-v2 = on # Write PROXY header |
||||
|
||||
## ssl config |
||||
pem-dir = "/etc/tls/private" |
||||
tls-protos = TLSv1.2 TLSv1.3 |
||||
# ocsp |
||||
ocsp-dir = "/etc/hitch/ocsp" |
||||
ocsp-verify-staple = on |
||||
|
||||
syslog = on |
||||
log-level = 1 |
||||
tcp-fastopen = on |
@ -1,38 +0,0 @@
@@ -1,38 +0,0 @@
|
||||
vcl 4.0; |
||||
|
||||
backend default { |
||||
.host = "localhost"; |
||||
.port = "8080"; |
||||
} |
||||
|
||||
sub vcl_recv { |
||||
# Implementing websocket support (https://www.varnish-cache.org/docs/4.0/users-guide/vcl-example-websockets.html) |
||||
if (req.http.Upgrade ~ "(?i)websocket") { |
||||
return (pipe); |
||||
} |
||||
} |
||||
|
||||
sub vcl_pipe { |
||||
if (req.http.upgrade) { |
||||
set bereq.http.upgrade = req.http.upgrade; |
||||
set bereq.http.connection = req.http.connection; |
||||
} |
||||
} |
||||
|
||||
sub vcl_backend_response { |
||||
# Set 1s ttl if origin response HTTP status code is anything other than 200 |
||||
if (beresp.status != 200) { |
||||
set beresp.ttl = 1s; |
||||
set beresp.uncacheable = true; |
||||
return (deliver); |
||||
} |
||||
if (bereq.url ~ "m3u8") { |
||||
# assuming chunks are 2 seconds long |
||||
set beresp.ttl = 1s; |
||||
set beresp.grace = 0s; |
||||
} |
||||
if (bereq.url ~ "ts") { |
||||
set beresp.ttl = 10m; |
||||
set beresp.grace = 5m; |
||||
} |
||||
} |
@ -1,35 +0,0 @@
@@ -1,35 +0,0 @@
|
||||
package admin |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"net/http" |
||||
|
||||
"github.com/owncast/owncast/controllers" |
||||
"github.com/owncast/owncast/core/data" |
||||
) |
||||
|
||||
// SetCustomColorVariableValues sets the custom color variables.
|
||||
func SetCustomColorVariableValues(w http.ResponseWriter, r *http.Request) { |
||||
if !requirePOST(w, r) { |
||||
return |
||||
} |
||||
|
||||
type request struct { |
||||
Value map[string]string `json:"value"` |
||||
} |
||||
|
||||
decoder := json.NewDecoder(r.Body) |
||||
var values request |
||||
|
||||
if err := decoder.Decode(&values); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, "unable to update appearance variable values") |
||||
return |
||||
} |
||||
|
||||
if err := data.SetCustomColorVariableValues(values.Value); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
controllers.WriteSimpleResponse(w, true, "custom appearance variables updated") |
||||
} |
@ -1,92 +0,0 @@
@@ -1,92 +0,0 @@
|
||||
package admin |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"net/http" |
||||
"os" |
||||
"path/filepath" |
||||
|
||||
"github.com/owncast/owncast/config" |
||||
"github.com/owncast/owncast/controllers" |
||||
"github.com/owncast/owncast/utils" |
||||
) |
||||
|
||||
// UploadCustomEmoji allows POSTing a new custom emoji to the server.
|
||||
func UploadCustomEmoji(w http.ResponseWriter, r *http.Request) { |
||||
if !requirePOST(w, r) { |
||||
return |
||||
} |
||||
|
||||
type postEmoji struct { |
||||
Name string `json:"name"` |
||||
Data string `json:"data"` |
||||
} |
||||
|
||||
emoji := new(postEmoji) |
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(emoji); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
bytes, _, err := utils.DecodeBase64Image(emoji.Data) |
||||
if err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
// Prevent path traversal attacks
|
||||
emojiFileName := filepath.Base(emoji.Name) |
||||
targetPath := filepath.Join(config.CustomEmojiPath, emojiFileName) |
||||
|
||||
err = os.MkdirAll(config.CustomEmojiPath, 0o700) |
||||
if err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
if utils.DoesFileExists(targetPath) { |
||||
controllers.WriteSimpleResponse(w, false, fmt.Sprintf("An emoji with the name %q already exists", emojiFileName)) |
||||
return |
||||
} |
||||
|
||||
if err = os.WriteFile(targetPath, bytes, 0o600); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
controllers.WriteSimpleResponse(w, true, fmt.Sprintf("Emoji %q has been uploaded", emojiFileName)) |
||||
} |
||||
|
||||
// DeleteCustomEmoji deletes a custom emoji.
|
||||
func DeleteCustomEmoji(w http.ResponseWriter, r *http.Request) { |
||||
if !requirePOST(w, r) { |
||||
return |
||||
} |
||||
|
||||
type deleteEmoji struct { |
||||
Name string `json:"name"` |
||||
} |
||||
|
||||
emoji := new(deleteEmoji) |
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(emoji); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
// var emojiFileName = filepath.Base(emoji.Name)
|
||||
targetPath := filepath.Join(config.CustomEmojiPath, emoji.Name) |
||||
|
||||
if err := os.Remove(targetPath); err != nil { |
||||
if os.IsNotExist(err) { |
||||
controllers.WriteSimpleResponse(w, false, fmt.Sprintf("Emoji %q doesn't exist", emoji.Name)) |
||||
} else { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
} |
||||
return |
||||
} |
||||
|
||||
controllers.WriteSimpleResponse(w, true, fmt.Sprintf("Emoji %q has been deleted", emoji.Name)) |
||||
} |
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
package admin |
||||
|
||||
import ( |
||||
"bytes" |
||||
"io/fs" |
||||
"net/http" |
||||
"os" |
||||
"path/filepath" |
||||
"strings" |
||||
|
||||
"github.com/owncast/owncast/router/middleware" |
||||
"github.com/owncast/owncast/static" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// ServeAdmin will return admin web assets.
|
||||
func ServeAdmin(w http.ResponseWriter, r *http.Request) { |
||||
// If the ETags match then return a StatusNotModified
|
||||
if responseCode := middleware.ProcessEtags(w, r); responseCode != 0 { |
||||
w.WriteHeader(responseCode) |
||||
return |
||||
} |
||||
|
||||
adminFiles := static.GetAdmin() |
||||
path := strings.TrimPrefix(r.URL.Path, "/") |
||||
|
||||
// Determine if the requested path is a directory.
|
||||
// If so, append index.html to the request.
|
||||
if info, err := fs.Stat(adminFiles, path); err == nil && info.IsDir() { |
||||
path = filepath.Join(path, "index.html") |
||||
} else if _, err := fs.Stat(adminFiles, path+"index.html"); err == nil { |
||||
path = filepath.Join(path, "index.html") |
||||
} |
||||
|
||||
f, err := adminFiles.Open(path) |
||||
if os.IsNotExist(err) { |
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
info, err := f.Stat() |
||||
if os.IsNotExist(err) { |
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
// Set a cache control max-age header
|
||||
middleware.SetCachingHeaders(w, r) |
||||
d, err := adminFiles.ReadFile(path) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
w.WriteHeader(http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
http.ServeContent(w, r, info.Name(), info.ModTime(), bytes.NewReader(d)) |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue