package pdcp import ( "encoding/json" "fmt" "io" "os" "path/filepath" "github.com/projectdiscovery/retryablehttp-go" "github.com/projectdiscovery/utils/env" fileutil "github.com/projectdiscovery/utils/file" folderutil "github.com/projectdiscovery/utils/folder" urlutil "github.com/projectdiscovery/utils/url" "gopkg.in/yaml.v3" ) var ( PDCPDir = filepath.Join(folderutil.HomeDirOrDefault(""), ".pdcp") PDCPCredFile = filepath.Join(PDCPDir, "credentials.yaml") ErrNoCreds = fmt.Errorf("no credentials found in %s", PDCPDir) ) const ( userProfileURL = "https://%s/v1/user?utm_source=%s" apiKeyEnv = "PDCP_API_KEY" apiServerEnv = "PDCP_API_SERVER" ApiKeyHeaderName = "X-Api-Key" dashBoardEnv = "PDCP_DASHBOARD_URL" ) type PDCPCredentials struct { Username string `yaml:"username"` Email string `yaml:"email"` APIKey string `yaml:"api-key"` Server string `yaml:"server"` } type PDCPUserProfileResponse struct { UserName string `json:"name"` Email string `json:"email"` // there are more fields but we don't need them /// below fields are added later on and not part of the response } // PDCPCredHandler is interface for adding / retrieving pdcp credentials // from file system type PDCPCredHandler struct{} // GetCreds retrieves the credentials from the file system or environment variables func (p *PDCPCredHandler) GetCreds() (*PDCPCredentials, error) { credsFromEnv := p.getCredsFromEnv() if credsFromEnv != nil { return credsFromEnv, nil } if !fileutil.FolderExists(PDCPDir) || !fileutil.FileExists(PDCPCredFile) { return nil, ErrNoCreds } bin, err := os.Open(PDCPCredFile) if err != nil { return nil, err } // for future use-cases var creds []PDCPCredentials err = yaml.NewDecoder(bin).Decode(&creds) if err != nil { return nil, err } if len(creds) == 0 { return nil, ErrNoCreds } return &creds[0], nil } // getCredsFromEnv retrieves the credentials from the environment // if not or incomplete credentials are found it return nil func (p *PDCPCredHandler) getCredsFromEnv() *PDCPCredentials { apiKey := env.GetEnvOrDefault(apiKeyEnv, "") apiServer := env.GetEnvOrDefault(apiServerEnv, DefaultApiServer) if apiKey == "" || apiServer == "" { return nil } return &PDCPCredentials{APIKey: apiKey, Server: apiServer} } // SaveCreds saves the credentials to the file system func (p *PDCPCredHandler) SaveCreds(resp *PDCPCredentials) error { if resp == nil { return fmt.Errorf("invalid response") } if !fileutil.FolderExists(PDCPDir) { _ = fileutil.CreateFolder(PDCPDir) } bin, err := yaml.Marshal([]*PDCPCredentials{resp}) if err != nil { return err } return os.WriteFile(PDCPCredFile, bin, 0600) } // ValidateAPIKey validates the api key and retrieves associated user metadata like username // from given api server/host func (p *PDCPCredHandler) ValidateAPIKey(key string, host string, toolName string) (*PDCPCredentials, error) { // get address from url urlx, err := urlutil.Parse(host) if err != nil { return nil, err } req, err := retryablehttp.NewRequest("GET", fmt.Sprintf(userProfileURL, urlx.Host, toolName), nil) if err != nil { return nil, err } req.Header.Set(ApiKeyHeaderName, key) resp, err := retryablehttp.DefaultHTTPClient.Do(req) if err != nil { return nil, err } if resp.StatusCode != 200 { _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() return nil, fmt.Errorf("invalid status code: %d", resp.StatusCode) } defer resp.Body.Close() bin, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var profile PDCPUserProfileResponse err = json.Unmarshal(bin, &profile) if err != nil { return nil, err } if profile.Email == "" { return nil, fmt.Errorf("invalid response from server got %v", string(bin)) } return &PDCPCredentials{Username: profile.UserName, Email: profile.Email, APIKey: key, Server: host}, nil } func init() { DashBoardURL = env.GetEnvOrDefault("PDCP_DASHBOARD_URL", DashBoardURL) }