simple api

This commit is contained in:
shoopea 2025-10-01 22:28:48 +02:00
parent 963fd34724
commit 6cf998997e
55 changed files with 99 additions and 59579 deletions

106
admin.go
View File

@ -3,9 +3,6 @@ package main
import (
"context"
"embed"
"fmt"
"html/template"
"io/fs"
"net/http"
"os/signal"
"syscall"
@ -13,24 +10,11 @@ import (
"github.com/gin-gonic/gin"
"github.com/robfig/cron/v3"
"github.com/sethvargo/go-password/password"
log "github.com/sirupsen/logrus"
)
type AdminConfig struct {
Users []*User `json:"users"`
Secrets *SecretsConfig `json:"secrets"`
Addr string `json:"addr"`
URL string `json:"url"`
}
type SecretsConfig struct {
PasswordPepper string `json:"password_pepper"`
ContextKey string `json:"context_key"`
ContextExpiration int `json:"context_expiration"`
ScryptN int `json:"scrypt_n"`
ScryptR int `json:"scrypt_r"`
ScryptP int `json:"scrypt_p"`
Addr string `json:"addr"`
}
//go:embed assets
@ -41,44 +25,12 @@ func NewAdmin() *AdminConfig {
defer log.WithFields(log.Fields{}).Debugf("done")
a := &AdminConfig{
Addr: "0.0.0.0:8080",
Secrets: NewSecrets(),
Users: make([]*User, 0),
URL: "https://backup.example.com/",
Addr: "0.0.0.0:8080",
}
return a
}
func NewSecrets() *SecretsConfig {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
pepper, _ := password.Generate(20, 5, 0, false, false)
ctx, _ := password.Generate(20, 5, 0, false, false)
return &SecretsConfig{
PasswordPepper: pepper,
ContextKey: ctx,
ContextExpiration: 3600,
ScryptN: 32768,
ScryptR: 8,
ScryptP: 1,
}
}
func (a *AdminConfig) NewAdminUser() {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
p, _ := password.Generate(20, 5, 0, false, false)
u, _ := NewUser("admin", p)
a.Users = append(a.Users, u)
log.WithFields(log.Fields{}).Warnf("Admin user password : %s", p)
}
func (a *AdminConfig) Run() {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
@ -93,46 +45,12 @@ func (a *AdminConfig) Run() {
r := gin.Default()
if t, err := template.ParseFS(assets, "assets/templates/*.html"); err != nil {
log.WithFields(log.Fields{"call": "template.ParseFS", "error": err}).Errorf("")
return
} else {
r.SetHTMLTemplate(t)
}
r.GET("/run", ApiRun)
r.GET("/run/:app", ApiRunApp)
r.GET("/", HttpAnyIndex)
r.POST("/", HttpAnyIndex)
r.GET("/save", ApiSave)
r.GET("/run", func(c *gin.Context) {
cfg.Run()
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
})
r.GET("/save", func(c *gin.Context) {
if err := cfg.Save(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
})
} else {
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
}
})
fsys, _ := fs.Sub(assets, "assets/static")
r.StaticFS("/assets", http.FS(fsys))
protected := r.Group("p", HttpAuth())
protected.GET("test", HttpAnyHome)
protected.GET("home", HttpAnyHome)
unprotected := r.Group("u", HttpNoAuth())
unprotected.GET("signin", HttpGetSignIn)
unprotected.POST("submit", HttpPostSubmit)
unprotected.GET("recover", HttpGetRecover)
r.GET("/config", ApiConfig)
srv := &http.Server{
Addr: a.Addr,
@ -168,15 +86,3 @@ func (a *AdminConfig) Run() {
}
}
func FindUserID(user string) (uint64, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
for _, v := range cfg.Admin.Users {
if v.Username == user {
return v.ID, nil
}
}
return 0, fmt.Errorf("no user")
}

50
api.go Normal file
View File

@ -0,0 +1,50 @@
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func ApiRun(c *gin.Context) {
cfg.Run()
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
}
func ApiRunApp(c *gin.Context) {
if _, ok := cfg.apps[c.Param("app")]; ok {
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
}
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "no app found",
})
}
func ApiSave(c *gin.Context) {
if err := cfg.Save(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": err,
})
} else {
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
}
}
func ApiConfig(c *gin.Context) {
if b, err := cfg.Pretty(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": err,
})
} else {
c.Data(http.StatusOK, "application/json", b)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,426 +0,0 @@
/*!
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
direction: ltr /* rtl:ignore */;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

View File

@ -1,8 +0,0 @@
/*!
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

View File

@ -1,423 +0,0 @@
/*!
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
direction: ltr ;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

View File

@ -1,8 +0,0 @@
/*!
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,17 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Home :: zBackup</title>
<!-- Bootstrap core CSS -->
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="text-center">
<div class="container">
<h1 class="h3 mb-3 font-weight-normal">Logged in.</h1>
</div>
</body>
</html>

View File

@ -1,24 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Recover :: zBackup</title>
<!-- Bootstrap core CSS -->
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="text-center">
<div class="container">
<form class="form-signin" method="POST" action="/u/submit">
<h1 class="h3 mb-3 font-weight-normal">Recover password</h1>
<div class="form-group">
<input type="username" class="form-control" id="username" placeholder="Username" name="username" required autofocus>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" >Recover</button>
<p class="mt-5 mb-3 text-muted">&copy; 2023-2024</p>
</form>
</div>
</body>
</html>

View File

@ -1,29 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Login :: zBackup</title>
<!-- Bootstrap core CSS -->
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="text-center">
<div class="container">
<form class="form-signin" method="POST" action="/u/submit">
<h1 class="h3 mb-3 font-weight-normal">Sign in</h1>
{{if (ne .Error "")}}<div class="alert alert-danger">{{.Error}}</div>{{end}}
<div class="form-group">
<input type="username" class="form-control" id="username" placeholder="Username" name="username" required autofocus>
</div>
<div class="form-group">
<input type="password" class="form-control" id="password" placeholder="Password" name="password" required>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" >Sign in</button>
<p class="mt-5 mb-3 text-muted">&copy; 2023-2024</p>
</form>
<a href="/u/recover">Lost password</a>
</div>
</body>
</html>

View File

@ -49,6 +49,7 @@ func main() {
} else if c, err := LoadConfigFile("backup.json"); err == nil {
cfg = c
} else {
log.Debugf("loading default config")
cfg, _ = LoadConfigByte(sampleCfg)
}
@ -56,12 +57,6 @@ func main() {
if cfg.Admin == nil {
cfg.Admin = NewAdmin()
}
if cfg.Admin.Secrets == nil {
cfg.Admin.Secrets = NewSecrets()
}
if len(cfg.Admin.Users) == 0 {
cfg.Admin.NewAdminUser()
}
cfg.Admin.Run()
} else {
cfg.Run()

View File

@ -52,6 +52,13 @@ type AppConfig struct {
Active bool `json:"active,omitempty"`
}
type EmailConfig struct {
Active bool `json:"active"`
SmtpHost string `json:"smtp,omitempty"`
FromEmail string `json:"email_from,omitempty"`
ToEmail []string `json:"email_to,omitempty"`
}
// Load config from file
func LoadConfigFile(path string) (*Config, error) {
log.WithFields(log.Fields{"path": path}).Debugf("starting")
@ -96,22 +103,24 @@ func LoadConfigByte(conf []byte) (*Config, error) {
}
if c.Email != nil {
if len(c.Email.SmtpHost) == 0 {
err := fmt.Errorf("no smtp")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
if c.Email.Active {
if len(c.Email.SmtpHost) == 0 {
err := fmt.Errorf("no smtp")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
if len(c.Email.FromEmail) == 0 {
err := fmt.Errorf("no email from")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
if len(c.Email.FromEmail) == 0 {
err := fmt.Errorf("no email from")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
if len(c.Email.ToEmail) == 0 {
err := fmt.Errorf("no email to")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
if len(c.Email.ToEmail) == 0 {
err := fmt.Errorf("no email to")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
}
}
@ -180,8 +189,8 @@ func LoadConfigByte(conf []byte) (*Config, error) {
return c, nil
}
// Save config
func (c *Config) Save() error {
// Pretty config
func (c *Config) Pretty() ([]byte, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
@ -191,10 +200,22 @@ func (c *Config) Save() error {
b, err := json.Marshal(cfg)
if err != nil {
log.WithFields(log.Fields{"error": err, "call": "json.Marshal"}).Errorf("")
return err
return nil, err
}
r := pretty.PrettyOptions(b, &pretty.Options{Indent: " "})
return pretty.PrettyOptions(b, &pretty.Options{Indent: " "}), nil
}
// Save config
func (c *Config) Save() error {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
r, err := cfg.Pretty()
if err != nil {
log.WithFields(log.Fields{"error": err, "call": "cfg.Pretty"}).Errorf("")
return err
}
f, err := os.Create(*cfgFile)
if err != nil {

View File

@ -16,12 +16,6 @@ type Email struct {
items []string
}
type EmailConfig struct {
SmtpHost string `json:"smtp"`
FromEmail string `json:"email_from"`
ToEmail []string `json:"email_to"`
}
func NewEmail(now time.Time) *Email {
log.WithFields(log.Fields{"now": now}).Debugf("starting")
defer log.WithFields(log.Fields{"now": now}).Debugf("done")
@ -32,8 +26,9 @@ func NewEmail(now time.Time) *Email {
func (e *Email) AddItem(item string) {
log.WithFields(log.Fields{"item": item}).Debugf("starting")
defer log.WithFields(log.Fields{"item": item}).Debugf("done")
e.items = append(e.items, item)
if cfg.Email.Active {
e.items = append(e.items, item)
}
}
func (e *Email) Send(addr, from string, to []string) error {

148
http.go
View File

@ -1,148 +0,0 @@
package main
import (
"net/http"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func HttpAuth() gin.HandlerFunc {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
return func(c *gin.Context) {
// Search signed-in userID
userID := 0
if userID == 0 {
// Return 404 and abort handlers chain.
c.String(http.StatusNotFound, "404 page not found")
c.AbortWithStatus(http.StatusNotFound)
}
}
}
func HttpNoAuth() gin.HandlerFunc {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
return func(c *gin.Context) {
}
}
func HttpPostSubmit(c *gin.Context) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
t, err := GetCSRFToken(c)
if err != nil {
c.String(http.StatusBadRequest, "bad token")
return
}
if !t.Valid() {
c.String(http.StatusBadRequest, "expired token")
return
}
switch t.GetPath() {
case "signin":
log.WithFields(log.Fields{"call": "Context.Request.ParseForm", "err": err}).Debugf("submit signin")
HttpSubmitSignIn(c)
HttpAnyIndex(c)
default:
log.WithFields(log.Fields{"call": "Context.Request.ParseForm", "err": err}).Debugf("submit %s", t.GetPath())
c.String(http.StatusBadRequest, "")
}
if GetWebSessionUserID(c) > 0 {
c.Redirect(http.StatusTemporaryRedirect, "/p/home")
} else {
SetCSRFToken(c)
warning, _ := c.Cookie("warning")
c.SetCookie("warning", "", -1, "/", cfg.Admin.Addr, false, true)
c.HTML(http.StatusOK, "page-signin.html", gin.H{
"Error": warning,
})
}
}
func HttpGetRecover(c *gin.Context) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
SetCSRFToken(c)
c.HTML(http.StatusOK, "page-recover.html", gin.H{})
}
func HttpGetSignIn(c *gin.Context) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
SetCSRFToken(c)
c.HTML(http.StatusOK, "page-signin.html", gin.H{})
}
func HttpAnyIndex(c *gin.Context) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
if GetWebSessionUserID(c) > 0 {
c.Redirect(http.StatusTemporaryRedirect, "/p/home")
} else {
c.Redirect(http.StatusTemporaryRedirect, "/u/signin")
}
}
func HttpAnyHome(c *gin.Context) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
if GetWebSessionUserID(c) == 0 {
c.Redirect(http.StatusTemporaryRedirect, "/u/signin")
} else {
SetCSRFToken(c)
c.HTML(http.StatusOK, "page-home.html", gin.H{})
}
}
func GetWebSessionUserID(c *gin.Context) uint64 {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
return 0
}
func HttpSubmitSignIn(c *gin.Context) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
err := c.Request.ParseForm()
if err != nil {
c.SetCookie("warning", "Unable to parse form", 0, "/", cfg.Admin.URL, false, true)
log.WithFields(log.Fields{"call": "Context.Request.ParseForm", "err": err}).Debugf("")
return
}
username := c.Request.FormValue("username")
password := c.Request.FormValue("password")
userID, err := FindUserID(username)
if err != nil {
c.SetCookie("warning", "Invalid user or password", 0, "/", cfg.Admin.URL, false, true)
log.WithFields(log.Fields{"call": "FindUserID", "attr": username, "err": err}).Debugf("")
return
}
if !VerifyUserPassword(userID, password) {
c.SetCookie("warning", "Invalid user or password", 0, "/", cfg.Admin.URL, false, true)
log.WithFields(log.Fields{"call": "VerifyUserPassword", "attr": "***"}).Debugf("auth not ok")
return
}
t := NewSessionToken(userID)
c.SetCookie("session", t.Encode(), 9999999999, "/", cfg.Admin.URL, false, true)
c.SetCookie("warning", "", -1, "/", cfg.Admin.URL, false, true)
}

View File

@ -1,298 +0,0 @@
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"fmt"
"hash/crc32"
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
var sessionMap map[string]uint64
type SessionToken struct {
Identifier [32]byte
Verifier [32]byte
}
type CSRFToken struct {
Seed [4]byte
Path [16]byte
Time int64
Checksum uint32
}
func NewSessionToken(userID uint64) *SessionToken {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
identifier := make([]byte, 32)
_, err := rand.Read(identifier)
if err != nil {
log.WithFields(log.Fields{"call": "rand.Read", "attr": "identifier", "err": err}).Debugf("")
return &SessionToken{}
}
verifier := make([]byte, 32)
_, err = rand.Read(verifier)
if err != nil {
log.WithFields(log.Fields{"call": "rand.Read", "attr": "verifier", "err": err}).Debugf("")
return &SessionToken{}
}
hash := hmac.New(sha256.New, verifier)
hash.Write(verifier)
hashVerifier := hash.Sum(nil)
t := SessionToken{}
copy(t.Verifier[:], verifier[:32])
copy(t.Identifier[:], hashVerifier[:32])
return &t
}
func (t *SessionToken) Bytes() []byte {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
buf := new(bytes.Buffer)
_ = binary.Write(buf, binary.LittleEndian, t)
return buf.Bytes()
}
func (t *SessionToken) Load(b []byte) error {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
if len(b) != binary.Size(t) {
return fmt.Errorf("wrong size")
}
r := bytes.NewReader(b)
err := binary.Read(r, binary.LittleEndian, t)
if err != nil {
return err
}
return nil
}
func (t *SessionToken) GetSessionID() uint64 {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
if sessionID, ok := sessionMap[string(t.Identifier[:])]; !ok {
return 0
} else {
return sessionID
}
}
func GetSessionTokenParam(c *gin.Context) (*SessionToken, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
err := c.Request.ParseForm()
if err != nil {
log.WithFields(log.Fields{"call": "Context.Request.ParseForm", "err": err}).Errorf("")
return nil, err
}
session := c.Param("key")
return DecodeSessionToken(session)
}
func GetSessionTokenCookie(c *gin.Context) (*SessionToken, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
if session, err := c.Cookie("session"); err != nil {
log.WithFields(log.Fields{"call": "Context.Cookie", "attr": "session", "err": err}).Errorf("")
return nil, err
} else {
return DecodeSessionToken(session)
}
}
func (t *SessionToken) Encode() string {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
return base64.StdEncoding.EncodeToString(t.Bytes())
}
func DecodeSessionToken(s string) (*SessionToken, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
log.WithFields(log.Fields{"call": "base64.StdEncoding.DecodeString", "attr": "session", "err": err}).Errorf("")
return nil, err
}
t := SessionToken{}
err = t.Load(b)
if err != nil {
return nil, err
}
return &t, nil
}
func (t *CSRFToken) GetPath() string {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
buf := bytes.Buffer{}
for _, b := range t.Path {
if b > 0 {
buf.WriteByte(b)
} else {
break
}
}
return buf.String()
}
func (t *CSRFToken) Bytes() []byte {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
buf := new(bytes.Buffer)
_ = binary.Write(buf, binary.LittleEndian, t)
return buf.Bytes()
}
func (t *CSRFToken) Load(b []byte) error {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
if len(b) != binary.Size(t) {
return fmt.Errorf("wrong size")
}
r := bytes.NewReader(b)
err := binary.Read(r, binary.LittleEndian, t)
if err != nil {
log.WithFields(log.Fields{"call": "binary.Read", "attr": "b", "err": err}).Errorf("")
return err
}
chk := t.Checksum
t.Checksum = 0
if crc32.ChecksumIEEE(t.Bytes()) != chk {
return fmt.Errorf("wrong checksum")
}
t.Checksum = chk
return nil
}
func NewCSRFToken(c *gin.Context) *CSRFToken {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
t := CSRFToken{}
copy(t.Path[:], c.Request.URL.Path[0:])
t.Time = time.Now().UTC().Unix()
_, err := rand.Read(t.Seed[:])
log.WithFields(log.Fields{"call": "rand.Read", "attr": "seed", "err": err}).Errorf("")
t.Checksum = crc32.ChecksumIEEE(t.Bytes())
return &t
}
func GetCSRFToken(c *gin.Context) (*CSRFToken, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
if context, err := c.Cookie("context"); err != nil {
return nil, fmt.Errorf("no context param")
} else {
return DecodeCSRFToken(context)
}
}
func (t *CSRFToken) Encode() string {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
hash := sha256.New()
hash.Write([]byte(cfg.Admin.Secrets.ContextKey))
key := hash.Sum(nil)
block, _ := aes.NewCipher(key)
ciphertext := make([]byte, 32) // size of CSRFToken
if binary.Size(t) != 32 {
log.WithFields(log.Fields{"err": fmt.Errorf("size is wrong")}).Fatalf("")
}
iv := make([]byte, aes.BlockSize)
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext[:], t.Bytes())
return base64.StdEncoding.EncodeToString(ciphertext)
}
func DecodeCSRFToken(s string) (*CSRFToken, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, err
}
if len(b) != binary.Size(CSRFToken{}) {
return nil, fmt.Errorf("wrong size")
}
hash := sha256.New()
hash.Write([]byte(cfg.Admin.Secrets.ContextKey))
key := hash.Sum(nil)
block, err := aes.NewCipher(key)
iv := make([]byte, aes.BlockSize)
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(b, b)
t := CSRFToken{}
err = t.Load(b)
if err != nil {
return nil, err
}
return &t, nil
}
func (t *CSRFToken) Valid() bool {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
if time.Now().UTC().Sub(time.Unix(t.Time, 0)) > time.Duration(cfg.Admin.Secrets.ContextExpiration)*time.Second {
return false
}
return true
}
func SetCSRFToken(c *gin.Context) {
c.SetCookie("context", NewCSRFToken(c).Encode(), cfg.Admin.Secrets.ContextExpiration, "/", cfg.Admin.URL, false, true)
return
}

102
user.go
View File

@ -1,102 +0,0 @@
package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/scrypt"
)
type User struct {
ID uint64 `json:"id"`
Username string `json:"username"`
Salt string `json:"salt"`
Password string `json:"passwd"`
Email string `json:"email"`
}
func NewUser(name, passwd string) (*User, error) {
log.WithFields(log.Fields{"name": name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": name}).Debugf("done")
userID := uint64(1)
for _, v := range cfg.Admin.Users {
userID = max(v.ID+1, userID)
if v.Username == name {
err := errors.New("user already exists")
log.WithFields(log.Fields{"name": name, "error": err}).Errorf("")
return nil, err
}
}
u := &User{
ID: userID,
Username: name,
}
salt := make([]byte, 32)
if _, err := rand.Read(salt); err != nil {
log.WithFields(log.Fields{"name": name, "call": "rand.Read", "error": err}).Errorf("")
return nil, err
}
u.Salt = hex.EncodeToString(salt)
if pass, err := u.HashPassword(passwd); err != nil {
log.WithFields(log.Fields{"name": name, "call": "HashPassword", "error": err}).Errorf("")
return nil, err
} else {
u.Password = hex.EncodeToString(pass)
}
return u, nil
}
func (u *User) HashPassword(passwd string) ([]byte, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
//peppering the pass
hash := hmac.New(sha256.New, []byte(cfg.Admin.Secrets.PasswordPepper))
hash.Write([]byte(passwd))
hashPass := hash.Sum(nil)
//salting the hash
salt := make([]byte, 32)
if _, err := hex.Decode(salt, []byte(u.Salt)); err != nil {
log.WithFields(log.Fields{"call": "hex.Decode", "error": err}).Errorf("")
return nil, err
}
if h, err := scrypt.Key(hashPass, salt, cfg.Admin.Secrets.ScryptN, cfg.Admin.Secrets.ScryptR, cfg.Admin.Secrets.ScryptP, 32); err != nil {
log.WithFields(log.Fields{"call": "scrypt.Key", "error": err}).Errorf("")
return h, err
} else {
return h, nil
}
}
func VerifyUserPassword(userID uint64, clearPass string) bool {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
var u *User
for _, v := range cfg.Admin.Users {
if v.ID == userID {
u = v
}
}
hashPass, err := u.HashPassword(clearPass)
if err != nil {
log.WithFields(log.Fields{"call": "user.HashPassword", "attr": "***", "error": err}).Errorf("")
return false
}
return hex.EncodeToString(hashPass) == u.Password
}