Compare commits

..

No commits in common. "users" and "master" have entirely different histories.

8 changed files with 13 additions and 431 deletions

View File

@ -32,10 +32,6 @@ func main() {
log.Fatal(errors.Wrap(err, "migrating database"))
}
if err := db.CreateAdminUser(database, "admin"); err != nil {
log.Fatalln(err)
}
go rushlink.StartMetricsServer(*metricsListen, database, filestore)
rushlink.StartMainServer(*httpListen, database, filestore, *rootURL)
}

View File

@ -8,7 +8,6 @@ import (
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
@ -245,12 +244,6 @@ func (rl *rushlink) viewActionSuccess(w http.ResponseWriter, r *http.Request, p
}
func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
// Check if the user is authenticated
user := rl.authenticateUser(w, r, false, nil)
if user == nil {
return
}
if err := r.ParseMultipartForm(formParseMaxMemory); err != nil {
msg := fmt.Sprintf("could not parse form: %v\n", err)
rl.renderError(w, r, http.StatusBadRequest, msg)
@ -269,7 +262,7 @@ func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
return
}
if shortensPrs {
rl.newRedirectPasteHandler(w, r, user, shortens[0])
rl.newRedirectPasteHandler(w, r, shortens[0])
return
}
if fileHeadersPrs {
@ -279,163 +272,17 @@ func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
rl.renderInternalServerError(w, r, err)
return
}
rl.newFileUploadPasteHandler(w, r, user, file, *fileHeader)
rl.newFileUploadPasteHandler(w, r, file, *fileHeader)
return
}
}
func (rl *rushlink) createUserHandler(w http.ResponseWriter, r *http.Request) {
// Check if the user is authenticated as an admin
user := rl.authenticateUser(w, r, true, nil)
if user == nil {
return
}
if err := r.ParseMultipartForm(formParseMaxMemory); err != nil {
msg := fmt.Sprintf("could not parse form: %v\n", err)
rl.renderError(w, r, http.StatusBadRequest, msg)
return
}
new_username := r.FormValue("username")
new_password := r.FormValue("password")
new_admin, _ := strconv.ParseBool(r.FormValue("admin"))
// Create the user
if err := db.NewUser(rl.db, new_username, new_password, new_admin); err != nil {
// Failed to create the user, return a 500 Internal Server Error response
http.Error(w, "Failed to create user", http.StatusInternalServerError)
return
}
// Return a 201 Created response
w.WriteHeader(http.StatusCreated)
}
func (rl *rushlink) setWWWAuthenticate(w http.ResponseWriter, r *http.Request) {
// Set authentication headers for Basic Authentication
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(http.StatusUnauthorized)
}
func (rl *rushlink) authenticateUser(w http.ResponseWriter, r *http.Request, shouldBeAdmin bool, canAlsoBe *string) *db.User {
// Check if the user is authenticated
username, password, ok := r.BasicAuth()
if !ok {
// User is not authenticated, return a 401 Unauthorized response
rl.setWWWAuthenticate(w, r)
return nil
}
// Authenticate the user
user, err := db.Authenticate(rl.db, username, password)
if err != nil {
rl.setWWWAuthenticate(w, r)
log.Printf("authentication failure: %s", err)
return nil
}
if (shouldBeAdmin && !user.Admin && (canAlsoBe == nil || *canAlsoBe != user.User)) {
// Authentication failed, return a 401 Unauthorized response
rl.setWWWAuthenticate(w, r)
log.Printf("user '%s' should be admin (or '%s'), but isn't", username, canAlsoBe)
return nil
}
return user
}
func (rl *rushlink) deleteUserHandler(w http.ResponseWriter, r *http.Request) {
// Parse the user ID from the request URL
vars := mux.Vars(r)
userName := vars["user"]
// Check if the user is authenticated as an admin or self
user := rl.authenticateUser(w, r, true, &userName)
if user == nil {
return
}
// Delete the user
if err := db.DeleteUser(rl.db, userName); err != nil {
// Failed to delete the user, return a 500 Internal Server Error response
http.Error(w, "Failed to delete user", http.StatusInternalServerError)
return
}
// Return a 204 No Content response
w.WriteHeader(http.StatusNoContent)
}
func (rl *rushlink) changeUserHandler(w http.ResponseWriter, r *http.Request) {
// Parse the user ID from the request URL
vars := mux.Vars(r)
userName := vars["user"]
// Check if the user is authenticated as an admin or self
user := rl.authenticateUser(w, r, true, &userName)
if user == nil {
return
}
// Parse the request multipart form
if err := r.ParseMultipartForm(formParseMaxMemory); err != nil {
msg := fmt.Sprintf("could not parse form: %v\n", err)
rl.renderError(w, r, http.StatusBadRequest, msg)
return
}
// Get the user record from the database
updatedUser := new(db.User)
if err := rl.db.Where("user = ?", userName).First(&updatedUser).Error; err != nil {
// User record not found, return a 404 Not Found response
http.NotFound(w, r)
return
}
// Get the updated user data from the form
if newUsername := r.FormValue("new_username"); newUsername != "" {
updatedUser.User = newUsername
}
if newPassword := r.FormValue("new_password"); newPassword != "" {
hashedPassword, err := db.HashPassword(newPassword)
if err != nil {
msg := fmt.Sprintf("could not hash password: %v\n", err)
rl.renderError(w, r, http.StatusInternalServerError, msg)
return
}
updatedUser.Password = hashedPassword
}
if newAdminStr := r.FormValue("new_admin"); newAdminStr != "" {
newAdmin, err := strconv.ParseBool(newAdminStr)
if err != nil {
msg := fmt.Sprintf("could not parse admin status: %v\n", err)
rl.renderError(w, r, http.StatusBadRequest, msg)
return
}
updatedUser.Admin = newAdmin
}
// Check if the user is trying to update their own admin status
if updatedUser.Admin && !user.Admin {
// User is trying to update their own admin status, return a 403 Forbidden response
http.Error(w, "You do not have permission to update your own admin status", http.StatusForbidden)
return
}
// Update the user in the database
if err := db.ChangeUser(rl.db, userName, updatedUser); err != nil {
// Failed to update the user, return a 500 Internal Server Error response
http.Error(w, "Failed to update user", http.StatusInternalServerError)
return
}
// Return a 200 OK response
w.WriteHeader(http.StatusOK)
}
func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Request, user *db.User, file multipart.File, header multipart.FileHeader) {
func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Request, file multipart.File, header multipart.FileHeader) {
var fu *db.FileUpload
var paste *db.Paste
if err := rl.db.Transaction(func(tx *db.Database) error {
var err error
fu, err = db.NewFileUpload(rl.fs, file, user, header.Filename)
fu, err = db.NewFileUpload(rl.fs, file, header.Filename)
if err != nil {
panic(errors.Wrap(err, "creating fileUpload"))
}
@ -443,7 +290,7 @@ func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Req
panic(errors.Wrap(err, "saving fileUpload in db"))
}
paste, err = shortenFileUploadID(tx, user, fu.PubID)
paste, err = shortenFileUploadID(tx, fu.PubID)
return err
}); err != nil {
panic(err)
@ -451,7 +298,7 @@ func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Req
rl.viewActionSuccess(w, r, paste, fu)
}
func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Request, user *db.User, rawurl string) {
func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Request, rawurl string) {
userURL, err := url.Parse(rawurl)
if err != nil {
msg := fmt.Sprintf("invalid url (%v): %v", err, rawurl)
@ -470,7 +317,7 @@ func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Reque
var paste *db.Paste
if err := rl.db.Transaction(func(tx *db.Database) error {
var err error
paste, err = shortenURL(tx, user, userURL)
paste, err = shortenURL(tx, userURL)
return err
}); err != nil {
panic(err)
@ -523,19 +370,19 @@ func (rl *rushlink) deletePasteHandler(w http.ResponseWriter, r *http.Request) {
//
// Returns the new paste key if the fileUpload was successfully added to the
// database
func shortenFileUploadID(tx *db.Database, user *db.User, id uuid.UUID) (*db.Paste, error) {
return shorten(tx, user, db.PasteTypeFileUpload, id[:])
func shortenFileUploadID(tx *db.Database, id uuid.UUID) (*db.Paste, error) {
return shorten(tx, db.PasteTypeFileUpload, id[:])
}
// Add a new URL to the database
//
// Returns the new paste key if the url was successfully shortened
func shortenURL(tx *db.Database, user *db.User, userURL *url.URL) (*db.Paste, error) {
return shorten(tx, user, db.PasteTypeRedirect, []byte(userURL.String()))
func shortenURL(tx *db.Database, userURL *url.URL) (*db.Paste, error) {
return shorten(tx, db.PasteTypeRedirect, []byte(userURL.String()))
}
// Add a paste (of any kind) to the database with arbitrary content.
func shorten(tx *db.Database, user *db.User, ty db.PasteType, content []byte) (*db.Paste, error) {
func shorten(tx *db.Database, ty db.PasteType, content []byte) (*db.Paste, error) {
// Generate the paste key
var keyEntropy int
if ty == db.PasteTypeFileUpload || ty == db.PasteTypePaste {
@ -556,7 +403,6 @@ func shorten(tx *db.Database, user *db.User, ty db.PasteType, content []byte) (*
p := db.Paste{
Type: ty,
State: db.PasteStatePresent,
CreatedBy: user.ID,
Content: content,
Key: pasteKey,
DeleteToken: deleteToken,

View File

@ -38,9 +38,6 @@ type FileUpload struct {
// UUID publically identifies this FileUpload.
PubID uuid.UUID `gorm:"uniqueIndex"`
// User ID that created this file
CreatedBy uint `gorm:"index"`
// FileName contains the original filename of this FileUpload.
FileName string
@ -129,7 +126,7 @@ func (fs *FileStore) filePath(pubID uuid.UUID, fileName string) string {
//
// Internally, this function detects the type of the file stored in `r` using
// `http.DetectContentType`.
func NewFileUpload(fs *FileStore, r io.Reader, user *User, fileName string) (*FileUpload, error) {
func NewFileUpload(fs *FileStore, r io.Reader, fileName string) (*FileUpload, error) {
// Generate a file ID
pubID, err := uuid.NewRandom()
if err != nil {
@ -171,7 +168,6 @@ func NewFileUpload(fs *FileStore, r io.Reader, user *User, fileName string) (*Fi
fu := &FileUpload{
State: FileUploadStatePresent,
PubID: pubID,
CreatedBy: user.ID,
FileName: baseName,
ContentType: contentType,
Checksum: hash.Sum32(),

View File

@ -42,56 +42,5 @@ func Gormigrate(db *gorm.DB) *gormigrate.Gormigrate {
return tx.Migrator().DropTable(&FileUpload{}, &Paste{})
},
},
{
ID: "202304301337",
Migrate: func(tx *gorm.DB) error {
type User struct {
ID uint `gorm:"primaryKey"`
User string `gorm:"uniqueIndex"`
Password string
Admin bool
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
}
type FileUpload struct {
ID uint `gorm:"primaryKey"`
State FileUploadState `gorm:"index"`
PubID uuid.UUID `gorm:"uniqueIndex"`
CreatedBy uint `gorm:"index"`
FileName string
ContentType string
Checksum uint32
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
}
type Paste struct {
ID uint `gorm:"primaryKey"`
Type PasteType `gorm:"index"`
State PasteState `gorm:"index"`
CreatedBy uint `gorm:"index"`
Content []byte
Key string `gorm:"uniqueIndex"`
DeleteToken string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
}
return tx.AutoMigrate(&User{}, &FileUpload{}, &Paste{})
},
Rollback: func(tx *gorm.DB) error {
if err := tx.Migrator().DropTable(&User{}); err != nil {
return err
}
if err := tx.Migrator().DropColumn(&FileUpload{}, "CreatedBy"); err != nil {
return err
}
if err := tx.Migrator().DropColumn(&Paste{}, "CreatedBy"); err != nil {
return err
}
return nil
},
},
})
}

View File

@ -1,72 +0,0 @@
package db
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
const (
pwdSaltSize = 16
pwdHashSize = 32
// chosen from https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
pwdMemory = 64 * 1024
pwdThreads = 1
pwdIterations = 2
pwdParams = "m=65536,t=2,p=1,v=19"
pwdVersion = "v=19"
pwdAlgo = "argon2id"
)
func HashPassword(password string) (string, error) {
if argon2.Version != 19 {
// go has no static asserts
panic("Unexpected argon2 version")
}
// Generate a salt for the password hash
salt := make([]byte, pwdSaltSize)
if _, err := rand.Read(salt); err != nil {
return "", err
}
// Hash the password using argon2id
hash := argon2.IDKey([]byte(password), salt, pwdIterations, pwdMemory, pwdThreads, pwdHashSize)
// Encode the salt and hash as a string in PHC format
encodedSalt := base64.RawStdEncoding.EncodeToString(salt)
encodedHash := base64.RawStdEncoding.EncodeToString(hash)
return fmt.Sprintf("$%s$%s$%s$%s$%s", pwdAlgo, pwdVersion, pwdParams, encodedSalt, encodedHash), nil
}
var errInvalidDBPasswordFormat = errors.New("invalid password format in db")
func comparePassword(hashedPassword string, password string) (bool, error) {
// Extract the salt and hash from the hashed password string
fields := strings.Split(hashedPassword, "$")
if len(fields) != 6 || fields[1] != pwdAlgo || fields[2] != pwdVersion || fields[3] != pwdParams {
return false, errInvalidDBPasswordFormat
}
encodedSalt, encodedHash := fields[4], fields[5]
// Decode the salt and hash from base64
salt, err := base64.RawStdEncoding.DecodeString(encodedSalt)
if err != nil {
return false, err
}
hash, err := base64.RawStdEncoding.DecodeString(encodedHash)
if err != nil {
return false, err
}
// Hash the password using the extracted salt and parameters
computedHash := argon2.IDKey([]byte(password), salt, pwdIterations, pwdMemory, pwdThreads, pwdHashSize)
// Compare the computed hash with the stored hash
return subtle.ConstantTimeCompare(hash, computedHash) == 1, nil
}

View File

@ -24,7 +24,6 @@ type Paste struct {
ID uint `gorm:"primaryKey"`
Type PasteType `gorm:"index"`
State PasteState `gorm:"index"`
CreatedBy uint `gorm:"index"`
Content []byte
Key string `gorm:"uniqueIndex"`
DeleteToken string

View File

@ -1,129 +0,0 @@
package db
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
User string `gorm:"uniqueIndex"`
Password string
Admin bool
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
}
func NewUser(db *gorm.DB, username string, password string, admin bool) error {
// Generate a new UUID for the user
id, err := uuid.NewRandom()
if err != nil {
return err
}
// Hash the password using argon2id
hashedPassword, err := HashPassword(password)
if err != nil {
return err
}
// Create a new user record
user := &User{
ID: uint(id.ID()),
User: username,
Password: hashedPassword,
Admin: admin,
}
return db.Create(user).Error
}
func Authenticate(db *gorm.DB, username string, password string) (*User, error) {
// Get the user record by username
var user User
if err := db.Where("user = ?", username).First(&user).Error; err != nil {
return nil, errors.New("user not found")
}
// Compare the hashed password with the provided password
valid, err := comparePassword(user.Password, password)
if err != nil {
return nil, err
}
if !valid {
return nil, errors.New("invalid password")
}
return &user, nil
}
// DeleteUser deletes a user with the specified username from the database.
func DeleteUser(db *gorm.DB, username string) error {
// Find the user by username
var user User
if err := db.Where("user = ?", username).First(&user).Error; err != nil {
return err
}
// Delete the user
if err := db.Delete(&user).Error; err != nil {
return err
}
return nil
}
func ChangeUser(db *gorm.DB, username string, updatedUser *User) error {
// Retrieve the existing user
var existingUser User
if err := db.Where("user = ?", username).First(&existingUser).Error; err != nil {
return err
}
// Update the user fields
existingUser.User = updatedUser.User
existingUser.Password = updatedUser.Password
existingUser.Admin = updatedUser.Admin
// Save the updated user to the database
if err := db.Save(&existingUser).Error; err != nil {
return err
}
return nil
}
func CreateAdminUser(db *gorm.DB, adminUsername string) error {
// Check if the admin user already exists
var admins []User
if err := db.Unscoped().Limit(1).Where("user = ?", adminUsername).Find(&admins).Error; err != nil {
return err
}
if len(admins) > 0 {
// already exists
return nil
}
// Generate a random 24-char password
passwordBytes := make([]byte, 24)
if _, err := rand.Read(passwordBytes); err != nil {
return err
}
password := base64.URLEncoding.EncodeToString(passwordBytes)
// Create the admin user
if err := NewUser(db, adminUsername, password, true); err != nil {
return err
}
// Print out the generated password
fmt.Printf("Generated password for admin user %s: %s\n", adminUsername, password)
return nil
}

View File

@ -106,9 +106,6 @@ func InitMainRouter(r *mux.Router, rl *rushlink) {
r.HandleFunc("/{path:js/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD")
r.HandleFunc("/", rl.indexGetHandler).Methods("GET", "HEAD")
r.HandleFunc("/", rl.newPasteHandler).Methods("POST")
r.HandleFunc("/users", rl.createUserHandler).Methods("POST")
r.HandleFunc("/users/{user}", rl.changeUserHandler).Methods("POST")
r.HandleFunc("/users/{user}", rl.deleteUserHandler).Methods("DELETE")
r.HandleFunc("/"+urlKeyExpr, rl.viewPasteHandler).Methods("GET", "HEAD")
r.HandleFunc("/"+urlKeyWithExtExpr, rl.viewPasteHandler).Methods("GET", "HEAD")
r.HandleFunc("/"+urlKeyExpr+"/nr", rl.viewPasteHandlerNoRedirect).Methods("GET", "HEAD")