package rest import ( "bytes" "context" "encoding/json" "fmt" "io" "log" "net/http" "strings" "sync/atomic" "time" mjson "git.ali33.ru/fcg-xvii/go-tools/json" jwt "github.com/dgrijalva/jwt-go" ) func NewRest(addr string, secret []byte, commands *CommandStore) *Rest { return &Rest{ commands: commands, secret: secret, addr: addr, } } type Rest struct { secret []byte addr string opened atomic.Bool server *http.Server commands *CommandStore } func (s *Rest) TokenGenerate(m mjson.Map, expire int64) (string, error) { token := jwt.New(jwt.SigningMethodHS256) claims := token.Claims.(jwt.MapClaims) for key, val := range m { claims[key] = val } if expire > 0 { claims["exp"] = time.Now().Add(time.Minute * 30).Unix() } tokenString, err := token.SignedString(s.secret) return tokenString, err } // Listen start server in other goroutine func (s *Rest) Listen(timeout time.Duration) (err error) { if s.opened.Swap(true) { return ErrAlreadyOpened } ctx, _ := context.WithTimeout(context.Background(), timeout) go func() { mux := http.NewServeMux() mux.HandleFunc("/", s.handle) s.server = &http.Server{ Addr: s.addr, Handler: mux, } err = s.server.ListenAndServe() s.opened.Store(false) }() <-ctx.Done() return } // Close func (s *Rest) Close() error { if !s.opened.Load() { return ErrNotOpened } return s.server.Close() } func responseNotFound(w http.ResponseWriter) { w.WriteHeader(404) } func responseError(w http.ResponseWriter, err error, code int) { w.WriteHeader(code) w.Write([]byte(err.Error())) } // handle func (s *Rest) handle(w http.ResponseWriter, r *http.Request) { log.Println("handle", r.URL.Path) // Инициализация restRequest rr := &Request{ Request: r, data: mjson.Map{}, files: make(map[string]io.ReadCloser), tokenGenerator: s.TokenGenerate, } // Парсим Bearer токен и извлекаем claims authHeader := r.Header.Get("Authorization") if authHeader != "" { if parts := strings.Split(authHeader, " "); len(parts) == 2 && parts[0] == "Bearer" { tokenString := parts[1] token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return s.secret, nil }) if err != nil { log.Printf("Failed to parse JWT: %s", err) http.Error(w, "Invalid token", http.StatusUnauthorized) return } if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { rr.auth = mjson.Map(claims) } } } // Если это многокомпонентный запрос, обрабатываем файлы if strings.Index(r.Header.Get("Content-Type"), "multipart/form-data") == 0 { err := r.ParseMultipartForm(32 << 20) // max memory 32MB, после этого файлы будут сохранены во временных файлах if err != nil { responseError(w, fmt.Errorf("failed to parse multipart form: %w", err), 500) return } multiPartForm := r.MultipartForm data, check := multiPartForm.Value["data"] if check { err := json.NewDecoder(bytes.NewBuffer([]byte(data[0]))).Decode(&rr.data) if err != nil { responseError(w, fmt.Errorf("failed to decode JSON: %w", err), 500) return } } for filename, headers := range multiPartForm.File { for _, header := range headers { file, err := header.Open() if err != nil { responseError(w, fmt.Errorf("failed to open file %s: %w", filename, err), 500) } rr.files[filename] = file } } defer rr.Close() } else { err := json.NewDecoder(r.Body).Decode(&rr.data) if err != nil { responseError(w, fmt.Errorf("failed to decode JSON: %w", err), 500) return } } // get command command, check := s.commands.GetCommand(r.URL.Path) if !check { responseNotFound(w) return } // validate resp := command.Validate(rr) if resp != nil { if err := resp.Send(w); err != nil { responseError(w, err, 500) } return } // execute resp = command.Execute(rr) if err := resp.Send(w); err != nil { responseError(w, err, 500) } resp.Close() }