diff --git a/.build/gotenberg b/.build/gotenberg index acd520dd..4b39e207 100755 Binary files a/.build/gotenberg and b/.build/gotenberg differ diff --git a/.gitignore b/.gitignore index 0bc0cc98..2e1b0fb9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store -.ci/coverage.txt \ No newline at end of file +.ci/coverage.txt +.vscode \ No newline at end of file diff --git a/app/context/context.go b/app/context/context.go index 55b7d657..c813ba7e 100644 --- a/app/context/context.go +++ b/app/context/context.go @@ -7,46 +7,15 @@ import ( "net/http" "github.com/thecodingmachine/gotenberg/app/converter" - ghttp "github.com/thecodingmachine/gotenberg/app/http" ) type key uint32 const ( - contentTypeKey key = iota - converterKey + converterKey key = iota resultFilePathKey ) -// WithContentType populates a request's context with the given content type -// and returns the updated request. -func WithContentType(r *http.Request, contentType ghttp.ContentType) *http.Request { - ctx := r.Context() - ctx = context.WithValue(ctx, contentTypeKey, contentType) - r = r.WithContext(ctx) - - return r -} - -type contentTypeNotFoundError struct{} - -const contentTypeNotFoundErrorMessage = "The 'Content-Type' was not found in request context" - -func (e *contentTypeNotFoundError) Error() string { - return contentTypeNotFoundErrorMessage -} - -// GetContentType returns the content type if found in -// the request's context. Otherwise throws an error. -func GetContentType(r *http.Request) (ghttp.ContentType, error) { - ct, ok := r.Context().Value(contentTypeKey).(ghttp.ContentType) - if !ok { - return "", &contentTypeNotFoundError{} - } - - return ct, nil -} - // WithConverter populates a request's context with the given converter // and returns the updated request. func WithConverter(r *http.Request, converter *converter.Converter) *http.Request { diff --git a/app/context/context_test.go b/app/context/context_test.go index b9d14b51..3d5a60b1 100644 --- a/app/context/context_test.go +++ b/app/context/context_test.go @@ -6,31 +6,8 @@ import ( "testing" "github.com/thecodingmachine/gotenberg/app/converter" - ghttp "github.com/thecodingmachine/gotenberg/app/http" ) -func TestWithContentType(t *testing.T) { - req := WithContentType(httptest.NewRequest(http.MethodPost, "/", nil), ghttp.MultipartFormDataContentType) - if ct, _ := req.Context().Value(contentTypeKey).(ghttp.ContentType); ct != ghttp.MultipartFormDataContentType { - t.Errorf("Context returned a wrong content type: got %v want %v", ct, ghttp.MultipartFormDataContentType) - } -} - -func TestGetContentType(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "/", nil) - - // case 1: uses a request without a content type entry in its context. - if _, err := GetContentType(req); err == nil { - t.Error("Context should not have a content type entry!") - } - - // case 2: uses a request with a content type entry in its context. - req = WithContentType(req, ghttp.MultipartFormDataContentType) - if _, err := GetContentType(req); err != nil { - t.Error("Context should have a content type entry!") - } -} - func TestWithConverter(t *testing.T) { req := WithConverter(httptest.NewRequest(http.MethodPost, "/", nil), &converter.Converter{}) if c, _ := req.Context().Value(converterKey).(*converter.Converter); c == nil { @@ -53,14 +30,6 @@ func TestGetConverter(t *testing.T) { } } -func TestWithResultFilePath(t *testing.T) { - filePath := "file.pdf" - req := WithResultFilePath(httptest.NewRequest(http.MethodPost, "/", nil), filePath) - if path, _ := req.Context().Value(resultFilePathKey).(string); path != filePath { - t.Errorf("Context returned a wrong converter: got %s want %s", path, filePath) - } -} - func TestGetResultFilePath(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/", nil) @@ -76,10 +45,11 @@ func TestGetResultFilePath(t *testing.T) { } } -func TestContentTypeNotFoundError(t *testing.T) { - err := &contentTypeNotFoundError{} - if err.Error() != contentTypeNotFoundErrorMessage { - t.Errorf("Error returned a wrong message: got %s want %s", err.Error(), contentTypeNotFoundErrorMessage) +func TestWithResultFilePath(t *testing.T) { + filePath := "file.pdf" + req := WithResultFilePath(httptest.NewRequest(http.MethodPost, "/", nil), filePath) + if path, _ := req.Context().Value(resultFilePathKey).(string); path != filePath { + t.Errorf("Context returned a wrong converter: got %s want %s", path, filePath) } } diff --git a/app/converter/converter.go b/app/converter/converter.go index 2086f440..b1bd466a 100644 --- a/app/converter/converter.go +++ b/app/converter/converter.go @@ -9,7 +9,6 @@ import ( gfile "github.com/thecodingmachine/gotenberg/app/converter/file" "github.com/thecodingmachine/gotenberg/app/converter/process" - ghttp "github.com/thecodingmachine/gotenberg/app/http" "github.com/satori/go.uuid" ) @@ -21,7 +20,7 @@ type Converter struct { } // NewConverter instantiates a converter by parsing a request. -func NewConverter(r *http.Request, contentType ghttp.ContentType) (*Converter, error) { +func NewConverter(r *http.Request) (*Converter, error) { c := &Converter{ workingDir: fmt.Sprintf("./%s/", uuid.NewV4().String()), } @@ -30,33 +29,23 @@ func NewConverter(r *http.Request, contentType ghttp.ContentType) (*Converter, e return nil, err } - switch contentType { - case ghttp.MultipartFormDataContentType: - reader, err := r.MultipartReader() - if err != nil { - return nil, err + reader, err := r.MultipartReader() + if err != nil { + return nil, err + } + + for { + part, err := reader.NextPart() + if err == io.EOF { + break } - for { - part, err := reader.NextPart() - if err == io.EOF { - break - } - - if part.FileName() == "" { - continue - } - - f, err := gfile.NewFile(c.workingDir, part) - if err != nil { - return nil, err - } - - c.files = append(c.files, f) + fileName := part.FileName() + if fileName == "" { + continue } - break - default: - f, err := gfile.NewFile(c.workingDir, r.Body) + + f, err := gfile.NewFile(c.workingDir, part, fileName) if err != nil { return nil, err } diff --git a/app/converter/converter_test.go b/app/converter/converter_test.go index 5dce20bd..23e747da 100644 --- a/app/converter/converter_test.go +++ b/app/converter/converter_test.go @@ -1,7 +1,6 @@ package converter import ( - "bytes" "io" "mime/multipart" "net/http" @@ -12,23 +11,9 @@ import ( "github.com/thecodingmachine/gotenberg/app/config" "github.com/thecodingmachine/gotenberg/app/converter/process" - ghttp "github.com/thecodingmachine/gotenberg/app/http" ) func makeRequest(filesPaths ...string) *http.Request { - if len(filesPaths) == 0 { - req := httptest.NewRequest(http.MethodPost, "/", new(bytes.Buffer)) - req.Header.Set("Content-Type", string(ghttp.OctetStreamContentType)) - return req - } - - if len(filesPaths) == 1 { - file, _ := os.Open(filesPaths[0]) - req := httptest.NewRequest(http.MethodPost, "/", file) - req.Header.Set("Content-Type", string(ghttp.OctetStreamContentType)) - return req - } - r, w := io.Pipe() mpw := multipart.NewWriter(w) @@ -56,27 +41,27 @@ func makeRequest(filesPaths ...string) *http.Request { func TestNewConverter(t *testing.T) { // case 1: uses a request with a single file. path, _ := filepath.Abs("../../_tests/file.docx") - if _, err := NewConverter(makeRequest(path), ghttp.OctetStreamContentType); err != nil { + if _, err := NewConverter(makeRequest(path)); err != nil { t.Error("Converter should have been instantiated!") } // case 2: uses a request with wrong file type. path, _ = filepath.Abs("../../_tests/configurations/gotenberg.yml") - if _, err := NewConverter(makeRequest(path), ghttp.OctetStreamContentType); err == nil { + if _, err := NewConverter(makeRequest(path)); err == nil { t.Error("Converter should not have been instantiated!") } // case 3: uses a request with two files. oPath, _ := filepath.Abs("../../_tests/file.docx") path, _ = filepath.Abs("../../_tests/file.pdf") - if _, err := NewConverter(makeRequest(oPath, path), ghttp.MultipartFormDataContentType); err != nil { + if _, err := NewConverter(makeRequest(oPath, path)); err != nil { t.Error("Converter should have been instantiated!") } // case 4: uses a request with one Office file and one wrong file type. oPath, _ = filepath.Abs("../../_tests/file.docx") path, _ = filepath.Abs("../../_tests/configurations/gotenberg.yml") - if _, err := NewConverter(makeRequest(oPath, path), ghttp.MultipartFormDataContentType); err == nil { + if _, err := NewConverter(makeRequest(oPath, path)); err == nil { t.Error("Converter should not have been instantiated!") } } @@ -88,7 +73,7 @@ func TestConvert(t *testing.T) { // case 1: uses a request with a single file. path, _ = filepath.Abs("../../_tests/file.docx") - c, _ := NewConverter(makeRequest(path), ghttp.OctetStreamContentType) + c, _ := NewConverter(makeRequest(path)) if _, err := c.Convert(); err != nil { t.Error("Converter should have been able to convert an Office document to PDF!") } @@ -96,7 +81,7 @@ func TestConvert(t *testing.T) { // case 2: uses a request with two files. oPath, _ := filepath.Abs("../../_tests/file.docx") path, _ = filepath.Abs("../../_tests/file.pdf") - c, _ = NewConverter(makeRequest(oPath, path), ghttp.MultipartFormDataContentType) + c, _ = NewConverter(makeRequest(oPath, path)) if _, err := c.Convert(); err != nil { t.Error("Converter should have been able to convert two files to PDF and merge them!") } @@ -106,7 +91,7 @@ func TestConvert(t *testing.T) { appConfig, _ = config.NewAppConfig(path) process.Load(appConfig.CommandsConfig) path, _ = filepath.Abs("../../_tests/file.docx") - c, _ = NewConverter(makeRequest(path), ghttp.OctetStreamContentType) + c, _ = NewConverter(makeRequest(path)) if _, err := c.Convert(); err == nil { t.Error("Converter should not have been able to convert an Office document to PDF!") } @@ -117,7 +102,7 @@ func TestConvert(t *testing.T) { process.Load(appConfig.CommandsConfig) oPath, _ = filepath.Abs("../../_tests/file.docx") path, _ = filepath.Abs("../../_tests/file.pdf") - c, _ = NewConverter(makeRequest(oPath, path), ghttp.MultipartFormDataContentType) + c, _ = NewConverter(makeRequest(oPath, path)) if _, err := c.Convert(); err == nil { t.Error("Converter should not have been able to merge PDF!") } @@ -125,7 +110,7 @@ func TestConvert(t *testing.T) { func TestClear(t *testing.T) { path, _ := filepath.Abs("../../_tests/file.docx") - c, _ := NewConverter(makeRequest(path), ghttp.OctetStreamContentType) + c, _ := NewConverter(makeRequest(path)) if err := c.Clear(); err != nil { t.Error("Converter should have been able to clear itself!") } diff --git a/app/converter/file/file.go b/app/converter/file/file.go index 48e6fc32..7e59889e 100644 --- a/app/converter/file/file.go +++ b/app/converter/file/file.go @@ -5,8 +5,7 @@ import ( "fmt" "io" "os" - - ghttp "github.com/thecodingmachine/gotenberg/app/http" + "path/filepath" "github.com/satori/go.uuid" ) @@ -32,11 +31,42 @@ const ( OfficeType ) +// filesTypes associates a file extension with its file kind counterpart. +var filesTypes = map[string]Type{ + ".pdf": PDFType, + ".html": HTMLType, + ".doc": OfficeType, + ".docx": OfficeType, + ".odt": OfficeType, + ".xls": OfficeType, + ".xlsx": OfficeType, + ".ods": OfficeType, + ".ppt": OfficeType, + ".pptx": OfficeType, + ".odp": OfficeType, +} + +type fileTypeNotFoundError struct { + fileName string +} + +func (e *fileTypeNotFoundError) Error() string { + return fmt.Sprintf("File type was not found for '%s'", e.fileName) +} + // NewFile creates a file in the considered directory. // Returns a *File instance or an error if something bad happened. -func NewFile(workingDir string, r io.Reader) (*File, error) { +func NewFile(workingDir string, r io.Reader, fileName string) (*File, error) { + ext := filepath.Ext(fileName) + + t, ok := filesTypes[ext] + if !ok { + return nil, &fileTypeNotFoundError{fileName: fileName} + } + f := &File{ - Path: MakeFilePath(workingDir), + Path: MakeFilePath(workingDir, ext), + Type: t, } file, err := os.Create(f.Path) @@ -54,103 +84,11 @@ func NewFile(workingDir string, r io.Reader) (*File, error) { // resets the read pointer. file.Seek(0, 0) - t, err := findFileType(file) - if err != nil { - return nil, err - } - - f.Type = t - - f, err = reworkFilePath(workingDir, f) - if err != nil { - return nil, err - } - return f, nil } // MakeFilePath is a simple helper which generates a random file name // and associates it with the considered directory to make a path. -func MakeFilePath(workingDir string) string { - return fmt.Sprintf("%s%s", workingDir, uuid.NewV4().String()) -} - -// filesTypes associates a content type with its file kind counterpart. -var filesTypes = map[ghttp.ContentType]Type{ - ghttp.PDFContentType: PDFType, - ghttp.HTMLContentType: HTMLType, - ghttp.OctetStreamContentType: OfficeType, - ghttp.ZipContentType: OfficeType, -} - -type fileTypeNotFoundError struct{} - -const fileTypeNotFoundErrorMessage = "The file type was not found for the given 'Content-Type'" - -func (e *fileTypeNotFoundError) Error() string { - return fileTypeNotFoundErrorMessage -} - -// findFileType tries to detect what kind of file is the given file. -func findFileType(f *os.File) (Type, error) { - ct, err := ghttp.SniffContentType(f) - if err != nil { - return 999, err - } - - t, ok := filesTypes[ct] - if !ok { - return 999, &fileTypeNotFoundError{} - } - - return t, nil -} - -// Ext represents a file extension. -type Ext string - -const ( - // PDFExt represents a... PDF extension. - PDFExt Ext = ".pdf" - // HTMLExt represents an... HTML extension. - HTMLExt Ext = ".html" - // OfficeExt is a empty string, as Office documents - // have a lot of different extensions (.docx, .doc and so on). - OfficeExt Ext = "" -) - -// filesExtensions associates a kind of file with its extension. -var filesExtensions = map[Type]Ext{ - PDFType: PDFExt, - HTMLType: HTMLExt, - OfficeType: OfficeExt, -} - -type fileExtNotFoundError struct{} - -const fileExtNotFoundErrorMessage = "The file extension was not found for the given file type" - -func (e *fileExtNotFoundError) Error() string { - return fileExtNotFoundErrorMessage -} - -// reworkFilePath renames a file in the considered directory and adds its extension. -func reworkFilePath(workingDir string, f *File) (*File, error) { - ext, ok := filesExtensions[f.Type] - if !ok { - return nil, &fileExtNotFoundError{} - } - - if ext != OfficeExt { - newPath := fmt.Sprintf("%s%s", MakeFilePath(workingDir), ext) - - err := os.Rename(f.Path, newPath) - if err != nil { - return nil, err - } - - f.Path = newPath - } - - return f, nil +func MakeFilePath(workingDir string, ext string) string { + return fmt.Sprintf("%s%s%s", workingDir, uuid.NewV4().String(), ext) } diff --git a/app/converter/file/file_test.go b/app/converter/file/file_test.go index cf3df8bf..36d8943b 100644 --- a/app/converter/file/file_test.go +++ b/app/converter/file/file_test.go @@ -2,6 +2,7 @@ package file import ( "bytes" + "fmt" "os" "path/filepath" "testing" @@ -11,56 +12,27 @@ func TestNewFile(t *testing.T) { workingDir := "test" os.Mkdir(workingDir, 0666) - // case 1: uses an empty reader. - if _, err := NewFile(workingDir, new(bytes.Buffer)); err == nil { - t.Error("File should not have been instantiated!") - } - - // case 2: uses a reader from a wrong file type. - path, _ := filepath.Abs("../../../_tests/configurations/gotenberg.yml") - r, _ := os.Open(path) - defer r.Close() - if _, err := NewFile(workingDir, r); err == nil { + // case 2: uses a wrong file name. + if _, err := NewFile(workingDir, new(bytes.Buffer), "file.yml"); err == nil { t.Error("File should not have been instantiated!") } // case 3: uses a reader from a correct file type. - path, _ = filepath.Abs("../../../_tests/file.pdf") - r, _ = os.Open(path) + filePath, _ := filepath.Abs("../../../_tests/file.pdf") + r, _ := os.Open(filePath) defer r.Close() - if _, err := NewFile(workingDir, r); err != nil { + if _, err := NewFile(workingDir, r, "file.pdf"); err != nil { t.Error("File should have been instantiated!") } os.RemoveAll(workingDir) } - -func TestReworkFilePath(t *testing.T) { - workingDir := "test" - os.Mkdir(workingDir, 0666) - - f := &File{ - Path: MakeFilePath(workingDir), - Type: 999, - } - - if _, err := reworkFilePath(workingDir, f); err == nil { - t.Error("It should not have been able to found the file extension!") - } - - os.RemoveAll(workingDir) -} - func TestFileTypeNotFoundError(t *testing.T) { - err := &fileTypeNotFoundError{} - if err.Error() != fileTypeNotFoundErrorMessage { - t.Errorf("Error returned a wrong message: got %s want %s", err.Error(), fileTypeNotFoundErrorMessage) - } -} + fileName := "file.wp" + err := &fileTypeNotFoundError{fileName: fileName} + expected := fmt.Sprintf("File type was not found for '%s'", fileName) -func TestFileExtNotFoundError(t *testing.T) { - err := &fileExtNotFoundError{} - if err.Error() != fileExtNotFoundErrorMessage { - t.Errorf("Error returned a wrong message: got %s want %s", err.Error(), fileExtNotFoundErrorMessage) + if err.Error() != expected { + t.Errorf("Error returned a wrong message: got %s want %s", err.Error(), expected) } } diff --git a/app/converter/process/process.go b/app/converter/process/process.go index 326538aa..c015194a 100644 --- a/app/converter/process/process.go +++ b/app/converter/process/process.go @@ -3,7 +3,6 @@ package process import ( "bytes" - "fmt" "os/exec" "text/template" "time" @@ -37,7 +36,7 @@ func (e *impossibleConversionError) Error() string { func Unconv(workingDir string, file *gfile.File) (string, error) { cmdData := &conversionData{ FilePath: file.Path, - ResultFilePath: fmt.Sprintf("%s%s", gfile.MakeFilePath(workingDir), gfile.PDFExt), + ResultFilePath: gfile.MakeFilePath(workingDir, ".pdf"), } var ( @@ -81,7 +80,7 @@ type mergeData struct { func Merge(workingDir string, filesPaths []string) (string, error) { cmdData := &mergeData{ FilesPaths: filesPaths, - ResultFilePath: fmt.Sprintf("%s%s", gfile.MakeFilePath(workingDir), gfile.PDFExt), + ResultFilePath: gfile.MakeFilePath(workingDir, ".pdf"), } cmdTemplate := commandsConfig.Merge.Template @@ -127,7 +126,6 @@ func run(command string, timeout int) error { if err := cmd.Process.Kill(); err != nil { return err } - return &commandTimeoutError{} case err := <-done: if err != nil { diff --git a/app/converter/process/process_test.go b/app/converter/process/process_test.go index ab97b6f5..5d383cdd 100644 --- a/app/converter/process/process_test.go +++ b/app/converter/process/process_test.go @@ -1,6 +1,7 @@ package process import ( + "fmt" "os" "path/filepath" "testing" @@ -9,6 +10,18 @@ import ( gfile "github.com/thecodingmachine/gotenberg/app/converter/file" ) +func makeFile(workingDir string, fileName string) *gfile.File { + filePath := fmt.Sprintf("%s%s", "../../../_tests/", fileName) + absPath, _ := filepath.Abs(filePath) + + r, _ := os.Open(absPath) + defer r.Close() + + f, _ := gfile.NewFile(workingDir, r, fileName) + + return f +} + func TestLoad(t *testing.T) { path, _ := filepath.Abs("../../../_tests/configurations/gotenberg.yml") c, _ := config.NewAppConfig(path) @@ -28,29 +41,17 @@ func TestUnconv(t *testing.T) { os.Mkdir(workingDir, 0666) // case 1: uses an HTML file type. - path, _ = filepath.Abs("../../../_tests/file.html") - r, _ := os.Open(path) - defer r.Close() - f, _ := gfile.NewFile(workingDir, r) - if _, err := Unconv(workingDir, f); err != nil { + if _, err := Unconv(workingDir, makeFile(workingDir, "file.html")); err != nil { t.Error("HTML conversion to PDF should have worked!") } // case 2: uses an Office file type. - path, _ = filepath.Abs("../../../_tests/file.docx") - r, _ = os.Open(path) - defer r.Close() - f, _ = gfile.NewFile(workingDir, r) - if _, err := Unconv(workingDir, f); err != nil { + if _, err := Unconv(workingDir, makeFile(workingDir, "file.docx")); err != nil { t.Error("Office conversion to PDF should have worked!") } // case 3: uses a PDF file type. - path, _ = filepath.Abs("../../../_tests/file.pdf") - r, _ = os.Open(path) - defer r.Close() - f, _ = gfile.NewFile(workingDir, r) - if _, err := Unconv(workingDir, f); err == nil { + if _, err := Unconv(workingDir, makeFile(workingDir, "file.pdf")); err == nil { t.Error("PDF conversion to PDF should not have worked!") } @@ -58,11 +59,7 @@ func TestUnconv(t *testing.T) { path, _ = filepath.Abs("../../../_tests/configurations/timeout-gotenberg.yml") c, _ = config.NewAppConfig(path) Load(c.CommandsConfig) - path, _ = filepath.Abs("../../../_tests/file.docx") - r, _ = os.Open(path) - defer r.Close() - f, _ = gfile.NewFile(workingDir, r) - if _, err := Unconv(workingDir, f); err == nil { + if _, err := Unconv(workingDir, makeFile(workingDir, "file.docx")); err == nil { t.Error("Office conversion to PDF should have reached timeout!") } diff --git a/app/handlers.go b/app/handlers.go index 821c5b67..32804c10 100644 --- a/app/handlers.go +++ b/app/handlers.go @@ -42,18 +42,15 @@ func enforceContentLengthHandler(next http.Handler) http.Handler { } // enforceContentTypeHandler checks if the "Content-Type" entry -// from the request's header matches one of the allowed content types. +// from the request's header matches the allowed content type. func enforceContentTypeHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ct, err := ghttp.FindAuthorizedContentType(r.Header) - if err != nil { + if err := ghttp.CheckAuthorizedContentType(r.Header); err != nil { http.Error(w, err.Error(), http.StatusUnsupportedMediaType) logger.Error(err) return } - r = context.WithContentType(r, ct) - next.ServeHTTP(w, r) }) } @@ -61,14 +58,7 @@ func enforceContentTypeHandler(next http.Handler) http.Handler { // convertHandler is in charge of converting the file(s) from the request to PDF. func convertHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ct, err := context.GetContentType(r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - logger.Error(err) - return - } - - c, err := converter.NewConverter(r, ct) + c, err := converter.NewConverter(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) logger.Error(err) diff --git a/app/handlers_test.go b/app/handlers_test.go index a46efe30..63a61e07 100644 --- a/app/handlers_test.go +++ b/app/handlers_test.go @@ -1,7 +1,6 @@ package app import ( - "bytes" "io" "mime/multipart" "net/http" @@ -24,19 +23,6 @@ func fakeSuccessHandler(w http.ResponseWriter, r *http.Request) { } func makeRequest(filesPaths ...string) *http.Request { - if len(filesPaths) == 0 { - req := httptest.NewRequest(http.MethodPost, "/", new(bytes.Buffer)) - req.Header.Set("Content-Type", string(ghttp.OctetStreamContentType)) - return req - } - - if len(filesPaths) == 1 { - file, _ := os.Open(filesPaths[0]) - req := httptest.NewRequest(http.MethodPost, "/", file) - req.Header.Set("Content-Type", string(ghttp.OctetStreamContentType)) - return req - } - r, w := io.Pipe() mpw := multipart.NewWriter(w) @@ -86,7 +72,7 @@ func TestEnforceContentTypeHandler(t *testing.T) { // case 1: sends a wrong content type. req := httptest.NewRequest(http.MethodPost, "/", nil) - req.Header.Set("Content-Type", string(ghttp.PDFContentType)) + req.Header.Set("Content-Type", "application/pdf") rr := httptest.NewRecorder() h.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusUnsupportedMediaType { @@ -95,7 +81,7 @@ func TestEnforceContentTypeHandler(t *testing.T) { // case 2: sends a good content type. req = httptest.NewRequest(http.MethodPost, "/", nil) - req.Header.Set("Content-Type", string(ghttp.OctetStreamContentType)) + req.Header.Set("Content-Type", string(ghttp.MultipartFormDataContentType)) rr = httptest.NewRecorder() h.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { @@ -106,7 +92,7 @@ func TestEnforceContentTypeHandler(t *testing.T) { func TestConvertHandler(t *testing.T) { h := alice.New(convertHandler).ThenFunc(fakeSuccessHandler) - // case 1: sends a request without a content type entry in its context. + // case 1: sends a request without body. req := httptest.NewRequest(http.MethodPost, "/", nil) rr := httptest.NewRecorder() h.ServeHTTP(rr, req) @@ -114,22 +100,14 @@ func TestConvertHandler(t *testing.T) { t.Errorf("Handler returned a wrong status code: got %v want %v", status, http.StatusInternalServerError) } - // case 2: sends a request without body. - req = context.WithContentType(httptest.NewRequest(http.MethodPost, "/", nil), ghttp.OctetStreamContentType) - rr = httptest.NewRecorder() - h.ServeHTTP(rr, req) - if status := rr.Code; status != http.StatusInternalServerError { - t.Errorf("Handler returned a wrong status code: got %v want %v", status, http.StatusInternalServerError) - } - - // case 3: sends a request with two files and using an unsuitable timeout for merge commande. + // case 2: sends a request with two files and using an unsuitable timeout for merge commande. path, _ := filepath.Abs("../_tests/configurations/merge-timeout-gotenberg.yml") appConfig, _ := config.NewAppConfig(path) process.Load(appConfig.CommandsConfig) oPath, _ := filepath.Abs("../_tests/file.docx") path, _ = filepath.Abs("../_tests/configurations/gotenberg.yml") - req = context.WithContentType(makeRequest(oPath, path), ghttp.MultipartFormDataContentType) + req = makeRequest(oPath, path) rr = httptest.NewRecorder() h.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusInternalServerError { @@ -143,7 +121,7 @@ func TestConvertHandler(t *testing.T) { oPath, _ = filepath.Abs("../_tests/file.docx") path, _ = filepath.Abs("../_tests/file.pdf") - req = context.WithContentType(makeRequest(oPath, path), ghttp.MultipartFormDataContentType) + req = makeRequest(oPath, path) rr = httptest.NewRecorder() h.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { diff --git a/app/http/http.go b/app/http/http.go index 6c133a14..2b5babde 100644 --- a/app/http/http.go +++ b/app/http/http.go @@ -4,71 +4,30 @@ package http import ( "fmt" "net/http" - "os" "strings" ) // ContentType is a string which represents a content type. type ContentType string -const ( - // PDFContentType represents... the PDF content type. - PDFContentType ContentType = "application/pdf" - // HTMLContentType represents... the HTML content type. - HTMLContentType ContentType = "text/html" - // OctetStreamContentType represents... the octet stream content type. - OctetStreamContentType ContentType = "application/octet-stream" - // ZipContentType represents... the zip content type. - ZipContentType ContentType = "application/zip" - // MultipartFormDataContentType represents... the multipart form data content type. - MultipartFormDataContentType ContentType = "multipart/form-data" -) +// MultipartFormDataContentType represents... the multipart form data content type. +const MultipartFormDataContentType ContentType = "multipart/form-data" type notAuthorizedContentTypeError struct{} func (e *notAuthorizedContentTypeError) Error() string { - return fmt.Sprintf("Accepted values for 'Content-Type': %s, %s", OctetStreamContentType, MultipartFormDataContentType) + return fmt.Sprintf("Accepted value for 'Content-Type': %s", MultipartFormDataContentType) } -// FindAuthorizedContentType tries to return a content type according to a request header. +// CheckAuthorizedContentType check if the request header header has an authorized content type. // If no authorized content type found, throws an error. -func FindAuthorizedContentType(h http.Header) (ContentType, error) { - ct := findContentType(h.Get("Content-Type"), OctetStreamContentType, MultipartFormDataContentType) +func CheckAuthorizedContentType(h http.Header) error { + ct := findContentType(h.Get("Content-Type"), MultipartFormDataContentType) if ct == "" { - return "", ¬AuthorizedContentTypeError{} + return ¬AuthorizedContentTypeError{} } - return ct, nil -} - -type notAuthorizedFileContentTypeError struct{} - -const notAuthorizedFileContentTypeErrorMessage = "Unable to detect an authorized file content type" - -func (e *notAuthorizedFileContentTypeError) Error() string { - return notAuthorizedFileContentTypeErrorMessage -} - -// SniffContentType tries to detect the content type of a file. -// If no authorized content type found, throws an error. -func SniffContentType(f *os.File) (ContentType, error) { - // only the first 512 bytes are used to sniff the content type. - buffer := make([]byte, 512) - n, err := f.Read(buffer) - if err != nil { - return "", err - } - - // resets the read pointer. - f.Seek(0, 0) - - // using n if size of buffer < 512 bytes. - ct := findContentType(http.DetectContentType(buffer[:n]), PDFContentType, HTMLContentType, OctetStreamContentType, ZipContentType) - if ct == "" { - return "", ¬AuthorizedFileContentTypeError{} - } - - return ct, nil + return nil } // findContentType parses a string representing a content type and tries to find diff --git a/app/http/http_test.go b/app/http/http_test.go index c0d90cde..958c227e 100644 --- a/app/http/http_test.go +++ b/app/http/http_test.go @@ -4,55 +4,28 @@ import ( "fmt" "net/http" "net/http/httptest" - "os" - "path/filepath" "testing" ) -func TestFindAuthorizedContentType(t *testing.T) { +func TestCheckAuthorizedContentType(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/", nil) // case 1: uses a request without a content type entry in its header. - if _, err := FindAuthorizedContentType(req.Header); err == nil { + if err := CheckAuthorizedContentType(req.Header); err == nil { t.Error("It should not have been able to retrieve an authorized content type from header!") } // case 2: uses a request with a content type entry in its header. req.Header.Set("Content-Type", string(MultipartFormDataContentType)) - if _, err := FindAuthorizedContentType(req.Header); err != nil { + if err := CheckAuthorizedContentType(req.Header); err != nil { t.Error("It should have been able to retrieve an authorized content type from header!") } } -func TestSniffContentType(t *testing.T) { - // case 1: uses a file with a wrong content type. - path, _ := filepath.Abs("../../_tests/configurations/gotenberg.yml") - f, _ := os.Open(path) - defer f.Close() - if _, err := SniffContentType(f); err == nil { - t.Error("It should not have been able to retrieve an authorized content type from an YAML file!") - } - - // case 2: uses a file with a correct content type. - path, _ = filepath.Abs("../../_tests/file.pdf") - f, _ = os.Open(path) - defer f.Close() - if _, err := SniffContentType(f); err != nil { - t.Error("It should have been able to retrieve an authorized content type from a PDF file!") - } -} - func TestNotAuthorizedContentTypeError(t *testing.T) { err := ¬AuthorizedContentTypeError{} - message := fmt.Sprintf("Accepted values for 'Content-Type': %s, %s", OctetStreamContentType, MultipartFormDataContentType) + message := fmt.Sprintf("Accepted value for 'Content-Type': %s", MultipartFormDataContentType) if err.Error() != message { t.Errorf("Error returned a wrong message: got %s want %s", err.Error(), message) } } - -func TestNotAuthorizedFileContentTypeError(t *testing.T) { - err := ¬AuthorizedFileContentTypeError{} - if err.Error() != notAuthorizedFileContentTypeErrorMessage { - t.Errorf("Error returned a wrong message: got %s want %s", err.Error(), notAuthorizedFileContentTypeErrorMessage) - } -}