httpgoproxyfileserver

reverse proxy serve file after it has been decrypted


I need to put an *os.File into the request.Body but I don't know how to handle the closure of the file descriptor (and the elimination of the temporary file). I cannot use http.ServeFile to serve the file.

I have 3 softwares: A) User interface ( that I cannot edit ) B) Backend which serves files C) An http reverse proxy whose role is to sign the requests coming from A going to B, to encrypt files going from A to B, and decrypto files going from B to A.

The software A is expected to receive the body of the decrypted file content. I know it would be easier just to decrypt the file in a directory using B and then have A open it, but unfortunately I cannot edit A which expects the file in the request body.

the work flow of the softwares is the following: software A sends a request to C which signs the request and forwards it to B, then C proceeds to decrypt the file and has to serve the decrypted content to A in a the request body.

type transportPageServeFile struct {
    http.RoundTripper
    // vault handles encryption and decryption of files
    vault *vault.Container
}

func (t *transportPageServeFile) RoundTrip(req *http.Request) (resp *http.Response, err error) {
    resp, err = t.RoundTripper.RoundTrip(req)
    if err != nil {
        return nil, errors.Wrap(err, "transportPageServeFile")
    }
    // create temporary destination dir for the decrypted file
    dstDir, err := utils.MkTmpDir()
    if err != nil {
        return nil, errors.Wrap(err, "transportPageServeFile")
    }
    defer os.RemoveAll(dstDir) // i'm deleting the directory where the temporary file is stored, deleting it before it even gets transmitted to software A
    fileName := req.Header.Get(api.FileNameHeader)
    decryptedFileDst := fmt.Sprintf("%s/%s", dstDir, fileName)
    // DecryptFileFromReader takes a reader and decrypts the file, saving it in the decided destination
    err = t.vault.DecryptFileFromReader(resp.Body, decryptedFileDst)
    if err != nil {
        return nil, errors.Wrap(err, "transportPageServeFile")
    }
    defer resp.Body.Close()
    // now open the saved decrypted file
    decryptedFile, err := os.Open(decryptedFileDst)
    if err != nil {
        return nil, errors.Wrap(err,"transportPageServeFile")
    }
    defer decryptedFile.Close() // close file before serving the content?
    resp.Body = decryptedFile
    return resp, nil
}
// pageServeFile is the handler of /serveFile
func (rpc *RPC) pageServeFile(c *gin.Context) {
    transport := &transportPageServeFile{RoundTripper:http.DefaultTransport, vault:rpc.vault}
    target, err := url.Parse(rpc.apiEndpoint)
    if err != nil {
        rpc.onError(c, errors.Wrapf(err, "pageServeFile"))
        return
    }
    proxy := httputil.NewSingleHostReverseProxy(target)
    proxy.Transport = transport
    proxy.ServeHTTP(c.Writer, c.Request)
}

How do I copy the file in the request body (using a reader) and when/how do I close the file descriptor once the response is forwarded?


Solution

  • The response body is an io.ReadCloser, so it is reasonable to assume that the reverse proxy calls Close. And indeed it does, as you can see by running the following program:

    package main
    
    import (
        "fmt"
        "io"
        "net/http"
        "net/http/httptest"
        "net/http/httputil"
        "net/url"
        "runtime/debug"
    )
    
    type dbg struct{ io.ReadCloser }
    
    func (d dbg) Close() error {
        fmt.Println("Close() called:")
        debug.PrintStack()
    
        return d.ReadCloser.Close()
    }
    
    func main() {
        // srv simulates "software B"
        srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("Hello, world"))
        }))
    
        target, _ := url.Parse(srv.URL)
        p := httputil.NewSingleHostReverseProxy(target)
    
        // To modify the response, use the aptly named ModifyResponse field; no
        // need to implement a RoundTripper.
        p.ModifyResponse = func(res *http.Response) error {
            res.Body = dbg{res.Body}
            return nil
        }
    
        req := httptest.NewRequest("GET", "http://example.com/hello", nil)
        rec := httptest.NewRecorder()
    
        p.ServeHTTP(rec, req)
    }
    
    // Output:
    // Close() called:
    // goroutine 1 [running]:
    // runtime/debug.Stack(0x10, 0x0, 0x0)
    //         /usr/local/go/src/runtime/debug/stack.go:24 +0x9d
    // runtime/debug.PrintStack()
    //         /usr/local/go/src/runtime/debug/stack.go:16 +0x22
    // main.dbg.Close(0x75b780, 0xc00013e140, 0xc000152000, 0x7f61fc523088)
    //         /tmp/tmp.H9O7cizbkv/main.go:17 +0x7f
    // net/http/httputil.(*ReverseProxy).ServeHTTP(0xc0000ac0f0, 0x75e460, 0xc000032440, 0xc00010a000)
    //         /usr/local/go/src/net/http/httputil/reverseproxy.go:311 +0x8a6
    // main.main()
    //         /tmp/tmp.H9O7cizbkv/main.go:41 +0x152