From dd7e923bffa0c4dd2f3eccc8a4fca3de343a3dd0 Mon Sep 17 00:00:00 2001 From: nezu Date: Sun, 20 Oct 2024 23:04:38 +0200 Subject: [PATCH] intial release --- .gitignore | 5 + asar/asar.go | 380 ++++++++++++++++++++++++++++++++++++++++++++++++++ asar/integrity.go | 42 ++++++ asar/pickle/pickle.go | 225 ++++++++++++++++++++++++++++++ asar/storage.go | 99 +++++++++++++ go.mod | 23 +++ go.sum | 23 +++ main.go | 202 +++++++++++++++++++++++++++ patcher/patcher.go | 170 ++++++++++++++++++++++ platform/linux.go | 17 +++ platform/windows.go | 48 +++++++ 11 files changed, 1234 insertions(+) create mode 100644 asar/asar.go create mode 100644 asar/integrity.go create mode 100644 asar/pickle/pickle.go create mode 100644 asar/storage.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 patcher/patcher.go create mode 100644 platform/linux.go create mode 100644 platform/windows.go diff --git a/.gitignore b/.gitignore index 5b90e79..a4cb47a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,8 @@ go.work.sum # env file .env +# other +.vscode/ +*.AppImage +squashfs-root/ +/lychee-slicer* \ No newline at end of file diff --git a/asar/asar.go b/asar/asar.go new file mode 100644 index 0000000..2e9d93b --- /dev/null +++ b/asar/asar.go @@ -0,0 +1,380 @@ +package asar + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "math" + "path/filepath" + "strconv" + "strings" + + "vaclive.party/software/lychee-slicer/asar/pickle" +) + +type EntryMetadata struct { + Unpacked *bool `json:"unpacked,omitempty"` +} + +type FilesystemDirectoryEntry struct { + EntryMetadata + Files map[string]FilesystemEntry + nezuMeta *map[string]string +} + +func (f *FilesystemDirectoryEntry) UnmarshalJSON(data []byte) error { + var raw struct { + Unpacked *bool `json:"unpacked"` + Files *map[string]json.RawMessage `json:"files"` + NezuMeta *map[string]string `json:"nezuMeta,omitempty"` + } + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Files == nil { + return fmt.Errorf("missing files") + } + f.Unpacked = raw.Unpacked + f.Files = make(map[string]FilesystemEntry) + f.nezuMeta = raw.NezuMeta + for name, entry := range *raw.Files { + var dir FilesystemDirectoryEntry + if err := json.Unmarshal(entry, &dir); err == nil { + f.Files[name] = &dir + continue + } + + var file FilesystemFileEntry + if err := json.Unmarshal(entry, &file); err == nil && file.Size != nil { + f.Files[name] = &file + continue + } + + var link FilesystemLinkEntry + if err := json.Unmarshal(entry, &link); err == nil && link.Link != nil { + f.Files[name] = &link + continue + } + + return fmt.Errorf("unexpected entry type: %s", name) + } + return nil +} + +func (f *FilesystemDirectoryEntry) MarshalJSON() ([]byte, error) { + raw := struct { + Unpacked *bool `json:"unpacked,omitempty"` + Files map[string]json.RawMessage `json:"files"` + NezuMeta *map[string]string `json:"nezuMeta,omitempty"` + }{ + Unpacked: f.Unpacked, + Files: make(map[string]json.RawMessage), + NezuMeta: f.nezuMeta, + } + for name, entry := range f.Files { + data, err := json.Marshal(entry) + if err != nil { + return nil, err + } + raw.Files[name] = data + } + return json.Marshal(raw) +} + +type FilesystemFileEntry struct { + EntryMetadata + Executable *bool `json:"executable,omitempty"` + Offset *string `json:"offset,omitempty"` + Size *int64 `json:"size"` + Integrity *FileIntegrity `json:"integrity,omitempty"` +} + +type FilesystemLinkEntry struct { + EntryMetadata + Link *string `json:"link"` +} + +type FilesystemEntry interface{} + +type Filesystem struct { + storage FilesystemStorage + header *FilesystemDirectoryEntry +} + +func newFileSystemDirectoryEntry() *FilesystemDirectoryEntry { + return &FilesystemDirectoryEntry{Files: make(map[string]FilesystemEntry)} +} + +func newFileSystemFileEntry() *FilesystemFileEntry { + return &FilesystemFileEntry{} +} + +func newFileSystemLinkEntry() *FilesystemLinkEntry { + return &FilesystemLinkEntry{} +} + +func NewFilesystem() *Filesystem { + return &Filesystem{ + storage: *NewFilesystemStorage(bytes.NewReader([]byte{}), NewBytesReadAtWriter([]byte{})), + header: newFileSystemDirectoryEntry(), + } +} + +func OpenFilesystem(reader io.ReaderAt, readerSize int64) (*Filesystem, error) { + sizeBytes := make([]byte, 8) + if _, err := reader.ReadAt(sizeBytes, 0); err != nil { + return nil, err + } + + sizePickle := pickle.CreateFromBuffer(sizeBytes) + headerSize := sizePickle.CreateIterator().ReadUInt32() + + headerBytes := make([]byte, headerSize) + if _, err := reader.ReadAt(headerBytes, 8); err != nil { + return nil, err + } + + headerPickle := pickle.CreateFromBuffer(headerBytes) + headerJson := headerPickle.CreateIterator().ReadString() + + var header FilesystemDirectoryEntry + if err := json.Unmarshal([]byte(headerJson), &header); err != nil { + return nil, err + } + + sectionReader := io.NewSectionReader(reader, 8+int64(headerSize), readerSize-8-int64(headerSize)) + + return &Filesystem{ + storage: *NewFilesystemStorage(sectionReader, NewBytesReadAtWriter([]byte{})), + header: &header, + }, nil +} + +func (fs *Filesystem) searchNodeFromDirectory(p string) (*FilesystemDirectoryEntry, error) { + entry := fs.header + // windows moment + dirs := strings.FieldsFunc(p, func(r rune) bool { return r == '/' || r == '\\' }) + for _, dir := range dirs { + if dir != "." { + if entry.Files != nil { + if _, ok := entry.Files[dir]; !ok { + entry.Files[dir] = newFileSystemDirectoryEntry() + } + entry = entry.Files[dir].(*FilesystemDirectoryEntry) + } else { + return nil, fmt.Errorf("unexpected directory state while traversing: %s", p) + } + } + } + return entry, nil +} + +func (fs *Filesystem) searchNodeFromPath(p string) (FilesystemEntry, error) { + if p == "." { + return fs.header, nil + } + println(filepath.Base(p), filepath.Dir(p)) + name := filepath.Base(p) + dirNode, err := fs.searchNodeFromDirectory(filepath.Dir(p)) + if err != nil { + return nil, err + } + if dirNode.Files == nil { + dirNode.Files = make(map[string]FilesystemEntry) + } + if _, ok := dirNode.Files[name]; !ok { + dirNode.Files[name] = newFileSystemFileEntry() + } + return dirNode.Files[name], nil +} + +func (fs *Filesystem) GetMetaFlag(flag string) string { + if fs.header.nezuMeta != nil { + if value, ok := (*fs.header.nezuMeta)[flag]; ok { + return value + } + } + return "" +} + +func (fs *Filesystem) SetMetaFlag(flag string, value string) error { + if fs.header.nezuMeta == nil { + fs.header.nezuMeta = &map[string]string{} + } + (*fs.header.nezuMeta)[flag] = value + return nil +} + +func (fs *Filesystem) InsertDirectory(p string) (map[string]FilesystemEntry, error) { + node, err := fs.searchNodeFromPath(p) + if err != nil { + return nil, err + } + dirNode := node.(*FilesystemDirectoryEntry) + return dirNode.Files, nil +} + +func (fs *Filesystem) InsertFile(p string, data []byte, executable bool) error { + size := int64(len(data)) + if size > math.MaxUint32 { + return fmt.Errorf("%s: file size exceeds 4GB", p) + } + + dirNode, err := fs.searchNodeFromDirectory(filepath.Dir(p)) + if err != nil { + return err + } + if dirNode.Unpacked != nil && *dirNode.Unpacked { + return fmt.Errorf("unpacked directories are not supported: %s", p) + } + + node, err := fs.searchNodeFromPath(p) + if err != nil { + return err + } + fileNode := node.(*FilesystemFileEntry) + + fileNode.Size = &size + offset := fmt.Sprintf("%d", fs.storage.Size()) + fileNode.Offset = &offset + fileNode.Integrity = getFileIntegrity(data) + if executable { + fileNode.Executable = &executable + } + fs.storage.Write(data) + return nil +} + +func (fs *Filesystem) ReadFile(p string, followLinks bool) ([]byte, error) { + file, err := fs.GetFile(p, followLinks) + if err != nil { + return nil, err + } + if file == nil { + return nil, fmt.Errorf("file not found: %s", p) + } + if linkEntry, ok := file.(*FilesystemLinkEntry); ok { + return []byte(*linkEntry.Link), nil + } + + fileEntry := file.(*FilesystemFileEntry) + if fileEntry.Unpacked != nil && *fileEntry.Unpacked { + return nil, fmt.Errorf("unpacked files are not supported: %s", p) + } + + offset, err := strconv.ParseInt(*fileEntry.Offset, 10, 64) + if err != nil { + return nil, err + } + + ret := make([]byte, *fileEntry.Size) + if _, err := fs.storage.ReadAt(ret, offset); err != nil { + return nil, err + } + + // FIXME: integrity check + + return ret, nil +} + +func (fs *Filesystem) InsertLink(p string, target string) error { + dirNode, err := fs.searchNodeFromDirectory(filepath.Dir(p)) + if err != nil { + return err + } + if dirNode.Unpacked != nil && *dirNode.Unpacked { + return fmt.Errorf("unexpected directories not set to unpack: %s", p) + } + + name := filepath.Base(p) + if _, ok := dirNode.Files[name]; !ok { + dirNode.Files[name] = newFileSystemLinkEntry() + } + + linkNode := dirNode.Files[name].(*FilesystemLinkEntry) + linkNode.Link = &target + return nil +} + +func (fs *Filesystem) ListFiles() []string { + files := []string{} + + // recursive function to traverse the filesystem + var traverse func(string, FilesystemEntry) + traverse = func(p string, entry FilesystemEntry) { + switch e := entry.(type) { + case *FilesystemDirectoryEntry: + for name, child := range e.Files { + path := name + if p != "" { + path = p + "/" + name + } + traverse(path, child) + } + case *FilesystemFileEntry: + files = append(files, p) + case *FilesystemLinkEntry: + files = append(files, p) + } + } + + traverse("", fs.header) + return files +} + +func (fs *Filesystem) GetNode(p string) (FilesystemEntry, error) { + node, err := fs.searchNodeFromDirectory(filepath.Dir(p)) + if err != nil { + return nil, err + } + name := filepath.Base(p) + if name != "" { + fmt.Println(p, filepath.Base(p), filepath.Dir(p), name, len(node.Files)) + return node.Files[name], nil + } + return node, nil +} + +func (fs *Filesystem) GetFile(p string, followLinks bool) (FilesystemEntry, error) { + info, err := fs.GetNode(p) + if err != nil { + return nil, err + } + if linkEntry, ok := info.(*FilesystemLinkEntry); ok && followLinks { + return fs.GetFile(*linkEntry.Link, followLinks) + } + if _, ok := info.(*FilesystemDirectoryEntry); ok { + return nil, fmt.Errorf("not a file: %s", p) + } + return info, nil +} + +func (fs *Filesystem) Save(writer io.Writer) error { + headerJson, err := json.Marshal(fs.header) + if err != nil { + return err + } + + headerPickle := pickle.CreateEmpty() + headerPickle.WriteString(string(headerJson)) + headerBytes := headerPickle.ToBuffer() + + sizePickle := pickle.CreateEmpty() + sizePickle.WriteUInt32(uint32(len(headerBytes))) + sizeBytes := sizePickle.ToBuffer() + + if _, err := writer.Write(sizeBytes); err != nil { + return err + } + + if _, err := writer.Write(headerBytes); err != nil { + return err + } + + if _, err := fs.storage.WriteTo(writer); err != nil { + return err + } + + return nil +} diff --git a/asar/integrity.go b/asar/integrity.go new file mode 100644 index 0000000..f5513bc --- /dev/null +++ b/asar/integrity.go @@ -0,0 +1,42 @@ +package asar + +import ( + "crypto" + "fmt" +) + +const ( + ALGORITHM = "SHA256" + BLOCK_SIZE = 4 * 1024 * 1024 +) + +type FileIntegrity struct { + Algorithm string `json:"algorithm"` + Hash string `json:"hash"` + BlockSize int `json:"blockSize"` + Blocks []string `json:"blocks"` +} + +func getFileIntegrity(data []byte) *FileIntegrity { + blockHashes := []string{} + for i := 0; i < len(data); i += BLOCK_SIZE { + end := i + BLOCK_SIZE + if end > len(data) { + end = len(data) + } + blockHashes = append(blockHashes, hashBlock(data[i:end])) + } + + return &FileIntegrity{ + Algorithm: ALGORITHM, + Hash: hashBlock(data), + BlockSize: BLOCK_SIZE, + Blocks: blockHashes, + } +} + +func hashBlock(block []byte) string { + hash := crypto.SHA256.New() + hash.Write(block) + return fmt.Sprintf("%x", hash.Sum(nil)) +} diff --git a/asar/pickle/pickle.go b/asar/pickle/pickle.go new file mode 100644 index 0000000..383c88f --- /dev/null +++ b/asar/pickle/pickle.go @@ -0,0 +1,225 @@ +package pickle + +import ( + "encoding/binary" + "errors" + "fmt" + "math" +) + +const ( + SIZE_UINT32 = 4 + PAYLOAD_UNIT = 64 + CAPACITY_READ_ONLY = 1 << 53 +) + +func alignInt(i, alignment int) int { + return i + ((alignment - (i % alignment)) % alignment) +} + +type PickleIterator struct { + payload []byte + payloadOffset int + readIndex int + endIndex int +} + +func NewPickleIterator(pickle *Pickle) *PickleIterator { + return &PickleIterator{ + payload: pickle.header, + payloadOffset: pickle.headerSize, + readIndex: 0, + endIndex: pickle.getPayloadSize(), + } +} + +func (pi *PickleIterator) ReadBool() bool { + return pi.ReadInt() != 0 +} + +func (pi *PickleIterator) ReadInt() int32 { + buf := pi.readBytes(binary.Size(int32(0))) + return int32(binary.LittleEndian.Uint32(buf)) +} + +func (pi *PickleIterator) ReadUInt32() uint32 { + buf := pi.readBytes(binary.Size(uint32(0))) + return binary.LittleEndian.Uint32(buf) +} + +func (pi *PickleIterator) ReadInt64() int64 { + buf := pi.readBytes(binary.Size(int64(0))) + return int64(binary.LittleEndian.Uint64(buf)) +} + +func (pi *PickleIterator) ReadUInt64() uint64 { + buf := pi.readBytes(binary.Size(uint64(0))) + return binary.LittleEndian.Uint64(buf) +} + +func (pi *PickleIterator) ReadFloat() float32 { + return math.Float32frombits(pi.ReadUInt32()) +} + +func (pi *PickleIterator) readDouble() float64 { + return math.Float64frombits(pi.ReadUInt64()) +} + +func (pi *PickleIterator) ReadString() string { + length := pi.ReadInt() + return string(pi.readBytes(int(length))) +} + +func (pi *PickleIterator) readBytes(length int) []byte { + readPayloadOffset := pi.getReadPayloadOffsetAndAdvance(length) + return pi.payload[readPayloadOffset : readPayloadOffset+length] +} + +func (pi *PickleIterator) getReadPayloadOffsetAndAdvance(length int) int { + if length > pi.endIndex-pi.readIndex { + pi.readIndex = pi.endIndex + panic(errors.New("failed to read data with length of " + fmt.Sprint(length))) + } + readPayloadOffset := pi.payloadOffset + pi.readIndex + pi.advance(length) + return readPayloadOffset +} + +func (pi *PickleIterator) advance(size int) { + alignedSize := alignInt(size, SIZE_UINT32) + if pi.endIndex-pi.readIndex < alignedSize { + pi.readIndex = pi.endIndex + } else { + pi.readIndex += alignedSize + } +} + +type Pickle struct { + header []byte + headerSize int + capacityAfterHeader int + writeOffset int +} + +func newPickle(buffer []byte) *Pickle { + p := &Pickle{} + if buffer != nil { + p.header = buffer + p.headerSize = len(buffer) - p.getPayloadSize() + p.capacityAfterHeader = CAPACITY_READ_ONLY + p.writeOffset = 0 + if p.headerSize > len(buffer) { + p.headerSize = 0 + } + if p.headerSize != alignInt(p.headerSize, SIZE_UINT32) { + p.headerSize = 0 + } + if p.headerSize == 0 { + p.header = make([]byte, 0) + } + } else { + p.header = make([]byte, 0) + p.headerSize = SIZE_UINT32 + p.capacityAfterHeader = 0 + p.writeOffset = 0 + p.resize(PAYLOAD_UNIT) + p.setPayloadSize(0) + } + return p +} + +func CreateEmpty() *Pickle { + return newPickle(nil) +} + +func CreateFromBuffer(buffer []byte) *Pickle { + return newPickle(buffer) +} + +func (p *Pickle) GetHeader() []byte { + return p.header +} + +func (p *Pickle) GetHeaderSize() int { + return p.headerSize +} + +func (p *Pickle) CreateIterator() *PickleIterator { + return NewPickleIterator(p) +} + +func (p *Pickle) ToBuffer() []byte { + return p.header[:p.headerSize+p.getPayloadSize()] +} + +func (p *Pickle) WriteBool(value bool) { + if value { + p.WriteInt(1) + } + p.WriteInt(0) +} + +func (p *Pickle) WriteInt(value int32) { + p.writeBytes(value, binary.LittleEndian) +} + +func (p *Pickle) WriteUInt32(value uint32) { + p.writeBytes(value, binary.LittleEndian) +} + +func (p *Pickle) WriteInt64(value int64) { + p.writeBytes(value, binary.LittleEndian) +} + +func (p *Pickle) WriteUInt64(value uint64) { + p.writeBytes(value, binary.LittleEndian) +} + +func (p *Pickle) WriteFloat(value float32) { + p.writeBytes(math.Float32bits(value), binary.LittleEndian) +} + +func (p *Pickle) WriteDouble(value float64) { + p.writeBytes(math.Float64bits(value), binary.LittleEndian) +} + +func (p *Pickle) WriteString(value string) { + length := len(value) + p.WriteInt(int32(length)) + p.writeBytes([]byte(value), binary.LittleEndian) +} + +func (p *Pickle) setPayloadSize(payloadSize int) { + binary.LittleEndian.PutUint32(p.header[:4], uint32(payloadSize)) +} + +func (p *Pickle) getPayloadSize() int { + return int(binary.LittleEndian.Uint32(p.header[:4])) +} + +func (p *Pickle) writeBytes(data any, byteOrder binary.ByteOrder) { + length := binary.Size(data) + if length == -1 { + panic(errors.New("unsupported data type")) + } + dataLength := alignInt(length, SIZE_UINT32) + newSize := p.writeOffset + dataLength + if newSize > p.capacityAfterHeader { + p.resize(max(p.capacityAfterHeader*2, newSize)) + } + binary.Encode(p.header[p.headerSize+p.writeOffset:], byteOrder, data) + + endOffset := p.headerSize + p.writeOffset + length + for i := endOffset; i < endOffset+dataLength-length; i++ { + p.header[i] = 0 + } + + p.setPayloadSize(newSize) + p.writeOffset = newSize +} + +func (p *Pickle) resize(newCapacity int) { + newCapacity = alignInt(newCapacity, PAYLOAD_UNIT) + p.header = append(p.header, make([]byte, newCapacity)...) + p.capacityAfterHeader = newCapacity +} diff --git a/asar/storage.go b/asar/storage.go new file mode 100644 index 0000000..4111ff9 --- /dev/null +++ b/asar/storage.go @@ -0,0 +1,99 @@ +package asar + +import ( + "bytes" + "errors" + "io" +) + +type SizedReaderAt interface { + io.ReaderAt + Size() int64 +} + +type WriterReaderAtWriterTo interface { + io.Writer + io.ReaderAt + io.WriterTo +} + +type BytesReadAtWriter struct { + s []byte + i int64 +} + +func NewBytesReadAtWriter(s []byte) *BytesReadAtWriter { + return &BytesReadAtWriter{s: s, i: 0} +} + +func (b *BytesReadAtWriter) Write(p []byte) (n int, err error) { + b.s = append(b.s, p...) + return len(p), nil +} + +func (b *BytesReadAtWriter) ReadAt(p []byte, off int64) (n int, err error) { + // cannot modify state - see io.ReaderAt + if off < 0 { + return 0, errors.New("negative offset") + } + if off >= int64(len(b.s)) { + return 0, io.EOF + } + n = copy(p, b.s[off:]) + if n < len(p) { + err = io.EOF + } + return +} + +func (b *BytesReadAtWriter) WriteTo(w io.Writer) (n int64, err error) { + return io.Copy(w, bytes.NewReader(b.s)) +} + +type FilesystemStorage struct { + existing SizedReaderAt + new WriterReaderAtWriterTo + newOffset int64 +} + +func NewFilesystemStorage(existing SizedReaderAt, new WriterReaderAtWriterTo) *FilesystemStorage { + return &FilesystemStorage{ + existing: existing, + new: new, + newOffset: 0, + } +} + +func (fs *FilesystemStorage) Size() int64 { + return fs.existing.Size() + fs.newOffset +} + +func (fs *FilesystemStorage) ReadAt(p []byte, off int64) (n int, err error) { + existingSize := fs.existing.Size() + if off < existingSize { + // error when cross boundary + if off+int64(len(p)) > existingSize { + return 0, io.ErrUnexpectedEOF + } + return fs.existing.ReadAt(p, off) + } + + return fs.new.ReadAt(p, off-existingSize) +} + +func (fs *FilesystemStorage) Write(p []byte) (n int, err error) { + n, err = fs.new.Write(p) + fs.newOffset += int64(n) + return +} + +func (fs *FilesystemStorage) WriteTo(w io.Writer) (n int64, err error) { + // write existing + n, err = io.Copy(w, io.NewSectionReader(fs.existing, 0, fs.existing.Size())) + if err != nil { + return n, err + } + + n2, err := fs.new.WriteTo(w) + return n + n2, err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3c4cab3 --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module vaclive.party/software/lychee-slicer + +go 1.23.2 + +require ( + github.com/elazarl/goproxy v0.0.0-20240909085733-6741dbfc16a1 + github.com/tdewolff/parse/v2 v2.7.18 + github.com/winlabs/gowin32 v0.0.0-20240930213947-f504d7e14639 + golang.org/x/sys v0.21.0 +) + +require ( + github.com/tidwall/gjson v1.14.2 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect +) + +require ( + github.com/ffred/guitocons v0.0.0-20180103100707-e6ef37a75a5e + github.com/tidwall/sjson v1.2.5 + golang.org/x/net v0.26.0 // indirect + golang.org/x/text v0.16.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..43c5a0c --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +github.com/elazarl/goproxy v0.0.0-20240909085733-6741dbfc16a1 h1:g7YUigN4dW2+zpdusdTTghZ+5Py3BaUMAStvL8Nk+FY= +github.com/elazarl/goproxy v0.0.0-20240909085733-6741dbfc16a1/go.mod h1:thX175TtLTzLj3p7N/Q9IiKZ7NF+p72cvL91emV0hzo= +github.com/ffred/guitocons v0.0.0-20180103100707-e6ef37a75a5e h1:h+70xO+I2H1tVB5yTRbaPO+JuTTs44o8vWIcX9nhQkw= +github.com/ffred/guitocons v0.0.0-20180103100707-e6ef37a75a5e/go.mod h1:EM9wg5H8ZTtFWm2/QsskKRrDc75gCOC6ZBthnYxbbZI= +github.com/tdewolff/parse/v2 v2.7.18 h1:uSqjEMT2lwCj5oifBHDcWU2kN1pbLrRENgFWDJa57eI= +github.com/tdewolff/parse/v2 v2.7.18/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= +github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= +github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/winlabs/gowin32 v0.0.0-20240930213947-f504d7e14639 h1:9MJGHk/ErosklMjfeS3xTwvDUET8xHH+b9H2oi/GqbY= +github.com/winlabs/gowin32 v0.0.0-20240930213947-f504d7e14639/go.mod h1:N51TYkG9JGR5sytj0EoPl31Xg2kuB507lxEmrwSNvfQ= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ce16af6 --- /dev/null +++ b/main.go @@ -0,0 +1,202 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "os/exec" + + "github.com/elazarl/goproxy" + + "github.com/tidwall/sjson" + "vaclive.party/software/lychee-slicer/patcher" + "vaclive.party/software/lychee-slicer/platform" +) + +const ip = "127.0.0.1" + +type graphqlRequest struct { + OperationName string `json:"operationName"` +} + +type userData struct { + operationName string +} + +func main() { + verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") + program := flag.String("program", "", "program to patch and/or proxy") + runPatch := flag.Bool("patch", false, "only patch the program") + runProxy := flag.Bool("proxy", false, "only start the proxy and program") + flag.Parse() + if *program == "" { + *program = platform.FindProgram() + if *program == "" { + flag.Usage() + log.Fatal("Program not specified") + } + } + + if !*runPatch && !*runProxy { + log.Println("No flags specified, running both patch and proxy") + *runProxy = true + *runPatch = true + } + + if *runPatch { + err := patcher.Patch(*program) + if err != nil { + log.Fatal("Patch:", err) + } + } + + if !*runProxy { + return + } + + log.Println("Starting proxy") + proxy := goproxy.NewProxyHttpServer() + proxy.Verbose = *verbose + + host := "graph.mango3d.io:443" + proxy.OnRequest(goproxy.ReqHostIs(host)).HandleConnectFunc(goproxy.AlwaysMitm) + proxy.OnRequest(goproxy.ReqHostIs(host)).DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (ret_req *http.Request, ret_res *http.Response) { + defer func() { + if e := recover(); e != nil { + ctx.Warnf("Panic: %v", e) + ret_req = req + ret_res = goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusBadGateway, "Panic") + } + }() + + if req.Method != "POST" { + return req, nil + } + + body, err := io.ReadAll(req.Body) + if err != nil { + panic(err) + } + req.Body.Close() + req.Body = io.NopCloser(bytes.NewBuffer(body)) + + var graphql graphqlRequest + if err := json.Unmarshal(body, &graphql); err != nil { + ctx.Warnf("Unmarshal: %v", err) + return req, nil + } + + ctx.UserData = &userData{operationName: graphql.OperationName} + + fmt.Println("OperationName:", graphql.OperationName) + return req, nil + }) + + modifyResponse := func(name string, callback func(string) string) { + proxy.OnResponse(operationNameIs(name)).DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) (ret *http.Response) { + defer func() { + if e := recover(); e != nil { + ret = goproxy.NewResponse(resp.Request, goproxy.ContentTypeText, http.StatusBadGateway, "Panic") + } + }() + + bb, err := io.ReadAll(resp.Body) + if err != nil { + panic(err) + } + resp.Body.Close() + + b := string(bb) + log.Println(name+":", b) + + b = callback(b) + + resp.Body = io.NopCloser(bytes.NewBuffer([]byte(b))) + return resp + }) + } + + modifyResponse("GetUser", func(b string) string { + b, _ = sjson.Set(b, "data.user.status.isPro", true) + b, _ = sjson.Set(b, "data.user.status.isLifetime", true) + b, _ = sjson.Set(b, "data.user.status.type", "lifetime") + b, _ = sjson.Set(b, "data.user.status.featureTags", []string{"LYCHEE_SLICER_PRO_SLA", "LYCHEE_SLICER_PRO_FDM", "LYCHEE_SLICER_PREMIUM"}) + b, _ = sjson.Set(b, "data.user.licenses.0.macAddressesLimit", 9999999) + return b + }) + + modifyResponse("userCheck", func(b string) string { + b, _ = sjson.Set(b, "data.consumer.user.status.isPremium", true) + b, _ = sjson.Set(b, "data.consumer.user.status.isPro", true) + b, _ = sjson.Set(b, "data.consumer.user.status.isLifetime", true) + b, _ = sjson.Set(b, "data.consumer.user.status.featureTags", []string{"LYCHEE_SLICER_PRO_SLA", "LYCHEE_SLICER_PRO_FDM", "LYCHEE_SLICER_PREMIUM"}) + b, _ = sjson.Set(b, "data.consumer.user.status.type", "lifetime") + return b + }) + + modifyResponse("checkLicenseWithoutDuration", func(b string) string { + b, _ = sjson.Set(b, "data.checkLicenseWithoutDuration.isPro", true) + b, _ = sjson.Set(b, "data.checkLicenseWithoutDuration.isLifetime", true) + b, _ = sjson.Set(b, "data.checkLicenseWithoutDuration.type", "lifetime") + b, _ = sjson.Set(b, "data.checkLicenseWithoutDuration.mac_addresses_limit", 9999999) + return b + }) + + modifyResponse("getLicense", func(b string) string { + return b + }) + + modifyResponse("CanUserGetTrial", func(b string) string { + return b + }) + + port, err := getFreePort() + if err != nil { + log.Fatal("GetFreePort:", err) + } + + ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", ip, port)) + if err != nil { + log.Fatal("Listen:", err) + } + + log.Println("Listening on", ln.Addr().String(), port) + + go func() { + cmd := exec.Command(*program, fmt.Sprintf("--proxy-server=%s:%d", ip, port), "--ignore-certificate-errors") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + log.Fatal(cmd.Run()) + os.Exit(0) + }() + + log.Fatal(http.Serve(ln, proxy)) +} + +func getFreePort() (port int, err error) { + var a *net.TCPAddr + if a, err = net.ResolveTCPAddr("tcp", "localhost:0"); err == nil { + var l *net.TCPListener + if l, err = net.ListenTCP("tcp", a); err == nil { + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil + } + } + return +} + +func operationNameIs(operationName string) goproxy.RespCondition { + return goproxy.RespConditionFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) bool { + if ctx.UserData == nil { + return false + } + userData := ctx.UserData.(*userData) + return userData.operationName == operationName + }) +} diff --git a/patcher/patcher.go b/patcher/patcher.go new file mode 100644 index 0000000..4b267db --- /dev/null +++ b/patcher/patcher.go @@ -0,0 +1,170 @@ +package patcher + +import ( + "bytes" + "errors" + "io" + "log" + "os" + "path/filepath" + + "github.com/tdewolff/parse/v2" + "github.com/tdewolff/parse/v2/js" + "vaclive.party/software/lychee-slicer/asar" + "vaclive.party/software/lychee-slicer/platform" +) + +type walker struct { + funName string + fun *js.FuncDecl +} + +func (w *walker) Enter(n js.INode) js.IVisitor { + switch n := n.(type) { + case *js.BindingElement: + if binding, ok := n.Binding.(*js.Var); ok && string(binding.Name()) == w.funName { + if fun, ok := n.Default.(*js.FuncDecl); ok { + w.fun = fun + } + } + case *js.FuncDecl: + if n.Name != nil && string(n.Name.Data) == w.funName { + w.fun = n + } + } + + if w.fun != nil { + return nil + } + return w +} + +func (w *walker) Exit(n js.INode) {} + +func findFunction(ast *js.AST, name string) *js.FuncDecl { + w := &walker{funName: name} + js.Walk(w, ast) + return w.fun +} + +func patchFile(program []byte) ([]byte, error) { + ast, err := js.Parse(parse.NewInputBytes([]byte(program)), js.Options{}) + if err != nil { + return nil, err + } + + fun := findFunction(ast, "verify") + if fun == nil { + return nil, errors.New("verify function not found") + } + + // who needs verification anyway? + fun.Body.List = []js.IStmt{ + &js.ReturnStmt{ + Value: &js.LiteralExpr{ + TokenType: js.TrueToken, + Data: []byte("true"), + }, + }, + } + + stream := bytes.NewBuffer(nil) + ast.JS(stream) + + return stream.Bytes(), nil +} + +func Patch(program string) error { + asarFile := filepath.Join(filepath.Dir(program), "resources", "app.asar") + log.Printf("Opening %s", asarFile) + + old, err := os.OpenFile(asarFile, os.O_RDONLY, 0) + if err != nil { + return err + } + defer old.Close() + + stat, err := old.Stat() + if err != nil { + return err + } + + fs, err := asar.OpenFilesystem(old, stat.Size()) + if err != nil { + return err + } + + if v := fs.GetMetaFlag("patched"); v != "" { + log.Printf("Already patched") + return nil + } + + patch := func(name string) error { + log.Printf("Patching %s", name) + b, err := fs.ReadFile(name, true) + if err != nil { + return err + } + + b, err = patchFile(b) + if err != nil { + return err + } + + return fs.InsertFile(name, b, false) + } + + if err := patch("node_modules/@mango3d/sign/dist/cjs/index.js"); err != nil { + return err + } + + if err := patch("node_modules/@mango3d/sign/dist/esm/index.js"); err != nil { + return err + } + + if err := fs.SetMetaFlag("patched", "true"); err != nil { + return err + } + + saved, err := os.CreateTemp("", "patched") + if err != nil { + return err + } + defer saved.Close() + defer os.Remove(saved.Name()) + + if err := fs.Save(saved); err != nil { + return err + } + + fs = nil + old.Close() + + _, err = saved.Seek(0, io.SeekStart) + if err != nil { + return err + } + + log.Printf("Replacing original asar with patched") + new, err := os.Create(asarFile) + if err != nil { + if err, ok := err.(*os.PathError); ok && platform.IsWindowsRetryable(err.Err) { + log.Printf("Windows rename failed, retrying") + saved.Close() + + return platform.WindowsRename(saved.Name(), asarFile) + } + return err + } + defer new.Close() + + _, err = io.Copy(new, saved) + if err != nil { + return err + } + + new.Close() + saved.Close() + + return err +} diff --git a/platform/linux.go b/platform/linux.go new file mode 100644 index 0000000..60ebdc5 --- /dev/null +++ b/platform/linux.go @@ -0,0 +1,17 @@ +//go:build linux + +package platform + +func IsWindowsRetryable(_ error) bool { + return false +} + +func WindowsRename(from, to string) error { + panic("not implemented") +} + +func FindProgram() string { + // who knows, could be a million things, including this executable itself + // let the user/packager decide where to find it + return "" +} diff --git a/platform/windows.go b/platform/windows.go new file mode 100644 index 0000000..fa68e62 --- /dev/null +++ b/platform/windows.go @@ -0,0 +1,48 @@ +//go:build windows + +package platform + +import ( + "os" + "path/filepath" + "syscall" + + "github.com/ffred/guitocons" + "github.com/winlabs/gowin32" + "github.com/winlabs/gowin32/wrappers" +) + +func init() { + guitocons.Guitocons() +} + +func IsWindowsRetryable(err error) bool { + if err == syscall.ERROR_ACCESS_DENIED { + return true + } + return false +} + +func WindowsRename(from, to string) error { + return wrappers.SHFileOperation(&wrappers.SHFILEOPSTRUCT{ + Func: wrappers.FO_MOVE, + From: gowin32.MakeDoubleNullTerminatedLpstr(from), + To: gowin32.MakeDoubleNullTerminatedLpstr(to), + Flags: wrappers.FOF_NOCONFIRMATION, + }) +} + +func FindProgram() string { + // get program files path + pf, err := gowin32.GetKnownFolderPath(gowin32.KnownFolderProgramFiles) + if err != nil { + return "" + } + + p := filepath.Join(pf, "LycheeSlicer", "LycheeSlicer.exe") + if _, err := os.Stat(p); err == nil { + return p + } + + return "" +}