diff --git a/docker/minIO/docker-compose.yml b/docker/minIO/docker-compose.yml index a5c8fa1..4f6b6c5 100644 --- a/docker/minIO/docker-compose.yml +++ b/docker/minIO/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: minio: - image: minio/minio:RELEASE.2024-03-19T00-00-00Z + image: minio/minio:latest container_name: minio restart: unless-stopped environment: @@ -14,4 +14,4 @@ services: ports: - "9000:9000" - "9001:9001" - command: server /data --console-address ":9001" \ No newline at end of file + command: server /data --console-address ":9001" diff --git a/docker/postgres/docker-compose.yml b/docker/postgres/docker-compose.yml index 1ffdffe..f4a5a19 100644 --- a/docker/postgres/docker-compose.yml +++ b/docker/postgres/docker-compose.yml @@ -7,4 +7,4 @@ services: ports: - "5432:5432" volumes: - - ./data:/var/lib/postgresql/dat + - ./data:/var/lib/postgresql/data diff --git a/go-socket b/go-socket index d6c0a7c..0635fe1 100755 Binary files a/go-socket and b/go-socket differ diff --git a/machine-client/index.html b/machine-client/index.html index d62ffba..9a962e3 100644 --- a/machine-client/index.html +++ b/machine-client/index.html @@ -86,6 +86,8 @@ + + @@ -260,9 +262,10 @@ fields: [ { id: 'mu-token', label: 'token', ph: '' }, { id: 'mu-connectionid', label: 'connectionid', ph: 'UUID' }, - { id: 'mu-msgContent', label: 'msgContent', ph: 'message text' }, + { id: 'mu-msgContent', label: 'msgContent', ph: 'message text (optional if mediaKey set)' }, + { id: 'mu-attachedFile', label: 'attachedFile', ph: 'key returned by /new/file (optional)' }, ], - submit: () => httpPost('/msg/user', { token:'mu-token', connectionid:'mu-connectionid', msgContent:'mu-msgContent' }) + submit: () => httpPost('/msg/user', { token:'mu-token', connectionid:'mu-connectionid', msgContent:'mu-msgContent', attachedFile:'mu-attachedFile' }) }, 'msg-group': { title: 'POST /msg/group — send message to group', @@ -273,6 +276,28 @@ ], submit: () => httpPost('/msg/group', { token:'mg-token', groupid:'mg-groupid', content:'mg-content' }) }, + 'file-upload': { + title: 'POST /new/file — upload file (multipart, token in header)', + renderCustom: () => ` +
+
+
+
+ +
+ ` + }, + 'file-download': { + title: 'POST /get/file — get presigned download URL (token in header)', + renderCustom: () => ` +
+
+
+
+ +
+ ` + }, 'websocket': { title: 'WS /ws — WebSocket connection', renderCustom: () => { @@ -324,6 +349,7 @@ if (def.renderCustom) { fieldsEl.innerHTML = def.renderCustom(); + autofillTokens(); } else { let html = ''; for (const f of def.fields) { @@ -420,6 +446,45 @@ log('WS →', msg, 'log-info'); } + async function submitFileUpload() { + const token = document.getElementById('fu-token').value; + const connectionid = document.getElementById('fu-connectionid').value; + const fileInput = document.getElementById('fu-file'); + if (!fileInput.files.length) { log('HTTP ERR', 'no file selected', 'log-err'); return; } + const form = new FormData(); + form.append('connectionid', connectionid); + form.append('file', fileInput.files[0]); + log('HTTP /new/file', `→ connectionid=${connectionid} file=${fileInput.files[0].name}`, 'log-info'); + try { + const resp = await fetch(baseUrl() + '/new/file', { method: 'POST', headers: { token }, body: form }); + const text = await resp.text(); + log(`HTTP ${resp.status}`, text, resp.ok ? 'log-http' : 'log-err'); + if (resp.ok) { + const keyEl = document.getElementById('fd-key'); + if (keyEl) keyEl.value = text; + const dmKeyEl = document.getElementById('mu-attachedFile'); + if (dmKeyEl) dmKeyEl.value = text; + } + } catch(e) { + log('HTTP ERR', e.message, 'log-err'); + } + } + + async function submitFileDownload() { + const token = document.getElementById('fd-token').value; + const connectionid = document.getElementById('fd-connectionid').value; + const key = document.getElementById('fd-key').value; + const params = new URLSearchParams({ connectionid, key }); + log('HTTP /get/file', `→ ${params.toString()}`, 'log-info'); + try { + const resp = await fetch(baseUrl() + '/get/file', { method: 'POST', headers: { token }, body: params }); + const text = await resp.text(); + log(`HTTP ${resp.status}`, text, resp.ok ? 'log-http' : 'log-err'); + } catch(e) { + log('HTTP ERR', e.message, 'log-err'); + } + } + function setWsStatus(connected) { const el = document.getElementById('ws-status'); if (!el) return; diff --git a/main.go b/main.go index 302580e..0042875 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,8 @@ import ( "log" "net/http" - http2 "go-socket/packages/http" + "go-socket/packages/httpRequest" + "go-socket/packages/minio" "go-socket/packages/postgresql" "go-socket/packages/wsServer" ) @@ -13,6 +14,11 @@ import ( func withCORS(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "token, Content-Type") + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } h(w, r) } } @@ -20,21 +26,24 @@ func withCORS(h http.HandlerFunc) http.HandlerFunc { func main() { ctx := context.Background() postgresql.Init(ctx) + minio.Init(ctx) - http.HandleFunc("/new/user", withCORS(http2.HandleUserNew)) - http.HandleFunc("/new/connection", withCORS(http2.HandleUserNewConnection)) - http.HandleFunc("/new/token", withCORS(http2.HandleUserNewToken)) - http.HandleFunc("/mod/user/appearence", withCORS(http2.HandleUserModifyAppearance)) - http.HandleFunc("/mod/user/about", withCORS(http2.HandleUserModifyAbout)) - http.HandleFunc("/mod/connection/accept", withCORS(http2.HandleUserElevateConnection)) + http.HandleFunc("/new/user", withCORS(httpRequest.HandleUserNew)) + http.HandleFunc("/new/connection", withCORS(httpRequest.HandleUserNewConnection)) + http.HandleFunc("/new/token", withCORS(httpRequest.HandleUserNewToken)) + http.HandleFunc("/new/file", withCORS(httpRequest.HandleFileUpload)) + http.HandleFunc("/mod/user/appearence", withCORS(httpRequest.HandleUserModifyAppearance)) + http.HandleFunc("/mod/user/about", withCORS(httpRequest.HandleUserModifyAbout)) + http.HandleFunc("/mod/connection/accept", withCORS(httpRequest.HandleUserElevateConnection)) - http.HandleFunc("/get/connections", withCORS(http2.HandleUserGetConnections)) - http.HandleFunc("/get/connection/messages", withCORS(http2.HandleUserGetConnectionMessages)) + http.HandleFunc("/get/connections", withCORS(httpRequest.HandleUserGetConnections)) + http.HandleFunc("/get/connection/messages", withCORS(httpRequest.HandleUserGetConnectionMessages)) + http.HandleFunc("/get/file", withCORS(httpRequest.HandleFileDownload)) - http.HandleFunc("/del/user", withCORS(http2.HandleUserDelete)) - http.HandleFunc("/del/connection", withCORS(http2.HandleUserDeleteConnection)) + http.HandleFunc("/del/user", withCORS(httpRequest.HandleUserDelete)) + http.HandleFunc("/del/connection", withCORS(httpRequest.HandleUserDeleteConnection)) - http.HandleFunc("/msg/user", withCORS(http2.HandleDm)) + http.HandleFunc("/msg/user", withCORS(httpRequest.HandleDm)) http.HandleFunc("/ws", wsServer.ServeWsConnection) log.Println("listening on :8080") diff --git a/packages/globals/globals.go b/packages/globals/globals.go index 7f8b626..5a9d428 100644 --- a/packages/globals/globals.go +++ b/packages/globals/globals.go @@ -7,7 +7,7 @@ const ( FileStorageBucketName string = "communicator" MaxPostBytes uint32 = 4 << 10 MaxPostWithFileBytes uint32 = 1 << 30 - FileProcessingPartSize uint64 = 24 << 10 + FileProcessingPartSize uint64 = 12 << 20 FileProcessingThreads uint = 3 FileDownloadLinkTtl time.Duration = 24 * time.Hour ) diff --git a/packages/http/connectionsAndDms.go b/packages/httpRequest/connectionsAndDms.go similarity index 95% rename from packages/http/connectionsAndDms.go rename to packages/httpRequest/connectionsAndDms.go index 6ec4898..b53f81d 100644 --- a/packages/http/connectionsAndDms.go +++ b/packages/httpRequest/connectionsAndDms.go @@ -1,10 +1,11 @@ -package http +package httpRequest import ( json2 "encoding/json" "maps" "net/http" "slices" + "strings" "time" "go-socket/packages/Enums/ConnectionState" @@ -43,11 +44,18 @@ func HandleDm(response http.ResponseWriter, request *http.Request) { } msgContent := request.FormValue("msgContent") - if msgContent == "" { + attachedFile := request.FormValue("attachedFile") + + if msgContent == "" && attachedFile == "" { http.Error(response, "empty msgContent", http.StatusBadRequest) return } + if attachedFile != "" && !strings.HasPrefix(attachedFile, targetConnection.String()+"/") { + http.Error(response, "invalid attachedFile", http.StatusBadRequest) + return + } + var target *types.User if user.Id == conn.RequestorId { @@ -64,11 +72,12 @@ func HandleDm(response http.ResponseWriter, request *http.Request) { return } message := &types.Message{ - Id: uuid.New(), - Content: msgContent, - CreatedAt: time.Now(), - Sender: user.Id, - Receiver: conn.Id, + Id: uuid.New(), + Content: msgContent, + AttachedFile: attachedFile, + CreatedAt: time.Now(), + Sender: user.Id, + Receiver: conn.Id, } wsServer.WsSendMessageCloseIfTimeout(target, types.WsEventMessage{ diff --git a/packages/http/file.go b/packages/httpRequest/file.go similarity index 50% rename from packages/http/file.go rename to packages/httpRequest/file.go index 6add618..48bd979 100644 --- a/packages/http/file.go +++ b/packages/httpRequest/file.go @@ -1,12 +1,12 @@ -package http +package httpRequest import ( + "net/http" + "strings" + "go-socket/packages/convertions" "go-socket/packages/globals" "go-socket/packages/minio" - "net/http" - - "github.com/google/uuid" ) func HandleFileUpload(response http.ResponseWriter, request *http.Request) { @@ -47,17 +47,54 @@ func HandleFileUpload(response http.ResponseWriter, request *http.Request) { defer file.Close() contentType := header.Header.Get("Content-Type") + key := minio.GetKey(connectionId, contentType) - key := - - if err = minio.Upload(ctx, fileId.String(), file, header.Size, contentType, map[string]string{ - "orginalName": header.Filename, - "uploaderId": user.Id.String(), + if err = minio.Upload(ctx, key, file, header.Size, contentType, map[string]string{ + "originalName": header.Filename, + "uploaderId": user.Id.String(), }); err != nil { http.Error(response, "upload failed", http.StatusInternalServerError) return } - response.WriteHeader(http.StatusAccepted) - response.Write([]byte(fileId.String())) + response.WriteHeader(http.StatusCreated) + response.Write([]byte(key)) +} + +func HandleFileDownload(response http.ResponseWriter, request *http.Request) { + if !postValidCheckWithResponseOnFail(&response, request, false) { + return + } + ctx := request.Context() + + user, err := getUserByToken(ctx, request.Header.Get("token")) + if err != nil { + http.Error(response, "invalid token", http.StatusUnauthorized) + return + } + + connectionId, err := convertions.ConvertStringUuid(request.FormValue("connectionid")) + if err != nil { + http.Error(response, "invalid connectionid", http.StatusBadRequest) + return + } + if _, ok := user.Connections[connectionId]; !ok { + http.Error(response, "no such connection", http.StatusUnauthorized) + return + } + + key := request.FormValue("key") + if !strings.HasPrefix(key, connectionId.String()+"/") { + http.Error(response, "no such file", http.StatusUnauthorized) + return + } + + url, err := minio.GetDownloadUrl(ctx, key) + if err != nil { + http.Error(response, "no such file", http.StatusUnauthorized) + return + } + + response.WriteHeader(http.StatusOK) + response.Write([]byte(url.String())) } diff --git a/packages/http/get.go b/packages/httpRequest/get.go similarity index 97% rename from packages/http/get.go rename to packages/httpRequest/get.go index c9afe68..8dbf81c 100644 --- a/packages/http/get.go +++ b/packages/httpRequest/get.go @@ -1,4 +1,4 @@ -package http +package httpRequest import ( "context" diff --git a/packages/http/helper.go b/packages/httpRequest/helper.go similarity index 96% rename from packages/http/helper.go rename to packages/httpRequest/helper.go index 1eea783..d1fe383 100644 --- a/packages/http/helper.go +++ b/packages/httpRequest/helper.go @@ -1,4 +1,4 @@ -package http +package httpRequest import ( "net/http" diff --git a/packages/http/user.go b/packages/httpRequest/user.go similarity index 97% rename from packages/http/user.go rename to packages/httpRequest/user.go index 1bed768..0f5e8d9 100644 --- a/packages/http/user.go +++ b/packages/httpRequest/user.go @@ -1,4 +1,4 @@ -package http +package httpRequest import ( json2 "encoding/json" @@ -26,7 +26,7 @@ func HandleUserNewToken(response http.ResponseWriter, request *http.Request) { return } - password := request.FormValue("passwords") + password := request.FormValue("password") if len(password) < 8 { http.Error(response, "no or short passwords", http.StatusBadRequest) @@ -85,7 +85,7 @@ func HandleUserNew(response http.ResponseWriter, request *http.Request) { return } - password := request.FormValue("passwords") + password := request.FormValue("password") if len(password) < 8 { http.Error(response, "no or short passwords", http.StatusBadRequest) return diff --git a/packages/minio/minio.go b/packages/minio/minio.go index e496cf3..1b454a1 100644 --- a/packages/minio/minio.go +++ b/packages/minio/minio.go @@ -5,42 +5,27 @@ import ( "io" "mime" "net/url" + "strconv" + "time" "go-socket/packages/globals" + "github.com/google/uuid" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) var minClient *minio.Client -var canonicalExt = map[string]string{ - "image/jpeg": ".jpg", - "image/png": ".png", - "image/gif": ".gif", - "image/webp": ".webp", - "video/mp4": ".mp4", - "application/pdf": ".pdf", -} - -func extensionFromContentType(ct string) string { - if ext, ok := canonicalExt[ct]; ok { - return ext +func GetKey(connectionId uuid.UUID, mimeType string) string { + extensions, err := mime.ExtensionsByType(mimeType) + if err != nil || len(extensions) == 0 { + extensions = []string{".unknown"} } - exts, err := mime.ExtensionsByType(ct) - if err != nil || len(exts) == 0 { - return ".unk" - } - return exts[0] + return connectionId.String() + "/" + strconv.FormatInt(time.Now().UnixMilli(), 10) + extensions[0] } -func GetKey(hash string, contentType string) string { - return hash + extensionFromContentType(contentType) -} - -func Init() { - ctx := context.Background() - +func Init(ctx context.Context) { var err error minClient, err = minio.New("localhost:9000", &minio.Options{ Creds: credentials.NewStaticV4("root", "change_to_env", ""), @@ -58,7 +43,10 @@ func Init() { if !exists { err = minClient.MakeBucket(ctx, globals.FileStorageBucketName, minio.MakeBucketOptions{}) if err != nil { - return + exists, checkErr := minClient.BucketExists(ctx, globals.FileStorageBucketName) + if checkErr != nil || !exists { + panic(err) + } } } diff --git a/packages/postgresql/postgresql.go b/packages/postgresql/postgresql.go index 230d1e1..3aa3d5e 100644 --- a/packages/postgresql/postgresql.go +++ b/packages/postgresql/postgresql.go @@ -60,7 +60,8 @@ func Init(ctx context.Context) { sender_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, receiver_id UUID NOT NULL REFERENCES user_connections(id) ON DELETE CASCADE, created_at TIMESTAMP NOT NULL DEFAULT NOW(), - content TEXT NOT NULL + content TEXT NOT NULL, + attached_file TEXT NOT NULL DEFAULT '' ) `) if err != nil { @@ -182,21 +183,21 @@ func ConnectionUpdateState(ctx context.Context, conn *types.Connection) error { func ConnectionMessageSave(ctx context.Context, message *types.Message) error { if message.Id != (uuid.UUID{}) { _, err := dbConn.Exec(ctx, ` - INSERT INTO direct_messages (id, sender_id, receiver_id, created_at, content) VALUES ($1, $2, $3, $4, $5) - `, message.Id, message.Sender, message.Receiver, message.CreatedAt, message.Content) + INSERT INTO direct_messages (id, sender_id, receiver_id, created_at, content, attached_file) VALUES ($1, $2, $3, $4, $5, $6) + `, message.Id, message.Sender, message.Receiver, message.CreatedAt, message.Content, message.AttachedFile) return err } return dbConn.QueryRow(ctx, ` - INSERT INTO direct_messages (sender_id, receiver_id, created_at, content) VALUES ($1, $2, $3, $4) + INSERT INTO direct_messages (sender_id, receiver_id, created_at, content, attached_file) VALUES ($1, $2, $3, $4, $5) RETURNING id - `, message.Sender, message.Receiver, message.CreatedAt, message.Content).Scan(&message.Id) + `, message.Sender, message.Receiver, message.CreatedAt, message.Content, message.AttachedFile).Scan(&message.Id) } func ConnectionGetMessagesBefore(ctx context.Context, before time.Time, connection uuid.UUID, cap uint32) ([]*types.Message, error) { rows, err := dbConn.Query(ctx, ` - SELECT id, sender_id, receiver_id, created_at, content + SELECT id, sender_id, receiver_id, created_at, content, attached_file FROM ( - SELECT id, sender_id, receiver_id, created_at, content + SELECT id, sender_id, receiver_id, created_at, content, attached_file FROM direct_messages WHERE receiver_id = $1 AND created_at < $2 @@ -213,7 +214,7 @@ func ConnectionGetMessagesBefore(ctx context.Context, before time.Time, connecti messages := make([]*types.Message, 0, cap) for rows.Next() { msg := &types.Message{} - if err = rows.Scan(&msg.Id, &msg.Sender, &msg.Receiver, &msg.CreatedAt, &msg.Content); err != nil { + if err = rows.Scan(&msg.Id, &msg.Sender, &msg.Receiver, &msg.CreatedAt, &msg.Content, &msg.AttachedFile); err != nil { return nil, err } messages = append(messages, msg) diff --git a/packages/types/types.go b/packages/types/types.go index d59ab40..04177a2 100644 --- a/packages/types/types.go +++ b/packages/types/types.go @@ -82,12 +82,12 @@ type ConnectionElevationData struct { } type Message struct { - Id uuid.UUID `json:"id"` - AttachedMedia string `json:"attachedMedia"` - Content string `json:"content"` - CreatedAt time.Time `json:"createdAt"` - Sender uuid.UUID `json:"sender"` - Receiver uuid.UUID `json:"receiver"` + Id uuid.UUID `json:"id"` + AttachedFile string `json:"attachedFile"` + Content string `json:"content"` + CreatedAt time.Time `json:"createdAt"` + Sender uuid.UUID `json:"sender"` + Receiver uuid.UUID `json:"receiver"` } type LoginReturn struct { @@ -96,7 +96,7 @@ type LoginReturn struct { } type WsEventMessage struct { - Type WsEventType.WsEventType `json:"types"` + Type WsEventType.WsEventType `json:"type"` Event any `json:"event"` }