Files
opencloud/pkg/jmap/jmap_http.go
2025-12-09 09:15:36 +01:00

243 lines
7.8 KiB
Go

package jmap
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/version"
)
type HttpJmapApiClient struct {
baseurl url.URL
client *http.Client
masterUser string
masterPassword string
userAgent string
}
var (
_ ApiClient = &HttpJmapApiClient{}
_ SessionClient = &HttpJmapApiClient{}
_ BlobClient = &HttpJmapApiClient{}
)
/*
func bearer(req *http.Request, token string) {
req.Header.Add("Authorization", "Bearer "+base64.StdEncoding.EncodeToString([]byte(token)))
}
*/
func NewHttpJmapApiClient(baseurl url.URL, client *http.Client, masterUser string, masterPassword string) *HttpJmapApiClient {
return &HttpJmapApiClient{
baseurl: baseurl,
client: client,
masterUser: masterUser,
masterPassword: masterPassword,
userAgent: "OpenCloud/" + version.GetString(),
}
}
func (h *HttpJmapApiClient) Close() error {
h.client.CloseIdleConnections()
return nil
}
type AuthenticationError struct {
Err error
}
func (e AuthenticationError) Error() string {
return fmt.Sprintf("failed to find user for authentication: %v", e.Err.Error())
}
func (e AuthenticationError) Unwrap() error {
return e.Err
}
func (h *HttpJmapApiClient) auth(username string, _ *log.Logger, req *http.Request) error {
masterUsername := username + "%" + h.masterUser
req.SetBasicAuth(masterUsername, h.masterPassword)
return nil
}
func (h *HttpJmapApiClient) GetSession(username string, logger *log.Logger) (SessionResponse, Error) {
wellKnownUrl := h.baseurl.JoinPath(".well-known", "jmap").String()
req, err := http.NewRequest(http.MethodGet, wellKnownUrl, nil)
if err != nil {
logger.Error().Err(err).Msgf("failed to create GET request for %v", wellKnownUrl)
return SessionResponse{}, SimpleError{code: JmapErrorInvalidHttpRequest, err: err}
}
h.auth(username, logger, req)
req.Header.Add("Cache-Control", "no-cache, no-store, must-revalidate") // spec recommendation
res, err := h.client.Do(req)
if err != nil {
logger.Error().Err(err).Msgf("failed to perform GET %v", wellKnownUrl)
return SessionResponse{}, SimpleError{code: JmapErrorInvalidHttpRequest, err: err}
}
if res.StatusCode < 200 || res.StatusCode > 299 {
logger.Error().Str("status", res.Status).Msg("HTTP response status code is not 200")
return SessionResponse{}, SimpleError{code: JmapErrorServerResponse, err: fmt.Errorf("JMAP API response status is %v", res.Status)}
}
if res.Body != nil {
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
logger.Error().Err(err).Msg("failed to close response body")
}
}(res.Body)
}
body, err := io.ReadAll(res.Body)
if err != nil {
logger.Error().Err(err).Msg("failed to read response body")
return SessionResponse{}, SimpleError{code: JmapErrorReadingResponseBody, err: err}
}
var data SessionResponse
err = json.Unmarshal(body, &data)
if err != nil {
logger.Error().Str("url", wellKnownUrl).Err(err).Msg("failed to decode JSON payload from .well-known/jmap response")
return SessionResponse{}, SimpleError{code: JmapErrorDecodingResponseBody, err: err}
}
return data, nil
}
func (h *HttpJmapApiClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request) ([]byte, Error) {
jmapUrl := session.JmapUrl.String()
bodyBytes, err := json.Marshal(request)
if err != nil {
logger.Error().Err(err).Msg("failed to marshall JSON payload")
return nil, SimpleError{code: JmapErrorEncodingRequestBody, err: err}
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, jmapUrl, bytes.NewBuffer(bodyBytes))
if err != nil {
logger.Error().Err(err).Msgf("failed to create POST request for %v", jmapUrl)
return nil, SimpleError{code: JmapErrorCreatingRequest, err: err}
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("User-Agent", h.userAgent)
h.auth(session.Username, logger, req)
res, err := h.client.Do(req)
if err != nil {
logger.Error().Err(err).Msgf("failed to perform POST %v", jmapUrl)
return nil, SimpleError{code: JmapErrorSendingRequest, err: err}
}
if res.StatusCode < 200 || res.StatusCode > 299 {
logger.Error().Str("status", res.Status).Msg("HTTP response status code is not 2xx")
return nil, SimpleError{code: JmapErrorServerResponse, err: err}
}
if res.Body != nil {
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
logger.Error().Err(err).Msg("failed to close response body")
}
}(res.Body)
}
body, err := io.ReadAll(res.Body)
if err != nil {
logger.Error().Err(err).Msg("failed to read response body")
return nil, SimpleError{code: JmapErrorServerResponse, err: err}
}
return body, nil
}
func (h *HttpJmapApiClient) UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, contentType string, body io.Reader) (UploadedBlob, Error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadUrl, body)
if err != nil {
logger.Error().Err(err).Msgf("failed to create POST request for %v", uploadUrl)
return UploadedBlob{}, SimpleError{code: JmapErrorCreatingRequest, err: err}
}
req.Header.Add("Content-Type", contentType)
req.Header.Add("User-Agent", h.userAgent)
h.auth(session.Username, logger, req)
res, err := h.client.Do(req)
if err != nil {
logger.Error().Err(err).Msgf("failed to perform POST %v", uploadUrl)
return UploadedBlob{}, SimpleError{code: JmapErrorSendingRequest, err: err}
}
if res.StatusCode < 200 || res.StatusCode > 299 {
logger.Error().Str("status", res.Status).Msg("HTTP response status code is not 2xx")
return UploadedBlob{}, SimpleError{code: JmapErrorServerResponse, err: err}
}
if res.Body != nil {
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
logger.Error().Err(err).Msg("failed to close response body")
}
}(res.Body)
}
responseBody, err := io.ReadAll(res.Body)
if err != nil {
logger.Error().Err(err).Msg("failed to read response body")
return UploadedBlob{}, SimpleError{code: JmapErrorServerResponse, err: err}
}
var result UploadedBlob
err = json.Unmarshal(responseBody, &result)
if err != nil {
logger.Error().Str("url", uploadUrl).Err(err).Msg("failed to decode JSON payload from the upload response")
return UploadedBlob{}, SimpleError{code: JmapErrorDecodingResponseBody, err: err}
}
return result, nil
}
func (h *HttpJmapApiClient) DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string) (*BlobDownload, Error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadUrl, nil)
if err != nil {
logger.Error().Err(err).Msgf("failed to create GET request for %v", downloadUrl)
return nil, SimpleError{code: JmapErrorCreatingRequest, err: err}
}
req.Header.Add("User-Agent", h.userAgent)
h.auth(session.Username, logger, req)
res, err := h.client.Do(req)
if err != nil {
logger.Error().Err(err).Msgf("failed to perform GET %v", downloadUrl)
return nil, SimpleError{code: JmapErrorSendingRequest, err: err}
}
if res.StatusCode == http.StatusNotFound {
return nil, nil
}
if res.StatusCode < 200 || res.StatusCode > 299 {
logger.Error().Str("status", res.Status).Msg("HTTP response status code is not 2xx")
return nil, SimpleError{code: JmapErrorServerResponse, err: err}
}
sizeStr := res.Header.Get("Content-Length")
size := -1
if sizeStr != "" {
size, err = strconv.Atoi(sizeStr)
if err != nil {
logger.Warn().Err(err).Msgf("failed to parse Content-Length blob download response header value '%v'", sizeStr)
size = -1
}
}
return &BlobDownload{
Body: res.Body,
Size: size,
Type: res.Header.Get("Content-Type"),
ContentDisposition: res.Header.Get("Content-Disposition"),
CacheControl: res.Header.Get("Cache-Control"),
}, nil
}