diff --git a/go.mod b/go.mod index d0d77ae..39b4332 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module git.jpi.io/JPI/ranpass -go 1.14 +go 1.25 diff --git a/main.go b/main.go index f1721b1..49d0793 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "errors" "flag" "fmt" "log" @@ -8,6 +9,29 @@ import ( "strconv" ) +const maxPasswordLength = 512 + +// parseParam parses a positive integer form field. If the field is absent or +// empty, defaultVal is returned. If present but invalid or out of [1, max], +// an error is returned. +func parseParam(r *http.Request, name string, defaultVal, max int) (int, error) { + v := r.FormValue(name) + if v == "" { + return defaultVal, nil + } + n, err := strconv.Atoi(v) + if err != nil { + return 0, fmt.Errorf("%s: must be an integer", name) + } + if n <= 0 { + return 0, fmt.Errorf("%s: must be greater than zero", name) + } + if n > max { + return 0, fmt.Errorf("%s: must be at most %d", name, max) + } + return n, nil +} + type passwordData struct { Length int Digits int @@ -17,11 +41,9 @@ type passwordData struct { Password string } -var d, defaults passwordData +var defaults passwordData func generatePassword(w http.ResponseWriter, r *http.Request) { - var err error - if r.URL.Path != "/" { http.Error(w, "404 not found.", http.StatusNotFound) return @@ -29,56 +51,53 @@ func generatePassword(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": - d.Password, err = generate(defaults.Length, defaults.Digits, defaults.Symbols, false, false) + password, err := generate(defaults.Length, defaults.Digits, defaults.Symbols, false, false) if err != nil { - d.Password = "Error: " + err.Error() w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(d.Password)) + w.Write([]byte("Error: " + err.Error())) return } w.WriteHeader(http.StatusOK) - w.Write([]byte(d.Password)) + w.Write([]byte(password)) case "POST": - if err = r.ParseForm(); err != nil { + var d passwordData + + if err := r.ParseForm(); err != nil { w.WriteHeader(http.StatusUnprocessableEntity) w.Write([]byte(fmt.Sprintf("ParseForm() err: %v", err))) return } - d.Length, err = strconv.Atoi(r.FormValue("length")) - if err != nil { - d.Length = defaults.Length - } - d.Digits, err = strconv.Atoi(r.FormValue("digits")) - if err != nil { - d.Digits = defaults.Digits - } + var err error + var validationErr error - d.Symbols, err = strconv.Atoi(r.FormValue("symbols")) - if err != nil { - d.Symbols = defaults.Symbols - } + d.Length, err = parseParam(r, "length", defaults.Length, maxPasswordLength) + validationErr = errors.Join(validationErr, err) - if r.FormValue("noupper") == "on" { - d.NoUpper = true - } + d.Digits, err = parseParam(r, "digits", defaults.Digits, maxPasswordLength) + validationErr = errors.Join(validationErr, err) - if r.FormValue("denyrepeat") == "on" { - d.DenyRepeat = true - } + d.Symbols, err = parseParam(r, "symbols", defaults.Symbols, maxPasswordLength) + validationErr = errors.Join(validationErr, err) - d.Password, err = generate(d.Length, d.Digits, d.Symbols, d.NoUpper, !d.DenyRepeat) - if err != nil { - d.Password = "Error: " + err.Error() - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(d.Password)) + if validationErr != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(validationErr.Error())) return } - if d.Length == 0 { - d.Password = "Error: password can not have zero length" + + d.NoUpper = r.FormValue("noupper") == "on" + d.DenyRepeat = r.FormValue("denyrepeat") == "on" + + var password string + password, err = generate(d.Length, d.Digits, d.Symbols, d.NoUpper, !d.DenyRepeat) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Error: " + err.Error())) + return } w.WriteHeader(http.StatusOK) - w.Write([]byte(d.Password)) + w.Write([]byte(password)) default: w.WriteHeader(http.StatusBadRequest) w.Write([]byte("Sorry, only GET and POST methods are supported."))