restructure

This commit is contained in:
kayos@tcp.direct 2021-09-20 03:36:20 -07:00
parent 443870dfaf
commit e5cd04a91a
17 changed files with 743 additions and 354 deletions

View File

@ -1,4 +1,20 @@
# MapyWeb
basic web panel/user system
# CokePlate
boilerplate golang project
## features
* logging
- json
- pretty printing
* configuration
- toml
- write defaults if doesn't exist
* database
- dynamic (configurable) bitcask database initialization and access
* misc
- base64'd ascii/ansi banner defined in config

23
cmd/main.go Normal file
View File

@ -0,0 +1,23 @@
package main
import (
"github.com/rs/zerolog"
"mapyweb/config"
"mapyweb/db"
"mapyweb/extra"
)
var log zerolog.Logger
func init() {
config.Init()
extra.Banner()
log = config.StartLogger()
log.Info().Msg("Starting databases...")
db.StartDatabases()
}
func main() {
}

300
config/config.go Normal file
View File

@ -0,0 +1,300 @@
package config
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"runtime"
"github.com/rs/zerolog"
"github.com/spf13/viper"
)
const (
// Version roughly represents the applications current version.
Version = "0.2"
// Title is the name of the application used throughout the configuration process.
Title = "mapyweb"
)
var (
// BannerOnly when toggled causes the appllication to only print the banner and version then exit.
BannerOnly = false
// GenConfig when toggled causes the application to write its default config to the cwd and then exit.
GenConfig = false
// NoColor when true will disable the banner and any colored console output.
NoColor bool
)
// "http"
var (
// HTTPBindAddr is defined via our toml configuration file. It is the address that the http server listens on.
HTTPBindAddr string
// HTTPBindPort is defined via our toml configuration file. It is the port that the http server listens on.
HTTPBindPort string
// UseUnixSocket when toggled disables the TCP listener and listens on the given UnixSocketPath.
UseUnixSocket bool
// UnixSocketPath is the path of the unix socket used when UseUnixSocket is toggled.
UnixSocketPath = ""
// HTTPUseSSL determins whether or not we serve HTTPs
HTTPUseSSL bool
// SSLCertFile is our fullchain SSL Certificate
SSLCertFile string
// SSLKeyFile is our SSL Certificate private key
SSLKeyFile string
)
// "db"
var (
UseEmbeddedPostgres bool
SQLHost string
SQLPort string
SQLUsername string
SQLPassword string
SQLDatabase string
DataPath string
)
var (
// Filename returns the current location of our toml config file.
Filename string
)
var (
f *os.File
err error
noColorForce = false
customconfig = false
home string
configLocations []string
)
var (
// Debug is our global debug toggle
Debug bool
prefConfigLocation string
snek *viper.Viper
)
func init() {
if home, err = os.UserHomeDir(); err != nil {
panic(err)
}
prefConfigLocation = home + "/.config/" + Title
snek = viper.New()
}
func writeConfig() {
if runtime.GOOS != "windows" {
if _, err := os.Stat(prefConfigLocation); os.IsNotExist(err) {
if err = os.Mkdir(prefConfigLocation, 0755); err != nil {
println("error writing new config: " + err.Error())
}
}
newconfig := prefConfigLocation + "/" + "config.toml"
if err = snek.SafeWriteConfigAs(newconfig); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
Filename = newconfig
return
}
newconfig := Title
snek.SetConfigName(newconfig)
if err = snek.MergeInConfig(); err != nil {
if err = snek.SafeWriteConfigAs(newconfig + ".toml"); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
}
Filename = newconfig
}
// Init will initialize our toml configuration engine and define our default configuration values which can be written to a new configuration file if desired
func Init() {
snek.SetConfigType("toml")
snek.SetConfigName("config")
argParse()
if customconfig {
associate()
return
}
acquireClue()
setDefaults()
for _, loc := range configLocations {
snek.AddConfigPath(loc)
}
if err = snek.MergeInConfig(); err != nil {
writeConfig()
}
if len(Filename) < 1 {
Filename = snek.ConfigFileUsed()
}
associate()
}
func setDefaults() {
var (
// Add new configuration categories here
configSections = []string{"logger", "http", "db"}
deflogdir = home + "/.config/" + Title + "/logs/"
defNoColor = false
)
if runtime.GOOS == "windows" {
deflogdir = "logs/"
defNoColor = true
}
Opt := make(map[string]map[string]interface{})
Opt["logger"] = map[string]interface{}{
"debug": true,
"directory": deflogdir,
"nocolor": defNoColor,
"use_date_filename": true,
}
Opt["http"] = map[string]interface{}{
"use_unix_socket": false,
"unix_socket_path": "/var/run/cokeplate",
"bind_addr": "127.0.0.1",
"bind_port": "8080",
}
Opt["db"] = map[string]interface{}{
"embedded_pgsql": true,
"sql_host": "localhost",
"sql_port": "5432",
"sql_user": "postgres",
"sql_pass": "postgres",
"data_path": "./data",
}
for _, def := range configSections {
snek.SetDefault(def, Opt[def])
}
if GenConfig {
if err = snek.SafeWriteConfigAs("./config.toml"); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
os.Exit(0)
}
}
func acquireClue() {
configLocations = append(configLocations, "./")
if runtime.GOOS != "windows" {
configLocations = append(configLocations, prefConfigLocation)
configLocations = append(configLocations, "/etc/"+Title+"/")
configLocations = append(configLocations, "../")
configLocations = append(configLocations, "../../")
}
}
func loadCustomConfig(path string) {
if f, err = os.Open(path); err != nil {
println("Error opening specified config file: " + path)
panic("config file open fatal error: " + err.Error())
}
buf, err := ioutil.ReadAll(f)
err2 := snek.ReadConfig(bytes.NewBuffer(buf))
switch {
case err != nil:
fmt.Println("config file read fatal error: ", err.Error())
case err2 != nil:
fmt.Println("config file read fatal error: ", err2.Error())
default:
break
}
customconfig = true
}
func printUsage() {
println("\n" + Title + " v" + Version + " Usage\n")
println("-c <config.toml> - Specify config file")
println("--nocolor - disable color and banner ")
println("--banner - show banner + version and exit")
println("--genconfig - write default config to 'default.toml' then exit")
os.Exit(0)
}
// TODO: should probably just make a proper CLI with flags or something
func argParse() {
for i, arg := range os.Args {
switch arg {
case "-h":
printUsage()
case "--genconfig":
GenConfig = true
case "--nocolor":
noColorForce = true
case "--banner":
BannerOnly = true
case "--config":
fallthrough
case "-c":
if len(os.Args) <= i-1 {
panic("syntax error! expected file after -c")
}
loadCustomConfig(os.Args[i+1])
default:
continue
}
}
}
func bl(key string) bool {
return snek.GetBool(key)
}
func st(key string) string {
return snek.GetString(key)
}
func sl(key string) []string {
return snek.GetStringSlice(key)
}
func it(key string) int {
return snek.GetInt(key)
}
func associate() {
HTTPBindAddr = st("http.bind_addr")
HTTPBindPort = st("http.bind_port")
UseUnixSocket = bl("http.use_unix_socket")
SQLPassword = st("db.sql_password")
SQLUsername = st("db.sql_username")
SQLDatabase = st("db.sql_database")
SQLHost = st("db.sql_host")
SQLPort = st("db.sql_port")
DataPath = st("db.data_path")
Debug = bl("logger.debug")
logDir = st("logger.directory")
NoColor = bl("logger.nocolor")
if noColorForce {
NoColor = true
}
// if UseUnixSocket {
// UnixSocketPath = snek.GetString("http.unix_socket_path")
// }
if Debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
}

50
config/logger.go Normal file
View File

@ -0,0 +1,50 @@
package config
import (
"os"
"strings"
"time"
"github.com/rs/zerolog"
)
var (
CurrentLogFile string
logFile *os.File
logDir string
logger zerolog.Logger
)
// StartLogger instantiates an instance of our zerolog loggger so we can hook it in our main package.
// While this does return a logger, it should not be used for additional retrievals of the logger. Use GetLogger()
func StartLogger() zerolog.Logger {
logDir = snek.GetString("logger.directory")
if err := os.MkdirAll(logDir, 0755); err != nil {
println("cannot create log directory: " + logDir + "(" + err.Error() + ")")
os.Exit(1)
}
tnow := Title
if snek.GetBool("logger.use_date_filename") {
tnow = strings.Replace(time.Now().Format(time.RFC822), " ", "_", -1)
tnow = strings.Replace(tnow, ":", "-", -1)
}
CurrentLogFile = logDir + tnow + ".log"
if logFile, err = os.OpenFile(CurrentLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666); err != nil {
println("cannot create log file: " + err.Error())
os.Exit(1)
}
multi := zerolog.MultiLevelWriter(zerolog.ConsoleWriter{NoColor: NoColor, Out: os.Stdout}, logFile)
logger = zerolog.New(multi).With().Timestamp().Logger()
return logger
}
// GetLogger retrieves our global logger object
func GetLogger() zerolog.Logger {
// future logic here
return logger
}

59
db.go
View File

@ -1,59 +0,0 @@
package main
import (
"database/sql"
"fmt"
jsoniter "github.com/json-iterator/go"
"git.tcp.direct/tcp.direct/bitcask-mirror"
)
/*
create table users (
id int not null auto_increment,
banExpireDate datetime(3),
banReason varchar(255),
offensemanager int,
votepoints int default 0,
donationpoints int default 0,
maplePoints int default 0,
nxPrepaid int default 0,
name varchar(255),
password varchar(255),
pic varchar(255),
mac varchar(255),
accounttype int default 0,
age int default 0,
vipgrade int default 0,
nblockreason int default 0,
gender tinyint default 0,
msg2 tinyint default 0,
purchaseexp tinyint default 0,
pblockreason tinyint default 3,
chatunblockdate bigint default 0,
hascensorednxloginid boolean default 0,
gradecode tinyint default 0,
censorednxloginid varchar(255),
characterslots int default 4,
creationdate datetime(3),
primary key (id),
foreign key (offensemanager) references offense_managers(id)
);
*/
// Credentials is the json structure for user data
type Credentials struct {
Password string `json:"password"`
Username string `json:"username"`
Email string `json:"email"`
}
func initDB() {
}
func rowExists(query string, args ...interface{}) bool {
}

View File

@ -1,8 +1,7 @@
package main
package db
import (
"errors"
"os"
"time"
pgsql "github.com/fergusstrange/embedded-postgres"
@ -37,6 +36,24 @@ func getUser(username string) (u *User, err error) {
return u, nil
}
// UserExists queries our database for a user using the given username and returns true if it exists, false if it does not.
func UserExists(username string) bool {
u := new(User)
if err := db.Get(u, "SELECT id FROM accounts WHERE username=$1", username); err != nil {
return false
}
return true
}
// EmailTaken queries our database for a user using the given email address and returns true if the email is taken, false if it is not.
func EmailTaken(email string) bool {
u := new(User)
if err := db.Get(u, "SELECT id FROM accounts WHERE email=$1", email); err == nil {
return true
}
return false
}
// AttemptWebLogin checks the given credentials for validity and returns a User and an error.
func AttemptWebLogin(username, password, ipaddr, useragent string) (*User, error) {
var (
@ -44,21 +61,22 @@ func AttemptWebLogin(username, password, ipaddr, useragent string) (*User, error
err error
good = false
)
defer func() {
// TODO: make bitcask max value size larger because this 000 is gonna get huge
// to log invalid users
if u == nil {
u = &User{ID: 000}
}
if err := authlog.NewAttempt(int(u.ID), ipaddr, useragent, Web, good); err != nil {
if err := authlog.newAttempt(int(u.ID), ipaddr, useragent, Web, good); err != nil {
println(err.Error())
log.Error().Err(err).Msg("failed_login")
}
}()
if u, err = getUser(username); err != nil {
return u, err
}
println("provided: ", password)
println("hashed: ", u.Password)
if !CheckPasswordHash(password, u.Password) {
return nil, errors.New("invalid password")
}
@ -91,37 +109,5 @@ func RegisterNewUser(username, password, ipaddr string, gender int, admin bool)
}
_, err = db.NamedExec(`INSERT INTO accounts (username, password, creation, last_login, last_ip, ban, admin, gender)
VALUES (:username, :password, :creation, :last_login, :last_ip, 0, :admin, :gender);`, u)
return err
}
func main() {
var err error
postgres = pgsql.NewDatabase(pgsql.DefaultConfig().
DataPath("./data/pgsql").
BinariesPath("./postgresql").
Logger(os.Stdout))
if err := postgres.Start(); err != nil {
log.Error().Err(err).Msg("postgres_fail")
return
}
defer postgres.Stop()
if authlog, err = GetUserAuths("./data/authlog"); err != nil {
log.Error().Caller().Err(err).Msg("!!! GetUserAuths failed !!!")
}
db, err = sqlx.Connect("postgres",
"host=localhost port=5432 user=postgres password=postgres dbname=postgres sslmode=disable")
if err != nil {
log.Error().Caller().Err(err).Msg("postgres_fail")
return
}
Banner()
argParse()
}

View File

@ -1,4 +1,4 @@
package main
package db
import (
"encoding/binary"
@ -8,10 +8,10 @@ import (
"git.tcp.direct/tcp.direct/bitcask-mirror"
)
// AuthLog is for implementing an authentication log
// AuthLog is for implementing an authentication log.
type AuthLog interface {
NewAttempt(id int, ip, useragent string, source AuthSource, good bool) error
GetLog(id int) (map[time.Time]AuthAttempt, error)
newAttempt(id int, ip, useragent string, source AuthSource, good bool) error
getLog(id int) (map[time.Time]AuthAttempt, error)
}
type AuthSource uint32
@ -21,7 +21,7 @@ const (
Client // 1
)
// AuthAttempt represents a login attempt, used for our authlog
// AuthAttempt represents a login attempt, used for our AuthLog.
type AuthAttempt struct {
Time time.Time
IP string
@ -30,18 +30,18 @@ type AuthAttempt struct {
Good bool
}
// SecurityLog is an implementation of AuthLog
type SecurityLog struct {
// secLog is an implementation of AuthLog
type secLog struct {
DB *bitcask.Bitcask
}
// GetUserAuths opens a bitcask database at path or starts a new one, then returns a SecurityLog and an error.
func GetUserAuths(path string) (*SecurityLog, error) {
// GetUserAuths opens a bitcask database at path or starts a new one, then returns a secLog and an error.
func GetUserAuths(path string) (*secLog, error) {
casket, err := bitcask.Open(path)
if err != nil {
return nil, err
}
return &SecurityLog{DB: casket}, nil
return &secLog{DB: casket}, nil
}
func idToUbytes(id int) []byte {
@ -51,7 +51,7 @@ func idToUbytes(id int) []byte {
return buf
}
func (s *SecurityLog) GetLog(id int) (map[time.Time]AuthAttempt, error) {
func (s *secLog) getLog(id int) (map[time.Time]AuthAttempt, error) {
uidb := idToUbytes(id)
var logMap = make(map[time.Time]AuthAttempt)
if !s.DB.Has(uidb) {
@ -68,10 +68,10 @@ func (s *SecurityLog) GetLog(id int) (map[time.Time]AuthAttempt, error) {
return logMap, nil
}
func (s *SecurityLog) NewAttempt(id int, ip, useragent string, source AuthSource, good bool) error {
func (s *secLog) newAttempt(id int, ip, useragent string, source AuthSource, good bool) error {
uidb := idToUbytes(id)
current, err := s.GetLog(id)
current, err := s.getLog(id)
if err != nil {
return err
}
@ -79,8 +79,8 @@ func (s *SecurityLog) NewAttempt(id int, ip, useragent string, source AuthSource
current[time.Now()] = AuthAttempt{
Time: time.Now(),
IP: ip,
UserAgent: useragent,
Source: source,
UserAgent: useragent,
Good: good,
}

View File

@ -1,4 +1,4 @@
package main
package db
import "golang.org/x/crypto/bcrypt"

117
db/setup.go Normal file
View File

@ -0,0 +1,117 @@
package db
import (
"fmt"
"os"
pgsql "github.com/fergusstrange/embedded-postgres"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
"mapyweb/config"
)
var accountsSchema = `create table accounts
(
id serial
constraint accounts_pkey
primary key,
username varchar(13) not null,
password varchar not null,
creation date not null,
last_login date not null,
last_ip varchar not null,
ban integer not null,
admin integer not null,
gender smallint default 0 not null
);`
var authlog *secLog
func install(db *sqlx.DB) {
if _, err := db.Exec(accountsSchema); err != nil {
panic(err)
}
println("accounts table installed!")
}
func loginTest() {
var kayos *User
var err error
println("logging in as kayos...")
if kayos, err = AttemptWebLogin(
"kayos",
"yeet",
"192.168.69.5",
"yeetBrowser 420"); err != nil {
println("FAIL: " + err.Error())
return
}
println("logged in as kayos!")
println(kayos.Password)
}
func registerTest() {
println("registering kayos...")
if err := RegisterNewUser("kayos", "yeet", "127.0.0.1", 0, true); err != nil {
println("REG_TEST_FAIL: " + err.Error())
return
}
println("kayos registered!")
return
}
func argParse() {
for _, arg := range os.Args {
if arg == "--install" {
println("installing...")
install(db)
}
if arg == "--regtest" {
registerTest()
}
if arg == "--logintest" {
loginTest()
}
}
}
// StartDatabases initializes our embeded postgresql database and our embeded bitcask database.
func StartDatabases() {
var err error
// embedded postgres -
if config.UseEmbeddedPostgres {
postgres = pgsql.NewDatabase(pgsql.DefaultConfig().
DataPath("./data/pgsql").
BinariesPath("./postgresql").
Logger(os.Stdout))
if err := postgres.Start(); err != nil {
log.Error().Err(err).Msg("postgres_fail")
return
}
defer postgres.Stop()
db, err = sqlx.Connect("postgres",
"host=localhost port=5432 user=postgres password=postgres dbname=postgres sslmode=disable")
} else {
connstr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", config.SQLHost, config.SQLPort, config.SQLUsername, config.SQLPassword)
db, err = sqlx.Connect("postgres", connstr)
}
if err != nil {
log.Error().Caller().Err(err).Msg("postgres_fail")
return
}
// -------------------
// embedded bitcask for authlog
if authlog, err = GetUserAuths("./data/authlog"); err != nil {
log.Error().Caller().Err(err).Msg("!!! GetUserAuths failed !!!")
}
// -------------------
argParse()
}

View File

@ -1,4 +1,4 @@
package main
package extra
import (
"bytes"

21
go.mod
View File

@ -9,12 +9,13 @@ require (
github.com/gorilla/csrf v1.7.1
github.com/gorilla/mux v1.8.0
github.com/jmoiron/sqlx v1.3.4
github.com/json-iterator/go v1.1.12
github.com/lib/pq v1.10.2
github.com/matcornic/hermes/v2 v2.1.0
github.com/rs/zerolog v1.25.0
github.com/satori/go.uuid v1.2.0
github.com/savsgio/atreugo/v11 v11.8.3
github.com/spf13/viper v1.8.1
github.com/valyala/fasthttp v1.30.0
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
)
@ -23,28 +24,27 @@ require (
github.com/Masterminds/sprig v2.16.0+incompatible // indirect
github.com/PuerkitoBio/goquery v1.5.0 // indirect
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 // indirect
github.com/andybalholm/brotli v1.0.0 // indirect
github.com/andybalholm/brotli v1.0.2 // indirect
github.com/andybalholm/cascadia v1.0.0 // indirect
github.com/aokoli/goutils v1.0.1 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/fasthttp/router v1.4.3 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gofrs/flock v0.8.0 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/uuid v1.1.2 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.2.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 // indirect
github.com/klauspost/compress v1.10.10 // indirect
github.com/klauspost/compress v1.13.4 // indirect
github.com/klauspost/pgzip v1.2.4 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattn/go-runewidth v0.0.3 // indirect
github.com/mholt/archiver/v3 v3.5.0 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/olekukonko/tablewriter v0.0.1 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect
@ -52,6 +52,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/plar/go-adaptive-radix-tree v1.0.4 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/savsgio/gotils v0.0.0-20210907153846-c06938798b52 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/afero v1.6.0 // indirect
@ -61,13 +62,15 @@ require (
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/ulikunitz/xz v0.5.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 // indirect
github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
golang.org/x/exp v0.0.0-20200228211341-fcea875c7e85 // indirect
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
golang.org/x/text v0.3.5 // indirect
golang.org/x/text v0.3.6 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

40
go.sum
View File

@ -52,8 +52,9 @@ github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 h1:uHogIJ9bXH75ZYrXnVShH
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81/go.mod h1:6ZvnjTZX1LNo1oLpfaJK8h+MXqHxcBFBIwkgsv+xlv0=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E=
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@ -63,6 +64,8 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/atreugo/mock v0.0.0-20200601091009-13c275b330b0 h1:IVqe9WnancrkICl5HqEfGjrnkQ4+VsU5fodcuFVoG/A=
github.com/atreugo/mock v0.0.0-20200601091009-13c275b330b0/go.mod h1:HTHAc8RoZXMVTr6wZQN7Jjm3mYMnbfkqqKdnQgSoe9o=
github.com/badoux/checkmail v1.2.1 h1:TzwYx5pnsV6anJweMx2auXdekBwGr/yt1GgalIx9nBQ=
github.com/badoux/checkmail v1.2.1/go.mod h1:XroCOBU5zzZJcLvgwU15I+2xXyCdTWXyR9MGfRhBYy0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -101,6 +104,8 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fasthttp/router v1.4.3 h1:spS+LUnRryQ/+hbmYzs/xWGJlQCkeQI3hxGZdlVYhLU=
github.com/fasthttp/router v1.4.3/go.mod h1:9ytWCfZ5LcCcbD3S7pEXyBX9vZnOZmN918WiiaYUzr8=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fergusstrange/embedded-postgres v1.10.0 h1:YnwF6xAQYmKLAXXrrRx4rHDLih47YJwVPvg8jeKfdNg=
github.com/fergusstrange/embedded-postgres v1.10.0/go.mod h1:a008U8/Rws5FtIOTGYDYa7beVWsT3qVKyqExqYYjL+c=
@ -154,8 +159,9 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -186,8 +192,9 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@ -240,8 +247,6 @@ github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w=
github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
@ -251,8 +256,9 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEEeX6s=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.4 h1:TQ7CNpYKovDOmqzRHKxJh0BeaBI7UdQZYc6p7pMQh1A=
github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@ -293,12 +299,9 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
@ -342,6 +345,10 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/savsgio/atreugo/v11 v11.8.3 h1:5w9H7sBwJ4/MOYms3zZ3NRDPIyp2TBkhZ0GrerNGizA=
github.com/savsgio/atreugo/v11 v11.8.3/go.mod h1:d/+jyq8I/QyaWJvdGjvvvqVoG1gnqqjAg/hhSvnZloA=
github.com/savsgio/gotils v0.0.0-20210907153846-c06938798b52 h1:FODZE/jDkENIpW3JiMA9sXBQfNklTfClUNhR9k37dPY=
github.com/savsgio/gotils v0.0.0-20210907153846-c06938798b52/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@ -394,6 +401,12 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4=
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.30.0 h1:nBNzWrgZUUHohyLPU/jTvXdhrcaf2m5k3bWk+3Q049g=
github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 h1:L0rPdfzq43+NV8rfIx2kA4iSSLRj2jN5ijYHoeXRwvQ=
github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w=
github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe h1:9YnI5plmy+ad6BM+JCLJb2ZV7/TNiE5l7SNKfumYKgc=
@ -433,6 +446,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -511,8 +525,9 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -581,7 +596,9 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -591,8 +608,9 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

218
main.go
View File

@ -1,50 +1,77 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
"html/template"
"log"
"net"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/valyala/fasthttp"
"github.com/rs/zerolog/log"
"github.com/badoux/checkmail"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
"github.com/satori/go.uuid"
"golang.org/x/crypto/bcrypt"
"mapyweb/db"
"mapyweb/extra"
)
func init() {
Splash()
initDB()
extra.Banner()
db.StartDatabases()
}
func main() {
HTTPServ()
}
type authError string
const (
usernameExists authError = "username is taken"
emailExists authError = "email is taken"
rateLimited authError = "you are being ratelimited, slow down"
badEmail authError = "not a valid email address"
internalErr authError = "internal server error"
invalidRequest authError = "invalid request"
badCreds authError = "invalid credentials"
)
func HTTPServ() {
r := mux.NewRouter()
r.HandleFunc("/", IndexShow)
r.HandleFunc("/register", RegForm)
r.HandleFunc("/login", LoginForm)
r.HandleFunc("/", showIndex)
r.HandleFunc("/register", registrationForm)
r.HandleFunc("/login", loginForm)
r.HandleFunc("/EmailTest", EmailTest)
//without rate limiting
//r.HandleFunc("/login/submit", Login)
//r.HandleFunc("/register/submit", Register)
fmt.Println("Web server starting on port 42069")
log.Fatal(http.ListenAndServe(":42069", csrf.Protect([]byte("7e3e2a60a55a223589f0bf218f23251619182602ae19fd829803d18645379f66"), csrf.Secure(false))(r)))
log.Fatal().Err(http.ListenAndServe(":42069", csrf.Protect([]byte("7e3e2a60a55a223589f0bf218f23251619182602ae19fd829803d18645379f66"), csrf.Secure(false))(r))).
Msg("http failure")
}
func IndexShow(w http.ResponseWriter, r *http.Request) {
func logAndReturnError(w http.ResponseWriter, e authError) {
log.Error().Str("error", string(e)).Msg("auth_fail")
if _, err := fmt.Fprint(w, e); err != nil {
log.Debug().Err(err).Msg("failed to write to ResponseWriter")
}
}
func fprinter(w http.ResponseWriter, s string, a ...interface{}) {
if _, err := fmt.Fprint(w, s, a); err != nil {
log.Debug().Err(err).Msg("failed to write to ResponseWriter")
}
}
func showIndex(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("session_token")
if err != nil {
if err == http.ErrNoCookie {
@ -70,139 +97,122 @@ func IndexShow(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Finally, return the welcome message to the user
w.Write([]byte(fmt.Sprintf("Welcome %s!", response)))
fprinter(w, "Welcome %s!", response)
}
func LoginForm(w http.ResponseWriter, r *http.Request) {
func loginForm(w http.ResponseWriter, r *http.Request) {
ip := strings.Split(r.RemoteAddr, ":")[0]
fmt.Println(ip)
t, _ := template.ParseFiles("login.tmpl")
t.ExecuteTemplate(w, "login.tmpl", map[string]interface{}{
t, err := template.ParseFiles("login.tmpl")
if err != nil {
panic(err)
}
if err := t.ExecuteTemplate(w, "login.tmpl", map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(r),
})
}); err != nil {
panic(err)
}
}
func RegForm(w http.ResponseWriter, r *http.Request) {
func registrationForm(w http.ResponseWriter, r *http.Request) {
ip := strings.Split(r.RemoteAddr, ":")[0]
fmt.Println(ip)
t, _ := template.ParseFiles("register.tmpl")
t.ExecuteTemplate(w, "register.tmpl", map[string]interface{}{
t, err := template.ParseFiles("register.tmpl")
if err != nil {
panic(err)
}
if err := t.ExecuteTemplate(w, "register.tmpl", map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(r),
})
}); err != nil {
panic(err)
}
}
func Register(w http.ResponseWriter, r *http.Request) {
creds := &Credentials{}
err := json.NewDecoder(r.Body).Decode(creds)
ip := strings.Split(r.RemoteAddr, ":")[0]
if ip == "[" {
ip = "127.0.0.1"
func register(w http.ResponseWriter, r *http.Request) {
var ip string
var err error
ip, _, err = net.SplitHostPort(r.RemoteAddr)
if err != nil {
panic(err)
}
fmt.Println(ip + "/register/submit")
log.Debug().Str("ip", ip).Msg("register")
UsernameInput := r.PostFormValue("username")
PasswordInput := r.PostFormValue("password")
EmailInput := r.PostFormValue("email")
GenderInput := r.PostFormValue("gender")
usernameInput := r.PostFormValue("username")
passwordInput := r.PostFormValue("password")
emailInput := r.PostFormValue("email")
genderInput := r.PostFormValue("gender")
//debug outputs
//fmt.Println(UsernameInput)
//fmt.Println(PasswordInput)
//fmt.Println(EmailInput)
//fmt.Println(GenderInput)
// debug outputs
// fmt.Println(usernameInput)
// fmt.Println(passwordInput)
// fmt.Println(emailInput)
// fmt.Println(genderInput)
var gender int
// Verify Gender value is either 0(female) or 1(male)
// if not just silently send them a 400 because wtf
if GenderInput != "0" && GenderInput != "1" {
fmt.Println("Gender Input BAD")
w.WriteHeader(http.StatusBadRequest)
return
if gender, err = strconv.Atoi(genderInput); err != nil || gender > 1 {
logAndReturnError(w, invalidRequest)
}
//Usernames must only be letters, numbers, dashes, and underscores
// Usernames must only be letters, numbers, dashes, and underscores
var rxUsername = regexp.MustCompile("([\\w\\-]+)")
//Usernames must be under 16 characters
if len(UsernameInput) > 16 || !rxUsername.MatchString(UsernameInput) {
// Usernames must be under 16 characters
if len(usernameInput) > 16 || !rxUsername.MatchString(usernameInput) {
fmt.Println("ERROR: Username must only be alphanumeric and under 16 characters. \"-\" and \"_\" are also accepted.")
fmt.Println(w, "ERROR: Username must only be alphanumeric and under 16 characters. \"-\" and \"_\" are also accepted.")
return
}
//Check if username is taken
if rowExists("Select id from maplestory.accounts where username=$1", UsernameInput) {
fmt.Println("ERROR: Username exists.")
fmt.Fprintf(w, "ERROR: Username exists.")
// Check if username is taken
if db.UserExists(usernameInput) {
logAndReturnError(w, usernameExists)
return
}
//Check if email is taken
if rowExists("Select id from maplestory.accounts where email=$1", EmailInput) {
// Check if email is taken
if db.EmailTaken(emailInput) {
logAndReturnError(w, emailExists)
return
}
//Validates email addresses
err = checkmail.ValidateFormat(EmailInput)
// Validates email addresses
err = checkmail.ValidateFormat(emailInput)
if err != nil {
fmt.Println("ERROR: that is not a valid email address!")
fmt.Fprintf(w, "ERROR: that is not a valid email address!")
//return
panic(err)
}
fmt.Println("Passed checks, hashing password with bcrypt...")
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(PasswordInput), 16)
if _, err = db.Query("INSERT INTO maplestory.accounts (username,password,email,creation,last_login,last_ip,ban,admin,gender) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
UsernameInput, string(hashedPassword), EmailInput, time.Now(), time.Now(), ip, 0, 0, GenderInput); err != nil {
w.WriteHeader(http.StatusInternalServerError)
panic(err)
logAndReturnError(w, badEmail)
return
} else {
fmt.Println("Success!")
http.Redirect(w, r, "/?regsuccess=true", 301)
}
err = db.RegisterNewUser(usernameInput, passwordInput, r.RemoteAddr, genderInput, false)
if err != nil {
logAndReturnError(w, internalErr)
return
}
fmt.Println("Success!")
http.Redirect(w, r, "/?regsuccess=true", 301)
}
func Login(w http.ResponseWriter, r *http.Request) {
UsernameInput := r.PostFormValue("username")
PasswordInput := r.PostFormValue("password")
func newLogin(w http.ResponseWriter, r *http.Request) {
var u *db.User
var err error
// Get the existing entry present in the database for the given username
result := db.QueryRow("SELECT password FROM maplestory.accounts WHERE username=$1", UsernameInput)
usernameInput := r.PostFormValue("username")
passwordInput := r.PostFormValue("password")
// We create another instance of `Credentials` to store the credentials we get from the database
storedCreds := &Credentials{}
// Store the obtained password in `storedCreds`
err := result.Scan(&storedCreds.Password)
if err != nil {
// If an entry with the username does not exist, send an "Unauthorized"(401) status
if err == sql.ErrNoRows {
fmt.Println("Login failed!")
fmt.Fprintf(w, "Login failed!")
return
}
// If the error is of any other type, send a 500 status
w.WriteHeader(http.StatusInternalServerError)
return
if u, err = db.AttemptWebLogin(usernameInput, passwordInput, r.RemoteAddr, r.UserAgent()); err != nil {
logAndReturnError(w, badCreds)
}
// Compare the stored hashed password, with the hashed version of the password that was received
if err = bcrypt.CompareHashAndPassword([]byte(storedCreds.Password), []byte(PasswordInput)); err != nil {
// If the two passwords don't match, return a 401 status
w.WriteHeader(http.StatusUnauthorized)
}
// If we reach this point, that means the users password was correct, and that they are authorized
// The default 200 status is sent
fmt.Println("Authentication successful, setting session...")
//sets a 2 hour long session and cookie
// sets a 2 hour long session and cookie
sessionToken := uuid.NewV4().String()
_, err = cache.Do("SETEX", sessionToken, "7200", UsernameInput)
_, err = cache.Do("SETEX", sessionToken, "7200", usernameInput)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println("Session management failed!")
@ -214,6 +224,6 @@ func Login(w http.ResponseWriter, r *http.Request) {
return
}
fmt.Println("Login successful!")
fmt.Fprintf(w, "Login successful!")
fmt.Println("newLogin successful!")
fprinter(w, "newLogin successful!")
}

View File

@ -1,65 +0,0 @@
package main
import (
"os"
"github.com/jmoiron/sqlx"
)
var accountsSchema = `create table accounts
(
id serial
constraint accounts_pkey
primary key,
username varchar(13) not null,
password varchar not null,
creation date not null,
last_login date not null,
last_ip varchar not null,
ban integer not null,
admin integer not null,
gender smallint default 0 not null
);`
var authlog *SecurityLog
func install(db *sqlx.DB) {
if _, err := db.Exec(accountsSchema); err != nil {
panic(err)
}
println("accounts table installed!")
}
func argParse() {
var kayos *User
var err error
for _, arg := range os.Args {
if arg == "--install" {
println("installing...")
install(db)
return
}
if arg == "--regkayos" {
println("registering kayos...")
if err := RegisterNewUser("kayos", "yeet", "127.0.0.1", 0, true); err != nil {
println("REG_TEST_FAIL: " + err.Error())
return
}
println("kayos registered!")
return
}
if arg == "--loginkayos" {
println("logging in as kayos...")
if kayos, err = AttemptWebLogin(
"kayos",
"yeet",
"192.168.69.5",
"yeetBrowser 420"); err != nil {
println("FAIL: " + err.Error())
return
}
println("logged in as kayos!")
println(kayos.Password)
}
}
}

View File

@ -1,6 +0,0 @@
{
"username": "paul",
"password": "lolbad",
"email": "email@definitely.real",
"gender": 1
}

View File

@ -1,44 +0,0 @@
package main
import (
"bytes"
"compress/gzip"
"encoding/base64"
"io/ioutil"
)
const banner = "H4sIAAAAAAACA81WzYrDIBC+5xV6KXmAINhQy75ErtJbyDXs+992NVU/dWZMumxooRCcic73M2O6y/Nx/7qp1Rjz+zi6x/ny1H7t6n4hYYlxl6lW93dRrWKqz/5bBIqBGjASqzi+oQJQKUOtHbG7QRYoDk4gAaBWpXwTa4cVog5qkwQcOJa21WHJqNFnOQUhB0KiXDE4Z8H0whtOyt6XyApkWGSDOPvhN5lSVl7PVjbp0CFWIIqaTmu8QVDI9BvuHAsctxZJ2OYqyjkjB4le4komeJTfQFGwiqJCmwWTNiFLEumaKuuB9H2uDem9b6pdw6DR9yxHC4GxduTMgC17a6p7y1KbiYUVKnD0eRH1SNhVv9Sb6sUMZCnaUAPC+WX21d0yuDyspHlUbZvAoC23JRg1DOjqLEyUD94NNhMJ/QX3lV/rs7XCMQeoF2UWW75UPmRN9Q7WHJhnglZoVhFsEhMmWnk9EnNYxx5PZ9Pi4Onl15aixtzWm5Y3DQJ/PcaLrSfHctn4mYXJvUlOgVWu+GLM2jYIOKrn75T8i+HItw2hAv1BQcDkhtrbxbCRs0CW9++829MfBxoMKCsKbmx57X9Adj8mnnxLAQ4AAA=="
func qDeflate(data []byte) []byte {
var (
gz *gzip.Reader
out []byte
err error
)
r := bytes.NewReader(data)
if gz, err = gzip.NewReader(r); err != nil {
panic(err)
}
if out, err = ioutil.ReadAll(gz); err != nil {
return nil
}
return out
}
func b64d(data string) []byte {
var (
ret []byte
err error
)
if ret, err = base64.StdEncoding.DecodeString(data); err != nil {
panic(err)
}
return ret
}
// Splash displays banner
func Splash() {
print(qDeflate(b64d(banner)))
println("------------- v0.2 - kayos - ra - queed squad -------------")
println("")
}

40
web/router.go Normal file
View File

@ -0,0 +1,40 @@
package web
import (
"fmt"
"github.com/rs/zerolog/log"
"github.com/savsgio/atreugo/v11"
config "mapyweb/config"
)
func main() {
webconf := atreugo.Config{
Addr: fmt.Sprintf("%s:%s", config.HTTPBindAddr, config.HTTPBindPort),
TLSEnable: true,
CertKey: config.SSLKey,
CertFile: config.SSLCert,
Name: "MapyWeb",
Debug: config.Debug,
Compress: true,
}
server := atreugo.New(webconf)
server.GET("/", func(ctx *atreugo.RequestCtx) error {
return ctx.TextResponse("Hello World")
})
server.GET("/echo/{path:*}", func(ctx *atreugo.RequestCtx) error {
return ctx.TextResponse("Echo message: " + ctx.UserValue("path").(string))
})
v1 := server.NewGroupPath("/v1")
v1.GET("/", func(ctx *atreugo.RequestCtx) error {
return ctx.TextResponse("Hello V1 Group")
})
log.Error().Str("caller", "http").Err(server.ListenAndServe()).Msg("failed to listen")
}