New version
This commit is contained in:
92
npmproxy/externalProxy.go
Normal file
92
npmproxy/externalProxy.go
Normal file
@ -0,0 +1,92 @@
|
||||
package npmproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/golog"
|
||||
)
|
||||
|
||||
func (p *Proxy) externalHTTPServer(ctx context.Context) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: p.externalProxyHost,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
fmt.Printf("Failed to shutdown proxy server: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
mux.HandleFunc("/", p.incomingNpmRequestHandler)
|
||||
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Receives incoming requests from the npm cli and decides based on override rules what server it should forwarded to.
|
||||
func (p *Proxy) incomingNpmRequestHandler(res http.ResponseWriter, req *http.Request) {
|
||||
golog.Infof("Incoming NPM request for %s", req.URL.Path)
|
||||
pkgPath := strings.TrimLeft(req.URL.Path, "/")
|
||||
|
||||
// If no matching override is found, we forward the request to the default registry.
|
||||
_, ok := p.matchingOverride(pkgPath)
|
||||
if !ok {
|
||||
serveReverseProxy(p.DefaultRegistry, res, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Process the override by forwarding the request to the internal proxy.
|
||||
|
||||
serveReverseProxy(p.internalProxyUrl, res, req)
|
||||
|
||||
// golog.Infof("Received request for url: %v", proxyUrl)
|
||||
}
|
||||
|
||||
type ResponseWriterWrapper struct {
|
||||
http.ResponseWriter
|
||||
Body *bytes.Buffer
|
||||
}
|
||||
|
||||
func (rw *ResponseWriterWrapper) Write(b []byte) (int, error) {
|
||||
rw.Body.Write(b) // Capture the response body
|
||||
return rw.ResponseWriter.Write(b) // Send the response to the original writer
|
||||
}
|
||||
|
||||
func serveReverseProxy(target string, res http.ResponseWriter, req *http.Request) {
|
||||
// parse the OriginalUrl
|
||||
OriginalUrl, _ := url.Parse(target)
|
||||
|
||||
// create the reverse proxy
|
||||
proxy := httputil.NewSingleHostReverseProxy(OriginalUrl)
|
||||
|
||||
// Update the headers to allow for SSL redirection
|
||||
req.URL.Host = OriginalUrl.Host
|
||||
req.URL.Scheme = OriginalUrl.Scheme
|
||||
req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
|
||||
req.Host = OriginalUrl.Host
|
||||
|
||||
wrappedRes := &ResponseWriterWrapper{ResponseWriter: res, Body: new(bytes.Buffer)}
|
||||
|
||||
// Note that ServeHttp is non blocking and uses a go routine under the hood
|
||||
proxy.ServeHTTP(wrappedRes, req)
|
||||
|
||||
// Print the captured response body
|
||||
fmt.Println("Response body:", wrappedRes.Body.String())
|
||||
}
|
67
npmproxy/internalProxy.go
Normal file
67
npmproxy/internalProxy.go
Normal file
@ -0,0 +1,67 @@
|
||||
package npmproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/trading-peter/gowebbuild/fsutils"
|
||||
)
|
||||
|
||||
func (p *Proxy) internalHTTPServer(ctx context.Context) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: p.internalProxyHost,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
fmt.Printf("Failed to shutdown internal server for npm proxy: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
mux.HandleFunc("GET /{pkg}", func(w http.ResponseWriter, r *http.Request) {
|
||||
pkgName := r.PathValue("pkg")
|
||||
override, ok := p.matchingOverride(pkgName)
|
||||
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := p.findPackageSource(override, pkgName)
|
||||
if err != nil {
|
||||
serveReverseProxy(override.Upstream, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(pkg)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /files/{file}", func(w http.ResponseWriter, r *http.Request) {
|
||||
fileName := r.PathValue("file")
|
||||
filePath := filepath.Join(p.pkgCachePath, fileName)
|
||||
|
||||
if !fsutils.IsFile(filePath) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, filePath)
|
||||
})
|
||||
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
95
npmproxy/proxy.go
Normal file
95
npmproxy/proxy.go
Normal file
@ -0,0 +1,95 @@
|
||||
package npmproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Override struct {
|
||||
Namespace string
|
||||
Upstream string
|
||||
PackageRoot string
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
ProjectRoot string
|
||||
Port int
|
||||
InternalPort int
|
||||
DefaultRegistry string
|
||||
Overrides []Override
|
||||
pkgCachePath string
|
||||
externalProxyHost string
|
||||
internalProxyHost string
|
||||
internalProxyUrl string
|
||||
}
|
||||
|
||||
type ProxyOption func(*Proxy)
|
||||
|
||||
func WithPort(port int) ProxyOption {
|
||||
return func(p *Proxy) {
|
||||
p.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
func WithInternalPort(port int) ProxyOption {
|
||||
return func(p *Proxy) {
|
||||
p.InternalPort = port
|
||||
}
|
||||
}
|
||||
|
||||
func WithPkgCachePath(path string) ProxyOption {
|
||||
return func(p *Proxy) {
|
||||
p.pkgCachePath = path
|
||||
}
|
||||
}
|
||||
|
||||
func WithDefaultRegistry(registry string) ProxyOption {
|
||||
return func(p *Proxy) {
|
||||
p.DefaultRegistry = strings.TrimSuffix(registry, "/")
|
||||
}
|
||||
}
|
||||
|
||||
func New(overrides []Override, projectRoot string, options ...ProxyOption) *Proxy {
|
||||
p := &Proxy{
|
||||
ProjectRoot: projectRoot,
|
||||
Port: 1234,
|
||||
InternalPort: 1235,
|
||||
DefaultRegistry: "https://registry.npmjs.org",
|
||||
Overrides: overrides,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(p)
|
||||
}
|
||||
|
||||
if p.pkgCachePath == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
homeDir = "."
|
||||
}
|
||||
p.pkgCachePath = filepath.Join(homeDir, ".gowebbuild", "proxy", "cache")
|
||||
}
|
||||
|
||||
p.externalProxyHost = fmt.Sprintf("127.0.0.1:%d", p.Port)
|
||||
p.internalProxyHost = fmt.Sprintf("127.0.0.1:%d", p.InternalPort)
|
||||
p.internalProxyUrl = fmt.Sprintf("http://%s", p.internalProxyHost)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Proxy) Start(ctx context.Context) {
|
||||
go p.internalHTTPServer(ctx)
|
||||
p.externalHTTPServer(ctx)
|
||||
}
|
||||
|
||||
func (p *Proxy) matchingOverride(path string) (*Override, bool) {
|
||||
for _, o := range p.Overrides {
|
||||
if strings.HasPrefix(path, o.Namespace) {
|
||||
return &o, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
188
npmproxy/source.go
Normal file
188
npmproxy/source.go
Normal file
@ -0,0 +1,188 @@
|
||||
package npmproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/kataras/golog"
|
||||
"github.com/mholt/archiver/v4"
|
||||
"github.com/trading-peter/gowebbuild/fsutils"
|
||||
)
|
||||
|
||||
func (p *Proxy) readPackageJson(pkgPath string) (PackageJson, error) {
|
||||
pkgFile := filepath.Join(pkgPath, "package.json")
|
||||
|
||||
if !fsutils.IsFile(pkgFile) {
|
||||
return PackageJson{}, fmt.Errorf("package.json not found in %s", pkgPath)
|
||||
}
|
||||
|
||||
pkgData, err := os.ReadFile(pkgFile)
|
||||
if err != nil {
|
||||
return PackageJson{}, err
|
||||
}
|
||||
|
||||
pkg := PackageJson{}
|
||||
err = json.Unmarshal(pkgData, &pkg)
|
||||
if err != nil {
|
||||
return PackageJson{}, err
|
||||
}
|
||||
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func (p *Proxy) findDependencyVersionConstraint(projectPkg PackageJson, pkgName string) (*semver.Constraints, error) {
|
||||
if verStr, ok := projectPkg.Dependencies[pkgName]; ok {
|
||||
return semver.NewConstraint(verStr)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("package %s not found in project dependencies", pkgName)
|
||||
}
|
||||
|
||||
func (p *Proxy) findPackageSource(override *Override, pkgName string) (*Package, error) {
|
||||
pkgNameParts := strings.Split(pkgName, "/")
|
||||
pkgPath := filepath.Join(override.PackageRoot, pkgNameParts[len(pkgNameParts)-1])
|
||||
|
||||
// Read the projects package.json and figure out the requested semver version, which probably will be a contraint to assert against (like "^1.2.3").
|
||||
projectPkg, err := p.readPackageJson(p.ProjectRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqVersion, err := p.findDependencyVersionConstraint(projectPkg, pkgName)
|
||||
|
||||
pkg, err := p.readPackageJson(pkgPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pkgVersion, err := semver.NewVersion(pkg.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !reqVersion.Check(pkgVersion) {
|
||||
golog.Infof("Version %s in package sources for %s is not meeting the version constrains (%s) of the project. Forwarding request to upstream registry.", pkgVersion, pkgName, reqVersion)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
pkgArchive, err := p.createPackage(pkgPath, pkg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
integrity, shasum, err := p.createHashes(pkgArchive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Package{
|
||||
ID: pkg.Name,
|
||||
Name: pkg.Name,
|
||||
DistTags: DistTags{
|
||||
Latest: pkg.Version,
|
||||
},
|
||||
Versions: map[string]Version{
|
||||
pkg.Version: {
|
||||
ID: pkg.Name,
|
||||
Name: pkg.Name,
|
||||
Version: pkg.Version,
|
||||
Dependencies: pkg.Dependencies,
|
||||
Dist: Dist{
|
||||
Integrity: integrity,
|
||||
Shasum: shasum,
|
||||
Tarball: fmt.Sprintf("%s/files/%s", p.internalProxyUrl, filepath.Base(pkgArchive)),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Proxy) createPackage(pkgPath string, pkg PackageJson) (string, error) {
|
||||
err := os.MkdirAll(p.pkgCachePath, 0755)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pkgArchive := filepath.Join(p.pkgCachePath, sanitizePkgName(pkg.Name, pkg.Version)+".tar")
|
||||
|
||||
files, err := archiver.FilesFromDisk(nil, map[string]string{
|
||||
pkgPath: ".",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
filesFiltered := []archiver.File{}
|
||||
|
||||
filterRegex := regexp.MustCompile(`^node_modules|.git`)
|
||||
|
||||
for _, file := range files {
|
||||
if filterRegex.MatchString(file.NameInArchive) {
|
||||
continue
|
||||
}
|
||||
filesFiltered = append(filesFiltered, file)
|
||||
}
|
||||
|
||||
out, err := os.Create(pkgArchive)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
format := archiver.CompressedArchive{
|
||||
Archival: archiver.Tar{},
|
||||
}
|
||||
|
||||
err = format.Archive(context.Background(), out, filesFiltered)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pkgArchive, nil
|
||||
}
|
||||
|
||||
func (p *Proxy) createHashes(pkgArchive string) (string, string, error) {
|
||||
// Open the file
|
||||
file, err := os.Open(pkgArchive)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Create a new SHA256 hash
|
||||
hash := sha512.New()
|
||||
|
||||
// Copy the file data into the hash
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Get the hash sum
|
||||
hashSum := hash.Sum(nil)
|
||||
|
||||
// Generate the integrity string (SHA-256 base64-encoded)
|
||||
integrity := "sha512-" + base64.StdEncoding.EncodeToString(hashSum)
|
||||
|
||||
// Generate the shasum (hexadecimal representation)
|
||||
shasum := fmt.Sprintf("%x", hashSum)
|
||||
|
||||
return integrity, shasum, nil
|
||||
}
|
||||
|
||||
// Replace all characters of the pages name that are not allowed in a URL with a hyphen.
|
||||
func sanitizePkgName(pkgName string, version string) string {
|
||||
pkgName = strings.ReplaceAll(pkgName, "@", "")
|
||||
pkgName = strings.ReplaceAll(pkgName, "/", "_")
|
||||
version = strings.ReplaceAll(version, ".", "_")
|
||||
return fmt.Sprintf("%s_%s", pkgName, version)
|
||||
}
|
51
npmproxy/types.go
Normal file
51
npmproxy/types.go
Normal file
@ -0,0 +1,51 @@
|
||||
package npmproxy
|
||||
|
||||
type PackageJson struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
}
|
||||
|
||||
type Package struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
DistTags DistTags `json:"dist-tags"`
|
||||
Versions map[string]Version `json:"versions"`
|
||||
Readme string `json:"readme"`
|
||||
Repository Repository `json:"repository"`
|
||||
Author Author `json:"author"`
|
||||
License string `json:"license"`
|
||||
}
|
||||
|
||||
type DistTags struct {
|
||||
Latest string `json:"latest"`
|
||||
}
|
||||
|
||||
type Author struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type Dist struct {
|
||||
Integrity string `json:"integrity"`
|
||||
Shasum string `json:"shasum"`
|
||||
Tarball string `json:"tarball"`
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Author Author `json:"author"`
|
||||
License string `json:"license"`
|
||||
Repository Repository `json:"repository"`
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
Readme string `json:"readme"`
|
||||
Dist Dist `json:"dist"`
|
||||
}
|
Reference in New Issue
Block a user