Replace strings.Split(RemoteAddr, ":") with net.SplitHostPort for correct IPv6 address extraction in vote deduplication and comment IP tracking. Harden image proxy: add SSRF-safe transport blocking private/reserved IPs at connection time with DNS rebinding protection, sanitize error messages to prevent information leakage, add response size limit via io.LimitReader. Fix shadowed error variables in BlockedUsers, SetTitle, and Delete methods. Exclude gosec taint analysis false positives at linter config level.
133 lines
3.9 KiB
Go
133 lines
3.9 KiB
Go
// Package cmd has all top-level commands dispatched by main's flag.Parse
|
|
// The entry point of each command is Execute function
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
log "github.com/go-pkgz/lgr"
|
|
)
|
|
|
|
// CommonOptionsCommander extends flags.Commander with SetCommon
|
|
// All commands should implement this interfaces
|
|
type CommonOptionsCommander interface {
|
|
SetCommon(commonOpts CommonOpts)
|
|
Execute(args []string) error
|
|
HandleDeprecatedFlags() []DeprecatedFlag
|
|
}
|
|
|
|
// CommonOpts sets externally from main, shared across all commands
|
|
type CommonOpts struct {
|
|
RemarkURL string
|
|
SharedSecret string
|
|
Revision string
|
|
}
|
|
|
|
// SupportCmdOpts is set of commands shared among similar commands like backup/restore and such.
|
|
// Order of fields defines the help command output order.
|
|
type SupportCmdOpts struct {
|
|
Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"`
|
|
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" default:"" description:"admin basic auth password"`
|
|
Timeout time.Duration `long:"timeout" default:"60m" description:"timeout for the command run"`
|
|
}
|
|
|
|
// DeprecatedFlag contains information about deprecated option
|
|
type DeprecatedFlag struct {
|
|
Old string
|
|
New string
|
|
Version string
|
|
Collision bool
|
|
}
|
|
|
|
// SetCommon satisfies CommonOptionsCommander interface and sets common option fields
|
|
// The method called by main for each command
|
|
func (c *CommonOpts) SetCommon(commonOpts CommonOpts) {
|
|
c.RemarkURL = strings.TrimSuffix(commonOpts.RemarkURL, "/") // allow RemarkURL with trailing /
|
|
c.SharedSecret = commonOpts.SharedSecret
|
|
c.Revision = commonOpts.Revision
|
|
}
|
|
|
|
// HandleDeprecatedFlags sets new flags from deprecated and returns their list
|
|
func (c *CommonOpts) HandleDeprecatedFlags() []DeprecatedFlag { return nil }
|
|
|
|
// fileParser used to convert template strings like blah-{{.SITE}}-{{.YYYYMMDD}} the final format
|
|
type fileParser struct {
|
|
site string
|
|
file string
|
|
path string
|
|
}
|
|
|
|
// parse apply template and also concat path and file. In case if file contains path separator path will be ignored
|
|
func (p *fileParser) parse(now time.Time) (string, error) {
|
|
// file/location parameters my have template masks
|
|
fileTemplate := struct {
|
|
YYYYMMDD string
|
|
YYYY string
|
|
YYYYMM string
|
|
MM string
|
|
DD string
|
|
TS string
|
|
UNIX int64
|
|
SITE string
|
|
}{
|
|
YYYYMMDD: now.Format("20060102"),
|
|
YYYY: now.Format("2006"),
|
|
YYYYMM: now.Format("200601"),
|
|
MM: now.Format("01"),
|
|
DD: now.Format("02"),
|
|
UNIX: now.Unix(),
|
|
SITE: p.site,
|
|
TS: now.Format("20060102T150405"),
|
|
}
|
|
|
|
bb := bytes.Buffer{}
|
|
fname := p.file
|
|
if !strings.Contains(p.file, string(filepath.Separator)) {
|
|
fname = filepath.Join(p.path, p.file)
|
|
}
|
|
|
|
if err := template.Must(template.New("bb").Parse(fname)).Execute(&bb, fileTemplate); err != nil {
|
|
return "", fmt.Errorf("failed to parse %q: %w", fname, err)
|
|
}
|
|
return bb.String(), nil
|
|
}
|
|
|
|
// resetEnv clears sensitive env vars
|
|
func resetEnv(envs ...string) {
|
|
for _, env := range envs {
|
|
if err := os.Unsetenv(env); err != nil {
|
|
log.Printf("[WARN] can't unset env %s, %s", env, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// responseError returns error with status and response body
|
|
func responseError(resp *http.Response) error {
|
|
body, e := io.ReadAll(resp.Body)
|
|
if e != nil {
|
|
body = []byte("")
|
|
}
|
|
if resp.StatusCode == http.StatusUnauthorized {
|
|
return fmt.Errorf("error response %q, ensure you have set ADMIN_PASSWD and provided it to the command you're running: %s", resp.Status, body)
|
|
}
|
|
return fmt.Errorf("error response %q, %s", resp.Status, body)
|
|
}
|
|
|
|
// mkdir -p for all dirs
|
|
func makeDirs(dirs ...string) error {
|
|
for _, dir := range dirs {
|
|
if err := os.MkdirAll(dir, 0o700); err != nil { // if path is already a directory, MkdirAll does nothing
|
|
return fmt.Errorf("can't make directory %s: %w", dir, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|