Compare commits
	
		
			5 Commits
		
	
	
		
			v0.3.4
			...
			8e88f09709
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8e88f09709 | |||
| d44801b0ae | |||
| a4bf701ac3 | |||
| 99bddcd03f | |||
| 6fdd55def8 | 
@@ -2,8 +2,8 @@ pipeline:
 | 
				
			|||||||
  test:
 | 
					  test:
 | 
				
			||||||
    image: golang:latest
 | 
					    image: golang:latest
 | 
				
			||||||
    commands:
 | 
					    commands:
 | 
				
			||||||
      - go build ./cmd/client/client.go
 | 
					      - go build -o gpaste-client ./cmd/client/client.go
 | 
				
			||||||
      - go build ./cmd/server/server.go
 | 
					      - go build -o gpaste-server ./cmd/server/server.go
 | 
				
			||||||
      - go test -v ./...
 | 
					      - go test -v ./...
 | 
				
			||||||
      - go vet ./...
 | 
					      - go vet ./...
 | 
				
			||||||
    when:
 | 
					    when:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										28
									
								
								api/http.go
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								api/http.go
									
									
									
									
									
								
							@@ -119,13 +119,8 @@ func (s *HTTPServer) HandlerAPIFileGet(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (s *HTTPServer) processMultiPartFormUpload(w http.ResponseWriter, r *http.Request) {
 | 
					func (s *HTTPServer) processMultiPartFormUpload(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	reqID := middleware.GetReqID(r.Context())
 | 
						reqID := middleware.GetReqID(r.Context())
 | 
				
			||||||
	type resp struct {
 | 
					 | 
				
			||||||
		Message string `json:"message"`
 | 
					 | 
				
			||||||
		ID      string `json:"id"`
 | 
					 | 
				
			||||||
		URL     string `json:"url"`
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var responses []resp
 | 
						var responses []ResponseAPIFilePost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := r.ParseMultipartForm(1024 * 1024 * 10); err != nil {
 | 
						if err := r.ParseMultipartForm(1024 * 1024 * 10); err != nil {
 | 
				
			||||||
		s.Logger.Warnw("Error parsing multipart form.", "req_id", reqID, "err", err)
 | 
							s.Logger.Warnw("Error parsing multipart form.", "req_id", reqID, "err", err)
 | 
				
			||||||
@@ -149,7 +144,7 @@ func (s *HTTPServer) processMultiPartFormUpload(w http.ResponseWriter, r *http.R
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		s.Logger.Infow("Stored file.", "req_id", reqID, "id", f.ID, "filename", f.OriginalFilename, "remote_addr", r.RemoteAddr)
 | 
							s.Logger.Infow("Stored file.", "req_id", reqID, "id", f.ID, "filename", f.OriginalFilename, "remote_addr", r.RemoteAddr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		responses = append(responses, resp{Message: "OK", ID: f.ID, URL: "TODO"})
 | 
							responses = append(responses, ResponseAPIFilePost{Message: "OK", ID: f.ID, URL: "TODO"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -162,10 +157,7 @@ func (s *HTTPServer) processMultiPartFormUpload(w http.ResponseWriter, r *http.R
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (s *HTTPServer) HandlerAPILogin(w http.ResponseWriter, r *http.Request) {
 | 
					func (s *HTTPServer) HandlerAPILogin(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	reqID := middleware.GetReqID(r.Context())
 | 
						reqID := middleware.GetReqID(r.Context())
 | 
				
			||||||
	expectedRequest := struct {
 | 
						var expectedRequest RequestAPILogin
 | 
				
			||||||
		Username string `json:"username"`
 | 
					 | 
				
			||||||
		Password string `json:"password"`
 | 
					 | 
				
			||||||
	}{}
 | 
					 | 
				
			||||||
	decoder := json.NewDecoder(r.Body)
 | 
						decoder := json.NewDecoder(r.Body)
 | 
				
			||||||
	defer r.Body.Close()
 | 
						defer r.Body.Close()
 | 
				
			||||||
	if err := decoder.Decode(&expectedRequest); err != nil {
 | 
						if err := decoder.Decode(&expectedRequest); err != nil {
 | 
				
			||||||
@@ -179,9 +171,7 @@ func (s *HTTPServer) HandlerAPILogin(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	response := struct {
 | 
						response := ResponseAPILogin{
 | 
				
			||||||
		Token string `json:"token"`
 | 
					 | 
				
			||||||
	}{
 | 
					 | 
				
			||||||
		Token: token,
 | 
							Token: token,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -193,17 +183,12 @@ func (s *HTTPServer) HandlerAPILogin(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type RequestAPIUserCreate struct {
 | 
					 | 
				
			||||||
	Username string `json:"username"`
 | 
					 | 
				
			||||||
	Password string `json:"password"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *HTTPServer) HandlerAPIUserCreate(w http.ResponseWriter, r *http.Request) {
 | 
					func (s *HTTPServer) HandlerAPIUserCreate(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	reqID := middleware.GetReqID(r.Context())
 | 
						reqID := middleware.GetReqID(r.Context())
 | 
				
			||||||
	defer r.Body.Close()
 | 
						defer r.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	level, err := AuthLevelFromRequest(r)
 | 
						role, err := RoleFromRequest(r)
 | 
				
			||||||
	if err != nil || level < gpaste.AuthLevelAdmin {
 | 
						if err != nil || role != users.RoleAdmin {
 | 
				
			||||||
		w.WriteHeader(http.StatusUnauthorized)
 | 
							w.WriteHeader(http.StatusUnauthorized)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -229,5 +214,6 @@ func (s *HTTPServer) HandlerAPIUserCreate(w http.ResponseWriter, r *http.Request
 | 
				
			|||||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
							w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						w.WriteHeader(http.StatusAccepted)
 | 
				
			||||||
	s.Logger.Infow("Created user.", "req_id", reqID, "remote_addr", r.RemoteAddr, "username", req.Username)
 | 
						s.Logger.Infow("Created user.", "req_id", reqID, "remote_addr", r.RemoteAddr, "username", req.Username)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										20
									
								
								api/json.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/json.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					package api
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RequestAPIUserCreate struct {
 | 
				
			||||||
 | 
						Username string `json:"username"`
 | 
				
			||||||
 | 
						Password string `json:"password"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RequestAPILogin struct {
 | 
				
			||||||
 | 
						Username string `json:"username"`
 | 
				
			||||||
 | 
						Password string `json:"password"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ResponseAPILogin struct {
 | 
				
			||||||
 | 
						Token string `json:"token"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type ResponseAPIFilePost struct {
 | 
				
			||||||
 | 
						Message string `json:"message"`
 | 
				
			||||||
 | 
						ID      string `json:"id"`
 | 
				
			||||||
 | 
						URL     string `json:"url"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -8,6 +8,7 @@ import (
 | 
				
			|||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.t-juice.club/torjus/gpaste"
 | 
						"git.t-juice.club/torjus/gpaste"
 | 
				
			||||||
 | 
						"git.t-juice.club/torjus/gpaste/users"
 | 
				
			||||||
	"github.com/go-chi/chi/v5/middleware"
 | 
						"github.com/go-chi/chi/v5/middleware"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -16,6 +17,7 @@ type authCtxKey int
 | 
				
			|||||||
const (
 | 
					const (
 | 
				
			||||||
	authCtxUsername authCtxKey = iota
 | 
						authCtxUsername authCtxKey = iota
 | 
				
			||||||
	authCtxAuthLevel
 | 
						authCtxAuthLevel
 | 
				
			||||||
 | 
						authCtxClaims
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *HTTPServer) MiddlewareAccessLogger(next http.Handler) http.Handler {
 | 
					func (s *HTTPServer) MiddlewareAccessLogger(next http.Handler) http.Handler {
 | 
				
			||||||
@@ -66,7 +68,8 @@ func (s *HTTPServer) MiddlewareAuthentication(next http.Handler) http.Handler {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx := context.WithValue(r.Context(), authCtxUsername, claims.Subject)
 | 
							ctx := context.WithValue(r.Context(), authCtxUsername, claims.Subject)
 | 
				
			||||||
		ctx = context.WithValue(ctx, authCtxAuthLevel, gpaste.AuthLevelUser)
 | 
							ctx = context.WithValue(ctx, authCtxAuthLevel, claims.Role)
 | 
				
			||||||
 | 
							ctx = context.WithValue(ctx, authCtxClaims, claims)
 | 
				
			||||||
		withCtx := r.WithContext(ctx)
 | 
							withCtx := r.WithContext(ctx)
 | 
				
			||||||
		s.Logger.Debugw("Request is authenticated.", "req_id", reqID, "username", claims.Subject)
 | 
							s.Logger.Debugw("Request is authenticated.", "req_id", reqID, "username", claims.Subject)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -79,7 +82,6 @@ func (s *HTTPServer) MiddlewareAuthentication(next http.Handler) http.Handler {
 | 
				
			|||||||
func UsernameFromRequest(r *http.Request) (string, error) {
 | 
					func UsernameFromRequest(r *http.Request) (string, error) {
 | 
				
			||||||
	rawUsername := r.Context().Value(authCtxUsername)
 | 
						rawUsername := r.Context().Value(authCtxUsername)
 | 
				
			||||||
	if rawUsername == nil {
 | 
						if rawUsername == nil {
 | 
				
			||||||
 | 
					 | 
				
			||||||
		return "", fmt.Errorf("no username")
 | 
							return "", fmt.Errorf("no username")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	username, ok := rawUsername.(string)
 | 
						username, ok := rawUsername.(string)
 | 
				
			||||||
@@ -89,14 +91,26 @@ func UsernameFromRequest(r *http.Request) (string, error) {
 | 
				
			|||||||
	return username, nil
 | 
						return username, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func AuthLevelFromRequest(r *http.Request) (gpaste.AuthLevel, error) {
 | 
					func RoleFromRequest(r *http.Request) (users.Role, error) {
 | 
				
			||||||
	rawLevel := r.Context().Value(authCtxAuthLevel)
 | 
						rawLevel := r.Context().Value(authCtxAuthLevel)
 | 
				
			||||||
	if rawLevel == nil {
 | 
						if rawLevel == nil {
 | 
				
			||||||
		return gpaste.AuthLevelUnset, fmt.Errorf("no username")
 | 
							return users.RoleUnset, fmt.Errorf("no username")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	level, ok := rawLevel.(gpaste.AuthLevel)
 | 
						level, ok := rawLevel.(users.Role)
 | 
				
			||||||
	if !ok {
 | 
						if !ok {
 | 
				
			||||||
		return gpaste.AuthLevelUnset, fmt.Errorf("no username")
 | 
							return users.RoleUnset, fmt.Errorf("no username")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return level, nil
 | 
						return level, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ClaimsFromRequest(r *http.Request) *gpaste.Claims {
 | 
				
			||||||
 | 
						rawClaims := r.Context().Value(authCtxAuthLevel)
 | 
				
			||||||
 | 
						if rawClaims == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						claims, ok := rawClaims.(*gpaste.Claims)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return claims
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										32
									
								
								auth.go
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								auth.go
									
									
									
									
									
								
							@@ -9,19 +9,17 @@ import (
 | 
				
			|||||||
	"github.com/google/uuid"
 | 
						"github.com/google/uuid"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type AuthLevel int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	AuthLevelUnset AuthLevel = iota
 | 
					 | 
				
			||||||
	AuthLevelUser
 | 
					 | 
				
			||||||
	AuthLevelAdmin
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type AuthService struct {
 | 
					type AuthService struct {
 | 
				
			||||||
	users      users.UserStore
 | 
						users      users.UserStore
 | 
				
			||||||
	hmacSecret []byte
 | 
						hmacSecret []byte
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Claims struct {
 | 
				
			||||||
 | 
						Role users.Role `json:"role,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						jwt.StandardClaims
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewAuthService(store users.UserStore, signingSecret []byte) *AuthService {
 | 
					func NewAuthService(store users.UserStore, signingSecret []byte) *AuthService {
 | 
				
			||||||
	return &AuthService{users: store, hmacSecret: signingSecret}
 | 
						return &AuthService{users: store, hmacSecret: signingSecret}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -37,13 +35,13 @@ func (as *AuthService) Login(username, password string) (string, error) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// TODO: Set iss and aud
 | 
						// TODO: Set iss and aud
 | 
				
			||||||
	claims := jwt.StandardClaims{
 | 
						claims := new(Claims)
 | 
				
			||||||
		Subject:   user.Username,
 | 
						claims.Subject = user.Username
 | 
				
			||||||
		ExpiresAt: time.Now().Add(7 * 24 * time.Hour).Unix(),
 | 
						claims.ExpiresAt = time.Now().Add(7 * 24 * time.Hour).Unix()
 | 
				
			||||||
		NotBefore: time.Now().Unix(),
 | 
						claims.NotBefore = time.Now().Unix()
 | 
				
			||||||
		IssuedAt:  time.Now().Unix(),
 | 
						claims.IssuedAt = time.Now().Unix()
 | 
				
			||||||
		Id:        uuid.NewString(),
 | 
						claims.Id = uuid.NewString()
 | 
				
			||||||
	}
 | 
						claims.Role = user.Role
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), claims)
 | 
						token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), claims)
 | 
				
			||||||
	signed, err := token.SignedString(as.hmacSecret)
 | 
						signed, err := token.SignedString(as.hmacSecret)
 | 
				
			||||||
@@ -54,8 +52,8 @@ func (as *AuthService) Login(username, password string) (string, error) {
 | 
				
			|||||||
	return signed, nil
 | 
						return signed, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (as *AuthService) ValidateToken(rawToken string) (*jwt.StandardClaims, error) {
 | 
					func (as *AuthService) ValidateToken(rawToken string) (*Claims, error) {
 | 
				
			||||||
	claims := &jwt.StandardClaims{}
 | 
						claims := &Claims{}
 | 
				
			||||||
	token, err := jwt.ParseWithClaims(rawToken, claims, func(t *jwt.Token) (interface{}, error) {
 | 
						token, err := jwt.ParseWithClaims(rawToken, claims, func(t *jwt.Token) (interface{}, error) {
 | 
				
			||||||
		return as.hmacSecret, nil
 | 
							return as.hmacSecret, nil
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	"git.t-juice.club/torjus/gpaste"
 | 
						"git.t-juice.club/torjus/gpaste"
 | 
				
			||||||
	"git.t-juice.club/torjus/gpaste/users"
 | 
						"git.t-juice.club/torjus/gpaste/users"
 | 
				
			||||||
 | 
						"github.com/google/go-cmp/cmp"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestAuth(t *testing.T) {
 | 
					func TestAuth(t *testing.T) {
 | 
				
			||||||
@@ -17,7 +18,7 @@ func TestAuth(t *testing.T) {
 | 
				
			|||||||
		username := randomString(8)
 | 
							username := randomString(8)
 | 
				
			||||||
		password := randomString(16)
 | 
							password := randomString(16)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		user := &users.User{Username: username}
 | 
							user := &users.User{Username: username, Role: users.RoleAdmin}
 | 
				
			||||||
		if err := user.SetPassword(password); err != nil {
 | 
							if err := user.SetPassword(password); err != nil {
 | 
				
			||||||
			t.Fatalf("error setting user password: %s", err)
 | 
								t.Fatalf("error setting user password: %s", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -30,9 +31,13 @@ func TestAuth(t *testing.T) {
 | 
				
			|||||||
			t.Fatalf("Error creating token: %s", err)
 | 
								t.Fatalf("Error creating token: %s", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if _, err := as.ValidateToken(token); err != nil {
 | 
							claims, err := as.ValidateToken(token)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
			t.Fatalf("Error validating token: %s", err)
 | 
								t.Fatalf("Error validating token: %s", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							if claims.Role != user.Role {
 | 
				
			||||||
 | 
								t.Fatalf("Token role is not correct: %s", cmp.Diff(claims.Role, user.Role))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		invalidToken := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDMyMjk3NjMsImp0aSI6ImUzNDk5NWI1LThiZmMtNDQyNy1iZDgxLWFmNmQ3OTRiYzM0YiIsImlhdCI6MTY0MjYyNDk2MywibmJmIjoxNjQyNjI0OTYzLCJzdWIiOiJYdE5Hemt5ZSJ9.VM6dkwSLaBv8cStkWRVVv9ADjdUrHGHrlB7GB7Ly7n8`
 | 
							invalidToken := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDMyMjk3NjMsImp0aSI6ImUzNDk5NWI1LThiZmMtNDQyNy1iZDgxLWFmNmQ3OTRiYzM0YiIsImlhdCI6MTY0MjYyNDk2MywibmJmIjoxNjQyNjI0OTYzLCJzdWIiOiJYdE5Hemt5ZSJ9.VM6dkwSLaBv8cStkWRVVv9ADjdUrHGHrlB7GB7Ly7n8`
 | 
				
			||||||
		if _, err := as.ValidateToken(invalidToken); err == nil {
 | 
							if _, err := as.ValidateToken(invalidToken); err == nil {
 | 
				
			||||||
			t.Fatalf("Invalid token passed validation")
 | 
								t.Fatalf("Invalid token passed validation")
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										167
									
								
								client/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								client/client.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,167 @@
 | 
				
			|||||||
 | 
					package client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"mime/multipart"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.t-juice.club/torjus/gpaste/api"
 | 
				
			||||||
 | 
						"git.t-juice.club/torjus/gpaste/files"
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Client struct {
 | 
				
			||||||
 | 
						BaseURL   string
 | 
				
			||||||
 | 
						AuthToken string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						httpClient http.Client
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *Client) Login(ctx context.Context, username, password string) error {
 | 
				
			||||||
 | 
						url := fmt.Sprintf("%s/api/login", c.BaseURL)
 | 
				
			||||||
 | 
						// TODO: Change timeout
 | 
				
			||||||
 | 
						ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
 | 
				
			||||||
 | 
						defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						body := new(bytes.Buffer)
 | 
				
			||||||
 | 
						requestData := api.RequestAPILogin{
 | 
				
			||||||
 | 
							Username: username,
 | 
				
			||||||
 | 
							Password: password,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						encoder := json.NewEncoder(body)
 | 
				
			||||||
 | 
						if err := encoder.Encode(&requestData); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("error encoding response: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("error creating request: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp, err := c.httpClient.Do(req)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("unable to perform request: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if resp.StatusCode != http.StatusOK {
 | 
				
			||||||
 | 
							return fmt.Errorf("got non-ok response from server: %s", resp.Status)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var responseData api.ResponseAPILogin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						decoder := json.NewDecoder(resp.Body)
 | 
				
			||||||
 | 
						if err := decoder.Decode(&responseData); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("unable to parse response: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.AuthToken = responseData.Token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *Client) UserCreate(ctx context.Context, username, password string) error {
 | 
				
			||||||
 | 
						url := fmt.Sprintf("%s/api/user", c.BaseURL)
 | 
				
			||||||
 | 
						body := new(bytes.Buffer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requestData := &api.RequestAPIUserCreate{
 | 
				
			||||||
 | 
							Username: username,
 | 
				
			||||||
 | 
							Password: password,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						encoder := json.NewEncoder(body)
 | 
				
			||||||
 | 
						if err := encoder.Encode(requestData); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("error encoding response: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("error creating request: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.AuthToken))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp, err := c.httpClient.Do(req)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("unable to perform request: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if resp.StatusCode != http.StatusAccepted {
 | 
				
			||||||
 | 
							return fmt.Errorf("got non-ok response from server: %s", resp.Status)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *Client) Download(ctx context.Context, id string) (io.ReadCloser, error) {
 | 
				
			||||||
 | 
						url := fmt.Sprintf("%s/api/file/%s", c.BaseURL, id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error creating request: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.AuthToken))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp, err := c.httpClient.Do(req)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("unable to perform request: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if resp.StatusCode != http.StatusOK {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("got non-ok response from server: %s", resp.Status)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return resp.Body, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *Client) Upload(ctx context.Context, files ...*files.File) ([]api.ResponseAPIFilePost, error) {
 | 
				
			||||||
 | 
						url := fmt.Sprintf("%s/api/file", c.BaseURL)
 | 
				
			||||||
 | 
						client := &http.Client{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: Change timeout
 | 
				
			||||||
 | 
						ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
 | 
				
			||||||
 | 
						defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: Improve buffering
 | 
				
			||||||
 | 
						buf := &bytes.Buffer{}
 | 
				
			||||||
 | 
						mw := multipart.NewWriter(buf)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, file := range files {
 | 
				
			||||||
 | 
							fw, err := mw.CreateFormFile(uuid.Must(uuid.NewRandom()).String(), file.OriginalFilename)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if _, err := io.Copy(fw, file.Body); err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							file.Body.Close()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						mw.Close()
 | 
				
			||||||
 | 
						req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, buf)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						req.Header.Add("Content-Type", mw.FormDataContentType())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp, err := client.Do(req)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var expectedResp []api.ResponseAPIFilePost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						decoder := json.NewDecoder(resp.Body)
 | 
				
			||||||
 | 
						if err := decoder.Decode(&expectedResp); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error decoding response: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, r := range expectedResp {
 | 
				
			||||||
 | 
							fmt.Printf("Uploaded file %s\n", r.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return expectedResp, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										165
									
								
								client/client_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								client/client_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,165 @@
 | 
				
			|||||||
 | 
					package client_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.t-juice.club/torjus/gpaste"
 | 
				
			||||||
 | 
						"git.t-juice.club/torjus/gpaste/api"
 | 
				
			||||||
 | 
						"git.t-juice.club/torjus/gpaste/client"
 | 
				
			||||||
 | 
						"git.t-juice.club/torjus/gpaste/files"
 | 
				
			||||||
 | 
						"git.t-juice.club/torjus/gpaste/users"
 | 
				
			||||||
 | 
						"github.com/google/go-cmp/cmp"
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestClient(t *testing.T) {
 | 
				
			||||||
 | 
						listener, err := net.Listen("tcp", ":0")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						port := listener.Addr().(*net.TCPAddr).Port
 | 
				
			||||||
 | 
						cfg := &gpaste.ServerConfig{
 | 
				
			||||||
 | 
							LogLevel:      "ERROR",
 | 
				
			||||||
 | 
							URL:           fmt.Sprintf("http://localhost:%d", port),
 | 
				
			||||||
 | 
							SigningSecret: "TEST",
 | 
				
			||||||
 | 
							Store:         &gpaste.ServerStoreConfig{Type: "memory"},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						srv := api.NewHTTPServer(cfg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							srv.Serve(listener)
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Cleanup(func() {
 | 
				
			||||||
 | 
							ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 | 
				
			||||||
 | 
							defer cancel()
 | 
				
			||||||
 | 
							srv.Shutdown(ctx)
 | 
				
			||||||
 | 
							listener.Close()
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add users
 | 
				
			||||||
 | 
						username := "admin"
 | 
				
			||||||
 | 
						password := "admin"
 | 
				
			||||||
 | 
						user := &users.User{
 | 
				
			||||||
 | 
							Username: username,
 | 
				
			||||||
 | 
							Role:     users.RoleAdmin,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := user.SetPassword(password); err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Error setting password: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := srv.Users.Store(user); err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Error storing user: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Login", func(t *testing.T) {
 | 
				
			||||||
 | 
							client := client.Client{BaseURL: cfg.URL}
 | 
				
			||||||
 | 
							ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
 | 
							defer cancel()
 | 
				
			||||||
 | 
							if err := client.Login(ctx, username, password); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Error logging in: %s", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							claims, err := srv.Auth.ValidateToken(client.AuthToken)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Errorf("unable to get claims from token: %s", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if claims.Role != user.Role {
 | 
				
			||||||
 | 
								t.Errorf("Claims have wrong role: %s", cmp.Diff(claims.Role, user.Role))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if claims.Subject != username {
 | 
				
			||||||
 | 
								t.Errorf("Claims have wrong role: %s", cmp.Diff(claims.Subject, username))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("UserCreate", func(t *testing.T) {
 | 
				
			||||||
 | 
								ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
 | 
								defer cancel()
 | 
				
			||||||
 | 
								username := "user"
 | 
				
			||||||
 | 
								password := "user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err := client.UserCreate(ctx, username, password); err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("Error creating user: %s", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								user, err := srv.Users.Get(username)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("Error getting new user: %s", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if user.Username != username {
 | 
				
			||||||
 | 
									t.Errorf("Username does not match.")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := user.ValidatePassword(password); err != nil {
 | 
				
			||||||
 | 
									t.Errorf("Unable to validate password: %s", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						t.Run("Upload", func(t *testing.T) {
 | 
				
			||||||
 | 
							client := client.Client{BaseURL: cfg.URL}
 | 
				
			||||||
 | 
							ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
 | 
							defer cancel()
 | 
				
			||||||
 | 
							fileContents := "this is the test file"
 | 
				
			||||||
 | 
							fileBody := io.NopCloser(strings.NewReader(fileContents))
 | 
				
			||||||
 | 
							file := &files.File{
 | 
				
			||||||
 | 
								OriginalFilename: "filename.txt",
 | 
				
			||||||
 | 
								Body:             fileBody,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							resp, err := client.Upload(ctx, file)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Error uploading: %s", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							retrieved, err := srv.Files.Get(resp[0].ID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Error getting uploaded file from store: %s", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							defer retrieved.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							buf := new(strings.Builder)
 | 
				
			||||||
 | 
							if _, err := io.Copy(buf, retrieved.Body); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("error reading body from store: %s", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if buf.String() != fileContents {
 | 
				
			||||||
 | 
								t.Errorf("File contents does not match: %s", cmp.Diff(buf.String(), fileContents))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						t.Run("Download", func(t *testing.T) {
 | 
				
			||||||
 | 
							client := client.Client{BaseURL: cfg.URL}
 | 
				
			||||||
 | 
							ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
 | 
							defer cancel()
 | 
				
			||||||
 | 
							fileContents := "this is the test file"
 | 
				
			||||||
 | 
							fileBody := io.NopCloser(strings.NewReader(fileContents))
 | 
				
			||||||
 | 
							file := &files.File{
 | 
				
			||||||
 | 
								ID:               uuid.NewString(),
 | 
				
			||||||
 | 
								OriginalFilename: "filename.txt",
 | 
				
			||||||
 | 
								Body:             fileBody,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := srv.Files.Store(file); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Error putting file in store: %s", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							body, err := client.Download(ctx, file.ID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Error uploading: %s", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							defer body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							buf := new(strings.Builder)
 | 
				
			||||||
 | 
							if _, err := io.Copy(buf, body); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("error reading body from store: %s", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if buf.String() != fileContents {
 | 
				
			||||||
 | 
								t.Errorf("File contents does not match: %s", cmp.Diff(buf.String(), fileContents))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -13,7 +13,7 @@ const (
 | 
				
			|||||||
type User struct {
 | 
					type User struct {
 | 
				
			||||||
	Username       string `json:"username"`
 | 
						Username       string `json:"username"`
 | 
				
			||||||
	HashedPassword []byte `json:"hashed_password"`
 | 
						HashedPassword []byte `json:"hashed_password"`
 | 
				
			||||||
	Roles          []Role `json:"roles"`
 | 
						Role           Role   `json:"role"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type UserStore interface {
 | 
					type UserStore interface {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,7 +20,7 @@ func RunUserStoreTest(newFunc func() (func(), users.UserStore), t *testing.T) {
 | 
				
			|||||||
			passwordMap[username] = password
 | 
								passwordMap[username] = password
 | 
				
			||||||
			user := &users.User{
 | 
								user := &users.User{
 | 
				
			||||||
				Username: username,
 | 
									Username: username,
 | 
				
			||||||
				Roles:    []users.Role{users.RoleAdmin},
 | 
									Role:     users.RoleAdmin,
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if err := user.SetPassword(password); err != nil {
 | 
								if err := user.SetPassword(password); err != nil {
 | 
				
			||||||
				t.Fatalf("Error setting password: %s", err)
 | 
									t.Fatalf("Error setting password: %s", err)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user