diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..7705642 --- /dev/null +++ b/.golangci.yaml @@ -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 diff --git a/Makefile b/Makefile index 88d3cfc..133b6e8 100644 --- a/Makefile +++ b/Makefile @@ -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 \ No newline at end of file diff --git a/cmd/pomme/main.go b/cmd/pomme/main.go index 9305028..1fbf913 100644 --- a/cmd/pomme/main.go +++ b/cmd/pomme/main.go @@ -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()) } diff --git a/frontend/embed.go b/frontend/embed.go index 2c592fa..2cf083a 100644 --- a/frontend/embed.go +++ b/frontend/embed.go @@ -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) { diff --git a/internal/api/api.go b/internal/api/api.go index 0ec39be..2e664d4 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -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 + } } diff --git a/internal/api/auth.go b/internal/api/auth.go index 5c19023..3bb4a3e 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -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) } diff --git a/internal/api/jwt.go b/internal/api/jwt.go index 1d147c3..eee1a3f 100644 --- a/internal/api/jwt.go +++ b/internal/api/jwt.go @@ -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 } diff --git a/internal/api/users.go b/internal/api/users.go index 98e16e3..51b6b6d 100644 --- a/internal/api/users.go +++ b/internal/api/users.go @@ -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()) } diff --git a/internal/db/db.go b/internal/db/db.go index d078d72..028e13d 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -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 } diff --git a/internal/util/util.go b/internal/util/util.go index ab47679..393de33 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,5 +1,6 @@ package util +// ValidateQuery does nothing. func ValidateQuery(request string) (string, error) { return "", nil } diff --git a/internal/zone.go b/internal/zone.go index 022c4f4..5293846 100644 --- a/internal/zone.go +++ b/internal/zone.go @@ -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 }