1
0
mirror of https://github.com/haiwen/seafile-server.git synced 2025-05-13 18:45:13 +00:00

Add download file API ()

* Add download file API

* Go add download file API

* Set http header and return http error

---------

Co-authored-by: 杨赫然 <heran.yang@seafile.com>
This commit is contained in:
feiniks 2024-09-12 11:02:41 +08:00 committed by GitHub
parent f0c95b4e77
commit b2bde11d89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 430 additions and 37 deletions

View File

@ -31,6 +31,7 @@ import (
"syscall"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/gorilla/mux"
"github.com/haiwen/seafile-server/fileserver/blockmgr"
"github.com/haiwen/seafile-server/fileserver/commitmgr"
"github.com/haiwen/seafile-server/fileserver/diff"
@ -232,6 +233,146 @@ func parseCryptKey(rsp http.ResponseWriter, repoID string, user string, version
return seafileKey, nil
}
func accessV2CB(rsp http.ResponseWriter, r *http.Request) *appError {
vars := mux.Vars(r)
repoID := vars["repoid"]
filePath := r.URL.Query().Get("p")
op := r.URL.Query().Get("op")
if filePath == "" {
msg := "No file path\n"
return &appError{nil, msg, http.StatusBadRequest}
}
decPath, err := url.PathUnescape(filePath)
if err != nil {
msg := fmt.Sprintf("File path %s can't be decoded\n", filePath)
return &appError{nil, msg, http.StatusBadRequest}
}
rpath := getCanonPath(decPath)
fileName := filepath.Base(rpath)
if op != "view" && op != "download" {
msg := "Operation is neither view or download\n"
return &appError{nil, msg, http.StatusBadRequest}
}
token := utils.GetAuthorizationToken(r.Header)
cookie := r.Header.Get("Cookie")
if token == "" && cookie == "" {
msg := "Both token and cookie are not set\n"
return &appError{nil, msg, http.StatusBadRequest}
}
user, appErr := checkFileAccess(repoID, token, cookie, filePath, "download")
if appErr != nil {
return appErr
}
repo := repomgr.Get(repoID)
if repo == nil {
msg := "Bad repo id"
return &appError{nil, msg, http.StatusBadRequest}
}
fileID, _, err := fsmgr.GetObjIDByPath(repo.StoreID, repo.RootID, rpath)
if err != nil {
msg := "Invalid file_path\n"
return &appError{nil, msg, http.StatusBadRequest}
}
etag := r.Header.Get("If-None-Match")
if etag == fileID {
return &appError{nil, "", http.StatusNotModified}
}
rsp.Header().Set("ETag", fileID)
rsp.Header().Set("Cache-Control", "private, no-cache")
ranges := r.Header["Range"]
byteRanges := strings.Join(ranges, "")
var cryptKey *seafileCrypt
if repo.IsEncrypted {
key, err := parseCryptKey(rsp, repoID, user, repo.EncVersion)
if err != nil {
return err
}
cryptKey = key
}
exists, _ := fsmgr.Exists(repo.StoreID, fileID)
if !exists {
msg := "Invalid file id"
return &appError{nil, msg, http.StatusBadRequest}
}
if !repo.IsEncrypted && len(byteRanges) != 0 {
if err := doFileRange(rsp, r, repo, fileID, fileName, op, byteRanges, user); err != nil {
return err
}
} else if err := doFile(rsp, r, repo, fileID, fileName, op, cryptKey, user); err != nil {
return err
}
return nil
}
type UserInfo struct {
User string `json:"user"`
}
func checkFileAccess(repoID, token, cookie, filePath, op string) (string, *appError) {
claims := SeahubClaims{
time.Now().Add(time.Second * 300).Unix(),
true,
jwt.RegisteredClaims{},
}
jwtToken := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), &claims)
tokenString, err := jwtToken.SignedString([]byte(seahubPK))
if err != nil {
err := fmt.Errorf("failed to sign jwt token: %v", err)
return "", &appError{err, "", http.StatusInternalServerError}
}
url := fmt.Sprintf("%s/repos/%s/check-access/?path=%s", seahubURL, repoID, filePath)
header := map[string][]string{
"Authorization": {"Token " + tokenString},
}
if cookie != "" {
header["Cookie"] = []string{cookie}
}
req := make(map[string]string)
req["op"] = op
if token != "" {
req["token"] = token
}
msg, err := json.Marshal(req)
if err != nil {
err := fmt.Errorf("failed to encode access token: %v", err)
return "", &appError{err, "", http.StatusInternalServerError}
}
status, body, err := utils.HttpCommon("POST", url, header, bytes.NewReader(msg))
if err != nil {
err := fmt.Errorf("failed to get access token info: %v", err)
return "", &appError{err, "", http.StatusInternalServerError}
}
if status != http.StatusOK {
msg := "No permission to access file\n"
return "", &appError{nil, msg, http.StatusForbidden}
}
info := new(UserInfo)
err = json.Unmarshal(body, &info)
if err != nil {
err := fmt.Errorf("failed to decode access token info: %v", err)
return "", &appError{err, "", http.StatusInternalServerError}
}
return info.User, nil
}
func doFile(rsp http.ResponseWriter, r *http.Request, repo *repomgr.Repo, fileID string,
fileName string, operation string, cryptKey *seafileCrypt, user string) *appError {
file, err := fsmgr.GetSeafile(repo.StoreID, fileID)
@ -3571,7 +3712,7 @@ func accessLinkCB(rsp http.ResponseWriter, r *http.Request) *appError {
}
rsp.Header().Set("ETag", fileID)
rsp.Header().Set("Cache-Control", "no-cache")
rsp.Header().Set("Cache-Control", "public, no-cache")
var cryptKey *seafileCrypt
if repo.IsEncrypted {

View File

@ -487,6 +487,8 @@ func newHTTPRouter() *mux.Router {
r.Handle("/f/{.*}{slash:\\/?}", appHandler(accessLinkCB))
//r.Handle("/d/{.*}", appHandler(accessDirLinkCB))
r.Handle("/repos/{repoid:[\\da-z]{8}-[\\da-z]{4}-[\\da-z]{4}-[\\da-z]{4}-[\\da-z]{12}}/files{slash:\\/?}", appHandler(accessV2CB))
// file syncing api
r.Handle("/repo/{repoid:[\\da-z]{8}-[\\da-z]{4}-[\\da-z]{4}-[\\da-z]{4}-[\\da-z]{12}}/permission-check{slash:\\/?}",
appHandler(permissionCheckCB))

View File

@ -27,6 +27,7 @@
#include "access-file.h"
#include "zip-download-mgr.h"
#include "http-server.h"
#include "seaf-utils.h"
#define FILE_TYPE_MAP_DEFAULT_LEN 1
#define BUFFER_SIZE 1024 * 64
@ -1116,11 +1117,15 @@ set_etag (evhtp_request_t *req,
}
static void
set_no_cache (evhtp_request_t *req)
set_no_cache (evhtp_request_t *req, gboolean private_cache)
{
evhtp_kv_t *kv;
kv = evhtp_kv_new ("Cache-Control", "no-cache", 1, 1);
if (private_cache) {
kv = evhtp_kv_new ("Cache-Control", "private, no-cache", 1, 1);
} else {
kv = evhtp_kv_new ("Cache-Control", "public, no-cache", 1, 1);
}
evhtp_kvs_add_kv (req->headers_out, kv);
}
@ -1466,6 +1471,145 @@ on_error:
evhtp_send_reply(req, error_code);
}
static void
access_v2_cb(evhtp_request_t *req, void *arg)
{
SeafRepo *repo = NULL;
char *error_str = NULL;
char *token = NULL;
char *user = NULL;
char *dec_path = NULL;
char *rpath = NULL;
char *filename = NULL;
char *file_id = NULL;
const char *repo_id = NULL;
const char *path = NULL;
const char *operation = NULL;
const char *byte_ranges = NULL;
const char *auth_token = NULL;
const char *cookie = NULL;
int error_code = EVHTP_RES_BADREQ;
SeafileCryptKey *key = NULL;
GError *error = NULL;
/* Skip the first '/'. */
char **parts = g_strsplit (req->uri->path->full + 1, "/", 0);
if (!parts || g_strv_length (parts) < 3 ||
strcmp (parts[2], "files") != 0) {
error_str = "Invalid URL\n";
goto out;
}
repo_id = parts[1];
path = evhtp_kv_find (req->uri->query, "p");
if (!path) {
error_str = "No file path\n";
goto out;
}
dec_path = g_uri_unescape_string(path, NULL);
rpath = format_dir_path (dec_path);
filename = g_path_get_basename (rpath);
operation = evhtp_kv_find (req->uri->query, "op");
if (!operation) {
error_str = "No operation\n";
goto out;
}
if (strcmp(operation, "view") != 0 &&
strcmp(operation, "download") != 0) {
error_str = "Operation is neither view or download\n";
goto out;
}
auth_token = evhtp_kv_find (req->headers_in, "Authorization");
token = seaf_parse_auth_token (auth_token);
cookie = evhtp_kv_find (req->headers_in, "Cookie");
if (!token && !cookie) {
error_str = "Both token and cookie are not set\n";
goto out;
}
if (http_tx_manager_check_file_access (repo_id, token, cookie, path, "download", &user) < 0) {
error_str = "No permission to access file\n";
error_code = EVHTP_RES_FORBIDDEN;
goto out;
}
repo = seaf_repo_manager_get_repo(seaf->repo_mgr, repo_id);
if (!repo) {
error_str = "Bad repo id\n";
goto out;
}
file_id = seaf_fs_manager_get_seafile_id_by_path (seaf->fs_mgr, repo->store_id, repo->version, repo->root_id, rpath, &error);
if (!file_id) {
error_str = "Invalid file_path\n";
if (error)
g_clear_error(&error);
goto out;
}
const char *etag = evhtp_kv_find (req->headers_in, "If-None-Match");
if (g_strcmp0 (etag, file_id) == 0) {
evhtp_send_reply (req, EVHTP_RES_NOTMOD);
error_code = EVHTP_RES_OK;
goto out;
}
set_etag (req, file_id);
set_no_cache (req, TRUE);
byte_ranges = evhtp_kv_find (req->headers_in, "Range");
if (repo->encrypted) {
key = seaf_passwd_manager_get_decrypt_key (seaf->passwd_mgr,
repo_id, user);
if (!key) {
error_str = "Repo is encrypted. Please provide password to view it.";
goto out;
}
}
if (!seaf_fs_manager_object_exists (seaf->fs_mgr,
repo->store_id, repo->version, file_id)) {
error_str = "Invalid file id\n";
goto out;
}
if (!repo->encrypted && byte_ranges) {
if (do_file_range (req, repo, file_id, filename, operation, byte_ranges, user) < 0) {
error_str = "Internal server error\n";
error_code = EVHTP_RES_SERVERR;
goto out;
}
} else if (do_file(req, repo, file_id, filename, operation, key, user) < 0) {
error_str = "Internal server error\n";
error_code = EVHTP_RES_SERVERR;
goto out;
}
error_code = EVHTP_RES_OK;
out:
g_strfreev (parts);
g_free (token);
g_free (user);
g_free (dec_path);
g_free (rpath);
g_free (filename);
g_free (file_id);
if (repo != NULL)
seaf_repo_unref (repo);
if (key != NULL)
g_object_unref (key);
if (error_code != EVHTP_RES_OK) {
evbuffer_add_printf(req->buffer_out, "%s\n", error_str);
evhtp_send_reply(req, error_code);
}
}
static int
do_block(evhtp_request_t *req, SeafRepo *repo, const char *user, const char *file_id,
const char *blk_id)
@ -1726,7 +1870,7 @@ access_link_cb(evhtp_request_t *req, void *arg)
goto out;
}
set_etag (req, file_id);
set_no_cache (req);
set_no_cache (req, FALSE);
byte_ranges = evhtp_kv_find (req->headers_in, "Range");
@ -2124,6 +2268,7 @@ access_file_init (evhtp_t *htp)
evhtp_set_regex_cb (htp, "^/zip/.*", access_zip_cb, NULL);
evhtp_set_regex_cb (htp, "^/f/.*", access_link_cb, NULL);
//evhtp_set_regex_cb (htp, "^/d/.*", access_dir_link_cb, NULL);
evhtp_set_regex_cb (htp, "^/repos/[\\da-z]{8}-[\\da-z]{4}-[\\da-z]{4}-[\\da-z]{4}-[\\da-z]{12}/files/.*", access_v2_cb, NULL);
return 0;
}

View File

@ -169,13 +169,24 @@ recv_response (void *contents, size_t size, size_t nmemb, void *userp)
* the client will time out.
*/
static int
http_get_common (CURL *curl, const char *url, const char *token,
http_get_common (CURL *curl, const char *url,
struct curl_slist **headers,
const char *token,
int *rsp_status, char **rsp_content, gint64 *rsp_size,
HttpRecvCallback callback, void *cb_data,
gboolean timeout)
{
int ret = 0;
if (token) {
char *token_header = g_strdup_printf ("Authorization: Token %s", token);
*headers = curl_slist_append (*headers, token_header);
g_free (token_header);
}
*headers = curl_slist_append (*headers, "User-Agent: Seafile Server");
*headers = curl_slist_append (*headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *headers);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
@ -259,13 +270,24 @@ send_request (void *ptr, size_t size, size_t nmemb, void *userp)
}
static int
http_post_common (CURL *curl, const char *url, const char *token,
http_post_common (CURL *curl, const char *url,
struct curl_slist **headers,
const char *token,
const char *req_content, gint64 req_size,
int *rsp_status, char **rsp_content, gint64 *rsp_size,
gboolean timeout, int timeout_sec)
{
int ret = 0;
if (token) {
char *token_header = g_strdup_printf ("Authorization: Token %s", token);
*headers = curl_slist_append (*headers, token_header);
g_free (token_header);
}
*headers = curl_slist_append (*headers, "User-Agent: Seafile Server");
*headers = curl_slist_append (*headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *headers);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_POST, 1L);
@ -342,25 +364,15 @@ http_post (Connection *conn, const char *url, const char *token,
int *rsp_status, char **rsp_content, gint64 *rsp_size,
gboolean timeout, int timeout_sec)
{
char *token_header;
struct curl_slist *headers = NULL;
int ret = 0;
CURL *curl;
curl = conn->curl;
headers = curl_slist_append (headers, "User-Agent: Seafile Server");
if (token) {
token_header = g_strdup_printf ("Authorization: Token %s", token);
headers = curl_slist_append (headers, token_header);
g_free (token_header);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
}
g_return_val_if_fail (req_content != NULL, -1);
ret = http_post_common (curl, url, token, req_content, req_size,
ret = http_post_common (curl, url, &headers, token, req_content, req_size,
rsp_status, rsp_content, rsp_size, timeout, timeout_sec);
if (ret < 0) {
conn->release = TRUE;
@ -451,7 +463,6 @@ char *
http_tx_manager_get_nickname (const char *modifier)
{
Connection *conn = NULL;
char *token_header;
struct curl_slist *headers = NULL;
int ret = 0;
CURL *curl;
@ -490,15 +501,9 @@ http_tx_manager_get_nickname (const char *modifier)
json_decref (content);
curl = conn->curl;
headers = curl_slist_append (headers, "User-Agent: Seafile Server");
token_header = g_strdup_printf ("Authorization: Token %s", jwt_token);
headers = curl_slist_append (headers, token_header);
headers = curl_slist_append (headers, "Content-Type: application/json");
g_free (token_header);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
url = g_strdup_printf("%s/user-list/", seaf->seahub_url);
ret = http_post_common (curl, url, jwt_token, req_content, strlen(req_content),
ret = http_post_common (curl, url, &headers, jwt_token, req_content, strlen(req_content),
&rsp_status, &rsp_content, &rsp_size, TRUE, 1);
if (ret < 0) {
conn->release = TRUE;
@ -506,8 +511,7 @@ http_tx_manager_get_nickname (const char *modifier)
}
if (rsp_status != HTTP_OK) {
seaf_warning ("Failed to get user list from seahub %d.\n",
rsp_status);
goto out;
}
nickname = parse_nickname (rsp_content, rsp_size);
@ -567,7 +571,6 @@ SeafileShareLinkInfo *
http_tx_manager_query_share_link_info (const char *token, const char *cookie, const char *type)
{
Connection *conn = NULL;
char *token_header;
char *cookie_header;
struct curl_slist *headers = NULL;
int ret = 0;
@ -592,20 +595,14 @@ http_tx_manager_query_share_link_info (const char *token, const char *cookie, co
}
curl = conn->curl;
headers = curl_slist_append (headers, "User-Agent: Seafile Server");
token_header = g_strdup_printf ("Authorization: Token %s", jwt_token);
headers = curl_slist_append (headers, token_header);
g_free (token_header);
if (cookie) {
cookie_header = g_strdup_printf ("Cookie: %s", cookie);
headers = curl_slist_append (headers, cookie_header);
g_free (cookie_header);
}
headers = curl_slist_append (headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
url = g_strdup_printf("%s/check-share-link-access/?token=%s&type=%s", seaf->seahub_url, token, type);
ret = http_get_common (curl, url, jwt_token, &rsp_status,
ret = http_get_common (curl, url, &headers, jwt_token, &rsp_status,
&rsp_content, &rsp_size, NULL, NULL, TRUE);
if (ret < 0) {
conn->release = TRUE;
@ -613,8 +610,7 @@ http_tx_manager_query_share_link_info (const char *token, const char *cookie, co
}
if (rsp_status != HTTP_OK) {
seaf_warning ("Failed to query access token from seahub: %d.\n",
rsp_status);
goto out;
}
info = parse_share_link_info (rsp_content, rsp_size);
@ -628,3 +624,108 @@ out:
return info;
}
char *
parse_file_access_info (const char *rsp_content, int rsp_size)
{
json_t *object;
json_error_t jerror;
const char *user = NULL;
object = json_loadb (rsp_content, rsp_size, 0, &jerror);
if (!object) {
seaf_warning ("Failed to parse response when check file access in Seahub: %s.\n", jerror.text);
return NULL;
}
user = json_object_get_string_member (object, "user");
if (!user) {
seaf_warning ("Failed to find user in json when check file access in Seahub.\n");
goto out;
}
out:
json_decref (object);
return g_strdup (user);
}
int
http_tx_manager_check_file_access (const char *repo_id, const char *token, const char *cookie,
const char *path, const char *op, char **user)
{
Connection *conn = NULL;
char *cookie_header;
struct curl_slist *headers = NULL;
int ret = -1;
CURL *curl;
json_t *content = NULL;
int rsp_status;
char *req_content = NULL;
char *jwt_token = NULL;
char *rsp_content = NULL;
gint64 rsp_size;
char *url = NULL;
jwt_token = gen_jwt_token ();
if (!jwt_token) {
return -1;
}
conn = connection_pool_get_connection (seaf->seahub_conn_pool);
if (!conn) {
g_free (jwt_token);
seaf_warning ("Failed to get connection: out of memory.\n");
return -1;
}
content = json_object ();
json_object_set_new (content, "op", json_string(op));
if (token) {
json_object_set_new (content, "token", json_string(token));
}
req_content = json_dumps (content, JSON_COMPACT);
if (!req_content) {
ret = -1;
seaf_warning ("Failed to dump json request.\n");
goto out;
}
curl = conn->curl;
if (cookie) {
cookie_header = g_strdup_printf ("Cookie: %s", cookie);
headers = curl_slist_append (headers, cookie_header);
g_free (cookie_header);
}
url = g_strdup_printf("%s/repos/%s/check-access/?path=%s", seaf->seahub_url, repo_id, path);
ret = http_post_common (curl, url, &headers, jwt_token, req_content, strlen(req_content),
&rsp_status, &rsp_content, &rsp_size, TRUE, 1);
if (ret < 0) {
conn->release = TRUE;
goto out;
}
if (rsp_status != HTTP_OK) {
ret = -1;
goto out;
}
*user = parse_file_access_info (rsp_content, rsp_size);
if (*user == NULL) {
ret = -1;
goto out;
}
out:
if (content)
json_decref (content);
g_free (url);
g_free (jwt_token);
g_free (req_content);
g_free (rsp_content);
curl_slist_free_all (headers);
connection_pool_return_connection (seaf->seahub_conn_pool, conn);
return ret;
}

View File

@ -51,4 +51,8 @@ http_tx_manager_get_nickname (const char *modifier);
SeafileShareLinkInfo *
http_tx_manager_query_share_link_info (const char *token, const char *cookie, const char *type);
int
http_tx_manager_check_file_access (const char *repo_id, const char *token, const char *cookie,
const char *path, const char *op, char **user);
#endif