linted, added a custom web server definition with timeouts, added new type of context key, and added linting to the makefile

This commit is contained in:
grumbulon 2022-12-31 10:05:20 -05:00
parent 11692efdd1
commit c3c533558f
11 changed files with 209 additions and 79 deletions

88
.golangci.yaml Normal file
View file

@ -0,0 +1,88 @@
# Refer to golangci-lint's example config file for more options and information:
# https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml
run:
timeout: 5m
modules-download-mode: readonly
skip-dirs:
- "coverage"
- ".github"
linters:
enable:
- errcheck
- errorlint
- gci
- gocritic
- goconst
- godot
- goimports
- govet
- gocritic
- goerr113
- gofmt
- gofumpt
- gosec
- maintidx
- makezero
- misspell
- nlreturn
- nolintlint
- prealloc
- predeclared
- staticcheck
- tagliatelle
- whitespace
- wrapcheck
- wsl
disable:
- structcheck
- revive
linters-settings:
govet:
check-shadowing: true
enable-all: true
disable-all: false
revive:
ignore-generated-header: false
severity: warning
confidence: 0.8
errorCode: 1
warningCode: 1
rules:
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: duplicated-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: errorf
- name: exported
- name: if-return
- name: increment-decrement
- name: modifies-value-receiver
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: var-declaration
- name: var-naming
linters-settings:
tagliatelle:
case:
use-field-name: false
rules:
# Any struct tag type can be used.
# Support string case: `camel`, `pascal`, `kebab`, `snake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower`
json: goCamel
yaml: goCamel
xml: goCamel
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0

View file

@ -13,3 +13,7 @@ frontend:
backend: $(GO_SOURCES)
go build ./cmd/pomme
lint:
golangci-lint run --timeout=5m --out-format colored-line-number:stdout
.PHONY: lint

View file

@ -1,9 +1,9 @@
package main
import (
"fmt"
"log"
"net/http"
"time"
"git.freecumextremist.com/grumbulon/pomme/frontend"
"git.freecumextremist.com/grumbulon/pomme/internal/api"
@ -12,17 +12,24 @@ import (
)
func main() {
pomme := chi.NewRouter()
pomme.Use(middleware.Logger)
pomme.Use(middleware.GetHead)
pomme.Use(middleware.Recoverer)
pomme.Mount("/", frontend.SvelteKitHandler("/"))
pomme.Mount("/api", api.Api())
pomme.Mount("/api", api.API())
log.Println("\t-------------------------------------")
log.Println("\t\tRunning on port 3000")
log.Println("\t-------------------------------------")
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", 3000), pomme))
s := &http.Server{
ReadTimeout: 3 * time.Second,
WriteTimeout: 15 * time.Second,
Addr: ":3000",
Handler: pomme,
}
log.Fatal(s.ListenAndServe())
}

View file

@ -17,7 +17,7 @@ import (
//go:embed all:build
var files embed.FS
// I stole this lol
// SvelteKitHandler -- I stole this lol.
func SvelteKitHandler(path string) http.Handler {
fsys, err := fs.Sub(files, "build")
if err != nil {
@ -27,7 +27,7 @@ func SvelteKitHandler(path string) http.Handler {
filesystem := http.FS(fsys)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, path)
path := strings.TrimPrefix(r.URL.Path, path) //nolint: govet
_, err := filesystem.Open(path)
if errors.Is(err, os.ErrNotExist) {

View file

@ -3,7 +3,6 @@ package api
import (
"context"
"fmt"
"log"
"net/http"
"time"
@ -14,11 +13,19 @@ import (
"github.com/go-chi/render"
)
type key int
const (
keyPrincipalContextID key = iota
)
// SetDBMiddleware is the http Handler func for the GORM middleware with context.
func SetDBMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
db := db.InitDb()
timeoutContext, _ := context.WithTimeout(context.Background(), time.Second)
ctx := context.WithValue(r.Context(), "DB", db.WithContext(timeoutContext))
timeoutContext, cancelContext := context.WithTimeout(context.Background(), time.Second)
ctx := context.WithValue(r.Context(), keyPrincipalContextID, db.WithContext(timeoutContext))
defer cancelContext()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
@ -28,19 +35,17 @@ func basicAuthFailed(w http.ResponseWriter, realm string) {
w.WriteHeader(http.StatusUnauthorized)
}
// API handler
func Api() http.Handler {
// API subroute handler.
func API() http.Handler {
api := chi.NewRouter()
// Protected routes
api.Group(func(api chi.Router) {
api.Use(jwtauth.Verifier(tokenAuth))
api.Use(jwtauth.Authenticator)
api.Post("/check", Ingest)
api.Get("/private", AuthTest)
})
// Open routes
@ -54,23 +59,25 @@ func Api() http.Handler {
return api
}
// Ingest is a function to ingest Zonefiles.
func Ingest(w http.ResponseWriter, r *http.Request) {
data := &internal.ZoneRequest{}
log.Println(data)
if err := render.Bind(r, data); err != nil {
http.Error(w, "Unable to parse Zonefile", http.StatusBadRequest)
return
}
_ = &internal.ZoneRequest{}
zonefile := data.Zone
render.Status(r, http.StatusAccepted)
render.Render(w, r, internal.NewZoneResponse(zonefile))
// todo write to database, maybe?
// todo -- add functions to apply to master zonefile if above check is OK
// todo -- add functions to apply to master zonefile if above check is OK.
render.Status(r, http.StatusAccepted)
}
// AuthTest is for testing protected routes.
func AuthTest(w http.ResponseWriter, r *http.Request) {
_, claims, _ := jwtauth.FromContext(r.Context())
w.Write([]byte(fmt.Sprintf("protected area. hi %v", claims["username"])))
_, err := w.Write([]byte(fmt.Sprintf("protected area. hi %v", claims["username"])))
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
}

View file

@ -10,29 +10,43 @@ import (
"gorm.io/gorm"
)
// Login checks user credentials and creates a jwt session.
func Login(w http.ResponseWriter, r *http.Request) {
var result internal.User
r.ParseForm()
err := r.ParseForm()
if err != nil {
http.Error(w, "Unable to parse request", http.StatusInternalServerError)
return
}
username := r.Form.Get("username")
if username == "" {
username = autoUname()
}
password := r.Form.Get("password")
if password == "" {
http.Error(w, "No password provided", http.StatusInternalServerError) // this should prob be handled by the frontend
}
db, ok := r.Context().Value("DB").(*gorm.DB)
db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB)
if !ok {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
db.Model(internal.User{Username: username}).First(&result)
err := bcrypt.CompareHashAndPassword([]byte(result.HashedPassword), []byte(password))
err = bcrypt.CompareHashAndPassword([]byte(result.HashedPassword), []byte(password))
if err != nil {
basicAuthFailed(w, "user")
return
}
@ -48,17 +62,23 @@ func Login(w http.ResponseWriter, r *http.Request) {
Value: token,
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(
err = json.NewEncoder(w).Encode(
internal.Response{
Message: "Successfully logged in",
HTTPResponse: 200,
})
http.Redirect(w, r, "/", http.StatusSeeOther)
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}
// Logout destroys a users JWT cookie.
func Logout(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
HttpOnly: true,
MaxAge: -1, // Delete the cookie.
@ -68,10 +88,17 @@ func Logout(w http.ResponseWriter, r *http.Request) {
Value: "",
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(
err := json.NewEncoder(w).Encode(
internal.Response{
Message: "Successfully logged out",
HTTPResponse: 200,
})
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}

View file

@ -1,7 +1,6 @@
package api
import (
"fmt"
"log"
"github.com/go-chi/jwtauth/v5"
@ -20,6 +19,6 @@ func makeToken(username string) string {
if err != nil {
log.Fatalln(err)
}
fmt.Println(tokenString)
return tokenString
}

View file

@ -1,31 +1,41 @@
package api
import (
"crypto/rand"
"encoding/json"
"fmt"
"math/rand"
"math/big"
"net/http"
"time"
"git.freecumextremist.com/grumbulon/pomme/internal"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// NewUser takes a POST request and user form and creates a user in the database.
func NewUser(w http.ResponseWriter, r *http.Request) {
db, ok := r.Context().Value("DB").(*gorm.DB)
db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB)
if !ok {
http.Error(w, "internal server error", http.StatusInternalServerError)
}
var result internal.User
r.ParseForm()
err := r.ParseForm()
if err != nil {
http.Error(w, "Unable to parse request", http.StatusInternalServerError)
return
}
username := r.Form.Get("username")
if username == "" {
username = autoUname()
}
password := r.Form.Get("password")
if password == "" {
http.Error(w, "No password entered", http.StatusInternalServerError)
}
@ -34,6 +44,7 @@ func NewUser(w http.ResponseWriter, r *http.Request) {
if result.Username != "" {
http.Error(w, "User already exists", http.StatusInternalServerError)
return
}
@ -46,14 +57,24 @@ func NewUser(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(
err = json.NewEncoder(w).Encode(
internal.Response{
Username: username,
HTTPResponse: http.StatusCreated,
})
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
}
func autoUname() string {
rand.Seed(time.Now().UnixNano())
return fmt.Sprintf("user%d", rand.Intn(99999-00000))
n, err := rand.Int(rand.Reader, big.NewInt(1000))
if err != nil {
return ""
}
return fmt.Sprintf("user%d", n.Int64())
}

View file

@ -6,6 +6,7 @@ import (
"gorm.io/gorm"
)
// InitDb is the init function for the database.
func InitDb() *gorm.DB {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
@ -13,7 +14,10 @@ func InitDb() *gorm.DB {
}
// Migrate the schema
db.AutoMigrate(&internal.User{})
err = db.AutoMigrate(&internal.User{})
if err != nil {
panic("failed to run DB migration")
}
return db
}

View file

@ -1,5 +1,6 @@
package util
// ValidateQuery does nothing.
func ValidateQuery(request string) (string, error) {
return "", nil
}

View file

@ -1,15 +1,15 @@
package internal
import (
"errors"
"fmt"
"log"
"net/http"
"strings"
"github.com/miekg/dns"
"gorm.io/gorm"
)
// ZoneRequest represents a Zone file request.
type ZoneRequest struct {
*Zone
@ -17,6 +17,7 @@ type ZoneRequest struct {
RequestID string `json:"id"`
}
// ZoneResponse represents a Zone file request response.
type ZoneResponse struct {
*Zone
@ -24,12 +25,14 @@ type ZoneResponse struct {
Elapsed int64 `json:"elapsed"`
}
// Zone struct represents a zonefile.
type Zone struct {
ID string `json:"id"`
UserID string `json:"user_id"`
UserID string `json:"user_id"` //nolint: tagliatelle
Body string `json:"body"`
}
// User struct represents a user.
type User struct {
gorm.Model
Username string
@ -37,55 +40,24 @@ type User struct {
HashedPassword string
}
// Response struct represents a json response.
type Response struct {
Username string `json:"username,omitempty"`
Message string `json:"message,omitempty"`
HTTPResponse int `json:"status,omitempty"`
}
// Parse will be used to parse zonefiles.
func (zone *ZoneRequest) Parse() error {
zp := dns.NewZoneParser(strings.NewReader(zone.Body), "", "")
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
log.Println(rr)
}
if err := zp.Err(); err != nil {
log.Println(err)
return errors.New("unable to parse Zonefile")
}
return nil
}
func (zone *ZoneRequest) Bind(r *http.Request) error {
if zone.Zone == nil {
return errors.New("missing required zone file fields")
}
zone.Zone.Body = strings.ToLower(zone.Zone.Body)
return nil
}
func NewZoneResponse(zone *Zone) *ZoneResponse {
resp := &ZoneResponse{Zone: zone}
if resp.User == nil {
if user, _ := dbGetUser(resp.UserID); user != nil {
resp.User = NewUserPayloadResponse(user)
}
return fmt.Errorf("unable to parse Zonefile: %w", err)
}
return resp
}
func NewUserPayloadResponse(user *User) *User {
return &User{Username: user.Username}
}
func dbGetUser(s string) (*User, error) {
return &User{Username: "user14651"}, nil
}
func (rd *ZoneResponse) Render(w http.ResponseWriter, r *http.Request) error {
// Pre-processing before a response is marshalled and sent across the wire
rd.Elapsed = 10
return nil
}