mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-01-19 00:27:25 +01:00
Compare commits
11 Commits
message-ca
...
http-clipb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebb386af58 | ||
|
|
b105ed6727 | ||
|
|
965110b2c3 | ||
|
|
c8ac104043 | ||
|
|
e39498702d | ||
|
|
9b97067b10 | ||
|
|
5244e0be14 | ||
|
|
6eb25f68ac | ||
|
|
efe7c3fa70 | ||
|
|
ce4b2ae9a0 | ||
|
|
4eb7dc563c |
@@ -555,8 +555,8 @@ func parseUsers(usersRaw []string) ([]*user.User, error) {
|
||||
role := user.Role(strings.TrimSpace(parts[2]))
|
||||
if !user.AllowedUsername(username) {
|
||||
return nil, fmt.Errorf("invalid auth-users: %s, username invalid", userLine)
|
||||
} else if err := user.ValidPasswordHash(passwordHash); err != nil {
|
||||
return nil, fmt.Errorf("invalid auth-users: %s, %s", userLine, err.Error())
|
||||
} else if err := user.ValidPasswordHash(passwordHash, user.DefaultUserPasswordBcryptCost); err != nil {
|
||||
return nil, fmt.Errorf("invalid auth-users: %s, password hash invalid, %s", userLine, err.Error())
|
||||
} else if !user.AllowedRole(role) {
|
||||
return nil, fmt.Errorf("invalid auth-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role)
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ func TestParseUsers_Success(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "single user",
|
||||
input: []string{"alice:$2a$10$abcdefghijklmnopqrstuvwxyz:user"},
|
||||
input: []string{"alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
|
||||
expected: []*user.User{
|
||||
{
|
||||
Name: "alice",
|
||||
Hash: "$2a$10$abcdefghijklmnopqrstuvwxyz",
|
||||
Hash: "$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S",
|
||||
Role: user.RoleUser,
|
||||
Provisioned: true,
|
||||
},
|
||||
@@ -39,19 +39,19 @@ func TestParseUsers_Success(t *testing.T) {
|
||||
{
|
||||
name: "multiple users with different roles",
|
||||
input: []string{
|
||||
"alice:$2a$10$abcdefghijklmnopqrstuvwxyz:user",
|
||||
"bob:$2b$10$abcdefghijklmnopqrstuvwxyz:admin",
|
||||
"alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user",
|
||||
"bob:$2a$10$jIcuBWcbxd6oW1aPvoJ5iOShzu3/UJ2kSxKbTZtDypG06nBflQagq:admin",
|
||||
},
|
||||
expected: []*user.User{
|
||||
{
|
||||
Name: "alice",
|
||||
Hash: "$2a$10$abcdefghijklmnopqrstuvwxyz",
|
||||
Hash: "$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S",
|
||||
Role: user.RoleUser,
|
||||
Provisioned: true,
|
||||
},
|
||||
{
|
||||
Name: "bob",
|
||||
Hash: "$2b$10$abcdefghijklmnopqrstuvwxyz",
|
||||
Hash: "$2a$10$jIcuBWcbxd6oW1aPvoJ5iOShzu3/UJ2kSxKbTZtDypG06nBflQagq",
|
||||
Role: user.RoleAdmin,
|
||||
Provisioned: true,
|
||||
},
|
||||
@@ -64,11 +64,11 @@ func TestParseUsers_Success(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "user with special characters in name",
|
||||
input: []string{"alice.test+123@example.com:$2y$10$abcdefghijklmnopqrstuvwxyz:user"},
|
||||
input: []string{"alice.test+123@example.com:$2a$10$RYUYAsl5zOnAIp6fH7BPX.Eug0rUfEUk92r8WiVusb0VK.vGojWBe:user"},
|
||||
expected: []*user.User{
|
||||
{
|
||||
Name: "alice.test+123@example.com",
|
||||
Hash: "$2y$10$abcdefghijklmnopqrstuvwxyz",
|
||||
Hash: "$2a$10$RYUYAsl5zOnAIp6fH7BPX.Eug0rUfEUk92r8WiVusb0VK.vGojWBe",
|
||||
Role: user.RoleUser,
|
||||
Provisioned: true,
|
||||
},
|
||||
@@ -110,23 +110,23 @@ func TestParseUsers_Errors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "invalid username",
|
||||
input: []string{"alice@#$%:$2a$10$abcdefghijklmnopqrstuvwxyz:user"},
|
||||
error: "invalid auth-users: alice@#$%:$2a$10$abcdefghijklmnopqrstuvwxyz:user, username invalid",
|
||||
input: []string{"alice@#$%:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
|
||||
error: "invalid auth-users: alice@#$%:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user, username invalid",
|
||||
},
|
||||
{
|
||||
name: "invalid password hash - wrong prefix",
|
||||
input: []string{"alice:plaintext:user"},
|
||||
error: "invalid auth-users: alice:plaintext:user, password hash but be a bcrypt hash, use 'ntfy user hash' to generate",
|
||||
error: "invalid auth-users: alice:plaintext:user, password hash invalid, password hash must be a bcrypt hash, use 'ntfy user hash' to generate",
|
||||
},
|
||||
{
|
||||
name: "invalid role",
|
||||
input: []string{"alice:$2a$10$abcdefghijklmnopqrstuvwxyz:invalid"},
|
||||
error: "invalid auth-users: alice:$2a$10$abcdefghijklmnopqrstuvwxyz:invalid, role invalid is not allowed, allowed roles are 'admin' or 'user'",
|
||||
input: []string{"alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:invalid"},
|
||||
error: "invalid auth-users: alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:invalid, role invalid is not allowed, allowed roles are 'admin' or 'user'",
|
||||
},
|
||||
{
|
||||
name: "empty username",
|
||||
input: []string{":$2a$10$abcdefghijklmnopqrstuvwxyz:user"},
|
||||
error: "invalid auth-users: :$2a$10$abcdefghijklmnopqrstuvwxyz:user, username invalid",
|
||||
input: []string{":$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
|
||||
error: "invalid auth-users: :$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user, username invalid",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ using Docker Compose (i.e. `docker-compose.yml`):
|
||||
NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
|
||||
NTFY_AUTH_FILE: /var/lib/ntfy/auth.db
|
||||
NTFY_AUTH_DEFAULT_ACCESS: deny-all
|
||||
NTFY_AUTH_USERS: 'phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin'
|
||||
NTFY_AUTH_USERS: 'phil:$$2a$$10$$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin' # Must escape '$' as '$$'
|
||||
NTFY_BEHIND_PROXY: true
|
||||
NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
|
||||
NTFY_ENABLE_LOGIN: true
|
||||
|
||||
@@ -176,6 +176,8 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||
- [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) - Script for Mac OS systems that monitors new or dropped connections to your network using ntfy (Shell)
|
||||
- [NtfyPwsh](https://github.com/ptmorris1/NtfyPwsh) - PowerShell module to help send messages to ntfy (PowerShell)
|
||||
- [ntfyrr](https://github.com/leukosaima/ntfyrr) - Currently an Overseerr webhook notification to ntfy helper service.
|
||||
- [ntfy for Sandstorm](https://apps.sandstorm.io/app/c6rk81r4qk6dm3k04x1kxmyccqewhh4npuxeyg1xrpfypn2ddy0h) - ntfy app for the Sandstorm platform
|
||||
- [ntfy-heartbeat-monitor](https://codeberg.org/RockWolf/ntfy-heartbeat-monitor) - Application for implementing heartbeat monitoring/alerting by utilizing ntfy
|
||||
|
||||
## Blog + forum posts
|
||||
|
||||
|
||||
@@ -3679,13 +3679,13 @@ authParam = base64_raw(authHeader) // -> QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM0
|
||||
The following command will generate the appropriate value for you on *nix systems:
|
||||
|
||||
```
|
||||
echo -n "Basic `echo -n 'testuser:fakepassword' | base64`" | base64 | tr -d '='
|
||||
echo -n "Basic `echo -n 'testuser:fakepassword' | base64 -w0`" | base64 -w0 | tr -d '='
|
||||
```
|
||||
|
||||
For access tokens, you can use this instead:
|
||||
|
||||
```
|
||||
echo -n "Bearer faketoken" | base64 | tr -d '='
|
||||
echo -n "Bearer faketoken" | base64 -w0 | tr -d '='
|
||||
```
|
||||
|
||||
## Advanced features
|
||||
|
||||
@@ -1475,6 +1475,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
||||
* Add mutex around message cache writes to avoid `database locked` errors ([#1397](https://github.com/binwiederhier/ntfy/pull/1397), [#1391](https://github.com/binwiederhier/ntfy/issues/1391), thanks to [@timofej673](https://github.com/timofej673))
|
||||
* Add build tags `nopayments`, `nofirebase` and `nowebpush` to allow excluding external dependencies, useful for
|
||||
packaging in Debian ([#1420](https://github.com/binwiederhier/ntfy/pull/1420), discussion in [#1258](https://github.com/binwiederhier/ntfy/issues/1258), thanks to [@thekhalifa](https://github.com/thekhalifa) for packaging ntfy for Debian/Ubuntu)
|
||||
* Make copying tokens, phone numbers, etc. possible on HTTP ([#1432](https://github.com/binwiederhier/ntfy/pull/1432)/[#1408](https://github.com/binwiederhier/ntfy/issues/1408)/[#1295](https://github.com/binwiederhier/ntfy/issues/1295), thanks to [@EdwinKM](https://github.com/EdwinKM), [@xxl6097](https://github.com/xxl6097) for reporting)
|
||||
|
||||
### ntfy Android app v1.16.1 (UNRELEASED)
|
||||
|
||||
|
||||
@@ -1066,7 +1066,7 @@ func (a *Manager) addUserTx(tx *sql.Tx, username, password string, role Role, ha
|
||||
var err error = nil
|
||||
if hashed {
|
||||
hash = password
|
||||
if err := ValidPasswordHash(hash); err != nil {
|
||||
if err := ValidPasswordHash(hash, a.config.BcryptCost); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@@ -1434,7 +1434,7 @@ func (a *Manager) changePasswordTx(tx *sql.Tx, username, password string, hashed
|
||||
var err error
|
||||
if hashed {
|
||||
hash = password
|
||||
if err := ValidPasswordHash(hash); err != nil {
|
||||
if err := ValidPasswordHash(hash, a.config.BcryptCost); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1162,7 +1162,7 @@ func TestManager_WithProvisionedUsers(t *testing.T) {
|
||||
// Re-open the DB (second app start)
|
||||
require.Nil(t, a.db.Close())
|
||||
conf.Users = []*User{
|
||||
{Name: "philuser", Hash: "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser},
|
||||
{Name: "philuser", Hash: "$2a$10$AAAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser},
|
||||
}
|
||||
conf.Access = map[string][]*Grant{
|
||||
"philuser": {
|
||||
@@ -1292,7 +1292,7 @@ func TestManager_UpdateNonProvisionedUsersToProvisionedUsers(t *testing.T) {
|
||||
// Re-open the DB (second app start)
|
||||
require.Nil(t, a.db.Close())
|
||||
conf.Users = []*User{
|
||||
{Name: "philuser", Hash: "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser},
|
||||
{Name: "philuser", Hash: "$2a$10$AAAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser},
|
||||
}
|
||||
conf.Access = map[string][]*Grant{
|
||||
"philuser": {
|
||||
@@ -1308,7 +1308,7 @@ func TestManager_UpdateNonProvisionedUsersToProvisionedUsers(t *testing.T) {
|
||||
require.Len(t, users, 2)
|
||||
require.Equal(t, "philuser", users[0].Name)
|
||||
require.Equal(t, RoleUser, users[0].Role)
|
||||
require.Equal(t, "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", users[0].Hash)
|
||||
require.Equal(t, "$2a$10$AAAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", users[0].Hash)
|
||||
require.True(t, users[0].Provisioned) // Updated to provisioned!
|
||||
|
||||
grants, err = a.Grants("philuser")
|
||||
|
||||
@@ -249,7 +249,8 @@ var (
|
||||
ErrInvalidArgument = errors.New("invalid argument")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrUserExists = errors.New("user already exists")
|
||||
ErrPasswordHashInvalid = errors.New("password hash but be a bcrypt hash, use 'ntfy user hash' to generate")
|
||||
ErrPasswordHashInvalid = errors.New("password hash must be a bcrypt hash, use 'ntfy user hash' to generate")
|
||||
ErrPasswordHashWeak = errors.New("password hash too weak, use 'ntfy user hash' to generate")
|
||||
ErrTierNotFound = errors.New("tier not found")
|
||||
ErrTokenNotFound = errors.New("token not found")
|
||||
ErrPhoneNumberNotFound = errors.New("phone number not found")
|
||||
|
||||
@@ -41,10 +41,16 @@ func AllowedTier(tier string) bool {
|
||||
}
|
||||
|
||||
// ValidPasswordHash checks if the given password hash is a valid bcrypt hash
|
||||
func ValidPasswordHash(hash string) error {
|
||||
func ValidPasswordHash(hash string, minCost int) error {
|
||||
if !strings.HasPrefix(hash, "$2a$") && !strings.HasPrefix(hash, "$2b$") && !strings.HasPrefix(hash, "$2y$") {
|
||||
return ErrPasswordHashInvalid
|
||||
}
|
||||
cost, err := bcrypt.Cost([]byte(hash))
|
||||
if err != nil { // Check if the hash is valid (length, format, etc.)
|
||||
return err
|
||||
} else if cost < minCost {
|
||||
return ErrPasswordHashWeak
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,10 @@ export const maybeWithBearerAuth = (headers, token) => {
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const withBasicAuth = (headers, username, password) => ({ ...headers, Authorization: basicAuth(username, password) });
|
||||
export const withBasicAuth = (headers, username, password) => ({
|
||||
...headers,
|
||||
Authorization: basicAuth(username, password)
|
||||
});
|
||||
|
||||
export const maybeWithAuth = (headers, user) => {
|
||||
if (user?.password) {
|
||||
@@ -139,7 +142,7 @@ export const getKebabCaseLangStr = (language) => language.replace(/_/g, "-");
|
||||
export const formatShortDateTime = (timestamp, language) =>
|
||||
new Intl.DateTimeFormat(getKebabCaseLangStr(language), {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
timeStyle: "short"
|
||||
}).format(new Date(timestamp * 1000));
|
||||
|
||||
export const formatShortDate = (timestamp, language) =>
|
||||
@@ -178,32 +181,32 @@ export const openUrl = (url) => {
|
||||
export const sounds = {
|
||||
ding: {
|
||||
file: ding,
|
||||
label: "Ding",
|
||||
label: "Ding"
|
||||
},
|
||||
juntos: {
|
||||
file: juntos,
|
||||
label: "Juntos",
|
||||
label: "Juntos"
|
||||
},
|
||||
pristine: {
|
||||
file: pristine,
|
||||
label: "Pristine",
|
||||
label: "Pristine"
|
||||
},
|
||||
dadum: {
|
||||
file: dadum,
|
||||
label: "Dadum",
|
||||
label: "Dadum"
|
||||
},
|
||||
pop: {
|
||||
file: pop,
|
||||
label: "Pop",
|
||||
label: "Pop"
|
||||
},
|
||||
"pop-swoosh": {
|
||||
file: popSwoosh,
|
||||
label: "Pop swoosh",
|
||||
label: "Pop swoosh"
|
||||
},
|
||||
beep: {
|
||||
file: beep,
|
||||
label: "Beep",
|
||||
},
|
||||
label: "Beep"
|
||||
}
|
||||
};
|
||||
|
||||
export const playSound = async (id) => {
|
||||
@@ -216,7 +219,7 @@ export const playSound = async (id) => {
|
||||
export async function* fetchLinesIterator(fileURL, headers) {
|
||||
const utf8Decoder = new TextDecoder("utf-8");
|
||||
const response = await fetch(fileURL, {
|
||||
headers,
|
||||
headers
|
||||
});
|
||||
const reader = response.body.getReader();
|
||||
let { value: chunk, done: readerDone } = await reader.read();
|
||||
@@ -225,7 +228,7 @@ export async function* fetchLinesIterator(fileURL, headers) {
|
||||
const re = /\n|\r|\r\n/gm;
|
||||
let startIndex = 0;
|
||||
|
||||
for (;;) {
|
||||
for (; ;) {
|
||||
const result = re.exec(chunk);
|
||||
if (!result) {
|
||||
if (readerDone) {
|
||||
@@ -270,3 +273,21 @@ export const urlB64ToUint8Array = (base64String) => {
|
||||
}
|
||||
return outputArray;
|
||||
};
|
||||
|
||||
export const copyToClipboard = (text) => {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.setAttribute("readonly", ""); // Avoid mobile keyboards from popping up
|
||||
textarea.style.position = "fixed"; // Avoid scroll jump
|
||||
textarea.style.left = "-9999px";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ import CloseIcon from "@mui/icons-material/Close";
|
||||
import { ContentCopy, Public } from "@mui/icons-material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import routes from "./routes";
|
||||
import { formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils";
|
||||
import { copyToClipboard, formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils";
|
||||
import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi";
|
||||
import { Pref, PrefGroup } from "./Pref";
|
||||
import db from "../app/db";
|
||||
@@ -370,7 +370,7 @@ const PhoneNumbers = () => {
|
||||
};
|
||||
|
||||
const handleCopy = (phoneNumber) => {
|
||||
navigator.clipboard.writeText(phoneNumber);
|
||||
copyToClipboard(phoneNumber);
|
||||
setSnackOpen(true);
|
||||
};
|
||||
|
||||
@@ -841,7 +841,7 @@ const TokensTable = (props) => {
|
||||
};
|
||||
|
||||
const handleCopy = async (token) => {
|
||||
await navigator.clipboard.writeText(token);
|
||||
copyToClipboard(token);
|
||||
setSnackOpen(true);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
import StackTrace from "stacktrace-js";
|
||||
import { CircularProgress, Link, Button } from "@mui/material";
|
||||
import { Trans, withTranslation } from "react-i18next";
|
||||
import { copyToClipboard } from "../app/utils";
|
||||
|
||||
class ErrorBoundaryImpl extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -64,7 +65,7 @@ class ErrorBoundaryImpl extends React.Component {
|
||||
stack += `${this.state.niceStack}\n\n`;
|
||||
}
|
||||
stack += `${this.state.originalStack}\n`;
|
||||
navigator.clipboard.writeText(stack);
|
||||
copyToClipboard(stack);
|
||||
}
|
||||
|
||||
renderUnsupportedIndexedDB() {
|
||||
|
||||
@@ -26,7 +26,10 @@ import { Trans, useTranslation } from "react-i18next";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { useRemark } from "react-remark";
|
||||
import styled from "@emotion/styled";
|
||||
import { formatBytes, formatShortDateTime, maybeActionErrors, openUrl, shortUrl, topicShortUrl, unmatchedTags } from "../app/utils";
|
||||
import {
|
||||
copyToClipboard,
|
||||
formatBytes, formatShortDateTime, maybeActionErrors, openUrl, shortUrl, topicShortUrl, unmatchedTags
|
||||
} from "../app/utils";
|
||||
import { formatMessage, formatTitle, isImage } from "../app/notificationUtils";
|
||||
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
|
||||
import subscriptionManager from "../app/SubscriptionManager";
|
||||
@@ -239,7 +242,7 @@ const NotificationItem = (props) => {
|
||||
await subscriptionManager.markNotificationRead(notification.id);
|
||||
};
|
||||
const handleCopy = (s) => {
|
||||
navigator.clipboard.writeText(s);
|
||||
copyToClipboard(s);
|
||||
props.onShowSnack();
|
||||
};
|
||||
const expired = attachment && attachment.expires && attachment.expires < Date.now() / 1000;
|
||||
|
||||
Reference in New Issue
Block a user