/**************************************************************************** * MiniVNC (c) 2022-2024 Marcio Teixeira * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * To view a copy of the GNU General Public License, go to the following * * location: . * ****************************************************************************/ #include #include "VNCServer.h" #include "VNCEncoder.h" #include "TightVNCSupport.h" #include "DebugLog.h" #if USE_TIGHT_AUTH #define IN_STREAM_COPY(arg) inStream.copyTo(&arg, sizeof(arg)); #define IN_STREAM_PEEK(arg, len) {size_t avail; arg = inStream.getDataBlock(&avail); if(avail < len) {dprintf("Insufficient data! %ld < %ld\n", avail, len);} inStream.skip(len);} #define OUT_STREAM_COPY(type, arg) *((type*)c)++ = arg; extern "C" { extern struct TightVNCServerAuthCaps tightAuthCaps; extern struct TightVNCServerInitCaps tightInitCaps; } pascal void tcpSendTightVNCTunnelTypes(TCPiopb *pb); pascal void tcpGetTightVncTunnelChoice(TCPiopb *pb); pascal void tcpGetTightVncAuthChoice(TCPiopb *pb); pascal void tcpProcessTightVncAuthChoice(TCPiopb *pb); pascal void tightVNCMessageFinished(TCPiopb *pb); void loadTightSupport() { dprintf("Loading TightVNC support\n"); vncFlags.clientTakesTightAuth = false; } void sendTightCapabilities() { if (vncFlags.clientTakesTightAuth) { dprintf("Sending TightVNC capabilities\n"); myWDS[1].ptr = (Ptr) &tightInitCaps; myWDS[1].length = sizeof(unsigned short) * 4 + sizeof(TightVNCCapabilites) * (tightInitCaps.numberOfServerMesg + tightInitCaps.numberOfClientMesg + tightInitCaps.numberOfEncodings); myWDS[2].ptr = 0; myWDS[2].length = 0; } } pascal void tcpSendTightVNCAuthTypes(TCPiopb *pb) { if (tcpSuccess(pb)) { vncFlags.clientTakesTightAuth = true; tcp.then(pb, tcpGetTightVncAuthChoice); myWDS[0].ptr = (Ptr) &tightAuthCaps; myWDS[0].length = sizeof(TightVNCServerAuthCaps); tcp.send(pb, stream, myWDS, kTimeOut, true); } } pascal void tcpGetTightVncAuthChoice(TCPiopb *pb) { if (tcpSuccess(pb)) { tcp.then(pb, tcpProcessTightVncAuthChoice); tcp.receive(pb, stream, (Ptr) &vncClientMessage, sizeof(TightVNCCapReply)); } } pascal void tcpProcessTightVncAuthChoice(TCPiopb *pb) { if (tcpSuccess(pb)) { #ifdef VNC_DEBUG dprintf("Tight auth choice: %ld\n", vncClientMessage.tightCapReq.code); #endif switch (vncClientMessage.tightCapReq.code) { case mNoAuthentication: tcpSendAuthResult(pb); break; case mVNCAuthentication: tcpSendAuthChallenge(pb); break; } } } void tightVNCSendReply(Ptr ptr, size_t length, TCPCompletionPtr proc); void tightVNCSendReply(Ptr ptr, size_t length, TCPCompletionPtr proc) { vncFlags.fbUpdateInProgress = true; myWDS[0].ptr = ptr; myWDS[0].length = length; myWDS[1].ptr = 0; myWDS[1].length = 0; TCPiopb *pb = &epb_send.pb; tcp.then(pb, proc); tcp.send(pb, stream, myWDS, kTimeOut, false); } void tightVNCSendReply(unsigned long message); void tightVNCSendReply(unsigned long message) { unsigned char *c = fbUpdateBuffer; OUT_STREAM_COPY(unsigned long, message); tightVNCSendReply( (Ptr) fbUpdateBuffer, // Data ptr sizeof(unsigned long), // Data length tightVNCMessageFinished // Completion proc ); } pascal void tightVNCMessageFinished(TCPiopb *pb); pascal void tightVNCMessageFinished(TCPiopb *pb) { if (tcpSuccess(pb)) { dprintf("==== Finishing deferred messages ====\n"); vncFlags.fbUpdateInProgress = false; returnFromTightVNCMessage(); } } // Convert from Mac epoch time (seconds since midnight January 1st, 1904) // to UNIX epoch time (seconds since midnight January 1st, 1970) in miliseconds static asm void macToUnixEpoch(uint64*) { machine 68020 #define tdArg 8(a6) #define tdPtr a0 #define tdLo d0 #define tdHi d1 #define tmp d2 link a6,#0000 // Link for debugger move.l tdArg,tdPtr move.l struct(uint64.lo)(tdPtr),tdLo move.l struct(uint64.hi)(tdPtr),tdHi subi.l #2082844800,tdLo moveq #0,tmp subx.l tmp,tdHi mulu.l #1000,tdHi:tdLo move.l tdLo, struct(uint64.lo)(tdPtr) move.l tdHi, struct(uint64.hi)(tdPtr) unlk a6 rts #undef tdArg #undef tdPtr #undef tdLo #undef tdHi #undef tmp } /** * processRequestPath parses the path in the request and converts it * into a dir specifier (vRefNum + dirId) and a trailing name. * * On input: * req.pathPtr = File or directory path * req.pathLen = Length of string * * On output: * req.vRefNum = Volume reference number * req.dirId = Directory id * req.isRoot = Is it root? * trailingName = last name on the path (can be directory or file) */ void processRequestPath(TightVNCFileUploadData &req, Str63 trailingName); void processRequestPath(TightVNCFileUploadData &req, Str63 trailingName) { // The TightVNC client sends a terminating '\0' as part // of the path, but it is unclear whether all clients do. // Point pathEnd to the last non-zero char regardless. const Boolean zeroTerm = req.pathPtr[req.pathLen - 1] == '\0'; const char *pathEnd = req.pathPtr + req.pathLen - (zeroTerm ? 1 : 0); const char *namePtr = req.pathPtr; req.isRoot = (namePtr[0] == '/') && (pathEnd - namePtr == 1); req.vRefNum = 0; req.dirId = 2; if (req.isRoot) { return; } // Walk the path, finding ioVRefNum and ioDrDirID as we go along OSErr myErr = noErr; CInfoPBRec cpb; cpb.dirInfo.ioCompletion = 0; cpb.dirInfo.ioNamePtr = trailingName; cpb.dirInfo.ioVRefNum = 0; cpb.dirInfo.ioFDirIndex = 0; cpb.dirInfo.ioDrDirID = 2; // 0 = working dir; 2 = root dir const char *nameEnd; do { // Skip the leading slash namePtr++; // Find next slash separator nameEnd = namePtr; while ((nameEnd != pathEnd) && (*nameEnd != '/')) { nameEnd++; } // Copy name to trailingName as pascal str trailingName[0] = nameEnd - namePtr; BlockMove (namePtr, trailingName + 1, trailingName[0]); // Are we at the root dir? if (cpb.dirInfo.ioVRefNum == 0) { // If yes, append colon to make a volume name trailingName[0]++; trailingName[trailingName[0]] = ':'; // Convert volume name into a volume reference number HParamBlockRec hpb; hpb.volumeParam.ioCompletion = 0; hpb.volumeParam.ioNamePtr = trailingName; hpb.volumeParam.ioVolIndex = -1; hpb.volumeParam.ioVRefNum = 0; if (PBHGetVInfo(&hpb, false) != noErr) { break; } req.vRefNum = hpb.volumeParam.ioVRefNum; cpb.dirInfo.ioVRefNum = hpb.volumeParam.ioVRefNum; } else { if (PBGetCatInfo(&cpb, false) != noErr) { break; } if (cpb.dirInfo.ioFlAttrib & ioDirMask) { // If we have a directory record the directory id req.dirId = cpb.dirInfo.ioDrDirID; } } namePtr = nameEnd; } while (namePtr != pathEnd); if (myErr != noErr) { dprintf("processRequestPath error %d\n", myErr); } } size_t getDirEntries(TightVNCFileUploadData &req, unsigned char *c, const unsigned char *end); pascal void tightVNCFileListContinuation(TCPiopb *pb); void tightVNCFileList(MessageData *pb); void tightVNCFileList(MessageData *pb) { /** Message Format: long message; unsigned char compressionLevel; unsigned long dirNameSize; Followed by char Dirname[dirNameSize] */ TightVNCFileUploadData &req = vncClientMessage.tightFileUploadData; unsigned long message; unsigned char compressionLevel; IN_STREAM_COPY(message); IN_STREAM_COPY(compressionLevel); IN_STREAM_COPY(req.pathLen); IN_STREAM_PEEK(req.pathPtr, req.pathLen); dprintf("Got file list request: %.*s\n", (unsigned short) req.pathLen, req.pathPtr); // Get the GMT conversion factor (this function cannot be called from an interrupt) MachineLocation loc; ReadLocation(&loc); req.gmtDelta = (loc.u.gmtDelta & 0x00FFFFFF) | ((loc.u.gmtDelta & (1L << 23)) ? 0xFF000000 : 0); OSErr myErr; Str63 dirName; processRequestPath(req, dirName); // Do a first pass to pre-compute the length of the data req.index = 0; const size_t uncompressedLength = sizeof(unsigned long) + getDirEntries(req, 0, 0); req.nEntries = req.index; // Make space for the header unsigned char *c = fbUpdateBuffer; unsigned char *end = fbUpdateBuffer + GetPtrSize((Ptr)fbUpdateBuffer); OUT_STREAM_COPY(long, 0xFC000103); // message OUT_STREAM_COPY(unsigned char, 0); // compressionLevel OUT_STREAM_COPY(unsigned long, uncompressedLength); // compressedSize OUT_STREAM_COPY(unsigned long, uncompressedLength); // uncompressedSize OUT_STREAM_COPY(unsigned long, req.nEntries); // nEntries req.index = 0; c += getDirEntries(req, c, end); tightVNCSendReply((Ptr) fbUpdateBuffer, c - fbUpdateBuffer, req.index == req.nEntries ? tightVNCMessageFinished : tightVNCFileListContinuation ); } pascal void tightVNCFileListContinuation(TCPiopb *pb) { if (tcpSuccess(pb)) { TightVNCFileUploadData &req = vncClientMessage.tightFileUploadData; unsigned char *c = fbUpdateBuffer; unsigned char *end = fbUpdateBuffer + GetPtrSize((Ptr)fbUpdateBuffer); c += getDirEntries(req, c, end); tightVNCSendReply((Ptr) fbUpdateBuffer, c - fbUpdateBuffer, req.index == req.nEntries ? tightVNCMessageFinished : tightVNCFileListContinuation ); } } size_t getDirEntries(TightVNCFileUploadData &req, unsigned char *c, const unsigned char *end) { OSErr myErr; HParamBlockRec myHPB; CInfoPBRec myCPB; Str63 myName; size_t myLen = 0; for (;;) { if (req.isRoot) { // List volumes myHPB.volumeParam.ioCompletion = 0; myHPB.volumeParam.ioVRefNum = 0; myHPB.volumeParam.ioNamePtr = myName; myHPB.volumeParam.ioVolIndex = req.index + 1; myErr = PBHGetVInfo(&myHPB, false); } else { // List files and folders myCPB.dirInfo.ioDrDirID = req.dirId; myCPB.dirInfo.ioCompletion = 0; myCPB.dirInfo.ioNamePtr = myName; myCPB.dirInfo.ioFDirIndex = req.index + 1; myCPB.dirInfo.ioVRefNum = req.vRefNum; myErr = PBGetCatInfo(&myCPB, false); } if (myErr != noErr) { break; } const size_t entryLen = sizeof(TightVNCFileListEntry) + myName[0]; if (c == NULL) { // Do a dry-run without writing any data, only counting bytes myLen += entryLen; req.index++; } else if ((end - c) < entryLen) { // Stop processing as soon as the buffer is filled break; } else { // Append the file entry to the reply if it fits const Boolean isDir = req.isRoot || (myCPB.dirInfo.ioFlAttrib & ioDirMask); const unsigned short flags = isDir ? 1 : 0; uint64 modTime, fileSize; modTime.hi = 0; fileSize.hi = 0; if (isDir) { modTime.lo = myHPB.volumeParam.ioVLsMod - req.gmtDelta; fileSize.lo = 0; } else { modTime.lo = myCPB.hFileInfo.ioFlMdDat - req.gmtDelta; fileSize.lo = myCPB.hFileInfo.ioFlLgLen + myCPB.hFileInfo.ioFlRLgLen; } macToUnixEpoch(&modTime); OUT_STREAM_COPY(uint64, fileSize); // fileSize OUT_STREAM_COPY(uint64, modTime); // lastModified OUT_STREAM_COPY(unsigned short, flags); // flags OUT_STREAM_COPY(unsigned long, myName[0]); // dirNameSize BlockMove(myName + 1, c, myName[0]); c += myName[0]; myLen += entryLen; req.index++; } } return myLen; } void mapFileExtension(StringPtr fileName, OSType *type, OSType *creator); void mapFileExtension(StringPtr fileName, OSType *type, OSType *creator) { struct Mapping { OSType type; OSType creator; }; // Find the file extension Str63 extension; extension[0] = 0; unsigned char periodAt; for (periodAt = fileName[0]; (periodAt != 0) && (fileName[periodAt] != '.'); periodAt--); if (periodAt) { extension[0] = fileName[0] - periodAt; BlockMove(fileName + periodAt + 1, extension + 1, extension[0]); } // Find a "fmap" resource whose name matches the extension Handle mapHandle = GetNamedResource('fmap', extension); if (mapHandle && (*mapHandle)) { const Mapping mapping = **(Mapping**)mapHandle; *type = mapping.type; *creator = mapping.creator; ReleaseResource(mapHandle); } else { *type = 'TEXT'; *creator = '????'; } dprintf("Creating '%#s' as '%.4s'/'%.4s' [ResEdit]\n", fileName, type, creator); } void tightVNCFileUploadStart(MessageData *pb); void tightVNCFileUploadStart(MessageData *pb) { /** Message Format: unsigned long message; unsigned long fNameSize; const char *filename; // char filename[fNameSize] unsigned char uploadFlags; uint64 initialOffset; */ TightVNCFileUploadData &req = vncClientMessage.tightFileUploadData; unsigned long message; unsigned char uploadFlags; uint64 initialOffset; IN_STREAM_COPY(message); IN_STREAM_COPY(req.pathLen); IN_STREAM_PEEK(req.pathPtr, req.pathLen); IN_STREAM_COPY(uploadFlags); IN_STREAM_COPY(initialOffset); dprintf("Got file upload start request: %.*s\n", (unsigned short)req.pathLen, req.pathPtr); // Open the file for writing Str63 fileName; processRequestPath(req, fileName); OSType type, creator; mapFileExtension(fileName, &type, &creator); OSErr err = HCreate(req.vRefNum, req.dirId, fileName, type, creator); if (err != noErr) { dprintf("Unable to create file %#p\n", fileName); } err = HOpenDF(req.vRefNum, req.dirId, fileName, fsWrPerm, &req.fRefNum); if (err != noErr) { dprintf("Unable to open file\n"); } // Write out the reply tightVNCSendReply(0xFC000107); } pascal void tightVNCFileUploadDataFragment(TCPiopb *pb); pascal void tightVNCFileUploadBuffersReturned(TCPiopb *pb); pascal void tightVNCFileUploadBuffersFilled(TCPiopb *pb); void tightVNCFileUploadData(MessageData *pb); void tightVNCFileUploadData(MessageData *pb) { /** Message Format: unsigned char compressionLevel; size_t compressedSize; size_t uncompressedSize; Followed by File[compressedSize], but if (realSize = compressedSize = 0) followed by uint32_t modTime */ TightVNCFileUploadData &req = vncClientMessage.tightFileUploadData; unsigned long message; unsigned char compressionLevel; IN_STREAM_COPY(message); IN_STREAM_COPY(compressionLevel); IN_STREAM_COPY(req.compressedSize); IN_STREAM_COPY(req.uncompressedSize); dprintf("Got file data (decompress: %ld => %ld)\n", req.compressedSize, req.uncompressedSize); tightVNCFileUploadDataFragment(&epb_recv.pb); } pascal void tightVNCFileUploadDataFragment(TCPiopb *pb) { TightVNCFileUploadData &req = vncClientMessage.tightFileUploadData; while (req.compressedSize > 0) { size_t avail; const char *data = inStream.getDataBlock(&avail); const size_t bytesRead = min(avail, req.compressedSize); inStream.skip(bytesRead); req.compressedSize -= bytesRead; long bytesWritten = bytesRead; OSErr err = FSWrite(req.fRefNum, &bytesWritten, data); if (err != noErr) { dprintf("Unable to write file data\n"); } if (inStream.finished()) { // Return the buffers tcp.then(pb, tightVNCFileUploadBuffersReturned); tcp.receiveReturnBuffers(pb); return; } } // Write out the reply tightVNCSendReply(0xFC000109); } pascal void tightVNCFileUploadBuffersReturned(TCPiopb *pb) { if (tcpSuccess(pb)) { tcp.then(pb, tightVNCFileUploadBuffersFilled); tcp.receiveNoCopy(pb, stream, myRDS, kNumRDS); } } pascal void tightVNCFileUploadBuffersFilled(TCPiopb *pb) { if (tcpSuccess(pb)) { inStream.setPosition(0); tightVNCFileUploadDataFragment(pb); } } void tightVNCFileUploadEnd(MessageData *pb); void tightVNCFileUploadEnd(MessageData *pb) { /** Message Format: long message; unsigned short flags; uint64 lastModified; */ TightVNCFileUploadData &req = vncClientMessage.tightFileUploadData; unsigned long message; unsigned short fileFlags; uint64 modificationTime; IN_STREAM_COPY(message); IN_STREAM_COPY(fileFlags); IN_STREAM_COPY(modificationTime); dprintf("Got file end\n"); // Close the file OSErr err = FSClose(req.fRefNum); // Write out the reply tightVNCSendReply(0xFC00010B); } void tightVNCMakeDirectory(MessageData *pb); void tightVNCMakeDirectory(MessageData *pb) { /** Message Format: unsigned long message; unsigned long dirNameSize; const char *dirName; // char dirname[dirNameSize] */ TightVNCFileUploadData &req = vncClientMessage.tightFileUploadData; unsigned long message; IN_STREAM_COPY(message); IN_STREAM_COPY(req.pathLen); IN_STREAM_PEEK(req.pathPtr, req.pathLen); dprintf("Got mkdir request: %.*s\n", (unsigned short)req.pathLen, req.pathPtr); // Open the file for writing Str63 dirName; processRequestPath(req, dirName); long createDirId; OSErr err = DirCreate(req.vRefNum, req.dirId, dirName, &createDirId); if (err != noErr) { dprintf("Unable to create directory %#p\n", dirName); } // Write out the reply tightVNCSendReply(0xFC000112); } void tightVNCRemoveFile(MessageData *pb); void tightVNCRemoveFile(MessageData *pb) { /** Message Format: unsigned long message; unsigned long fileNameSize; const char *fileName; // char fileName[fileNameSize] */ TightVNCFileUploadData &req = vncClientMessage.tightFileUploadData; unsigned long message; IN_STREAM_COPY(message); IN_STREAM_COPY(req.pathLen); IN_STREAM_PEEK(req.pathPtr, req.pathLen); dprintf("Got remove request: %.*s\n", (unsigned short)req.pathLen, req.pathPtr); Str63 fileName; processRequestPath(req, fileName); long createDirId; OSErr err = HDelete(req.vRefNum, req.dirId, fileName); if (err != noErr) { dprintf("Unable to delete %#p\n", fileName); } // Write out the reply tightVNCSendReply(0xFC000114); } DispatchMsgResult dispatchTightClientMessage(MessageData *pb) { READ_ALL(tightVncExtMsg); MAIN_LOOP_ONLY(); switch (pb->msgPtr->tightVncExtMsg) { case 0xFC000102: tightVNCFileList(pb); break; case 0xFC000106: tightVNCFileUploadStart(pb); break; case 0xFC000108: tightVNCFileUploadData(pb); break; case 0xFC00010A: tightVNCFileUploadEnd(pb); break; case 0xFC000111: tightVNCMakeDirectory(pb); break; case 0xFC000113: tightVNCRemoveFile(pb); break; default: dprintf("Invalid TightVNC message: %ld\n", pb->msgPtr->tightVncExtMsg); vncState = VNC_ERROR; break; } return returnToCaller; } #endif // USE_TIGHT_AUTH