first commit
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
package bpjs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
lzstring "github.com/daku10/go-lz-string"
|
||||
)
|
||||
|
||||
// DecompressPayload mengupayakan ekstraksi JSON menggunakan berbagai algoritma (GZIP, LZString, dll).
|
||||
func DecompressPayload(data []byte) (string, error) {
|
||||
log.Printf("DecompressPayload: Attempting decompression, data length: %d", len(data))
|
||||
|
||||
// Log hex dump for better debugging
|
||||
hexDump := make([]string, min(32, len(data)))
|
||||
for i := 0; i < len(hexDump); i++ {
|
||||
hexDump[i] = fmt.Sprintf("%02x", data[i])
|
||||
}
|
||||
log.Printf("DecompressPayload: Hex dump (first 32 bytes): %s", strings.Join(hexDump, " "))
|
||||
|
||||
// Method 1: Try LZ-string first (most common for BPJS)
|
||||
if result, err := tryLZStringMethods(data); err == nil {
|
||||
log.Println("DecompressPayload: LZ-string decompression successful")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Method 2: Try gzip
|
||||
if result, err := tryGzipDecompression(data); err == nil && isValidDecompressedResult(result) {
|
||||
log.Println("DecompressPayload: Gzip decompression successful")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Method 3: Try as plain text
|
||||
if isValidUTF8AndPrintable(string(data)) {
|
||||
result := string(data)
|
||||
if isValidDecompressedResult(result) {
|
||||
log.Println("DecompressPayload: Data is already valid text")
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Method 4: Try base64 decode then decompress
|
||||
if result, err := tryBase64ThenDecompress(data); err == nil {
|
||||
log.Println("DecompressPayload: Base64 then decompress successful")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
log.Printf("DecompressPayload: All methods failed")
|
||||
return "", errors.New("all decompression methods failed")
|
||||
}
|
||||
|
||||
// tryLZStringMethods attempts LZ-string decompression
|
||||
func tryLZStringMethods(data []byte) (string, error) {
|
||||
dataStr := string(data)
|
||||
log.Printf("tryLZStringMethods: Raw data length: %d", len(dataStr))
|
||||
|
||||
// Method 1: Clean corrupt prefix and find LZ-string pattern
|
||||
cleanedData := extractCleanLZString(dataStr)
|
||||
if cleanedData != "" {
|
||||
log.Printf("tryLZStringMethods: Found clean LZ-string: %s", cleanedData[:min(50, len(cleanedData))])
|
||||
|
||||
// Decompress according to BPJS standards
|
||||
if result, err := lzstring.DecompressFromEncodedURIComponent(cleanedData); err == nil && len(result) > 0 {
|
||||
if isValidDecompressedResult(result) {
|
||||
log.Printf("LZ-string decompression successful, length: %d", len(result))
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: Fallback direct decompression
|
||||
if result, err := lzstring.DecompressFromEncodedURIComponent(dataStr); err == nil && len(result) > 0 {
|
||||
if isValidDecompressedResult(result) {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Try base64 LZ-string
|
||||
if result, err := lzstring.DecompressFromBase64(dataStr); err == nil && len(result) > 0 {
|
||||
if isValidDecompressedResult(result) {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("LZ-string decompression failed")
|
||||
}
|
||||
|
||||
// extractCleanLZString extracts clean LZ-string from corrupt data
|
||||
func extractCleanLZString(data string) string {
|
||||
// Common LZ-string patterns from BPJS documentation
|
||||
patterns := []string{"EAuUA", "N4Ig", "BwIw", "CwIw", "DwIw", "EwIw", "FwIw", "GwIw", "HwIw"}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
if idx := strings.Index(data, pattern); idx >= 0 {
|
||||
// Extract from pattern to end
|
||||
candidate := data[idx:]
|
||||
log.Printf("extractCleanLZString: Found pattern '%s' at position %d", pattern, idx)
|
||||
|
||||
// Clean only valid base64 characters
|
||||
cleaned := extractBase64Only(candidate)
|
||||
if len(cleaned) > 100 { // Minimum length for valid data
|
||||
return cleaned
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractBase64Only extracts only base64 valid characters
|
||||
func extractBase64Only(s string) string {
|
||||
base64Chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
|
||||
var result strings.Builder
|
||||
|
||||
for _, char := range s {
|
||||
if strings.ContainsRune(base64Chars, char) {
|
||||
result.WriteRune(char)
|
||||
} else {
|
||||
// Stop at non-base64 character if already long enough
|
||||
if result.Len() > 100 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// tryGzipDecompression attempts gzip decompression
|
||||
func tryGzipDecompression(data []byte) (string, error) {
|
||||
reader, err := gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
decompressed, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(decompressed), nil
|
||||
}
|
||||
|
||||
// tryBase64ThenDecompress attempts base64 decode then decompress
|
||||
func tryBase64ThenDecompress(data []byte) (string, error) {
|
||||
decoded, err := base64.StdEncoding.DecodeString(string(data))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tryLZStringMethods(decoded)
|
||||
}
|
||||
|
||||
// isValidDecompressedResult validates decompressed result
|
||||
func isValidDecompressedResult(result string) bool {
|
||||
if len(result) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Trim whitespace and check UTF-8
|
||||
trimmed := strings.TrimSpace(result)
|
||||
if !utf8.ValidString(trimmed) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Must start with { or [ for JSON
|
||||
if len(trimmed) > 0 && (trimmed[0] == '{' || trimmed[0] == '[') {
|
||||
// Validate as JSON
|
||||
var js json.RawMessage
|
||||
if json.Unmarshal([]byte(result), &js) == nil {
|
||||
log.Printf("Decompressed result is valid JSON, length: %d", len(result))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// If not JSON, reject
|
||||
log.Printf("Decompressed result is not valid JSON")
|
||||
return false
|
||||
}
|
||||
|
||||
// isValidUTF8AndPrintable checks if string is valid UTF-8 and printable
|
||||
func isValidUTF8AndPrintable(s string) bool {
|
||||
if !utf8.ValidString(s) {
|
||||
log.Printf("isValidUTF8AndPrintable: String is not valid UTF-8")
|
||||
return false
|
||||
}
|
||||
|
||||
// Count valid characters
|
||||
validChars := 0
|
||||
totalChars := 0
|
||||
|
||||
for _, r := range s {
|
||||
totalChars++
|
||||
|
||||
if r >= 32 && r <= 126 { // Printable ASCII
|
||||
validChars++
|
||||
} else if r == '\n' || r == '\r' || r == '\t' { // Allowed control chars
|
||||
validChars++
|
||||
} else if r >= 160 { // Unicode characters
|
||||
validChars++
|
||||
}
|
||||
}
|
||||
|
||||
validRatio := float64(validChars) / float64(totalChars)
|
||||
log.Printf("isValidUTF8AndPrintable: Valid chars ratio: %.2f (%d/%d)", validRatio, validChars, totalChars)
|
||||
|
||||
// At least 70% should be valid characters
|
||||
return validRatio >= 0.7
|
||||
}
|
||||
|
||||
// hasLZStringPattern detects LZ-string patterns
|
||||
func hasLZStringPattern(s string) bool {
|
||||
if len(s) < 10 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Common LZ-string compressed data patterns
|
||||
commonLZPatterns := []string{
|
||||
"N4Ig", "BwIw", "CwIw", "DwIw", "EwIw", "FwIw", "GwIw", "HwIw",
|
||||
"IwIw", "JwIw", "KwIw", "LwIw", "MwIw", "NwIw", "OwIw", "PwIw",
|
||||
}
|
||||
|
||||
for _, pattern := range commonLZPatterns {
|
||||
if strings.HasPrefix(s, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if string contains only base64 characters without spaces or newlines
|
||||
base64Pattern := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
|
||||
if len(s) > 50 { // Only check long strings
|
||||
invalidChars := 0
|
||||
for _, char := range s {
|
||||
if !strings.ContainsRune(base64Pattern, char) {
|
||||
invalidChars++
|
||||
}
|
||||
}
|
||||
// If less than 5% invalid characters, likely LZ-string
|
||||
if float64(invalidChars)/float64(len(s)) < 0.05 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// cleanResponse cleans response string
|
||||
func cleanResponse(s string) string {
|
||||
// Remove UTF-8 BOM and other BOM variations
|
||||
s = strings.TrimPrefix(s, "\xef\xbb\xbf") // UTF-8 BOM
|
||||
s = strings.TrimPrefix(s, "\ufeff") // Unicode BOM
|
||||
s = strings.TrimPrefix(s, "\ufffe") // Unicode BOM (reverse)
|
||||
s = strings.TrimPrefix(s, "\xff\xfe") // UTF-16 LE BOM
|
||||
s = strings.TrimPrefix(s, "\xfe\xff") // UTF-16 BE BOM
|
||||
|
||||
// Remove control and non-printable characters
|
||||
var result strings.Builder
|
||||
for _, r := range s {
|
||||
if r >= 32 && r <= 126 || r == '\n' || r == '\r' || r == '\t' {
|
||||
result.WriteRune(r)
|
||||
} else if r > 126 && unicode.IsPrint(r) {
|
||||
// Allow Unicode printable characters
|
||||
result.WriteRune(r)
|
||||
}
|
||||
// Skip all other characters (including BOM fragments)
|
||||
}
|
||||
|
||||
cleaned := result.String()
|
||||
cleaned = strings.TrimSpace(cleaned)
|
||||
|
||||
// Find and extract valid JSON
|
||||
if idx := strings.Index(cleaned, "{"); idx >= 0 {
|
||||
cleaned = cleaned[idx:]
|
||||
// Find matching closing brace
|
||||
if endIdx := findMatchingBrace(cleaned); endIdx > 0 {
|
||||
cleaned = cleaned[:endIdx+1]
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("cleanResponse: Final cleaned length: %d", len(cleaned))
|
||||
log.Printf("cleanResponse: Final result preview: %s", cleaned[:min(200, len(cleaned))])
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// findMatchingBrace finds matching closing brace
|
||||
func findMatchingBrace(s string) int {
|
||||
if len(s) == 0 || s[0] != '{' {
|
||||
return -1
|
||||
}
|
||||
|
||||
braceCount := 0
|
||||
inString := false
|
||||
escaped := false
|
||||
|
||||
for i, char := range s {
|
||||
if escaped {
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
if char == '\\' {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
if char == '"' && !escaped {
|
||||
inString = !inString
|
||||
continue
|
||||
}
|
||||
|
||||
if !inString {
|
||||
if char == '{' {
|
||||
braceCount++
|
||||
} else if char == '}' {
|
||||
braceCount--
|
||||
if braceCount == 0 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
Reference in New Issue
Block a user