Compare commits
1 Commits
developmen
...
main
Author | SHA1 | Date |
---|---|---|
OLUWADAMILOLA OKUSANYA | 3ad3c65898 |
|
@ -1 +0,0 @@
|
||||||
**/userdata/*
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"gosimplenpm/handler"
|
||||||
|
)
|
||||||
|
|
||||||
|
type application struct {
|
||||||
|
logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) Routes() *mux.Router {
|
||||||
|
m := mux.NewRouter()
|
||||||
|
|
||||||
|
// main handler
|
||||||
|
m.HandleFunc("/{name}", handler.Get).Methods("GET")
|
||||||
|
m.HandleFunc("/{name}", handler.Publish).Methods("PUT")
|
||||||
|
// tar handlers
|
||||||
|
m.HandleFunc("/{name}/-/{tar}", handler.Tar).Methods("GET")
|
||||||
|
// tag handlers
|
||||||
|
m.HandleFunc("/-/package/{name}/dist-tags/{tag}", handler.DistTagDelete).Methods("DELETE")
|
||||||
|
m.HandleFunc("/-/package/{name}/dist-tags/{tag}", handler.DistTagPut).Methods("PUT")
|
||||||
|
m.HandleFunc("/-/package/{name}/dist-tags", handler.DistTagGet).Methods("GET")
|
||||||
|
return m
|
||||||
|
}
|
|
@ -1,26 +0,0 @@
|
||||||
package gosimplenpm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"gosimplenpm/config"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var configCmd = &cobra.Command{
|
|
||||||
Use: "config",
|
|
||||||
Aliases: []string{"conf"},
|
|
||||||
Short: "Display the config file",
|
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
|
||||||
err := config.PrintConfigFile()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error printing config: %+v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(configCmd)
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
package gosimplenpm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"gosimplenpm/config"
|
|
||||||
"gosimplenpm/handler"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
|
||||||
Use: "gosimplenpm",
|
|
||||||
Short: "Gosimplenpm is an implementation of npm registry server",
|
|
||||||
Long: `Gosimplenpm is an implemenation of npm registry server.
|
|
||||||
|
|
||||||
It creates the repository to host all your private npm repos. It can
|
|
||||||
serve the npm packages offline.
|
|
||||||
|
|
||||||
Documentation about the npm private registry:
|
|
||||||
https://docs.npmjs.com/packages-and-modules
|
|
||||||
`,
|
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
|
||||||
err := config.VerifyConfig()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error verifying config: %+v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
var cfg config.Config
|
|
||||||
err = config.LoadOrCreateConfig(&cfg)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error loading config: %+v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
lvl, err := logrus.ParseLevel(cfg.LogLevel)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%+v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
log := &logrus.Logger{
|
|
||||||
Out: os.Stdout,
|
|
||||||
Level: lvl,
|
|
||||||
Formatter: &logrus.TextFormatter{
|
|
||||||
FullTimestamp: true,
|
|
||||||
TimestampFormat: "2009-01-02 15:15:15",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
app := &handler.Application{
|
|
||||||
Conf: cfg,
|
|
||||||
Logger: log,
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("\n Server is starting....")
|
|
||||||
err = app.Start()
|
|
||||||
fmt.Println("Why!")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Server start up error: %+v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func Execute() {
|
|
||||||
err := rootCmd.Execute()
|
|
||||||
if err != nil {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
|
||||||
// hide help command
|
|
||||||
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
|
|
||||||
// hide help flag
|
|
||||||
rootCmd.PersistentFlags().BoolP("help", "h", false, "This help")
|
|
||||||
rootCmd.PersistentFlags().Lookup("help").Hidden = true
|
|
||||||
// TODO: get some variables from flags
|
|
||||||
rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface and port for server")
|
|
||||||
_ = rootCmd.MarkFlagRequired("listen")
|
|
||||||
rootCmd.Flags().BoolVarP(&config.CanLog, "verbose", "v", config.CanLog, "Logging level for server")
|
|
||||||
rootCmd.Flags().StringVar(&config.NpmRepoDir, "repodir", config.NpmRepoDir, "Repo dir to house published packages")
|
|
||||||
}
|
|
316
config/conf.go
316
config/conf.go
|
@ -1,316 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// File to store the app configuration, like username, password, token, repo dir, logging level
|
|
||||||
ConfigFilePath string
|
|
||||||
|
|
||||||
// Repo dir
|
|
||||||
NpmRepoDir string
|
|
||||||
|
|
||||||
// HTTPListen
|
|
||||||
HTTPListen string
|
|
||||||
|
|
||||||
// Set Verbose Logging
|
|
||||||
CanLog bool
|
|
||||||
|
|
||||||
// Logging Level
|
|
||||||
LoggingLvl string
|
|
||||||
|
|
||||||
// Username
|
|
||||||
RegUser string
|
|
||||||
|
|
||||||
// Password
|
|
||||||
RegPwd string
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
RepoDir string `json:"repoDir"`
|
|
||||||
IpAddress string `json:"ipAddress"`
|
|
||||||
LogLevel string `json:"logLevel"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkIfCorrectIPPort(s string) bool {
|
|
||||||
host, port, _ := net.SplitHostPort(s)
|
|
||||||
if host == "" || port == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if net.ParseIP(host) != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
_, err := net.ResolveIPAddr("ip", host)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func VerifyConfig() error {
|
|
||||||
if !checkIfCorrectIPPort(HTTPListen) {
|
|
||||||
return errors.New("ip address should be in the format of <ip>:<port>")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if config file exists
|
|
||||||
dirname, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
configDirPath, err := checkOrCreateConfigDir(dirname, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigFilePath = path.Join(configDirPath, "config.json")
|
|
||||||
|
|
||||||
if NpmRepoDir == "" {
|
|
||||||
NpmRepoDir = path.Join(dirname, ".gosimplenpm", "registry")
|
|
||||||
err := checkOrCreateRepoDir(NpmRepoDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if NpmRepoDir != "" {
|
|
||||||
err := checkOrCreateRepoDir(NpmRepoDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if CanLog {
|
|
||||||
LoggingLvl = "DEBUG"
|
|
||||||
fmt.Println("\n Enabled debug logging")
|
|
||||||
} else {
|
|
||||||
LoggingLvl = "INFO"
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkOrCreateConfigDir(fp string, canCreate bool) (string, error) {
|
|
||||||
configDirPath := path.Join(fp, ".gosimplenpm", "config")
|
|
||||||
ok := isDir(configDirPath)
|
|
||||||
if !ok && canCreate {
|
|
||||||
err := os.MkdirAll(configDirPath, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !ok && !canCreate {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
return configDirPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkOrCreateRepoDir(repoDirPath string) error {
|
|
||||||
ok := isDir(repoDirPath)
|
|
||||||
if !ok {
|
|
||||||
err := os.MkdirAll(repoDirPath, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createConfig(cfg *Config, recreate bool) error {
|
|
||||||
var scanner *bufio.Scanner
|
|
||||||
if recreate {
|
|
||||||
fmt.Println("\nNew config variables. Saving...")
|
|
||||||
} else {
|
|
||||||
fmt.Println("\nConfig file is not found. Creating...")
|
|
||||||
}
|
|
||||||
|
|
||||||
configFile, err := os.Create(ConfigFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer configFile.Close()
|
|
||||||
|
|
||||||
cfg.IpAddress = HTTPListen
|
|
||||||
cfg.LogLevel = LoggingLvl
|
|
||||||
|
|
||||||
// Get username
|
|
||||||
if cfg.Token == "" {
|
|
||||||
fmt.Println("Enter your username: ")
|
|
||||||
scanner = bufio.NewScanner(os.Stdin)
|
|
||||||
scanner.Scan()
|
|
||||||
err = scanner.Err()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
RegUser = scanner.Text()
|
|
||||||
|
|
||||||
fmt.Println("Enter your password: ")
|
|
||||||
scanner = bufio.NewScanner(os.Stdin)
|
|
||||||
scanner.Scan()
|
|
||||||
err = scanner.Err()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
RegPwd = scanner.Text()
|
|
||||||
|
|
||||||
token, err := generateAuthToken()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Token = token
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("The npm authToken is %s.\n", cfg.Token)
|
|
||||||
|
|
||||||
err = json.NewEncoder(configFile).Encode(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadConfig(cfg *Config) error {
|
|
||||||
configFile, err := os.Open(ConfigFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer configFile.Close()
|
|
||||||
|
|
||||||
err = json.NewDecoder(configFile).Decode(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadOrCreateConfig(cfg *Config) error {
|
|
||||||
var err error
|
|
||||||
ok := isFile(ConfigFilePath)
|
|
||||||
|
|
||||||
// If file is not found
|
|
||||||
if !ok {
|
|
||||||
err = createConfig(cfg, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
// File is found
|
|
||||||
err = loadConfig(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Token == "" || cfg.IpAddress != HTTPListen || cfg.LogLevel != LoggingLvl || cfg.RepoDir != NpmRepoDir {
|
|
||||||
// recreate the config file
|
|
||||||
err = createConfig(cfg, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func isFile(fp string) bool {
|
|
||||||
info, err := os.Stat(fp)
|
|
||||||
if os.IsNotExist(err) || !info.Mode().IsRegular() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func isDir(fp string) bool {
|
|
||||||
info, err := os.Stat(fp)
|
|
||||||
if os.IsNotExist(err) || !info.IsDir() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash password
|
|
||||||
func hashPassword(password string) (string, error) {
|
|
||||||
|
|
||||||
// Convert password string to byte slice
|
|
||||||
var passwordBytes = []byte(password)
|
|
||||||
// Hash password with Bcrypt's min cost
|
|
||||||
hashedPasswordBytes, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.MinCost)
|
|
||||||
return string(hashedPasswordBytes), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if two passwords match using Bcrypt's CompareHashAndPassword
|
|
||||||
// which return nil on success and an error on failure.
|
|
||||||
// func doPasswordsMatch(hashedPassword, currPassword string) bool {
|
|
||||||
// err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(currPassword))
|
|
||||||
// return err == nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func generateAuthToken() (string, error) {
|
|
||||||
hashed, err := hashPassword(RegPwd)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
token := fmt.Sprintf("%s::%s", RegUser, hashed)
|
|
||||||
return token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrintConfigFile() error {
|
|
||||||
// Check if config file exists
|
|
||||||
dirname, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
configDirPath, err := checkOrCreateConfigDir(dirname, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if configDirPath == "" {
|
|
||||||
return errors.New("config dir is not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigFilePath = path.Join(configDirPath, "config.json")
|
|
||||||
ok := isFile(ConfigFilePath)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("config file is not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
configFile, err := os.Open(ConfigFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer configFile.Close()
|
|
||||||
|
|
||||||
b, err := io.ReadAll(configFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var result map[string]interface{}
|
|
||||||
err = json.Unmarshal([]byte(b), &result)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pretty-print the result
|
|
||||||
marshaled, err := json.MarshalIndent(result, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Printing config located at %s: \n %s\n", ConfigFilePath, (marshaled))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
14
go.mod
14
go.mod
|
@ -2,16 +2,6 @@ module gosimplenpm
|
||||||
|
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require github.com/gorilla/mux v1.8.0
|
||||||
github.com/gorilla/mux v1.8.0
|
|
||||||
github.com/sirupsen/logrus v1.9.3
|
|
||||||
github.com/spf13/cobra v1.7.0
|
|
||||||
golang.org/x/crypto v0.10.0
|
|
||||||
golang.org/x/mod v0.11.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
// replace gosimplenpm/handler => ./handler
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
|
||||||
golang.org/x/sys v0.9.0 // indirect
|
|
||||||
)
|
|
||||||
|
|
29
go.sum
29
go.sum
|
@ -1,31 +1,2 @@
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
|
||||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
|
||||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
|
||||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
|
||||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
|
||||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gosimplenpm/config"
|
|
||||||
"gosimplenpm/middlewares"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Application struct {
|
|
||||||
Logger *logrus.Logger
|
|
||||||
Conf config.Config
|
|
||||||
Mux *mux.Router
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *Application) Routes() {
|
|
||||||
|
|
||||||
// Need to use UseEncodedPath as shown here https://github.com/gorilla/mux/blob/master/mux.go#L269
|
|
||||||
m := mux.NewRouter().StrictSlash(true).UseEncodedPath()
|
|
||||||
|
|
||||||
m.Use(middlewares.LogMiddleware(app.Logger))
|
|
||||||
|
|
||||||
// main handler
|
|
||||||
m.HandleFunc("/{name}", GetPackage(app.Logger, app.Conf)).Methods("GET")
|
|
||||||
m.HandleFunc("/{name}", middlewares.AuthMiddleware(app.Conf)(Publish(app.Logger, app.Conf))).Methods("PUT")
|
|
||||||
// tar handlers
|
|
||||||
m.HandleFunc("/{name}/-/{tar}", PackageTarGet(app.Logger, app.Conf)).Methods("GET")
|
|
||||||
// tag handlers
|
|
||||||
m.HandleFunc("/-/package/{name}/dist-tags/{tag}", middlewares.AuthMiddleware(app.Conf)(DistTagDelete(app.Logger, app.Conf))).Methods("DELETE")
|
|
||||||
m.HandleFunc("/-/package/{name}/dist-tags/{tag}", middlewares.AuthMiddleware(app.Conf)(DistTagPut(app.Logger, app.Conf))).Methods("PUT")
|
|
||||||
m.HandleFunc("/-/package/{name}/dist-tags", DistTagGet(app.Logger, app.Conf)).Methods("GET")
|
|
||||||
m.NotFoundHandler = http.HandlerFunc(NotFound)
|
|
||||||
app.Mux = m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *Application) Start() error {
|
|
||||||
app.Routes()
|
|
||||||
server := &http.Server{
|
|
||||||
Addr: app.Conf.IpAddress,
|
|
||||||
Handler: app.Mux,
|
|
||||||
ReadTimeout: 4 * time.Second,
|
|
||||||
WriteTimeout: 4 * time.Second,
|
|
||||||
}
|
|
||||||
return server.ListenAndServe()
|
|
||||||
}
|
|
|
@ -3,30 +3,22 @@ package handler
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"gosimplenpm/config"
|
|
||||||
"gosimplenpm/storage"
|
"gosimplenpm/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetPackage(lg *logrus.Logger, cfg config.Config) http.HandlerFunc {
|
func Get(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
packageName := mux.Vars(r)["name"]
|
||||||
escapedName := mux.Vars(r)["name"]
|
|
||||||
packageName, _ := url.PathUnescape(escapedName)
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "get-package",
|
|
||||||
}).Debugf("Package name => %s\n", packageName)
|
|
||||||
|
|
||||||
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
|
fileToServe, err := storage.GetPackageFromStore(packageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if fileToServe == "" {
|
||||||
ret := fmt.Sprintf("Package not found: %s", packageName)
|
ret := fmt.Sprintf("Package not found: %s", packageName)
|
||||||
http.Error(w, ret, http.StatusNotFound)
|
http.Error(w, ret, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
@ -34,5 +26,4 @@ func GetPackage(lg *logrus.Logger, cfg config.Config) http.HandlerFunc {
|
||||||
|
|
||||||
// serve file
|
// serve file
|
||||||
http.ServeFile(w, r, fileToServe)
|
http.ServeFile(w, r, fileToServe)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This handler is executed when the router cannot match any route
|
|
||||||
func NotFound(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Error(w, "Invalid url", http.StatusBadRequest)
|
|
||||||
}
|
|
|
@ -1,142 +1,9 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"gosimplenpm/config"
|
|
||||||
"gosimplenpm/serviceidos"
|
|
||||||
"gosimplenpm/storage"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type NPMClientPutRequest struct {
|
func Publish(w http.ResponseWriter, r *http.Request) {
|
||||||
Request serviceidos.IndexJson
|
|
||||||
}
|
|
||||||
|
|
||||||
func Publish(lg *logrus.Logger, cfg config.Config) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// (1) Parse Json Body
|
|
||||||
// (2) Check if package exists in the folder.
|
|
||||||
// (a) if it does, ckeck if it is the same version. If it is, return error. Else modify index.json from (2)
|
|
||||||
// (b) If it does not, add the latest tag to the new index.json
|
|
||||||
|
|
||||||
escapedName := mux.Vars(r)["name"]
|
|
||||||
packageName, _ := url.PathUnescape(escapedName)
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "publish",
|
|
||||||
}).Debugf("Package name => %s\n", packageName)
|
|
||||||
|
|
||||||
var cr NPMClientPutRequest
|
|
||||||
// Parse json body
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&cr.Request)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "publish",
|
|
||||||
}).Debugf("Error unmarshaling put request: %+v\n", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract relevant data from index.json
|
|
||||||
index := 0
|
|
||||||
var tag string
|
|
||||||
var version string
|
|
||||||
var versionData serviceidos.IndexJsonVersions
|
|
||||||
// TODO: Fix this as the order is not guaranteed
|
|
||||||
for key, value := range cr.Request.DistTags {
|
|
||||||
if index == 0 {
|
|
||||||
tag = key
|
|
||||||
version = value
|
|
||||||
break
|
|
||||||
}
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
versionData = cr.Request.Versions[version]
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "publish",
|
|
||||||
}).Debugf("For version(%s) with tag(%s), versionData => %+v\n", version, tag, versionData)
|
|
||||||
|
|
||||||
// Rewrite the tarball path
|
|
||||||
tarballFileName := strings.Split(versionData.Dist.Tarball, "/-/")[1]
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "publish",
|
|
||||||
}).Debugf("TarballName => %s\n", tarballFileName)
|
|
||||||
// versionData.Dist.Tarball = fmt.Sprintf("file://%s", packageFilePath)
|
|
||||||
versionData.Dist.Tarball = fmt.Sprintf("http://%s/%s/-/%s", r.Host, url.PathEscape(packageName), url.PathEscape(tarballFileName))
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "publish",
|
|
||||||
}).Debugf("versionData.Dist.Tarball => %s\n", versionData.Dist.Tarball)
|
|
||||||
|
|
||||||
tarBallFile := strings.Split(tarballFileName, "/")[1]
|
|
||||||
packageFilePath := path.Join(cfg.RepoDir, packageName, tarBallFile)
|
|
||||||
|
|
||||||
// Try to get the index.json from the store
|
|
||||||
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var jsonFile serviceidos.IndexJson
|
|
||||||
if !found {
|
|
||||||
// new package
|
|
||||||
jsonFile = cr.Request
|
|
||||||
jsonFile.DistTags["latest"] = version
|
|
||||||
} else {
|
|
||||||
// old package
|
|
||||||
err = storage.ReadIndexJson(fileToServe, &jsonFile, lg)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checking that you are not publishing over a pervious published version
|
|
||||||
if jsonFile.Versions[version].Version == version {
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "publish",
|
|
||||||
}).Debugf("Version %s of package %s already exists!!\n", version, packageName)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rewrite attachments
|
|
||||||
jsonFile.DistTags[tag] = version
|
|
||||||
nAttachments := make(map[string]serviceidos.IndexJsonAttachments)
|
|
||||||
nAttachments[fmt.Sprintf("%s-%s.tgz", packageName, version)] = cr.Request.Attachments[fmt.Sprintf("%s-%s.tgz", packageName, version)]
|
|
||||||
jsonFile.Attachments = nAttachments
|
|
||||||
|
|
||||||
// Merge in the new version data
|
|
||||||
jsonFile.Versions[version] = versionData
|
|
||||||
}
|
|
||||||
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "publish",
|
|
||||||
}).Debugln("FiletoServe ==> ", fileToServe)
|
|
||||||
|
|
||||||
// Write index.json
|
|
||||||
err = storage.WriteIndexJson(fileToServe, &jsonFile, lg)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "publish",
|
|
||||||
}).Debugln("Package path => ", packageFilePath)
|
|
||||||
// Write bundled package
|
|
||||||
packageData := jsonFile.Attachments[fmt.Sprintf("%s-%s.tgz", packageName, version)].Data
|
|
||||||
err = storage.WritePackageToStore(packageFilePath, packageData, lg)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,84 +1,9 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"gosimplenpm/config"
|
|
||||||
"gosimplenpm/serviceidos"
|
|
||||||
"gosimplenpm/storage"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/mod/semver"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func DistTagDelete(lg *logrus.Logger, cfg config.Config) http.HandlerFunc {
|
func DistTagDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
escapedName := mux.Vars(r)["name"]
|
|
||||||
packageName, _ := url.PathUnescape(escapedName)
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "dist-tags-delete",
|
|
||||||
}).Debugf("Package name => %s\n", packageName)
|
|
||||||
|
|
||||||
escapedName = mux.Vars(r)["tag"]
|
|
||||||
tag, _ := url.PathUnescape(escapedName)
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "dist-tags-delete",
|
|
||||||
}).Debugf("Tag => %s\n", tag)
|
|
||||||
|
|
||||||
if semver.IsValid(tag) {
|
|
||||||
http.Error(w, "Tag cannot be a semver version", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if tag == "latest" {
|
|
||||||
http.Error(w, "Cannot delete the latest tag", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
ret := fmt.Sprintf("Package not found: %s", packageName)
|
|
||||||
http.Error(w, ret, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var jsonFile serviceidos.IndexJson
|
|
||||||
err = storage.ReadIndexJson(fileToServe, &jsonFile, lg)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(jsonFile.DistTags, tag)
|
|
||||||
|
|
||||||
// Write index.json
|
|
||||||
err = storage.WriteIndexJson(fileToServe, &jsonFile, lg)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
distTagsString, _ := json.Marshal(jsonFile.DistTags)
|
|
||||||
|
|
||||||
response := serviceidos.TagPutResponse{
|
|
||||||
Ok: true,
|
|
||||||
ID: escapedName,
|
|
||||||
DistTags: string(distTagsString),
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonString, _ := json.Marshal(response)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(jsonString)))
|
|
||||||
w.Write(jsonString)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +1,9 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"gosimplenpm/config"
|
|
||||||
"gosimplenpm/serviceidos"
|
|
||||||
"gosimplenpm/storage"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func DistTagGet(lg *logrus.Logger, cfg config.Config) http.HandlerFunc {
|
func DistTagGet(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
escapedName := mux.Vars(r)["name"]
|
|
||||||
packageName, _ := url.PathUnescape(escapedName)
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "dist-tags-get",
|
|
||||||
}).Debugf("Package name => %s\n", packageName)
|
|
||||||
|
|
||||||
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
ret := fmt.Sprintf("Package not found: %s", packageName)
|
|
||||||
http.Error(w, ret, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var jsonFile serviceidos.IndexJson
|
|
||||||
err = storage.ReadIndexJson(fileToServe, &jsonFile, lg)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonString, _ := json.Marshal(jsonFile.DistTags)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(jsonString)))
|
|
||||||
w.Write(jsonString)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,92 +1,9 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"gosimplenpm/config"
|
|
||||||
"gosimplenpm/serviceidos"
|
|
||||||
"gosimplenpm/storage"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/mod/semver"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func DistTagPut(lg *logrus.Logger, cfg config.Config) http.HandlerFunc {
|
func DistTagPut(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
escapedName := mux.Vars(r)["name"]
|
|
||||||
packageName, _ := url.PathUnescape(escapedName)
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "dist-tags-put",
|
|
||||||
}).Debugf("Package name => %s\n", packageName)
|
|
||||||
|
|
||||||
escapedName = mux.Vars(r)["tag"]
|
|
||||||
tag, _ := url.PathUnescape(escapedName)
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "dist-tags-put",
|
|
||||||
}).Debugf("Tag => %s\n", tag)
|
|
||||||
|
|
||||||
if semver.IsValid(tag) {
|
|
||||||
http.Error(w, "Tag cannot be a semver version", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if tag == "latest" {
|
|
||||||
http.Error(w, "Cannot delete the latest tag", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, _ := io.ReadAll(r.Body)
|
|
||||||
var version string
|
|
||||||
_ = json.Unmarshal(body, &version)
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "dist-tags-put",
|
|
||||||
}).Debugf("Body => %s", version)
|
|
||||||
|
|
||||||
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
ret := fmt.Sprintf("Package not found: %s", packageName)
|
|
||||||
http.Error(w, ret, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var jsonFile serviceidos.IndexJson
|
|
||||||
err = storage.ReadIndexJson(fileToServe, &jsonFile, lg)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonFile.DistTags[tag] = version
|
|
||||||
|
|
||||||
// Write index.json
|
|
||||||
err = storage.WriteIndexJson(fileToServe, &jsonFile, lg)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
distTagsString, _ := json.Marshal(jsonFile.DistTags)
|
|
||||||
|
|
||||||
response := serviceidos.TagPutResponse{
|
|
||||||
Ok: true,
|
|
||||||
ID: escapedName,
|
|
||||||
DistTags: string(distTagsString),
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonString, _ := json.Marshal(response)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(jsonString)))
|
|
||||||
w.Write(jsonString)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +1,9 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"gosimplenpm/config"
|
|
||||||
"gosimplenpm/storage"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func PackageTarGet(lg *logrus.Logger, cfg config.Config) http.HandlerFunc {
|
func Tar(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Sample output of npm view
|
|
||||||
// Public
|
|
||||||
// dist
|
|
||||||
// .tarball: https://registry.npmjs.org/react/-/react-18.2.0.tgz
|
|
||||||
// LocalHost
|
|
||||||
// dist
|
|
||||||
// .tarball: http://localhost:4000/@ookusanya/package1/-/package1-0.2.0.tgz
|
|
||||||
|
|
||||||
escapedName := mux.Vars(r)["name"]
|
|
||||||
packageName, _ := url.PathUnescape(escapedName)
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "get-tar",
|
|
||||||
}).Debugf("Package name => %s\n", packageName)
|
|
||||||
escapedName = mux.Vars(r)["tar"]
|
|
||||||
tarFileName, _ := url.PathUnescape(escapedName)
|
|
||||||
lg.WithFields(logrus.Fields{
|
|
||||||
"function": "get-tar",
|
|
||||||
}).Debugf("Tarfile name => %s\n", tarFileName)
|
|
||||||
|
|
||||||
versionName := strings.Split(strings.Split(tarFileName, "-")[1], ".tgz")[0]
|
|
||||||
fileAsString, err := storage.GetTarFromStore(packageName, versionName, cfg.RepoDir, lg)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sending the tar as a base64 string
|
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len([]byte(fileAsString))))
|
|
||||||
io.Copy(w, bytes.NewReader([]byte(fileAsString)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
34
main.go
34
main.go
|
@ -1,35 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gosimplenpm/cmd/gosimplenpm"
|
"log"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
gosimplenpm.Execute()
|
app := new(application)
|
||||||
|
log.Print("Starting server on port 4000")
|
||||||
|
err := http.ListenAndServe(":4000", app.Routes())
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// func main() {
|
|
||||||
// log := &logrus.Logger{
|
|
||||||
// Out: os.Stdout,
|
|
||||||
// Level: logrus.DebugLevel,
|
|
||||||
// Formatter: &logrus.TextFormatter{
|
|
||||||
// FullTimestamp: true,
|
|
||||||
// TimestampFormat: "2009-01-02 15:15:15",
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
|
|
||||||
// var cfg config.Config
|
|
||||||
// err := config.LoadConfiguration("userdata/config.json", &cfg)
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatalf("Config is not loaded: %+v\n", err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// app := &gosimplenpm.Application{
|
|
||||||
// Conf: cfg,
|
|
||||||
// Logger: log,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// log.Infoln("Starting server on port 4000")
|
|
||||||
// err = http.ListenAndServe(":4000", app.Routes())
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
package middlewares
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gosimplenpm/config"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func AuthMiddleware(cfg config.Config) func(http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// get headers
|
|
||||||
authHeader := r.Header.Get("Authorization")
|
|
||||||
authFields := strings.Fields(authHeader)
|
|
||||||
if len(authFields) != 2 || strings.ToLower(authFields[0]) != "bearer" {
|
|
||||||
http.Error(w, "Authentication Error", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
token := authFields[1]
|
|
||||||
if token != cfg.Token {
|
|
||||||
http.Error(w, "Authentication Error", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package middlewares
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httputil"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func LogMiddleware(lg *logrus.Logger) func(http.Handler) http.Handler {
|
|
||||||
return func(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
lg.Debugf("%s - %s - %s", r.Method, r.URL, r.Host)
|
|
||||||
|
|
||||||
hasBody := false
|
|
||||||
if r.Method == "PUT" {
|
|
||||||
hasBody = true
|
|
||||||
}
|
|
||||||
|
|
||||||
requestDump, err := httputil.DumpRequest(r, hasBody)
|
|
||||||
if err != nil {
|
|
||||||
lg.Debugln(err)
|
|
||||||
}
|
|
||||||
lg.Debugln("RequestDump: ", string(requestDump))
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
package serviceidos
|
|
||||||
|
|
||||||
type IndexJsonAttachments struct {
|
|
||||||
ContentType string `json:"content_type"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
Length int `json:"length"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type IndexJsonDist struct {
|
|
||||||
Integrity string `json:"integrity"`
|
|
||||||
Shasum string `json:"shasum"`
|
|
||||||
Tarball string `json:"tarball"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type IndexJsonVersions struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Main string `json:"main,omitempty"`
|
|
||||||
Scripts map[string]string `json:"scripts,omitempty"`
|
|
||||||
Author string `json:"author,omitempty"`
|
|
||||||
License string `json:"license"`
|
|
||||||
Files []string `json:"files"`
|
|
||||||
Readme string `json:"readme,omitempty"`
|
|
||||||
ID string `json:"_id"`
|
|
||||||
NodeVersion string `json:"_nodeVersion"`
|
|
||||||
NpmVersion string `json:"_npmVersion"`
|
|
||||||
Dist IndexJsonDist `json:"dist"`
|
|
||||||
Dependencies map[string]string `json:"dependencies,omitempty"`
|
|
||||||
DevDependencies map[string]string `json:"devDependencies,omitempty"`
|
|
||||||
Resolutions map[string]string `json:"resolutions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type IndexJson struct {
|
|
||||||
ID string `json:"_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
DistTags map[string]string `json:"dist-tags"`
|
|
||||||
Versions map[string]IndexJsonVersions `json:"versions"`
|
|
||||||
Access string `json:"access"`
|
|
||||||
Attachments map[string]IndexJsonAttachments `json:"_attachments"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TagPutResponse struct {
|
|
||||||
Ok bool `json:"ok"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
DistTags string `json:"dist-tags"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TagDeleteResponse struct {
|
|
||||||
Ok bool `json:"ok"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
DistTags string `json:"dist-tags"`
|
|
||||||
}
|
|
172
storage/fs.go
172
storage/fs.go
|
@ -1,187 +1,33 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
|
||||||
"compress/gzip"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gosimplenpm/serviceidos"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetIndexJsonFromStore(packageName string, registryPath string, log *logrus.Logger) (string, bool, error) {
|
func GetPackageFromStore(packageName string) (string, error) {
|
||||||
fileToServe := ""
|
fileToServe := ""
|
||||||
found := false
|
searchDir, err := filepath.Abs("./examples")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("File repo not found: +%v/n", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
err := filepath.WalkDir(registryPath, func(fp string, info fs.DirEntry, e error) error {
|
err = filepath.WalkDir(searchDir, func(fp string, info fs.DirEntry, e error) error {
|
||||||
if strings.Contains(fp, path.Join(packageName, "index.json")) {
|
if strings.Contains(fp, path.Join(packageName, "index.json")) {
|
||||||
fileToServe = fp
|
fileToServe = fp
|
||||||
found = true
|
|
||||||
}
|
}
|
||||||
return e
|
return e
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(logrus.Fields{
|
fmt.Printf("List files error: +%v/n", err)
|
||||||
"function": "get-index-json-from-store",
|
|
||||||
}).Debugf("List files error: +%v\n", err)
|
|
||||||
return fileToServe, found, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileToServe == "" && !found {
|
|
||||||
fileToServe = path.Join(registryPath, packageName, "index.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileToServe, found, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetTarFromStore(packageName string, tarFileName string, registryPath string, log *logrus.Logger) (string, error) {
|
|
||||||
fileToServe := ""
|
|
||||||
|
|
||||||
err := filepath.WalkDir(registryPath, func(fp string, info fs.DirEntry, e error) error {
|
|
||||||
if strings.Contains(fp, path.Join(packageName, tarFileName)) {
|
|
||||||
fileToServe = fp
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"function": "get-tar-from-store",
|
|
||||||
}).Debugf("List files error: +%v\n", err)
|
|
||||||
return fileToServe, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileToServe == "" {
|
|
||||||
return fileToServe, fmt.Errorf("file %s is not found for package %s", tarFileName, packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.Open(fileToServe)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"function": "get-tar-from-store",
|
|
||||||
}).Debugf("Open error: %s\n", fileToServe)
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
archive, err := gzip.NewReader(file)
|
return fileToServe, nil
|
||||||
if err != nil {
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"function": "get-tar-from-store",
|
|
||||||
}).Debugf("Archive Open error: %s\n", fileToServe)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
tr := tar.NewReader(archive)
|
|
||||||
bs, err := io.ReadAll(tr)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"function": "get-tar-from-store",
|
|
||||||
}).Debugf("Archive Read error: %s\n", fileToServe)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(bs), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadIndexJson(fPath string, res *serviceidos.IndexJson, log *logrus.Logger) error {
|
|
||||||
jsonFile, err := os.Open(fPath)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"function": "read-index-json",
|
|
||||||
}).Debugf("File Not found: %s\n", fPath)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer jsonFile.Close()
|
|
||||||
|
|
||||||
err = json.NewDecoder(jsonFile).Decode(res)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"function": "read-index-json",
|
|
||||||
}).Debugf("Unmarshalerror: %+v\n", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func WriteIndexJson(fPath string, res *serviceidos.IndexJson, log *logrus.Logger) error {
|
|
||||||
// Need to create the directory first
|
|
||||||
parent := path.Dir(fPath)
|
|
||||||
err := os.MkdirAll(parent, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"function": "write-index-json",
|
|
||||||
}).Debugf("Folder (%s) creation failed.\n", fPath)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the file
|
|
||||||
jsonFile, err := os.Create(fPath)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"function": "write-index-json",
|
|
||||||
}).Debugf("Creation error for path(%s): %+v\n ", fPath, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer jsonFile.Close()
|
|
||||||
|
|
||||||
err = json.NewEncoder(jsonFile).Encode(res)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"function": "write-index-json",
|
|
||||||
}).Debugf("Marshalerror: %+v\n", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func WritePackageToStore(fPath string, data string, log *logrus.Logger) error {
|
|
||||||
dec, err := base64.StdEncoding.DecodeString(data)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"function": "write-package-to-store",
|
|
||||||
}).Debugf("Base64 Decode error: %+v\n", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dataFile, err := os.Create(fPath)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"function": "write-package-to-store",
|
|
||||||
}).Debugf("Creation error: %s\n", fPath)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer dataFile.Close()
|
|
||||||
|
|
||||||
_, err = dataFile.Write(dec)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"function": "write-package-to-store",
|
|
||||||
}).Debugf("Write error: %s\n", fPath)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = dataFile.Sync()
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"function": "write-package-to-store",
|
|
||||||
}).Debugf("Sync error: %s\n", fPath)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
{"_id":"@ookusanya/simplepackone","name":"@ookusanya/simplepackone","description":"This is a very rough implementation of a private npm registry.","dist-tags":{"latest":"1.2.0"},"versions":{"1.0.0":{"name":"@ookusanya/simplepackone","version":"1.0.0","description":"This is a very rough implementation of a private npm registry.","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" \u0026\u0026 exit 1"},"license":"MIT","files":null,"readme":"# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...","_id":"@ookusanya/simplepackone@1.0.0","_nodeVersion":"16.20.0","_npmVersion":"8.19.4","dist":{"integrity":"sha512-x9cAEHDPM0dLhdyd8baWLTNYskr41QXLNv5HS5WNDg1CVzwcMaAU+N9lYVuOvLf/xPHLielWmX+wLXEtRWQvGw==","shasum":"545a7737b59b1a6f064339a4ebe056e8a9b1511d","tarball":"http://localhost:4000/@ookusanya/simplepackone/-/@ookusanya/simplepackone-1.0.0.tgz"},"dependencies":{"eslint":"^7.x"}},"1.2.0":{"name":"@ookusanya/simplepackone","version":"1.2.0","description":"This is a very rough implementation of a private npm registry.","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" \u0026\u0026 exit 1"},"license":"MIT","files":null,"readme":"# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...","_id":"@ookusanya/simplepackone@1.2.0","_nodeVersion":"16.20.0","_npmVersion":"8.19.4","dist":{"integrity":"sha512-coOiU+ywV/do/+HwK91mfuei9491yfQUedLWjspAjDa58RniJvQNTF/cXDp/sooVdpjgEbCqKVyLOju6C1i3pw==","shasum":"1adafdbe8878372c9f4b554bf30d5bfb1b7896c5","tarball":"http://localhost:4000/@ookusanya%2Fsimplepackone/-/@ookusanya%2Fsimplepackone-1.2.0.tgz"},"dependencies":{"eslint":"^7.x","mocha":"^10.x","uuid":"^9.x"}}},"access":"","_attachments":{"@ookusanya/simplepackone-1.2.0.tgz":{"content_type":"application/octet-stream","data":"H4sIAAAAAAAC/+1XbWvjOBDuZ/+KwQtLCyZ10pfl+uncRGnMOnawne0VlgXVVmJtHctIcl849r/fSEmb9nof73LcXoaA0Lw9M6PMSG5pcUeX7Jg3JXvsfVcH/wD5vn9+egp/xbc0OIGDk7Nzv98/P/dRz+/7nwY+rgc7oE5pKjGUvyFJJHhZ/yNUiEaJmvVqsTx0J0wy9+hgT/8fajf9v1lxAohmx/1/ev6u//tnZ/19/++CfncA3IaumHsB7q9C3HWKNk/0WPFVWzPzrxANcz2jdc+k4qIxiv3eoOevuSVTheSt3kjWzBXldvd8q6y5a0WFAoOKDM2UNmqsqAR8dYmUQl5AI8AIQLWs4AvOyq8ufPwI7JFr6Lto+cN6o52uhNxi1rxgjbJ5TMP8ObiWYQhNwdkrWKZq3ljgb596j1bTxCyKilpm399yu46X7oX77RdkGWjnh/NT9n9KgtGU9Fbl7u//waD/7v4/Oxns+38X9AGuxLrXm3blOHnFFeCPAnb7E0jRLSuw4hVrNDVdDmKB4lbye6oZoBVItuRKy6ee43yAMaO6k0yhr2SU9HqWSZqKNoX18VZwLeQdRGj9wt1fyf9K/0fhkMQZ6elHveP+H/hng3f3f9/f9/9OyBmK9knyZaXhsDiCgfkYS+rugZZ0xWtRU0g2TwLHmTG54sq8AcyIqPBj4fYJlpI2mpUeLCRjZjbgLSqXzAMtAM2gxVeDGRq3Gt8EvFk6FAqENJraDBslFvqBSobKJVClRMFxrpRQiqLbzpwFr5mCQ10xx802Fu6RBSkZrYE36I3BswgeOD4OOo2jCQcTL4wPD5WKuitNDM/imq/4BsGY2zoo47RTmIGJ04OVKPnCrMym1Xa3NVeV55Rm5vHbTiNTGaZ9fngmj2MhQbG6Nh7w5bHOdRud1UEUpzUF1ZsSWdyHSqzeZoIlWnSyQUhW2nQFlswifmeFNl6M+kLUtXjA1BCyKbnJSF2YcY51vRX3zOayPuZGaAx1HYI5gHZ7qhuRqijGfsucdcEQF8tLX6UjDTz2TaM51r4V0uL9OU0c8PmEQJaM8+sgJRBmMEuTL+GIjMANMty7HlyH+SSZ54AaaRDnN5CMIYhv4HMYjzyH/DZLSZZBkkI4nUUhGXkQxsNoPgrjK7hEuzjJIQrxwYdO8wQM4MZVSNBu7ExJOpzgNrgMozC/8WAc5rHxOUanAcyCNA+H8yhIYTZPZ0lGEH7kxEkcxuMUUciUxHkPUREKyBfcQDYJoshCBXOMPrXxDZPZTRpeTXKYJNGIIPOSOFEYXEZkDYVJDaMgnHowCqbBFbFWCXpJwahtorueEMtCvAB/wzxMYgdrMkziPMWth1mm+YvpdZgRD4I0zExBxmmC7k050SKxTtAuJmsvptTw5kRQxeznGdnGMiJBhL4yY/xaeX8x72lPe9rTT0V/APlm4rcAGAAA","length":1089}}}
|
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue