Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 13 additions & 25 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
Expand Down Expand Up @@ -141,6 +138,9 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GH_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Create and push DockerHub manifest
run: |
GIT_SHA="$(git rev-parse HEAD)"
Expand All @@ -163,17 +163,11 @@ jobs:

for APP_NAME in bytesend smtp-proxy; do
for TAG in $TAGS; do
# Remove remote tags/manifests first so reruns are idempotent.
docker buildx imagetools rm bytesend/$APP_NAME:$TAG 2>/dev/null || true
docker buildx imagetools rm bytesend/$APP_NAME-amd64:$TAG 2>/dev/null || true
docker buildx imagetools rm bytesend/$APP_NAME-arm64:$TAG 2>/dev/null || true

docker manifest create \
bytesend/$APP_NAME:$TAG \
--amend bytesend/$APP_NAME-amd64:$TAG \
--amend bytesend/$APP_NAME-arm64:$TAG

docker manifest push bytesend/$APP_NAME:$TAG
# buildx imagetools create is idempotent and updates the target tag in-place.
docker buildx imagetools create \
--tag bytesend/$APP_NAME:$TAG \
bytesend/$APP_NAME-amd64:$TAG \
bytesend/$APP_NAME-arm64:$TAG
done
done

Expand All @@ -199,16 +193,10 @@ jobs:

for APP_NAME in bytesend smtp-proxy; do
for TAG in $TAGS; do
# Remove remote tags/manifests first so reruns are idempotent.
docker buildx imagetools rm ghcr.io/bytesend/$APP_NAME:$TAG 2>/dev/null || true
docker buildx imagetools rm ghcr.io/bytesend/$APP_NAME-amd64:$TAG 2>/dev/null || true
docker buildx imagetools rm ghcr.io/bytesend/$APP_NAME-arm64:$TAG 2>/dev/null || true

docker manifest create \
ghcr.io/bytesend/$APP_NAME:$TAG \
--amend ghcr.io/bytesend/$APP_NAME-amd64:$TAG \
--amend ghcr.io/bytesend/$APP_NAME-arm64:$TAG

docker manifest push ghcr.io/bytesend/$APP_NAME:$TAG
# buildx imagetools create is idempotent and updates the target tag in-place.
docker buildx imagetools create \
--tag ghcr.io/bytesend/$APP_NAME:$TAG \
ghcr.io/bytesend/$APP_NAME-amd64:$TAG \
ghcr.io/bytesend/$APP_NAME-arm64:$TAG
done
done
2 changes: 1 addition & 1 deletion apps/docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"background": {
"color": {
"light": "#F8F9FB",
"dark": "#0A0E1A"
"dark": "#0A0A0A"
}
},
"fonts": {
Expand Down
70 changes: 52 additions & 18 deletions apps/web/src/server/service/notification-provider-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,12 +621,10 @@ export class NotificationProviderService {
"Discord webhook URL is required"
);
}
if (!config.webhookUrl.includes("discord.com")) {
throw new ByteSendApiError(
"BAD_REQUEST",
"Invalid Discord webhook URL"
);
}
this.assertAllowedWebhookUrl(config.webhookUrl, "Discord", [
"discord.com",
"discordapp.com",
]);
break;

case "SLACK":
Expand All @@ -636,12 +634,9 @@ export class NotificationProviderService {
"Slack webhook URL is required"
);
}
if (!config.webhookUrl.includes("hooks.slack.com")) {
throw new ByteSendApiError(
"BAD_REQUEST",
"Invalid Slack webhook URL"
);
}
this.assertAllowedWebhookUrl(config.webhookUrl, "Slack", [
"hooks.slack.com",
]);
break;

case "MICROSOFT_TEAMS":
Expand All @@ -651,12 +646,11 @@ export class NotificationProviderService {
"Microsoft Teams webhook URL is required"
);
}
if (!config.webhookUrl.includes("outlook.webhook.office.com")) {
throw new ByteSendApiError(
"BAD_REQUEST",
"Invalid Microsoft Teams webhook URL"
);
}
this.assertAllowedWebhookUrl(config.webhookUrl, "Microsoft Teams", [
"outlook.webhook.office.com",
"webhook.office.com",
"outlook.office.com",
]);
break;

case "TELEGRAM":
Expand Down Expand Up @@ -684,4 +678,44 @@ export class NotificationProviderService {
break;
}
}

private static assertAllowedWebhookUrl(
rawUrl: string,
providerName: string,
allowedHostnames: string[]
) {
let parsedUrl: URL;

try {
parsedUrl = new URL(rawUrl);
} catch {
throw new ByteSendApiError(
"BAD_REQUEST",
`${providerName} webhook URL is invalid`
);
}

if (parsedUrl.protocol !== "https:") {
throw new ByteSendApiError(
"BAD_REQUEST",
`${providerName} webhook URL must use HTTPS`
);
}

const hostname = parsedUrl.hostname.toLowerCase();
const isAllowed = allowedHostnames.some((allowedHostname) => {
const normalizedAllowed = allowedHostname.toLowerCase();
return (
hostname === normalizedAllowed ||
hostname.endsWith(`.${normalizedAllowed}`)
);
});

if (!isAllowed) {
throw new ByteSendApiError(
"BAD_REQUEST",
`Invalid ${providerName} webhook URL`
);
}
}
}
Loading