Add api package
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2022-01-20 03:44:33 +01:00
parent faa3cc102f
commit e7c5a672ff
5 changed files with 21 additions and 17 deletions

233
api/http.go Normal file
View File

@@ -0,0 +1,233 @@
package api
import (
"encoding/json"
"io"
"net/http"
"strings"
"git.t-juice.club/torjus/gpaste"
"git.t-juice.club/torjus/gpaste/files"
"git.t-juice.club/torjus/gpaste/users"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/google/uuid"
"go.uber.org/zap"
)
type HTTPServer struct {
Files files.FileStore
Users users.UserStore
Auth *gpaste.AuthService
config *gpaste.ServerConfig
Logger *zap.SugaredLogger
AccessLogger *zap.SugaredLogger
http.Server
}
func NewHTTPServer(cfg *gpaste.ServerConfig) *HTTPServer {
srv := &HTTPServer{
config: cfg,
Logger: zap.NewNop().Sugar(),
AccessLogger: zap.NewNop().Sugar(),
}
srv.Files = files.NewMemoryFileStore()
srv.Users = users.NewMemoryUserStore()
srv.Auth = gpaste.NewAuthService(srv.Users, []byte(srv.config.SigningSecret))
// Create initial user
// TODO: Do properly
user := &users.User{Username: "admin"}
user.SetPassword("admin")
srv.Users.Store(user)
r := chi.NewRouter()
r.Use(middleware.RealIP)
r.Use(middleware.RequestID)
r.Use(srv.MiddlewareAccessLogger)
r.Use(srv.MiddlewareAuthentication)
r.Get("/", srv.HandlerIndex)
r.Post("/api/file", srv.HandlerAPIFilePost)
r.Get("/api/file/{id}", srv.HandlerAPIFileGet)
r.Post("/api/login", srv.HandlerAPILogin)
r.Post("/api/user", srv.HandlerAPIUserCreate)
srv.Handler = r
return srv
}
func (s *HTTPServer) HandlerIndex(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("index"))
}
func (s *HTTPServer) HandlerAPIFilePost(w http.ResponseWriter, r *http.Request) {
f := &files.File{
ID: uuid.Must(uuid.NewRandom()).String(),
Body: r.Body,
}
reqID := middleware.GetReqID(r.Context())
// Check if multipart form
ct := r.Header.Get("Content-Type")
if strings.Contains(ct, "multipart/form-data") {
s.processMultiPartFormUpload(w, r)
return
}
err := s.Files.Store(f)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
s.Logger.Warnw("Error storing file.", "req_id", reqID, "error", err, "id", f.ID, "remote_addr", r.RemoteAddr)
return
}
s.Logger.Infow("Stored file.", "req_id", reqID, "id", f.ID, "remote_addr", r.RemoteAddr)
var resp = struct {
Message string `json:"message"`
ID string `json:"id"`
URL string `json:"url"`
}{
Message: "OK",
ID: f.ID,
URL: "TODO",
}
w.WriteHeader(http.StatusAccepted)
encoder := json.NewEncoder(w)
if err := encoder.Encode(&resp); err != nil {
s.Logger.Warnw("Error encoding response to client.", "req_id", reqID, "error", err, "remote_addr", r.RemoteAddr)
}
}
func (s *HTTPServer) HandlerAPIFileGet(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if id == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
f, err := s.Files.Get(id)
if err != nil {
// TODO: LOG
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
if _, err := io.Copy(w, f.Body); err != nil {
reqID := middleware.GetReqID(r.Context())
s.Logger.Warnw("Error writing file to client.", "req_id", reqID, "error", err, "remote_addr", r.RemoteAddr)
}
}
func (s *HTTPServer) processMultiPartFormUpload(w http.ResponseWriter, r *http.Request) {
reqID := middleware.GetReqID(r.Context())
type resp struct {
Message string `json:"message"`
ID string `json:"id"`
URL string `json:"url"`
}
var responses []resp
if err := r.ParseMultipartForm(1024 * 1024 * 10); err != nil {
s.Logger.Warnw("Error parsing multipart form.", "req_id", reqID, "err", err)
}
for k := range r.MultipartForm.File {
ff, fh, err := r.FormFile(k)
if err != nil {
s.Logger.Warnw("Error reading file from multipart form.", "req_id", reqID, "error", err)
return
}
f := &files.File{
ID: uuid.Must(uuid.NewRandom()).String(),
OriginalFilename: fh.Filename,
Body: ff,
}
if err := s.Files.Store(f); err != nil {
w.WriteHeader(http.StatusInternalServerError)
s.Logger.Warnw("Error storing file.", "req_id", reqID, "error", err, "id", f.ID, "remote_addr", r.RemoteAddr)
return
}
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"})
}
w.WriteHeader(http.StatusAccepted)
encoder := json.NewEncoder(w)
if err := encoder.Encode(&responses); err != nil {
s.Logger.Warnw("Error encoding response to client.", "req_id", reqID, "error", err, "remote_addr", r.RemoteAddr)
}
}
func (s *HTTPServer) HandlerAPILogin(w http.ResponseWriter, r *http.Request) {
reqID := middleware.GetReqID(r.Context())
expectedRequest := struct {
Username string `json:"username"`
Password string `json:"password"`
}{}
decoder := json.NewDecoder(r.Body)
defer r.Body.Close()
if err := decoder.Decode(&expectedRequest); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
token, err := s.Auth.Login(expectedRequest.Username, expectedRequest.Password)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
response := struct {
Token string `json:"token"`
}{
Token: token,
}
s.Logger.Infow("User logged in.", "req_id", reqID, "username", expectedRequest.Username)
encoder := json.NewEncoder(w)
if err := encoder.Encode(&response); err != nil {
s.Logger.Infow("Error encoding json response to client.", "req_id", reqID, "error", err, "remote_addr", r.RemoteAddr)
}
}
type RequestAPIUserCreate struct {
Username string `json:"username"`
Password string `json:"password"`
}
func (s *HTTPServer) HandlerAPIUserCreate(w http.ResponseWriter, r *http.Request) {
reqID := middleware.GetReqID(r.Context())
defer r.Body.Close()
level, err := AuthLevelFromRequest(r)
if err != nil || level < gpaste.AuthLevelAdmin {
w.WriteHeader(http.StatusUnauthorized)
return
}
var req RequestAPIUserCreate
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&req); err != nil {
s.Logger.Debugw("Error parsing request.", "req_id", reqID, "error", err, "remote_addr", r.RemoteAddr)
w.WriteHeader(http.StatusBadRequest)
return
}
// TODO: Ensure user does not already exist
user := &users.User{Username: req.Username}
if err := user.SetPassword(req.Password); err != nil {
s.Logger.Warnw("Error setting user password.", "req_id", reqID, "error", err, "remote_addr", r.RemoteAddr)
w.WriteHeader(http.StatusBadRequest)
return
}
if err := s.Users.Store(user); err != nil {
s.Logger.Warnw("Error setting user password.", "req_id", reqID, "error", err, "remote_addr", r.RemoteAddr)
w.WriteHeader(http.StatusInternalServerError)
return
}
s.Logger.Infow("Created user.", "req_id", reqID, "remote_addr", r.RemoteAddr, "username", req.Username)
}

146
api/http_test.go Normal file
View File

@@ -0,0 +1,146 @@
package api_test
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"git.t-juice.club/torjus/gpaste"
"git.t-juice.club/torjus/gpaste/api"
"git.t-juice.club/torjus/gpaste/users"
)
func TestHandlers(t *testing.T) {
cfg := &gpaste.ServerConfig{
SigningSecret: "abc123",
Store: &gpaste.ServerStoreConfig{
Type: "memory",
},
URL: "http://localhost:8080",
}
hs := api.NewHTTPServer(cfg)
t.Run("HandlerIndex", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/", nil)
hs.Handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Returned unexpected status")
}
expectedBody := "index"
if body := rr.Body.String(); body != expectedBody {
t.Errorf("Body does not match expected. Got %s want %s", body, expectedBody)
}
})
t.Run("HandlerAPIFilePost", func(t *testing.T) {
rr := httptest.NewRecorder()
buf := &bytes.Buffer{}
mw := multipart.NewWriter(buf)
fw, err := mw.CreateFormFile("test", "test.txt")
if err != nil {
t.Fatalf("Unable to create form file: %s", err)
}
expectedData := "Test OMEGALUL PLS."
if _, err := io.WriteString(fw, expectedData); err != nil {
t.Fatalf("Unable to write body to buffer: %s", err)
}
mw.Close()
req := httptest.NewRequest(http.MethodPost, "/api/file", buf)
req.Header.Add("Content-Type", mw.FormDataContentType())
hs.Handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusAccepted {
t.Errorf("Returned unexpected status. Got %d want %d", status, http.StatusAccepted)
}
var expectedResp []struct {
Message string `json:"message"`
ID string `json:"id"`
URL string `json:"url"`
}
decoder := json.NewDecoder(rr.Result().Body)
if err := decoder.Decode(&expectedResp); err != nil {
t.Fatalf("error decoding response: %s", err)
}
if l := len(expectedResp); l != 1 {
t.Errorf("Response has wrong length. Got %d want %d", l, 1)
}
uploadID := expectedResp[0].ID
if uploadID == "" {
t.Errorf("Response has empty id")
}
t.Run("HandlerAPIFileGet", func(t *testing.T) {
rr := httptest.NewRecorder()
url := fmt.Sprintf("/api/file/%s", uploadID)
req := httptest.NewRequest(http.MethodGet, url, nil)
hs.Handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Returned unexpected status. Got %d want %d", status, http.StatusAccepted)
t.Logf(url)
}
if body := rr.Body.String(); body != expectedData {
t.Errorf("Returned body does not match expected.")
}
})
})
t.Run("HandlerAPILogin", func(t *testing.T) {
// TODO: Add test
username := "admin"
password := "admin"
user := &users.User{Username: username}
if err := user.SetPassword(password); err != nil {
t.Fatalf("Error setting user password: %s", err)
}
if err := hs.Users.Store(user); err != nil {
t.Fatalf("Error storing user: %s", err)
}
requestData := struct {
Username string `json:"username"`
Password string `json:"password"`
}{
Username: username,
Password: password,
}
body := new(bytes.Buffer)
encoder := json.NewEncoder(body)
if err := encoder.Encode(&requestData); err != nil {
t.Fatalf("Error encoding request body: %s", err)
}
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/login", body)
hs.Handler.ServeHTTP(rr, req)
responseData := struct {
Token string `json:"token"`
}{}
decoder := json.NewDecoder(rr.Body)
if err := decoder.Decode(&responseData); err != nil {
t.Fatalf("Error decoding response: %s", err)
}
if _, err := hs.Auth.ValidateToken(responseData.Token); err != nil {
t.Fatalf("Unable to validate received token: %s", err)
}
})
}

102
api/middleware.go Normal file
View File

@@ -0,0 +1,102 @@
package api
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"git.t-juice.club/torjus/gpaste"
"github.com/go-chi/chi/v5/middleware"
)
type authCtxKey int
const (
authCtxUsername authCtxKey = iota
authCtxAuthLevel
)
func (s *HTTPServer) MiddlewareAccessLogger(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
t1 := time.Now()
reqID := middleware.GetReqID(r.Context())
defer func() {
s.AccessLogger.Infow(r.Method,
"path", r.URL.Path,
"status", ww.Status(),
"written", ww.BytesWritten(),
"remote_addr", r.RemoteAddr,
"processing_time_ms", time.Since(t1).Milliseconds(),
"req_id", reqID)
}()
next.ServeHTTP(ww, r)
}
return http.HandlerFunc(fn)
}
func (s *HTTPServer) MiddlewareAuthentication(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
reqID := middleware.GetReqID(r.Context())
header := r.Header.Get("Authorization")
if header == "" {
s.Logger.Debugw("Request has no auth header.", "req_id", reqID)
next.ServeHTTP(w, r)
return
}
splitHeader := strings.Split(header, "Bearer ")
if len(splitHeader) != 2 {
s.Logger.Debugw("Request has invalid token.", "req_id", reqID)
next.ServeHTTP(w, r)
return
}
token := splitHeader[1]
claims, err := s.Auth.ValidateToken(token)
if err != nil {
s.Logger.Debugw("Request has invalid token.", "req_id", reqID)
next.ServeHTTP(w, r)
return
}
ctx := context.WithValue(r.Context(), authCtxUsername, claims.Subject)
ctx = context.WithValue(ctx, authCtxAuthLevel, gpaste.AuthLevelUser)
withCtx := r.WithContext(ctx)
s.Logger.Debugw("Request is authenticated.", "req_id", reqID, "username", claims.Subject)
next.ServeHTTP(w, withCtx)
}
return http.HandlerFunc(fn)
}
func UsernameFromRequest(r *http.Request) (string, error) {
rawUsername := r.Context().Value(authCtxUsername)
if rawUsername == nil {
return "", fmt.Errorf("no username")
}
username, ok := rawUsername.(string)
if !ok {
return "", fmt.Errorf("no username")
}
return username, nil
}
func AuthLevelFromRequest(r *http.Request) (gpaste.AuthLevel, error) {
rawLevel := r.Context().Value(authCtxAuthLevel)
if rawLevel == nil {
return gpaste.AuthLevelUnset, fmt.Errorf("no username")
}
level, ok := rawLevel.(gpaste.AuthLevel)
if !ok {
return gpaste.AuthLevelUnset, fmt.Errorf("no username")
}
return level, nil
}