/* * ci.c: Common Interface * * See the main source file 'vdr.c' for copyright information and * how to reach the author. * * $Id: ci.c 5.1 2021/06/09 09:41:18 kls Exp $ */ #include "ci.h" #include #include #include #include #include #include #include #include #include #include #include "device.h" #include "mtd.h" #include "pat.h" #include "receiver.h" #include "remux.h" #include "libsi/si.h" #include "skins.h" #include "tools.h" // Set these to 'true' for debug output: static bool DumpTPDUDataTransfer = false; static bool DebugProtocol = false; static bool DumpPolls = false; static bool DumpDateTime = false; #define dbgprotocol(a...) if (DebugProtocol) fprintf(stderr, a) // --- Helper functions ------------------------------------------------------ #define SIZE_INDICATOR 0x80 static const uint8_t *GetLength(const uint8_t *Data, int &Length) ///< Gets the length field from the beginning of Data. ///< Returns a pointer to the first byte after the length and ///< stores the length value in Length. { Length = *Data++; if ((Length & SIZE_INDICATOR) != 0) { int l = Length & ~SIZE_INDICATOR; Length = 0; for (int i = 0; i < l; i++) Length = (Length << 8) | *Data++; } return Data; } static uint8_t *SetLength(uint8_t *Data, int Length) ///< Sets the length field at the beginning of Data. ///< Returns a pointer to the first byte after the length. { uint8_t *p = Data; if (Length < 128) *p++ = Length; else { int n = sizeof(Length); for (int i = n - 1; i >= 0; i--) { int b = (Length >> (8 * i)) & 0xFF; if (p != Data || b) *++p = b; } *Data = (p - Data) | SIZE_INDICATOR; p++; } return p; } static char *CopyString(int Length, const uint8_t *Data) ///< Copies the string at Data. ///< Returns a pointer to a newly allocated string. { char *s = MALLOC(char, Length + 1); char *p = s; while (Length > 0) { int c = *Data; if (isprint(c)) // some CAMs send funny characters in their strings, let's just skip them *p++ = c; else if (c == 0x8A) // the character 0x8A is used as newline, so let's put a real '\n' in there *p++ = '\n'; Length--; Data++; } *p = 0; return s; } static char *GetString(int &Length, const uint8_t **Data) ///< Gets the string at Data. ///< Returns a pointer to a newly allocated string, or NULL in case of error. ///< Upon return Length and Data represent the remaining data after the string has been skipped. { if (Length > 0 && Data && *Data) { int l = 0; const uint8_t *d = GetLength(*Data, l); char *s = CopyString(l, d); Length -= d - *Data + l; *Data = d + l; return s; } return NULL; } // --- cCaPidReceiver -------------------------------------------------------- // A receiver that is used to make the device receive the ECM pids, as well as the // CAT and the EMM pids. class cCaPidReceiver : public cReceiver { private: int catVersion; cVector emmPids; uchar buffer[1024]; // CAT table length: 10 bit -> max. 1021 + 3 bytes uchar *bufp; #define CAT_MAXPACKETS 6 // 6 * 184 = 1104 bytes for CAT table uchar mtdCatBuffer[CAT_MAXPACKETS][TS_SIZE]; // TODO: handle multi table CATs! int mtdNumCatPackets; int length; cMutex mutex; bool handlingPid; void AddEmmPid(int Pid); void DelEmmPids(void); public: cCaPidReceiver(void); virtual ~cCaPidReceiver() { Detach(); } virtual void Receive(const uchar *Data, int Length); bool HasCaPids(void) const { return NumPids() - emmPids.Size() - 1 > 0; } void Reset(void) { DelEmmPids(); catVersion = -1; } bool HandlingPid(void); ///< The cCaPidReceiver adds/deletes PIDs to/from the base class cReceiver, ///< which in turn does the same on the cDevice it is attached to. The cDevice ///< then sets the PIDs on the assigned cCamSlot, which can cause a deadlock on the ///< cCamSlot's mutex if a cReceiver is detached from the device at the same time. ///< Since these PIDs, however, are none that have to be decrypted, ///< it is not necessary to set them in the CAM. Therefore this function is ///< used in cCamSlot::SetPid() to detect this situation, and thus avoid the ///< deadlock. }; cCaPidReceiver::cCaPidReceiver(void) { catVersion = -1; bufp = NULL; mtdNumCatPackets = 0; length = 0; handlingPid = false; cMutexLock MutexLock(&mutex); handlingPid = true; AddPid(CATPID); handlingPid = false; } void cCaPidReceiver::AddEmmPid(int Pid) { for (int i = 0; i < emmPids.Size(); i++) { if (emmPids[i] == Pid) return; } emmPids.Append(Pid); cMutexLock MutexLock(&mutex); handlingPid = true; AddPid(Pid); handlingPid = false; } void cCaPidReceiver::DelEmmPids(void) { cMutexLock MutexLock(&mutex); handlingPid = true; for (int i = 0; i < emmPids.Size(); i++) DelPid(emmPids[i]); emmPids.Clear(); handlingPid = false; } void cCaPidReceiver::Receive(const uchar *Data, int Length) { if (TsPid(Data) == CATPID) { cMtdCamSlot *MtdCamSlot = dynamic_cast(Device()->CamSlot()); const uchar *p = NULL; if (TsPayloadStart(Data)) { if (Data[5] == SI::TableIdCAT) { if (bufp) { // incomplete multi-packet CAT catVersion = -1; bufp = NULL; } length = (int(Data[6] & 0x0F) << 8) | Data[7]; // section length (12 bit field) if (length > 5) { int v = (Data[10] & 0x3E) >> 1; // version number if (v != catVersion) { if (Data[11] == 0 && Data[12] == 0) { // section number, last section number length += 3; // with TableIdCAT -> Data[5] if (length > TS_SIZE - 5) { int n = TS_SIZE - 5; memcpy(buffer, Data + 5, n); bufp = buffer + n; length -= n; } else { p = Data + 5; // no need to copy the data } if (MtdCamSlot) { mtdNumCatPackets = 0; memcpy(mtdCatBuffer[mtdNumCatPackets++], Data, TS_SIZE); } } else dsyslog("multi table CAT section - unhandled!"); catVersion = v; } else if (MtdCamSlot) { for (int i = 0; i < mtdNumCatPackets; i++) MtdCamSlot->PutCat(mtdCatBuffer[i], TS_SIZE); } } } } else if (bufp && length > 0) { int n = min(length, TS_SIZE - 4); if (bufp + n - buffer <= int(sizeof(buffer))) { memcpy(bufp, Data + 4, n); bufp += n; length -= n; if (length <= 0) { p = buffer; length = bufp - buffer; } if (MtdCamSlot) memcpy(mtdCatBuffer[mtdNumCatPackets++], Data, TS_SIZE); } else { esyslog("ERROR: buffer overflow in cCaPidReceiver::Receive()"); bufp = NULL; length = 0; } } if (p) { if (!SI::CRC32::crc32((const char *)p, length, 0xFFFFFFFF)) { // DelEmmPids(); for (int i = 8; i < length - 4; i++) { // -4 = checksum if (p[i] == 0x09) { int CaId = int(p[i + 2] << 8) | p[i + 3]; int EmmPid = Peek13(p + i + 4); AddEmmPid(EmmPid); if (MtdCamSlot) MtdMapPid(const_cast(p + i + 4), MtdCamSlot->MtdMapper()); switch (CaId >> 8) { case 0x01: for (int j = i + 7; j < i + p[i + 1] + 2; j += 4) { EmmPid = Peek13(p + j); AddEmmPid(EmmPid); if (MtdCamSlot) MtdMapPid(const_cast(p + j), MtdCamSlot->MtdMapper()); } break; } i += p[i + 1] + 2 - 1; // -1 to compensate for the loop increment } } if (MtdCamSlot) { // update crc32 uint32_t crc = SI::CRC32::crc32((const char *)p, length - 4, 0xFFFFFFFF); // [crc32] uchar *c = const_cast(p + length - 4); *c++ = crc >> 24; *c++ = crc >> 16; *c++ = crc >> 8; *c++ = crc; // modify CAT packets const uchar *t = p; for (int i = 0, j = 5; i < mtdNumCatPackets; i++, j = 4) { int n = min(length, TS_SIZE - j); memcpy(mtdCatBuffer[i] + j, t, n); t += n; length -= n; MtdCamSlot->PutCat(mtdCatBuffer[i], TS_SIZE); } } } else { esyslog("ERROR: wrong checksum in CAT"); catVersion = -1; } p = NULL; bufp = NULL; length = 0; } } } bool cCaPidReceiver::HandlingPid(void) { cMutexLock MutexLock(&mutex); return handlingPid; } // --- cCaActivationReceiver ------------------------------------------------- // A receiver that is used to make the device stay on a given channel and // keep the CAM slot assigned. #define UNSCRAMBLE_TIME 5 // seconds of receiving purely unscrambled data before considering the smart card "activated" #define TS_PACKET_FACTOR 1024 // only process every TS_PACKET_FACTORth packet to keep the load down class cCaActivationReceiver : public cReceiver { private: cCamSlot *camSlot; time_t lastScrambledTime; int numTsPackets; protected: virtual void Receive(const uchar *Data, int Length); public: cCaActivationReceiver(const cChannel *Channel, cCamSlot *CamSlot); virtual ~cCaActivationReceiver(); }; cCaActivationReceiver::cCaActivationReceiver(const cChannel *Channel, cCamSlot *CamSlot) :cReceiver(Channel, MINPRIORITY + 1) { camSlot = CamSlot; lastScrambledTime = time(NULL); numTsPackets = 0; } cCaActivationReceiver::~cCaActivationReceiver() { Detach(); } void cCaActivationReceiver::Receive(const uchar *Data, int Length) { if (numTsPackets++ % TS_PACKET_FACTOR == 0) { time_t Now = time(NULL); if (TsIsScrambled(Data)) lastScrambledTime = Now; else if (Now - lastScrambledTime > UNSCRAMBLE_TIME) { dsyslog("CAM %d: activated!", camSlot->MasterSlotNumber()); Skins.QueueMessage(mtInfo, tr("CAM activated!")); cDevice *d = Device(); Detach(); if (d) { if (cCamSlot *s = d->CamSlot()) s->CancelActivation(); // this will delete *this* object, so no more code referencing *this* after this call! } } } } // --- cCamResponse ---------------------------------------------------------- // CAM Response Actions: #define CRA_NONE 0 #define CRA_DISCARD -1 #define CRA_CONFIRM -2 #define CRA_SELECT -3 class cCamResponse : public cListObject { private: int camNumber; char *text; int action; public: cCamResponse(void); ~cCamResponse(); bool Parse(const char *s); int Matches(int CamNumber, const char *Text) const; }; cCamResponse::cCamResponse(void) { camNumber = -1; text = NULL; action = CRA_NONE; } cCamResponse::~cCamResponse() { free(text); } bool cCamResponse::Parse(const char *s) { // Number: s = skipspace(s); if (*s == '*') { camNumber = 0; // all CAMs s++; } else { char *e; camNumber = strtol(s, &e, 10); if (e == s || camNumber <= 0) return false; s = e; } // Text: s = skipspace(s); char *t = const_cast(s); // might have to modify it char *q = NULL; // holds a copy in case of backslashes bool InQuotes = false; while (*t) { if (*t == '"') { if (t == s) { // opening quotes InQuotes = true; s++; } else if (InQuotes) // closing quotes break; } else if (*t == '\\') { if (!q) { // need to make a copy in order to strip backslashes q = strdup(s); t = q + (t - s); s = q; } memmove(t, t + 1, strlen(t)); } else if (*t == ' ') { if (!InQuotes) break; } t++; } free(text); // just for safety text = NULL; if (t != s) { text = strndup(s, t - s); s = t + 1; } free(q); if (!text) return false; // Action: s = skipspace(s); if (strcasecmp(s, "DISCARD") == 0) action = CRA_DISCARD; else if (strcasecmp(s, "CONFIRM") == 0) action = CRA_CONFIRM; else if (strcasecmp(s, "SELECT") == 0) action = CRA_SELECT; else if (isnumber(s)) action = atoi(s); else return false; return true; } int cCamResponse::Matches(int CamNumber, const char *Text) const { if (!camNumber || camNumber == CamNumber) { if (strcmp(text, Text) == 0) return action; } return CRA_NONE; } // --- cCamResponses -------------------------------------------------------- class cCamResponses : public cConfig { public: int GetMatch(int CamNumber, const char *Text) const; }; int cCamResponses::GetMatch(int CamNumber, const char *Text) const { for (const cCamResponse *cr = First(); cr; cr = Next(cr)) { int Action = cr->Matches(CamNumber, Text); if (Action != CRA_NONE) { dsyslog("CAM %d: auto response %4d to '%s'\n", CamNumber, Action, Text); return Action; } } return CRA_NONE; } cCamResponses CamResponses; bool CamResponsesLoad(const char *FileName, bool AllowComments, bool MustExist) { return CamResponses.Load(FileName, AllowComments, MustExist); } // --- cTPDU ----------------------------------------------------------------- #define MAX_TPDU_SIZE 4096 #define MAX_TPDU_DATA (MAX_TPDU_SIZE - 4) #define DATA_INDICATOR 0x80 #define T_SB 0x80 #define T_RCV 0x81 #define T_CREATE_TC 0x82 #define T_CTC_REPLY 0x83 #define T_DELETE_TC 0x84 #define T_DTC_REPLY 0x85 #define T_REQUEST_TC 0x86 #define T_NEW_TC 0x87 #define T_TC_ERROR 0x88 #define T_DATA_LAST 0xA0 #define T_DATA_MORE 0xA1 class cTPDU { private: int size; uint8_t buffer[MAX_TPDU_SIZE]; const uint8_t *GetData(const uint8_t *Data, int &Length); public: cTPDU(void) { size = 0; } cTPDU(uint8_t Slot, uint8_t Tcid, uint8_t Tag, int Length = 0, const uint8_t *Data = NULL); uint8_t Slot(void) { return buffer[0]; } uint8_t Tcid(void) { return buffer[1]; } uint8_t Tag(void) { return buffer[2]; } const uint8_t *Data(int &Length) { return GetData(buffer + 3, Length); } uint8_t Status(void); uint8_t *Buffer(void) { return buffer; } int Size(void) { return size; } void SetSize(int Size) { size = Size; } int MaxSize(void) { return sizeof(buffer); } void Dump(int SlotNumber, bool Outgoing); }; cTPDU::cTPDU(uint8_t Slot, uint8_t Tcid, uint8_t Tag, int Length, const uint8_t *Data) { size = 0; buffer[0] = Slot; buffer[1] = Tcid; buffer[2] = Tag; switch (Tag) { case T_RCV: case T_CREATE_TC: case T_CTC_REPLY: case T_DELETE_TC: case T_DTC_REPLY: case T_REQUEST_TC: buffer[3] = 1; // length buffer[4] = Tcid; size = 5; break; case T_NEW_TC: case T_TC_ERROR: if (Length == 1) { buffer[3] = 2; // length buffer[4] = Tcid; buffer[5] = Data[0]; size = 6; } else esyslog("ERROR: invalid data length for TPDU tag 0x%02X: %d (%d/%d)", Tag, Length, Slot, Tcid); break; case T_DATA_LAST: case T_DATA_MORE: if (Length <= MAX_TPDU_DATA) { uint8_t *p = buffer + 3; p = SetLength(p, Length + 1); *p++ = Tcid; if (Length) memcpy(p, Data, Length); size = Length + (p - buffer); } else esyslog("ERROR: invalid data length for TPDU tag 0x%02X: %d (%d/%d)", Tag, Length, Slot, Tcid); break; default: esyslog("ERROR: unknown TPDU tag: 0x%02X (%d/%d)", Tag, Slot, Tcid); } } void cTPDU::Dump(int SlotNumber, bool Outgoing) { if (DumpTPDUDataTransfer && (DumpPolls || Tag() != T_SB)) { #define MAX_DUMP 256 fprintf(stderr, " %d: %s ", SlotNumber, Outgoing ? "-->" : "<--"); for (int i = 0; i < size && i < MAX_DUMP; i++) fprintf(stderr, "%02X ", buffer[i]); fprintf(stderr, "%s\n", size >= MAX_DUMP ? "..." : ""); if (!Outgoing) { fprintf(stderr, " "); for (int i = 0; i < size && i < MAX_DUMP; i++) fprintf(stderr, "%2c ", isprint(buffer[i]) ? buffer[i] : '.'); fprintf(stderr, "%s\n", size >= MAX_DUMP ? "..." : ""); } } } const uint8_t *cTPDU::GetData(const uint8_t *Data, int &Length) { if (size) { Data = GetLength(Data, Length); if (Length) { Length--; // the first byte is always the tcid return Data + 1; } } return NULL; } uint8_t cTPDU::Status(void) { if (size >= 4 && buffer[size - 4] == T_SB && buffer[size - 3] == 2) return buffer[size - 1]; return 0; } // --- cCiTransportConnection ------------------------------------------------ #define MAX_SESSIONS_PER_TC 16 class cCiTransportConnection { private: enum eState { stIDLE, stCREATION, stACTIVE, stDELETION }; cMutex mutex; cCamSlot *camSlot; uint8_t tcid; eState state; bool createConnectionRequested; bool deleteConnectionRequested; bool hasUserIO; cTimeMs alive; cTimeMs timer; cCiSession *sessions[MAX_SESSIONS_PER_TC + 1]; // session numbering starts with 1 cCiSession *tsPostProcessor; void SendTPDU(uint8_t Tag, int Length = 0, const uint8_t *Data = NULL); void SendTag(uint8_t Tag, uint16_t SessionId, uint32_t ResourceId = 0, int Status = -1); void Poll(void); uint32_t ResourceIdToInt(const uint8_t *Data); cCiSession *GetSessionBySessionId(uint16_t SessionId); void OpenSession(int Length, const uint8_t *Data); void CloseSession(uint16_t SessionId); void HandleSessions(cTPDU *TPDU); public: cCiTransportConnection(cCamSlot *CamSlot, uint8_t Tcid); virtual ~cCiTransportConnection(); void SetTsPostProcessor(cCiSession *CiSession); bool TsPostProcess(uint8_t *TsPacket); cCamSlot *CamSlot(void) { return camSlot; } uint8_t Tcid(void) const { return tcid; } void CreateConnection(void) { createConnectionRequested = true; } void DeleteConnection(void) { deleteConnectionRequested = true; } const char *GetCamName(void); bool Ready(void); bool HasUserIO(void) { return hasUserIO; } void SendData(int Length, const uint8_t *Data); bool Process(cTPDU *TPDU = NULL); cCiSession *GetSessionByResourceId(uint32_t ResourceId); }; // --- cCiSession ------------------------------------------------------------ // Session Tags: #define ST_SESSION_NUMBER 0x90 #define ST_OPEN_SESSION_REQUEST 0x91 #define ST_OPEN_SESSION_RESPONSE 0x92 #define ST_CREATE_SESSION 0x93 #define ST_CREATE_SESSION_RESPONSE 0x94 #define ST_CLOSE_SESSION_REQUEST 0x95 #define ST_CLOSE_SESSION_RESPONSE 0x96 // Session Status: #define SS_OK 0x00 #define SS_NOT_ALLOCATED 0xF0 // Resource Identifiers: #define RI_RESOURCE_MANAGER 0x00010041 #define RI_APPLICATION_INFORMATION 0x00020041 #define RI_CONDITIONAL_ACCESS_SUPPORT 0x00030041 #define RI_HOST_CONTROL 0x00200041 #define RI_DATE_TIME 0x00240041 #define RI_MMI 0x00400041 // Application Object Tags: #define AOT_NONE 0x000000 #define AOT_PROFILE_ENQ 0x9F8010 #define AOT_PROFILE 0x9F8011 #define AOT_PROFILE_CHANGE 0x9F8012 #define AOT_APPLICATION_INFO_ENQ 0x9F8020 #define AOT_APPLICATION_INFO 0x9F8021 #define AOT_ENTER_MENU 0x9F8022 #define AOT_CA_INFO_ENQ 0x9F8030 #define AOT_CA_INFO 0x9F8031 #define AOT_CA_PMT 0x9F8032 #define AOT_CA_PMT_REPLY 0x9F8033 #define AOT_TUNE 0x9F8400 #define AOT_REPLACE 0x9F8401 #define AOT_CLEAR_REPLACE 0x9F8402 #define AOT_ASK_RELEASE 0x9F8403 #define AOT_DATE_TIME_ENQ 0x9F8440 #define AOT_DATE_TIME 0x9F8441 #define AOT_CLOSE_MMI 0x9F8800 #define AOT_DISPLAY_CONTROL 0x9F8801 #define AOT_DISPLAY_REPLY 0x9F8802 #define AOT_TEXT_LAST 0x9F8803 #define AOT_TEXT_MORE 0x9F8804 #define AOT_KEYPAD_CONTROL 0x9F8805 #define AOT_KEYPRESS 0x9F8806 #define AOT_ENQ 0x9F8807 #define AOT_ANSW 0x9F8808 #define AOT_MENU_LAST 0x9F8809 #define AOT_MENU_MORE 0x9F880A #define AOT_MENU_ANSW 0x9F880B #define AOT_LIST_LAST 0x9F880C #define AOT_LIST_MORE 0x9F880D #define AOT_SUBTITLE_SEGMENT_LAST 0x9F880E #define AOT_SUBTITLE_SEGMENT_MORE 0x9F880F #define AOT_DISPLAY_MESSAGE 0x9F8810 #define AOT_SCENE_END_MARK 0x9F8811 #define AOT_SCENE_DONE 0x9F8812 #define AOT_SCENE_CONTROL 0x9F8813 #define AOT_SUBTITLE_DOWNLOAD_LAST 0x9F8814 #define AOT_SUBTITLE_DOWNLOAD_MORE 0x9F8815 #define AOT_FLUSH_DOWNLOAD 0x9F8816 #define AOT_DOWNLOAD_REPLY 0x9F8817 #define AOT_COMMS_CMD 0x9F8C00 #define AOT_CONNECTION_DESCRIPTOR 0x9F8C01 #define AOT_COMMS_REPLY 0x9F8C02 #define AOT_COMMS_SEND_LAST 0x9F8C03 #define AOT_COMMS_SEND_MORE 0x9F8C04 #define AOT_COMMS_RCV_LAST 0x9F8C05 #define AOT_COMMS_RCV_MORE 0x9F8C06 #define RESOURCE_CLASS_MASK 0xFFFF0000 cCiSession::cCiSession(uint16_t SessionId, uint32_t ResourceId, cCiTransportConnection *Tc) { sessionId = SessionId; resourceId = ResourceId; tc = Tc; } cCiSession::~cCiSession() { } void cCiSession::SetResourceId(uint32_t Id) { resourceId = Id; } void cCiSession::SetTsPostProcessor(void) { tc->SetTsPostProcessor(this); } int cCiSession::GetTag(int &Length, const uint8_t **Data) ///< Gets the tag at Data. ///< Returns the actual tag, or AOT_NONE in case of error. ///< Upon return Length and Data represent the remaining data after the tag has been skipped. { if (Length >= 3 && Data && *Data) { int t = 0; for (int i = 0; i < 3; i++) t = (t << 8) | *(*Data)++; Length -= 3; return t; } return AOT_NONE; } const uint8_t *cCiSession::GetData(const uint8_t *Data, int &Length) { Data = GetLength(Data, Length); return Length ? Data : NULL; } void cCiSession::SendData(int Tag, int Length, const uint8_t *Data) { uint8_t buffer[MAX_TPDU_SIZE]; uint8_t *p = buffer; *p++ = ST_SESSION_NUMBER; *p++ = 0x02; *p++ = (sessionId >> 8) & 0xFF; *p++ = sessionId & 0xFF; *p++ = (Tag >> 16) & 0xFF; *p++ = (Tag >> 8) & 0xFF; *p++ = Tag & 0xFF; p = SetLength(p, Length); if (p - buffer + Length < int(sizeof(buffer))) { if (Data) memcpy(p, Data, Length); p += Length; tc->SendData(p - buffer, buffer); } else esyslog("ERROR: CAM %d: data length (%d) exceeds buffer size", CamSlot()->SlotNumber(), Length); } cCamSlot *cCiSession::CamSlot(void) { return Tc()->CamSlot(); } void cCiSession::Process(int Length, const uint8_t *Data) { } // --- cCiResourceManager ---------------------------------------------------- class cCiResourceManager : public cCiSession { private: int state; public: cCiResourceManager(uint16_t SessionId, cCiTransportConnection *Tc); virtual void Process(int Length = 0, const uint8_t *Data = NULL); }; cCiResourceManager::cCiResourceManager(uint16_t SessionId, cCiTransportConnection *Tc) :cCiSession(SessionId, RI_RESOURCE_MANAGER, Tc) { dbgprotocol("Slot %d: new Resource Manager (session id %d)\n", CamSlot()->SlotNumber(), SessionId); state = 0; } void cCiResourceManager::Process(int Length, const uint8_t *Data) { if (Data) { int Tag = GetTag(Length, &Data); switch (Tag) { case AOT_PROFILE_ENQ: { dbgprotocol("Slot %d: <== Profile Enquiry (%d)\n", CamSlot()->SlotNumber(), SessionId()); dbgprotocol("Slot %d: ==> Profile (%d)\n", CamSlot()->SlotNumber(), SessionId()); SendData(AOT_PROFILE, CiResourceHandlers.NumIds() * sizeof(uint32_t), (uint8_t*)CiResourceHandlers.Ids()); state = 3; } break; case AOT_PROFILE: { dbgprotocol("Slot %d: <== Profile (%d)\n", CamSlot()->SlotNumber(), SessionId()); if (state == 1) { int l = 0; const uint8_t *d = GetData(Data, l); if (l > 0 && d) esyslog("ERROR: CAM %d: resource manager: unexpected data", CamSlot()->SlotNumber()); dbgprotocol("Slot %d: ==> Profile Change (%d)\n", CamSlot()->SlotNumber(), SessionId()); SendData(AOT_PROFILE_CHANGE); state = 2; } else { esyslog("ERROR: CAM %d: resource manager: unexpected tag %06X in state %d", CamSlot()->SlotNumber(), Tag, state); } } break; default: esyslog("ERROR: CAM %d: resource manager: unknown tag %06X", CamSlot()->SlotNumber(), Tag); } } else if (state == 0) { dbgprotocol("Slot %d: ==> Profile Enq (%d)\n", CamSlot()->SlotNumber(), SessionId()); SendData(AOT_PROFILE_ENQ); state = 1; } } // --- cCiApplicationInformation --------------------------------------------- cCiApplicationInformation::cCiApplicationInformation(uint16_t SessionId, cCiTransportConnection *Tc) :cCiSession(SessionId, RI_APPLICATION_INFORMATION, Tc) { dbgprotocol("Slot %d: new Application Information (session id %d)\n", CamSlot()->SlotNumber(), SessionId); state = 0; menuString = NULL; } cCiApplicationInformation::~cCiApplicationInformation() { free(menuString); } void cCiApplicationInformation::Process(int Length, const uint8_t *Data) { if (Data) { int Tag = GetTag(Length, &Data); switch (Tag) { case AOT_APPLICATION_INFO: { dbgprotocol("Slot %d: <== Application Info (%d)\n", CamSlot()->SlotNumber(), SessionId()); int l = 0; const uint8_t *d = GetData(Data, l); if ((l -= 1) < 0) break; applicationType = *d++; if ((l -= 2) < 0) break; applicationManufacturer = ntohs(get_unaligned((uint16_t *)d)); d += 2; if ((l -= 2) < 0) break; manufacturerCode = ntohs(get_unaligned((uint16_t *)d)); d += 2; free(menuString); menuString = GetString(l, &d); isyslog("CAM %d: %s, %02X, %04X, %04X", CamSlot()->SlotNumber(), menuString, applicationType, applicationManufacturer, manufacturerCode); state = 2; } break; default: esyslog("ERROR: CAM %d: application information: unknown tag %06X", CamSlot()->SlotNumber(), Tag); } } else if (state == 0) { dbgprotocol("Slot %d: ==> Application Info Enq (%d)\n", CamSlot()->SlotNumber(), SessionId()); SendData(AOT_APPLICATION_INFO_ENQ); state = 1; } } bool cCiApplicationInformation::EnterMenu(void) { if (state == 2) { dbgprotocol("Slot %d: ==> Enter Menu (%d)\n", CamSlot()->SlotNumber(), SessionId()); SendData(AOT_ENTER_MENU); return true; } return false; } // --- cCiCaPmt -------------------------------------------------------------- #define MAXCASYSTEMIDS 64 // Ca Pmt List Management: #define CPLM_MORE 0x00 #define CPLM_FIRST 0x01 #define CPLM_LAST 0x02 #define CPLM_ONLY 0x03 #define CPLM_ADD 0x04 #define CPLM_UPDATE 0x05 // Ca Pmt Cmd Ids: #define CPCI_OK_DESCRAMBLING 0x01 #define CPCI_OK_MMI 0x02 #define CPCI_QUERY 0x03 #define CPCI_NOT_SELECTED 0x04 class cCiCaPmt { friend class cCiConditionalAccessSupport; private: uint8_t cmdId; int esInfoLengthPos; cDynamicBuffer caDescriptors; cDynamicBuffer capmt; int source; int transponder; int programNumber; int caSystemIds[MAXCASYSTEMIDS + 1]; // list is zero terminated! void AddCaDescriptors(int Length, const uint8_t *Data); public: cCiCaPmt(uint8_t CmdId, int Source, int Transponder, int ProgramNumber, const int *CaSystemIds); uint8_t CmdId(void) { return cmdId; } void SetListManagement(uint8_t ListManagement); uint8_t ListManagement(void) { return capmt.Get(0); } void AddPid(int Pid, uint8_t StreamType); void MtdMapPids(cMtdMapper *MtdMapper); }; cCiCaPmt::cCiCaPmt(uint8_t CmdId, int Source, int Transponder, int ProgramNumber, const int *CaSystemIds) { cmdId = CmdId; source = Source; transponder = Transponder; programNumber = ProgramNumber; int i = 0; if (CaSystemIds) { for (; CaSystemIds[i]; i++) caSystemIds[i] = CaSystemIds[i]; } caSystemIds[i] = 0; GetCaDescriptors(source, transponder, programNumber, caSystemIds, caDescriptors, 0); capmt.Append(CPLM_ONLY); capmt.Append((ProgramNumber >> 8) & 0xFF); capmt.Append( ProgramNumber & 0xFF); capmt.Append(0x01); // version_number, current_next_indicator - apparently vn doesn't matter, but cni must be 1 esInfoLengthPos = capmt.Length(); capmt.Append(0x00); // program_info_length H (at program level) capmt.Append(0x00); // program_info_length L AddCaDescriptors(caDescriptors.Length(), caDescriptors.Data()); } void cCiCaPmt::SetListManagement(uint8_t ListManagement) { capmt.Set(0, ListManagement); } void cCiCaPmt::AddPid(int Pid, uint8_t StreamType) { if (Pid) { GetCaDescriptors(source, transponder, programNumber, caSystemIds, caDescriptors, Pid); capmt.Append(StreamType); capmt.Append((Pid >> 8) & 0xFF); capmt.Append( Pid & 0xFF); esInfoLengthPos = capmt.Length(); capmt.Append(0x00); // ES_info_length H (at ES level) capmt.Append(0x00); // ES_info_length L AddCaDescriptors(caDescriptors.Length(), caDescriptors.Data()); } } void cCiCaPmt::AddCaDescriptors(int Length, const uint8_t *Data) { if (esInfoLengthPos) { if (Length || cmdId == CPCI_QUERY) { capmt.Append(cmdId); capmt.Append(Data, Length); int l = capmt.Length() - esInfoLengthPos - 2; capmt.Set(esInfoLengthPos, (l >> 8) & 0xFF); capmt.Set(esInfoLengthPos + 1, l & 0xFF); } esInfoLengthPos = 0; } else esyslog("ERROR: adding CA descriptor without Pid!"); } static int MtdMapCaDescriptor(uchar *p, cMtdMapper *MtdMapper) { // See pat.c: cCaDescriptor::cCaDescriptor() for the layout of the data! if (*p == SI::CaDescriptorTag) { int l = *++p; if (l >= 4) { MtdMapPid(p + 3, MtdMapper); return l + 2; } else esyslog("ERROR: wrong length (%d) in MtdMapCaDescriptor()", l); } else esyslog("ERROR: wrong tag (%d) in MtdMapCaDescriptor()", *p); return -1; } static int MtdMapCaDescriptors(uchar *p, cMtdMapper *MtdMapper) { int Length = p[0] * 256 + p[1]; if (Length >= 3) { p += 3; int m = Length - 1; while (m > 0) { int l = MtdMapCaDescriptor(p, MtdMapper); if (l > 0) { p += l; m -= l; } } } return Length + 2; } static int MtdMapStream(uchar *p, cMtdMapper *MtdMapper) { // See ci.c: cCiCaPmt::AddPid() for the layout of the data! MtdMapPid(p + 1, MtdMapper); int l = MtdMapCaDescriptors(p + 3, MtdMapper); if (l > 0) return l + 3; return -1; } static int MtdMapStreams(uchar *p, cMtdMapper *MtdMapper, int Length) { int m = Length; while (m >= 5) { int l = MtdMapStream(p, MtdMapper); if (l > 0) { p += l; m -= l; } else break; } return Length; } void cCiCaPmt::MtdMapPids(cMtdMapper *MtdMapper) { uchar *p = capmt.Data(); int m = capmt.Length(); if (m >= 3) { MtdMapSid(p + 1, MtdMapper); p += 4; m -= 4; if (m >= 2) { int l = MtdMapCaDescriptors(p, MtdMapper); if (l >= 0) { p += l; m -= l; MtdMapStreams(p, MtdMapper, m); } } } } // --- cCiConditionalAccessSupport ------------------------------------------- // CA Enable Ids: #define CAEI_POSSIBLE 0x01 #define CAEI_POSSIBLE_COND_PURCHASE 0x02 #define CAEI_POSSIBLE_COND_TECHNICAL 0x03 #define CAEI_NOT_POSSIBLE_ENTITLEMENT 0x71 #define CAEI_NOT_POSSIBLE_TECHNICAL 0x73 #define CA_ENABLE_FLAG 0x80 #define CA_ENABLE(x) (((x) & CA_ENABLE_FLAG) ? (x) & ~CA_ENABLE_FLAG : 0) #define QUERY_WAIT_TIME 500 // ms to wait before sending a query #define QUERY_REPLY_TIMEOUT 2000 // ms to wait for a reply to a query #define QUERY_RETRIES 6 // max. number of retries to check if there is a reply to a query class cCiConditionalAccessSupport : public cCiSession { private: int state; int numCaSystemIds; int caSystemIds[MAXCASYSTEMIDS + 1]; // list is zero terminated! bool repliesToQuery; cTimeMs timer; int numRetries; public: cCiConditionalAccessSupport(uint16_t SessionId, cCiTransportConnection *Tc); virtual void Process(int Length = 0, const uint8_t *Data = NULL); const int *GetCaSystemIds(void) { return caSystemIds; } void SendPMT(cCiCaPmt *CaPmt); bool RepliesToQuery(void) { return repliesToQuery; } bool Ready(void) { return state >= 4; } bool ReceivedReply(void) { return state >= 5; } bool CanDecrypt(void) { return state == 6; } }; cCiConditionalAccessSupport::cCiConditionalAccessSupport(uint16_t SessionId, cCiTransportConnection *Tc) :cCiSession(SessionId, RI_CONDITIONAL_ACCESS_SUPPORT, Tc) { dbgprotocol("Slot %d: new Conditional Access Support (session id %d)\n", CamSlot()->SlotNumber(), SessionId); state = 0; // inactive caSystemIds[numCaSystemIds = 0] = 0; repliesToQuery = false; numRetries = 0; } void cCiConditionalAccessSupport::Process(int Length, const uint8_t *Data) { if (Data) { int Tag = GetTag(Length, &Data); switch (Tag) { case AOT_CA_INFO: { dbgprotocol("Slot %d: <== Ca Info (%d)", CamSlot()->SlotNumber(), SessionId()); cString Ids; numCaSystemIds = 0; int l = 0; const uint8_t *d = GetData(Data, l); while (l > 1) { uint16_t id = ((uint16_t)(*d) << 8) | *(d + 1); Ids = cString::sprintf("%s %04X", *Ids ? *Ids : "", id); dbgprotocol(" %04X", id); d += 2; l -= 2; if (numCaSystemIds < MAXCASYSTEMIDS) caSystemIds[numCaSystemIds++] = id; else { esyslog("ERROR: CAM %d: too many CA system IDs!", CamSlot()->SlotNumber()); break; } } caSystemIds[numCaSystemIds] = 0; dbgprotocol("\n"); if (state == 1) { timer.Set(0); numRetries = QUERY_RETRIES; state = 2; // got ca info } dsyslog("CAM %d: system ids:%s", CamSlot()->SlotNumber(), *Ids ? *Ids : " none"); } break; case AOT_CA_PMT_REPLY: { dbgprotocol("Slot %d: <== Ca Pmt Reply (%d)", CamSlot()->SlotNumber(), SessionId()); if (!repliesToQuery) { if (CamSlot()->IsMasterSlot()) dsyslog("CAM %d: replies to QUERY - multi channel decryption (MCD) possible", CamSlot()->SlotNumber()); repliesToQuery = true; if (CamSlot()->MtdAvailable()) { if (CamSlot()->IsMasterSlot()) dsyslog("CAM %d: supports multi transponder decryption (MTD)", CamSlot()->SlotNumber()); CamSlot()->MtdActivate(true); } } state = 5; // got ca pmt reply int l = 0; const uint8_t *d = GetData(Data, l); if (l > 1) { uint16_t pnr = ((uint16_t)(*d) << 8) | *(d + 1); dbgprotocol(" %d", pnr); d += 2; l -= 2; if (l > 0) { dbgprotocol(" %02X", *d); d += 1; l -= 1; if (l > 0) { if (l % 3 == 0 && l > 1) { // The EN50221 standard defines that the next byte is supposed // to be the CA_enable value at programme level. However, there are // CAMs (for instance the AlphaCrypt with firmware <= 3.05) that // insert a two byte length field here. // This is a workaround to skip this length field: uint16_t len = ((uint16_t)(*d) << 8) | *(d + 1); if (len == l - 2) { d += 2; l -= 2; } } unsigned char caepl = *d; dbgprotocol(" %02X", caepl); d += 1; l -= 1; bool ok = true; if (l <= 2) ok = CA_ENABLE(caepl) == CAEI_POSSIBLE; while (l > 2) { uint16_t pid = ((uint16_t)(*d) << 8) | *(d + 1); unsigned char caees = *(d + 2); dbgprotocol(" %d=%02X", pid, caees); d += 3; l -= 3; if (CA_ENABLE(caees) != CAEI_POSSIBLE) ok = false; } if (ok) state = 6; // descrambling possible } } } dbgprotocol("\n"); } break; default: esyslog("ERROR: CAM %d: conditional access support: unknown tag %06X", CamSlot()->SlotNumber(), Tag); } } else if (state == 0) { dbgprotocol("Slot %d: ==> Ca Info Enq (%d)\n", CamSlot()->SlotNumber(), SessionId()); SendData(AOT_CA_INFO_ENQ); state = 1; // enquired ca info } else if ((state == 2 || state == 3) && timer.TimedOut()) { if (numRetries-- > 0) { cCiCaPmt CaPmt(CPCI_QUERY, 0, 0, 0, NULL); SendPMT(&CaPmt); timer.Set(QUERY_WAIT_TIME); state = 3; // waiting for reply } else { dsyslog("CAM %d: doesn't reply to QUERY - only a single channel can be decrypted", CamSlot()->SlotNumber()); CamSlot()->MtdActivate(false); state = 4; // normal operation } } } void cCiConditionalAccessSupport::SendPMT(cCiCaPmt *CaPmt) { if (CaPmt && state >= 2) { dbgprotocol("Slot %d: ==> Ca Pmt (%d) %d %d\n", CamSlot()->SlotNumber(), SessionId(), CaPmt->ListManagement(), CaPmt->CmdId()); SendData(AOT_CA_PMT, CaPmt->capmt.Length(), CaPmt->capmt.Data()); state = 4; // sent ca pmt } } // --- cCiHostControl -------------------------------------------------------- class cCiHostControl : public cCiSession { public: cCiHostControl(uint16_t SessionId, cCiTransportConnection *Tc); virtual void Process(int Length = 0, const uint8_t *Data = NULL); }; cCiHostControl::cCiHostControl(uint16_t SessionId, cCiTransportConnection* Tc) :cCiSession(SessionId, RI_HOST_CONTROL, Tc) { dbgprotocol("Slot %d: new Host Control (session id %d)\n", CamSlot()->SlotNumber(), SessionId); } void cCiHostControl::Process(int Length, const uint8_t* Data) { if (Data) { int Tag = GetTag(Length, &Data); switch (Tag) { case AOT_TUNE: dbgprotocol("Slot %d: <== Host Control Tune (%d)\n", CamSlot()->SlotNumber(), SessionId()); break; case AOT_REPLACE: dbgprotocol("Slot %d: <== Host Control Replace (%d)\n", CamSlot()->SlotNumber(), SessionId()); break; case AOT_CLEAR_REPLACE: dbgprotocol("Slot %d: <== Host Control Clear Replace (%d)\n", CamSlot()->SlotNumber(), SessionId()); break; default: esyslog("ERROR: CAM %d: Host Control: unknown tag %06X", CamSlot()->SlotNumber(), Tag); } } } // --- cCiDateTime ----------------------------------------------------------- class cCiDateTime : public cCiSession { private: int interval; time_t lastTime; void SendDateTime(void); public: cCiDateTime(uint16_t SessionId, cCiTransportConnection *Tc); virtual void Process(int Length = 0, const uint8_t *Data = NULL); }; cCiDateTime::cCiDateTime(uint16_t SessionId, cCiTransportConnection *Tc) :cCiSession(SessionId, RI_DATE_TIME, Tc) { interval = 0; lastTime = 0; dbgprotocol("Slot %d: new Date Time (session id %d)\n", CamSlot()->SlotNumber(), SessionId); } void cCiDateTime::SendDateTime(void) { time_t t = time(NULL); struct tm tm_gmt; struct tm tm_loc; if (gmtime_r(&t, &tm_gmt) && localtime_r(&t, &tm_loc)) { int Y = tm_gmt.tm_year; int M = tm_gmt.tm_mon + 1; int D = tm_gmt.tm_mday; int L = (M == 1 || M == 2) ? 1 : 0; int MJD = 14956 + D + int((Y - L) * 365.25) + int((M + 1 + L * 12) * 30.6001); #define DEC2BCD(d) uint8_t(((d / 10) << 4) + (d % 10)) #pragma pack(1) struct tTime { uint16_t mjd; uint8_t h, m, s; short offset; }; #pragma pack() tTime T = { mjd : htons(MJD), h : DEC2BCD(tm_gmt.tm_hour), m : DEC2BCD(tm_gmt.tm_min), s : DEC2BCD(tm_gmt.tm_sec), offset : short(htons(tm_loc.tm_gmtoff / 60)) }; bool OldDumpTPDUDataTransfer = DumpTPDUDataTransfer; DumpTPDUDataTransfer &= DumpDateTime; if (DumpDateTime) dbgprotocol("Slot %d: ==> Date Time (%d)\n", CamSlot()->SlotNumber(), SessionId()); SendData(AOT_DATE_TIME, 7, (uint8_t*)&T); DumpTPDUDataTransfer = OldDumpTPDUDataTransfer; } } void cCiDateTime::Process(int Length, const uint8_t *Data) { if (Data) { int Tag = GetTag(Length, &Data); switch (Tag) { case AOT_DATE_TIME_ENQ: { interval = 0; int l = 0; const uint8_t *d = GetData(Data, l); if (l > 0) interval = *d; dbgprotocol("Slot %d: <== Date Time Enq (%d), interval = %d\n", CamSlot()->SlotNumber(), SessionId(), interval); lastTime = time(NULL); SendDateTime(); } break; default: esyslog("ERROR: CAM %d: date time: unknown tag %06X", CamSlot()->SlotNumber(), Tag); } } else if (interval && time(NULL) - lastTime > interval) { lastTime = time(NULL); SendDateTime(); } } // --- cCiMMI ---------------------------------------------------------------- // Display Control Commands: #define DCC_SET_MMI_MODE 0x01 #define DCC_DISPLAY_CHARACTER_TABLE_LIST 0x02 #define DCC_INPUT_CHARACTER_TABLE_LIST 0x03 #define DCC_OVERLAY_GRAPHICS_CHARACTERISTICS 0x04 #define DCC_FULL_SCREEN_GRAPHICS_CHARACTERISTICS 0x05 // MMI Modes: #define MM_HIGH_LEVEL 0x01 #define MM_LOW_LEVEL_OVERLAY_GRAPHICS 0x02 #define MM_LOW_LEVEL_FULL_SCREEN_GRAPHICS 0x03 // Display Reply IDs: #define DRI_MMI_MODE_ACK 0x01 #define DRI_LIST_DISPLAY_CHARACTER_TABLES 0x02 #define DRI_LIST_INPUT_CHARACTER_TABLES 0x03 #define DRI_LIST_GRAPHIC_OVERLAY_CHARACTERISTICS 0x04 #define DRI_LIST_FULL_SCREEN_GRAPHIC_CHARACTERISTICS 0x05 #define DRI_UNKNOWN_DISPLAY_CONTROL_CMD 0xF0 #define DRI_UNKNOWN_MMI_MODE 0xF1 #define DRI_UNKNOWN_CHARACTER_TABLE 0xF2 // Enquiry Flags: #define EF_BLIND 0x01 // Answer IDs: #define AI_CANCEL 0x00 #define AI_ANSWER 0x01 class cCiMMI : public cCiSession { private: char *GetText(int &Length, const uint8_t **Data); cCiMenu *menu, *fetchedMenu; cCiEnquiry *enquiry, *fetchedEnquiry; public: cCiMMI(uint16_t SessionId, cCiTransportConnection *Tc); virtual ~cCiMMI(); virtual void Process(int Length = 0, const uint8_t *Data = NULL); virtual bool HasUserIO(void) { return menu || enquiry; } cCiMenu *Menu(bool Clear = false); cCiEnquiry *Enquiry(bool Clear = false); void SendMenuAnswer(uint8_t Selection); bool SendAnswer(const char *Text); bool SendCloseMMI(void); }; cCiMMI::cCiMMI(uint16_t SessionId, cCiTransportConnection *Tc) :cCiSession(SessionId, RI_MMI, Tc) { dbgprotocol("Slot %d: new MMI (session id %d)\n", CamSlot()->SlotNumber(), SessionId); menu = fetchedMenu = NULL; enquiry = fetchedEnquiry = NULL; } cCiMMI::~cCiMMI() { if (fetchedMenu) { cMutexLock MutexLock(fetchedMenu->mutex); fetchedMenu->mmi = NULL; } delete menu; if (fetchedEnquiry) { cMutexLock MutexLock(fetchedEnquiry->mutex); fetchedEnquiry->mmi = NULL; } delete enquiry; } char *cCiMMI::GetText(int &Length, const uint8_t **Data) ///< Gets the text at Data. ///< Returns a pointer to a newly allocated string, or NULL in case of error. ///< Upon return Length and Data represent the remaining data after the text has been skipped. { int Tag = GetTag(Length, Data); if (Tag == AOT_TEXT_LAST) { char *s = GetString(Length, Data); dbgprotocol("Slot %d: <== Text Last (%d) '%s'\n", CamSlot()->SlotNumber(), SessionId(), s); return s; } else esyslog("ERROR: CAM %d: MMI: unexpected text tag: %06X", CamSlot()->SlotNumber(), Tag); return NULL; } void cCiMMI::Process(int Length, const uint8_t *Data) { if (Data) { int Tag = GetTag(Length, &Data); switch (Tag) { case AOT_DISPLAY_CONTROL: { dbgprotocol("Slot %d: <== Display Control (%d)\n", CamSlot()->SlotNumber(), SessionId()); int l = 0; const uint8_t *d = GetData(Data, l); if (l > 0) { switch (*d) { case DCC_SET_MMI_MODE: if (l == 2 && *++d == MM_HIGH_LEVEL) { struct tDisplayReply { uint8_t id; uint8_t mode; }; tDisplayReply dr = { id : DRI_MMI_MODE_ACK, mode : MM_HIGH_LEVEL }; dbgprotocol("Slot %d: ==> Display Reply (%d)\n", CamSlot()->SlotNumber(), SessionId()); SendData(AOT_DISPLAY_REPLY, 2, (uint8_t *)&dr); } break; default: esyslog("ERROR: CAM %d: MMI: unsupported display control command %02X", CamSlot()->SlotNumber(), *d); } } } break; case AOT_LIST_LAST: case AOT_MENU_LAST: { dbgprotocol("Slot %d: <== Menu Last (%d)\n", CamSlot()->SlotNumber(), SessionId()); delete menu; menu = new cCiMenu(this, Tag == AOT_MENU_LAST); int l = 0; const uint8_t *d = GetData(Data, l); if (l > 0) { // since the specification allows choiceNb to be undefined it is useless, so let's just skip it: d++; l--; if (l > 0) menu->titleText = GetText(l, &d); if (l > 0) menu->subTitleText = GetText(l, &d); if (l > 0) menu->bottomText = GetText(l, &d); int Action = CRA_NONE; int Select = -1; int Item = 0; while (l > 0) { char *s = GetText(l, &d); if (s) { if (!menu->AddEntry(s)) free(s); else if (Action == CRA_NONE) { Action = CamResponses.GetMatch(CamSlot()->SlotNumber(), s); if (Action == CRA_SELECT) Select = Item; } } else break; Item++; } if (Action != CRA_NONE) { delete menu; menu = NULL; cCondWait::SleepMs(100); if (Action == CRA_DISCARD) { SendCloseMMI(); dsyslog("CAM %d: DISCARD", CamSlot()->SlotNumber()); } else if (Action == CRA_CONFIRM) { SendMenuAnswer(1); dsyslog("CAM %d: CONFIRM", CamSlot()->SlotNumber()); } else if (Action == CRA_SELECT) { SendMenuAnswer(Select + 1); dsyslog("CAM %d: SELECT %d", CamSlot()->SlotNumber(), Select + 1); } } } } break; case AOT_ENQ: { dbgprotocol("Slot %d: <== Enq (%d)\n", CamSlot()->SlotNumber(), SessionId()); delete enquiry; enquiry = new cCiEnquiry(this); int l = 0; const uint8_t *d = GetData(Data, l); if (l > 0) { uint8_t blind = *d++; //XXX GetByte()??? l--; enquiry->blind = blind & EF_BLIND; enquiry->expectedLength = *d++; l--; // I really wonder why there is no text length field here... enquiry->text = CopyString(l, d); int Action = CamResponses.GetMatch(CamSlot()->SlotNumber(), enquiry->text); if (Action > CRA_NONE) { char s[enquiry->expectedLength * 2]; snprintf(s, sizeof(s), "%d", Action); if (int(strlen(s)) == enquiry->expectedLength) { delete enquiry; enquiry = NULL; SendAnswer(s); dsyslog("CAM %d: PIN", CamSlot()->SlotNumber()); } else esyslog("CAM %d: ERROR: unexpected PIN length %d, expected %d", CamSlot()->SlotNumber(), int(strlen(s)), enquiry->expectedLength); } } } break; case AOT_CLOSE_MMI: { int id = -1; int delay = -1; int l = 0; const uint8_t *d = GetData(Data, l); if (l > 0) { id = *d++; if (l > 1) delay = *d; } dbgprotocol("Slot %d: <== Close MMI (%d) id = %02X delay = %d\n", CamSlot()->SlotNumber(), SessionId(), id, delay); } break; default: esyslog("ERROR: CAM %d: MMI: unknown tag %06X", CamSlot()->SlotNumber(), Tag); } } } cCiMenu *cCiMMI::Menu(bool Clear) { if (Clear) fetchedMenu = NULL; else if (menu) { fetchedMenu = menu; menu = NULL; } return fetchedMenu; } cCiEnquiry *cCiMMI::Enquiry(bool Clear) { if (Clear) fetchedEnquiry = NULL; else if (enquiry) { fetchedEnquiry = enquiry; enquiry = NULL; } return fetchedEnquiry; } void cCiMMI::SendMenuAnswer(uint8_t Selection) { dbgprotocol("Slot %d: ==> Menu Answ (%d)\n", CamSlot()->SlotNumber(), SessionId()); SendData(AOT_MENU_ANSW, 1, &Selection); } bool cCiMMI::SendAnswer(const char *Text) { dbgprotocol("Slot %d: ==> Answ (%d)\n", CamSlot()->SlotNumber(), SessionId()); struct tAnswer { uint8_t id; char text[256]; };//XXX tAnswer answer; answer.id = Text ? AI_ANSWER : AI_CANCEL; int len = 0; if (Text) { len = min(sizeof(answer.text), strlen(Text)); memcpy(answer.text, Text, len); } SendData(AOT_ANSW, len + 1, (uint8_t *)&answer); return true; } bool cCiMMI::SendCloseMMI(void) { dbgprotocol("Slot %d: ==> Close MMI (%d)\n", CamSlot()->SlotNumber(), SessionId()); SendData(AOT_CLOSE_MMI, 0); return true; } // --- cCiMenu --------------------------------------------------------------- cCiMenu::cCiMenu(cCiMMI *MMI, bool Selectable) { mmi = MMI; mutex = NULL; selectable = Selectable; titleText = subTitleText = bottomText = NULL; numEntries = 0; } cCiMenu::~cCiMenu() { cMutexLock MutexLock(mutex); if (mmi) mmi->Menu(true); free(titleText); free(subTitleText); free(bottomText); for (int i = 0; i < numEntries; i++) free(entries[i]); } bool cCiMenu::AddEntry(char *s) { if (numEntries < MAX_CIMENU_ENTRIES) { entries[numEntries++] = s; return true; } return false; } bool cCiMenu::HasUpdate(void) { // If the mmi is gone, the menu shall be closed, which also qualifies as 'update'. return !mmi || mmi->HasUserIO(); } void cCiMenu::Select(int Index) { cMutexLock MutexLock(mutex); if (mmi && -1 <= Index && Index < numEntries) mmi->SendMenuAnswer(Index + 1); } void cCiMenu::Cancel(void) { Select(-1); } void cCiMenu::Abort(void) { cMutexLock MutexLock(mutex); if (mmi) mmi->SendCloseMMI(); } // --- cCiEnquiry ------------------------------------------------------------ cCiEnquiry::cCiEnquiry(cCiMMI *MMI) { mmi = MMI; mutex = NULL; text = NULL; blind = false; expectedLength = 0; } cCiEnquiry::~cCiEnquiry() { cMutexLock MutexLock(mutex); if (mmi) mmi->Enquiry(true); free(text); } void cCiEnquiry::Reply(const char *s) { cMutexLock MutexLock(mutex); if (mmi) mmi->SendAnswer(s); } void cCiEnquiry::Cancel(void) { Reply(NULL); } void cCiEnquiry::Abort(void) { cMutexLock MutexLock(mutex); if (mmi) mmi->SendCloseMMI(); } // --- cCiResourceHandler ---------------------------------------------------- cCiResourceHandler::cCiResourceHandler(void) { } cCiResourceHandler::~cCiResourceHandler() { } // --- cCiDefaultResourceHandler --------------------------------------------- class cCiDefaultResourceHandler : public cCiResourceHandler { public: virtual const uint32_t *ResourceIds(void) const; virtual cCiSession *GetNewCiSession(uint32_t ResourceId, uint16_t SessionId, cCiTransportConnection *Tc); }; const uint32_t *cCiDefaultResourceHandler::ResourceIds(void) const { static uint32_t Ids[] = { RI_RESOURCE_MANAGER, RI_APPLICATION_INFORMATION, RI_CONDITIONAL_ACCESS_SUPPORT, RI_HOST_CONTROL, RI_DATE_TIME, RI_MMI, 0 }; return Ids; } cCiSession *cCiDefaultResourceHandler::GetNewCiSession(uint32_t ResourceId, uint16_t SessionId, cCiTransportConnection *Tc) { switch (ResourceId) { case RI_RESOURCE_MANAGER: return new cCiResourceManager(SessionId, Tc); break; case RI_APPLICATION_INFORMATION: return new cCiApplicationInformation(SessionId, Tc); break; case RI_CONDITIONAL_ACCESS_SUPPORT: return new cCiConditionalAccessSupport(SessionId, Tc); break; case RI_HOST_CONTROL: return new cCiHostControl(SessionId, Tc); break; case RI_DATE_TIME: return new cCiDateTime(SessionId, Tc); break; case RI_MMI: return new cCiMMI(SessionId, Tc); break; default: return NULL; } } // --- cCiResourceHandlers --------------------------------------------------- cCiResourceHandlers CiResourceHandlers; cCiResourceHandlers::cCiResourceHandlers(void) { Register(new cCiDefaultResourceHandler); } void cCiResourceHandlers::Register(cCiResourceHandler *ResourceHandler) { if (ResourceHandler) { Add(ResourceHandler); if (const uint32_t *r = ResourceHandler->ResourceIds()) { while (*r) { resourceIds.Append(htonl(*r)); r++; } } } } cCiSession *cCiResourceHandlers::GetNewCiSession(uint32_t ResourceId, uint16_t SessionId, cCiTransportConnection *Tc) { for (cCiResourceHandler *r = Last(); r; r = Prev(r)) { if (cCiSession *CiSession = r->GetNewCiSession(ResourceId, SessionId, Tc)) return CiSession; } return NULL; } // --- cCiTransportConnection (cont'd) --------------------------------------- #define TC_POLL_TIMEOUT 300 // ms WORKAROUND: TC_POLL_TIMEOUT < 300ms doesn't work with DragonCAM #define TC_ALIVE_TIMEOUT 2000 // ms after which a transport connection is assumed dead cCiTransportConnection::cCiTransportConnection(cCamSlot *CamSlot, uint8_t Tcid) { dbgprotocol("Slot %d: creating connection %d/%d\n", CamSlot->SlotNumber(), CamSlot->SlotIndex(), Tcid); camSlot = CamSlot; tcid = Tcid; state = stIDLE; createConnectionRequested = false; deleteConnectionRequested = false; hasUserIO = false; alive.Set(TC_ALIVE_TIMEOUT); for (int i = 0; i <= MAX_SESSIONS_PER_TC; i++) // sessions[0] is not used, but initialized anyway sessions[i] = NULL; tsPostProcessor = NULL; } cCiTransportConnection::~cCiTransportConnection() { for (int i = 1; i <= MAX_SESSIONS_PER_TC; i++) delete sessions[i]; } void cCiTransportConnection::SetTsPostProcessor(cCiSession *CiSession) { tsPostProcessor = CiSession; } bool cCiTransportConnection::TsPostProcess(uint8_t *TsPacket) { cMutexLock MutexLock(&mutex); if (tsPostProcessor) return tsPostProcessor->TsPostProcess(TsPacket); return false; } bool cCiTransportConnection::Ready(void) { cCiConditionalAccessSupport *cas = (cCiConditionalAccessSupport *)GetSessionByResourceId(RI_CONDITIONAL_ACCESS_SUPPORT); return cas && cas->Ready(); } const char *cCiTransportConnection::GetCamName(void) { cCiApplicationInformation *ai = (cCiApplicationInformation *)GetSessionByResourceId(RI_APPLICATION_INFORMATION); return ai ? ai->GetMenuString() : NULL; } void cCiTransportConnection::SendTPDU(uint8_t Tag, int Length, const uint8_t *Data) { cTPDU TPDU(camSlot->SlotIndex(), tcid, Tag, Length, Data); camSlot->Write(&TPDU); timer.Set(TC_POLL_TIMEOUT); } void cCiTransportConnection::SendData(int Length, const uint8_t *Data) { // if Length ever exceeds MAX_TPDU_DATA this needs to be handled differently if (state == stACTIVE && Length > 0) SendTPDU(T_DATA_LAST, Length, Data); } void cCiTransportConnection::SendTag(uint8_t Tag, uint16_t SessionId, uint32_t ResourceId, int Status) { uint8_t buffer[16]; uint8_t *p = buffer; *p++ = Tag; *p++ = 0x00; // will contain length if (Status >= 0) *p++ = Status; if (ResourceId) { put_unaligned(htonl(ResourceId), (uint32_t *)p); p += 4; } put_unaligned(htons(SessionId), (uint16_t *)p); p += 2; buffer[1] = p - buffer - 2; // length SendData(p - buffer, buffer); } void cCiTransportConnection::Poll(void) { bool OldDumpTPDUDataTransfer = DumpTPDUDataTransfer; DumpTPDUDataTransfer &= DumpPolls; if (DumpPolls) dbgprotocol("Slot %d: ==> Poll\n", camSlot->SlotNumber()); SendTPDU(T_DATA_LAST); DumpTPDUDataTransfer = OldDumpTPDUDataTransfer; } uint32_t cCiTransportConnection::ResourceIdToInt(const uint8_t *Data) { return (ntohl(get_unaligned((uint32_t *)Data))); } cCiSession *cCiTransportConnection::GetSessionBySessionId(uint16_t SessionId) { return (SessionId <= MAX_SESSIONS_PER_TC) ? sessions[SessionId] : NULL; } cCiSession *cCiTransportConnection::GetSessionByResourceId(uint32_t ResourceId) { cCiSession *CiSession = NULL; for (int i = 1; i <= MAX_SESSIONS_PER_TC; i++) { if (cCiSession *s = sessions[i]) { if (s->ResourceId() == ResourceId) return s; // prefer exact match if ((s->ResourceId() & RESOURCE_CLASS_MASK) == (ResourceId & RESOURCE_CLASS_MASK)) CiSession = s; } } return CiSession; } void cCiTransportConnection::OpenSession(int Length, const uint8_t *Data) { if (Length == 6 && *(Data + 1) == 0x04) { uint32_t ResourceId = ResourceIdToInt(Data + 2); dbgprotocol("Slot %d: open session %08X\n", camSlot->SlotNumber(), ResourceId); if (!GetSessionByResourceId(ResourceId)) { for (int i = 1; i <= MAX_SESSIONS_PER_TC; i++) { if (!sessions[i]) { sessions[i] = CiResourceHandlers.GetNewCiSession(ResourceId, i, this); if (sessions[i]) SendTag(ST_OPEN_SESSION_RESPONSE, sessions[i]->SessionId(), sessions[i]->ResourceId(), SS_OK); else esyslog("ERROR: CAM %d: unknown resource identifier: %08X (%d/%d)", camSlot->SlotNumber(), ResourceId, camSlot->SlotIndex(), tcid); return; } } esyslog("ERROR: CAM %d: no free session slot for resource identifier %08X (%d/%d)", camSlot->SlotNumber(), ResourceId, camSlot->SlotIndex(), tcid); } else esyslog("ERROR: CAM %d: session for resource identifier %08X already exists (%d/%d)", camSlot->SlotNumber(), ResourceId, camSlot->SlotIndex(), tcid); } } void cCiTransportConnection::CloseSession(uint16_t SessionId) { dbgprotocol("Slot %d: close session %d\n", camSlot->SlotNumber(), SessionId); cCiSession *Session = GetSessionBySessionId(SessionId); if (Session && sessions[SessionId] == Session) { delete Session; sessions[SessionId] = NULL; SendTag(ST_CLOSE_SESSION_RESPONSE, SessionId, 0, SS_OK); } else { esyslog("ERROR: CAM %d: unknown session id: %d (%d/%d)", camSlot->SlotNumber(), SessionId, camSlot->SlotIndex(), tcid); SendTag(ST_CLOSE_SESSION_RESPONSE, SessionId, 0, SS_NOT_ALLOCATED); } } void cCiTransportConnection::HandleSessions(cTPDU *TPDU) { int Length; const uint8_t *Data = TPDU->Data(Length); if (Data && Length > 1) { switch (*Data) { case ST_SESSION_NUMBER: if (Length > 4) { uint16_t SessionId = ntohs(get_unaligned((uint16_t *)&Data[2])); cCiSession *Session = GetSessionBySessionId(SessionId); if (Session) Session->Process(Length - 4, Data + 4); else esyslog("ERROR: CAM %d: unknown session id: %d (%d/%d)", camSlot->SlotNumber(), SessionId, camSlot->SlotIndex(), tcid); } break; case ST_OPEN_SESSION_REQUEST: OpenSession(Length, Data); break; case ST_CLOSE_SESSION_REQUEST: if (Length == 4) CloseSession(ntohs(get_unaligned((uint16_t *)&Data[2]))); break; case ST_CREATE_SESSION_RESPONSE: // not implemented case ST_CLOSE_SESSION_RESPONSE: // not implemented default: esyslog("ERROR: CAM %d: unknown session tag: %02X (%d/%d)", camSlot->SlotNumber(), *Data, camSlot->SlotIndex(), tcid); } } } bool cCiTransportConnection::Process(cTPDU *TPDU) { if (TPDU) alive.Set(TC_ALIVE_TIMEOUT); else if (alive.TimedOut()) return false; switch (state) { case stIDLE: if (createConnectionRequested) { dbgprotocol("Slot %d: create connection %d/%d\n", camSlot->SlotNumber(), camSlot->SlotIndex(), tcid); createConnectionRequested = false; SendTPDU(T_CREATE_TC); state = stCREATION; } return true; case stCREATION: if (TPDU && TPDU->Tag() == T_CTC_REPLY) { dbgprotocol("Slot %d: connection created %d/%d\n", camSlot->SlotNumber(), camSlot->SlotIndex(), tcid); Poll(); state = stACTIVE; } else if (timer.TimedOut()) { dbgprotocol("Slot %d: timeout while creating connection %d/%d\n", camSlot->SlotNumber(), camSlot->SlotIndex(), tcid); state = stIDLE; } return true; case stACTIVE: if (deleteConnectionRequested) { dbgprotocol("Slot %d: delete connection requested %d/%d\n", camSlot->SlotNumber(), camSlot->SlotIndex(), tcid); deleteConnectionRequested = false; SendTPDU(T_DELETE_TC); state = stDELETION; return true; } if (TPDU) { switch (TPDU->Tag()) { case T_REQUEST_TC: esyslog("ERROR: CAM %d: T_REQUEST_TC not implemented (%d/%d)", camSlot->SlotNumber(), camSlot->SlotIndex(), tcid); break; case T_DATA_MORE: case T_DATA_LAST: HandleSessions(TPDU); // continue with T_SB case T_SB: if ((TPDU->Status() & DATA_INDICATOR) != 0) { dbgprotocol("Slot %d: receive data %d/%d\n", camSlot->SlotNumber(), camSlot->SlotIndex(), tcid); SendTPDU(T_RCV); } break; case T_DELETE_TC: dbgprotocol("Slot %d: delete connection %d/%d\n", camSlot->SlotNumber(), camSlot->SlotIndex(), tcid); SendTPDU(T_DTC_REPLY); state = stIDLE; return true; case T_RCV: case T_CREATE_TC: case T_CTC_REPLY: case T_DTC_REPLY: case T_NEW_TC: case T_TC_ERROR: break; default: esyslog("ERROR: unknown TPDU tag: 0x%02X (%s)", TPDU->Tag(), __FUNCTION__); } } else if (timer.TimedOut()) Poll(); hasUserIO = false; for (int i = 1; i <= MAX_SESSIONS_PER_TC; i++) { if (sessions[i]) { sessions[i]->Process(); if (sessions[i]->HasUserIO()) hasUserIO = true; } } break; case stDELETION: if (TPDU && TPDU->Tag() == T_DTC_REPLY || timer.TimedOut()) { dbgprotocol("Slot %d: connection deleted %d/%d\n", camSlot->SlotNumber(), camSlot->SlotIndex(), tcid); state = stIDLE; } return true; default: esyslog("ERROR: unknown state: %d (%s)", state, __FUNCTION__); } return true; } // --- cCiCaPidData ---------------------------------------------------------- class cCiCaPidData : public cListObject { public: bool active; int pid; int streamType; cCiCaPidData(int Pid, int StreamType) { active = false; pid = Pid; streamType = StreamType; } }; // --- cCiCaProgramData ------------------------------------------------------ class cCiCaProgramData : public cListObject { public: int programNumber; bool modified; cList pidList; cCiCaProgramData(int ProgramNumber) { programNumber = ProgramNumber; modified = false; } bool Active(void) { for (cCiCaPidData *p = pidList.First(); p; p = pidList.Next(p)) { if (p->active) return true; } return false; } }; // --- cCiAdapter ------------------------------------------------------------ cCiAdapter::cCiAdapter(void) :cThread("CI adapter") { for (int i = 0; i < MAX_CAM_SLOTS_PER_ADAPTER; i++) camSlots[i] = NULL; } cCiAdapter::~cCiAdapter() { Cancel(3); for (int i = 0; i < MAX_CAM_SLOTS_PER_ADAPTER; i++) delete camSlots[i]; } void cCiAdapter::AddCamSlot(cCamSlot *CamSlot) { if (CamSlot) { for (int i = 0; i < MAX_CAM_SLOTS_PER_ADAPTER; i++) { if (!camSlots[i]) { CamSlot->slotIndex = i; camSlots[i] = CamSlot; return; } } esyslog("ERROR: no free CAM slot in CI adapter"); } } cCamSlot *cCiAdapter::ItCamSlot(int &Iter) { if (Iter >= 0) { for (; Iter < MAX_CAM_SLOTS_PER_ADAPTER; ) { if (cCamSlot *Found = camSlots[Iter++]) return Found; } } return NULL; } void cCiAdapter::Action(void) { cTPDU TPDU; while (Running()) { int n = Read(TPDU.Buffer(), TPDU.MaxSize()); if (n > 0 && TPDU.Slot() < MAX_CAM_SLOTS_PER_ADAPTER) { TPDU.SetSize(n); cCamSlot *cs = camSlots[TPDU.Slot()]; TPDU.Dump(cs ? cs->SlotNumber() : 0, false); if (cs) cs->Process(&TPDU); } for (int i = 0; i < MAX_CAM_SLOTS_PER_ADAPTER; i++) { if (camSlots[i]) camSlots[i]->Process(); } } } // --- cCamSlot -------------------------------------------------------------- #define MODULE_CHECK_INTERVAL 500 // ms #define MODULE_RESET_TIMEOUT 2 // s cCamSlot::cCamSlot(cCiAdapter *CiAdapter, bool WantsTsData, cCamSlot *MasterSlot) { ciAdapter = CiAdapter; masterSlot = MasterSlot; assignedDevice = NULL; caPidReceiver = WantsTsData ? new cCaPidReceiver : NULL; caActivationReceiver = NULL; slotIndex = -1; mtdAvailable = false; mtdHandler = NULL; lastModuleStatus = msReset; // avoids initial reset log message resetTime = 0; resendPmt = false; for (int i = 0; i <= MAX_CONNECTIONS_PER_CAM_SLOT; i++) // tc[0] is not used, but initialized anyway tc[i] = NULL; if (MasterSlot) slotNumber = MasterSlot->SlotNumber(); if (ciAdapter) { CamSlots.Add(this); slotNumber = Index() + 1; ciAdapter->AddCamSlot(this); Reset(); } } cCamSlot::~cCamSlot() { Assign(NULL); delete caPidReceiver; delete caActivationReceiver; CamSlots.Del(this, false); DeleteAllConnections(); delete mtdHandler; } cCamSlot *cCamSlot::MtdSpawn(void) { cMutexLock MutexLock(&mutex); if (mtdHandler) return mtdHandler->GetMtdCamSlot(this); return this; } bool cCamSlot::Assign(cDevice *Device, bool Query) { cMutexLock MutexLock(&mutex); if (Device == assignedDevice) return true; if (ciAdapter) { int OldDeviceNumber = 0; if (assignedDevice && !Query) { OldDeviceNumber = assignedDevice->DeviceNumber() + 1; if (caPidReceiver) assignedDevice->Detach(caPidReceiver); assignedDevice->SetCamSlot(NULL); assignedDevice = NULL; } if (ciAdapter->Assign(Device, true)) { if (!Query) { StopDecrypting(); if (ciAdapter->Assign(Device)) { if (Device) { Device->SetCamSlot(this); assignedDevice = Device; if (caPidReceiver) { caPidReceiver->Reset(); Device->AttachReceiver(caPidReceiver); } dsyslog("CAM %d: assigned to device %d", MasterSlotNumber(), Device->DeviceNumber() + 1); } else { CancelActivation(); dsyslog("CAM %d: unassigned from device %d", MasterSlotNumber(), OldDeviceNumber); } } else return false; } return true; } } return false; } bool cCamSlot::Devices(cVector &DeviceNumbers) { cMutexLock MutexLock(&mutex); if (mtdHandler) return mtdHandler->Devices(DeviceNumbers); if (assignedDevice) DeviceNumbers.Append(assignedDevice->DeviceNumber()); return DeviceNumbers.Size() > 0; } void cCamSlot::NewConnection(void) { cMutexLock MutexLock(&mutex); for (int i = 1; i <= MAX_CONNECTIONS_PER_CAM_SLOT; i++) { if (!tc[i]) { tc[i] = new cCiTransportConnection(this, i); tc[i]->CreateConnection(); return; } } esyslog("ERROR: CAM %d: can't create new transport connection!", slotNumber); } void cCamSlot::DeleteAllConnections(void) { cMutexLock MutexLock(&mutex); for (int i = 1; i <= MAX_CONNECTIONS_PER_CAM_SLOT; i++) { delete tc[i]; tc[i] = NULL; } } void cCamSlot::Process(cTPDU *TPDU) { cMutexLock MutexLock(&mutex); if (TPDU) { int n = TPDU->Tcid(); if (1 <= n && n <= MAX_CONNECTIONS_PER_CAM_SLOT) { if (tc[n]) tc[n]->Process(TPDU); } } for (int i = 1; i <= MAX_CONNECTIONS_PER_CAM_SLOT; i++) { if (tc[i]) { if (!tc[i]->Process()) { Reset(); return; } } } if (moduleCheckTimer.TimedOut()) { eModuleStatus ms = ModuleStatus(); if (ms != lastModuleStatus) { switch (ms) { case msNone: dbgprotocol("Slot %d: no module present\n", slotNumber); isyslog("CAM %d: no module present", slotNumber); StopDecrypting(); DeleteAllConnections(); CancelActivation(); if (mtdHandler) mtdHandler->UnAssignAll(); else Assign(NULL); break; case msReset: dbgprotocol("Slot %d: module reset\n", slotNumber); isyslog("CAM %d: module reset", slotNumber); DeleteAllConnections(); break; case msPresent: dbgprotocol("Slot %d: module present\n", slotNumber); isyslog("CAM %d: module present", slotNumber); break; case msReady: dbgprotocol("Slot %d: module ready\n", slotNumber); isyslog("CAM %d: module ready", slotNumber); NewConnection(); resendPmt = true; break; default: esyslog("ERROR: unknown module status %d (%s)", ms, __FUNCTION__); } lastModuleStatus = ms; } moduleCheckTimer.Set(MODULE_CHECK_INTERVAL); } if (resendPmt && Ready()) { if (mtdHandler) { mtdHandler->StartDecrypting(); resendPmt = false; } else if (caProgramList.Count()) StartDecrypting(); } processed.Broadcast(); } cCiSession *cCamSlot::GetSessionByResourceId(uint32_t ResourceId) { cMutexLock MutexLock(&mutex); return tc[1] ? tc[1]->GetSessionByResourceId(ResourceId) : NULL; } void cCamSlot::Write(cTPDU *TPDU) { cMutexLock MutexLock(&mutex); if (ciAdapter && TPDU->Size()) { TPDU->Dump(SlotNumber(), true); ciAdapter->Write(TPDU->Buffer(), TPDU->Size()); } } bool cCamSlot::Reset(void) { cMutexLock MutexLock(&mutex); ChannelCamRelations.Reset(slotNumber); DeleteAllConnections(); if (ciAdapter) { dbgprotocol("Slot %d: reset...", slotNumber); if (ciAdapter->Reset(slotIndex)) { resetTime = time(NULL); dbgprotocol("ok.\n"); lastModuleStatus = msReset; return true; } dbgprotocol("failed!\n"); } return false; } bool cCamSlot::CanActivate(void) { return ModuleStatus() == msReady; } void cCamSlot::StartActivation(void) { cMutexLock MutexLock(&mutex); if (!caActivationReceiver) { if (cDevice *d = Device()) { LOCK_CHANNELS_READ; if (const cChannel *Channel = Channels->GetByNumber(cDevice::CurrentChannel())) { caActivationReceiver = new cCaActivationReceiver(Channel, this); d->AttachReceiver(caActivationReceiver); dsyslog("CAM %d: activating on device %d with channel %d (%s)", SlotNumber(), d->DeviceNumber() + 1, Channel->Number(), Channel->Name()); } } } } void cCamSlot::CancelActivation(void) { cMutexLock MutexLock(&mutex); if (mtdHandler) mtdHandler->CancelActivation(); else { delete caActivationReceiver; caActivationReceiver = NULL; } } bool cCamSlot::IsActivating(void) { if (mtdHandler) return mtdHandler->IsActivating(); return caActivationReceiver; } eModuleStatus cCamSlot::ModuleStatus(void) { cMutexLock MutexLock(&mutex); eModuleStatus ms = ciAdapter ? ciAdapter->ModuleStatus(slotIndex) : msNone; if (resetTime) { if (ms <= msReset) { if (time(NULL) - resetTime < MODULE_RESET_TIMEOUT) return msReset; } resetTime = 0; } return ms; } const char *cCamSlot::GetCamName(void) { cMutexLock MutexLock(&mutex); return tc[1] ? tc[1]->GetCamName() : NULL; } bool cCamSlot::Ready(void) { cMutexLock MutexLock(&mutex); return ModuleStatus() == msNone || tc[1] && tc[1]->Ready(); } bool cCamSlot::HasMMI(void) { return GetSessionByResourceId(RI_MMI); } bool cCamSlot::HasUserIO(void) { cMutexLock MutexLock(&mutex); return tc[1] && tc[1]->HasUserIO(); } bool cCamSlot::EnterMenu(void) { cMutexLock MutexLock(&mutex); cCiApplicationInformation *api = (cCiApplicationInformation *)GetSessionByResourceId(RI_APPLICATION_INFORMATION); return api ? api->EnterMenu() : false; } cCiMenu *cCamSlot::GetMenu(void) { cMutexLock MutexLock(&mutex); cCiMMI *mmi = (cCiMMI *)GetSessionByResourceId(RI_MMI); if (mmi) { cCiMenu *Menu = mmi->Menu(); if (Menu) Menu->mutex = &mutex; return Menu; } return NULL; } cCiEnquiry *cCamSlot::GetEnquiry(void) { cMutexLock MutexLock(&mutex); cCiMMI *mmi = (cCiMMI *)GetSessionByResourceId(RI_MMI); if (mmi) { cCiEnquiry *Enquiry = mmi->Enquiry(); if (Enquiry) Enquiry->mutex = &mutex; return Enquiry; } return NULL; } cCiCaPmtList::~cCiCaPmtList() { for (int i = 0; i < caPmts.Size(); i++) delete caPmts[i]; } cCiCaPmt *cCiCaPmtList::Add(uint8_t CmdId, int Source, int Transponder, int ProgramNumber, const int *CaSystemIds) { cCiCaPmt *p = new cCiCaPmt(CmdId, Source, Transponder, ProgramNumber, CaSystemIds); caPmts.Append(p); return p; } void cCiCaPmtList::Del(cCiCaPmt *CaPmt) { if (caPmts.RemoveElement(CaPmt)) delete CaPmt; } bool cCamSlot::RepliesToQuery(void) { cMutexLock MutexLock(&mutex); cCiConditionalAccessSupport *cas = (cCiConditionalAccessSupport *)GetSessionByResourceId(RI_CONDITIONAL_ACCESS_SUPPORT); return cas && cas->RepliesToQuery(); } void cCamSlot::BuildCaPmts(uint8_t CmdId, cCiCaPmtList &CaPmtList, cMtdMapper *MtdMapper) { cMutexLock MutexLock(&mutex); CaPmtList.caPmts.Clear(); const int *CaSystemIds = GetCaSystemIds(); if (CaSystemIds && *CaSystemIds) { if (caProgramList.Count()) { for (cCiCaProgramData *p = caProgramList.First(); p; p = caProgramList.Next(p)) { if (p->modified || resendPmt) { bool Active = p->Active(); cCiCaPmt *CaPmt = CaPmtList.Add(Active ? CmdId : CPCI_NOT_SELECTED, source, transponder, p->programNumber, CaSystemIds); for (cCiCaPidData *q = p->pidList.First(); q; q = p->pidList.Next(q)) { if (q->active) CaPmt->AddPid(q->pid, q->streamType); } if (caPidReceiver) { int CaPids[MAXRECEIVEPIDS + 1]; if (GetCaPids(source, transponder, p->programNumber, CaSystemIds, MAXRECEIVEPIDS + 1, CaPids) > 0) { if (Active) caPidReceiver->AddPids(CaPids); else { KeepSharedCaPids(p->programNumber, CaSystemIds, CaPids); caPidReceiver->DelPids(CaPids); } } } if (RepliesToQuery()) CaPmt->SetListManagement(Active ? CPLM_ADD : CPLM_UPDATE); if (MtdMapper) CaPmt->MtdMapPids(MtdMapper); p->modified = false; } } } else if (CmdId == CPCI_NOT_SELECTED) CaPmtList.Add(CmdId, 0, 0, 0, NULL); } } void cCamSlot::KeepSharedCaPids(int ProgramNumber, const int *CaSystemIds, int *CaPids) { int numPids = 0; int *pCaPids = CaPids; while (*pCaPids) { numPids++; pCaPids++; } if (numPids <= 0) return; int CaPids2[MAXRECEIVEPIDS + 1]; for (cCiCaProgramData *p = caProgramList.First(); p; p = caProgramList.Next(p)) { if (p->Active()) { if (GetCaPids(source, transponder, p->programNumber, CaSystemIds, MAXRECEIVEPIDS + 1, CaPids2) > 0) { int *pCaPids2 = CaPids2; while (*pCaPids2) { pCaPids = CaPids; while (*pCaPids) { if (*pCaPids == *pCaPids2) { dsyslog("CAM %d: keeping shared CA pid %d", SlotNumber(), *pCaPids); // To remove *pCaPids from CaPids we overwrite it with the last valie in the list, and then strip the last value: *pCaPids = CaPids[numPids - 1]; numPids--; CaPids[numPids] = 0; if (numPids <= 0) return; } else pCaPids++; } pCaPids2++; } } } } } void cCamSlot::SendCaPmts(cCiCaPmtList &CaPmtList) { cMutexLock MutexLock(&mutex); cCiConditionalAccessSupport *cas = (cCiConditionalAccessSupport *)GetSessionByResourceId(RI_CONDITIONAL_ACCESS_SUPPORT); if (cas) { for (int i = 0; i < CaPmtList.caPmts.Size(); i++) cas->SendPMT(CaPmtList.caPmts[i]); } resendPmt = false; } void cCamSlot::SendCaPmt(uint8_t CmdId) { cMutexLock MutexLock(&mutex); cCiCaPmtList CaPmtList; BuildCaPmts(CmdId, CaPmtList); SendCaPmts(CaPmtList); } void cCamSlot::MtdEnable(void) { mtdAvailable = true; } void cCamSlot::MtdActivate(bool On) { if (McdAvailable() && MtdAvailable()) { if (On) { if (!mtdHandler) { dsyslog("CAM %d: activating MTD support", SlotNumber()); mtdHandler = new cMtdHandler; } } else if (mtdHandler) { dsyslog("CAM %d: deactivating MTD support", SlotNumber()); delete mtdHandler; mtdHandler = NULL; } } } int cCamSlot::MtdPutData(uchar *Data, int Count) { return mtdHandler->Put(Data, Count); } const int *cCamSlot::GetCaSystemIds(void) { cMutexLock MutexLock(&mutex); cCiConditionalAccessSupport *cas = (cCiConditionalAccessSupport *)GetSessionByResourceId(RI_CONDITIONAL_ACCESS_SUPPORT); return cas ? cas->GetCaSystemIds() : NULL; } int cCamSlot::Priority(void) { if (mtdHandler) return mtdHandler->Priority(); cDevice *d = Device(); return d ? d->Priority() : IDLEPRIORITY; } bool cCamSlot::ProvidesCa(const int *CaSystemIds) { cMutexLock MutexLock(&mutex); cCiConditionalAccessSupport *cas = (cCiConditionalAccessSupport *)GetSessionByResourceId(RI_CONDITIONAL_ACCESS_SUPPORT); if (cas) { for (const int *ids = cas->GetCaSystemIds(); ids && *ids; ids++) { for (const int *id = CaSystemIds; *id; id++) { if (*id == *ids) return true; } } } return false; } void cCamSlot::AddPid(int ProgramNumber, int Pid, int StreamType) { cMutexLock MutexLock(&mutex); cCiCaProgramData *ProgramData = NULL; for (cCiCaProgramData *p = caProgramList.First(); p; p = caProgramList.Next(p)) { if (p->programNumber == ProgramNumber) { ProgramData = p; for (cCiCaPidData *q = p->pidList.First(); q; q = p->pidList.Next(q)) { if (q->pid == Pid) return; } } } if (!ProgramData) caProgramList.Add(ProgramData = new cCiCaProgramData(ProgramNumber)); ProgramData->pidList.Add(new cCiCaPidData(Pid, StreamType)); } void cCamSlot::SetPid(int Pid, bool Active) { if (caPidReceiver && caPidReceiver->HandlingPid()) return; cMutexLock MutexLock(&mutex); for (cCiCaProgramData *p = caProgramList.First(); p; p = caProgramList.Next(p)) { for (cCiCaPidData *q = p->pidList.First(); q; q = p->pidList.Next(q)) { if (q->pid == Pid) { if (q->active != Active) { q->active = Active; p->modified = true; } return; } } } } // see ISO/IEC 13818-1 #define STREAM_TYPE_VIDEO 0x02 #define STREAM_TYPE_AUDIO 0x04 #define STREAM_TYPE_PRIVATE 0x06 void cCamSlot::AddChannel(const cChannel *Channel) { cMutexLock MutexLock(&mutex); if (source != Channel->Source() || transponder != Channel->Transponder()) StopDecrypting(); source = Channel->Source(); transponder = Channel->Transponder(); if (Channel->Ca() >= CA_ENCRYPTED_MIN) { AddPid(Channel->Sid(), Channel->Vpid(), STREAM_TYPE_VIDEO); for (const int *Apid = Channel->Apids(); *Apid; Apid++) AddPid(Channel->Sid(), *Apid, STREAM_TYPE_AUDIO); for (const int *Dpid = Channel->Dpids(); *Dpid; Dpid++) AddPid(Channel->Sid(), *Dpid, STREAM_TYPE_PRIVATE); for (const int *Spid = Channel->Spids(); *Spid; Spid++) AddPid(Channel->Sid(), *Spid, STREAM_TYPE_PRIVATE); } } #define QUERY_REPLY_WAIT 100 // ms to wait between checks for a reply bool cCamSlot::CanDecrypt(const cChannel *Channel, cMtdMapper *MtdMapper) { if (Channel->Ca() < CA_ENCRYPTED_MIN) return true; // channel not encrypted if (!IsDecrypting()) return true; // any CAM can decrypt at least one channel cMutexLock MutexLock(&mutex); cCiConditionalAccessSupport *cas = (cCiConditionalAccessSupport *)GetSessionByResourceId(RI_CONDITIONAL_ACCESS_SUPPORT); if (cas && cas->RepliesToQuery()) { cCiCaPmt CaPmt(CPCI_QUERY, Channel->Source(), Channel->Transponder(), Channel->Sid(), GetCaSystemIds()); CaPmt.SetListManagement(CPLM_ADD); // WORKAROUND: CPLM_ONLY doesn't work with Alphacrypt 3.09 (deletes existing CA_PMTs) CaPmt.AddPid(Channel->Vpid(), STREAM_TYPE_VIDEO); for (const int *Apid = Channel->Apids(); *Apid; Apid++) CaPmt.AddPid(*Apid, STREAM_TYPE_AUDIO); for (const int *Dpid = Channel->Dpids(); *Dpid; Dpid++) CaPmt.AddPid(*Dpid, STREAM_TYPE_PRIVATE); for (const int *Spid = Channel->Spids(); *Spid; Spid++) CaPmt.AddPid(*Spid, STREAM_TYPE_PRIVATE); if (MtdMapper) CaPmt.MtdMapPids(MtdMapper); cas->SendPMT(&CaPmt); cTimeMs Timeout(QUERY_REPLY_TIMEOUT); do { processed.TimedWait(mutex, QUERY_REPLY_WAIT); if ((cas = (cCiConditionalAccessSupport *)GetSessionByResourceId(RI_CONDITIONAL_ACCESS_SUPPORT)) != NULL) { // must re-fetch it, there might have been a reset if (cas->ReceivedReply()) return cas->CanDecrypt(); } else return false; } while (!Timeout.TimedOut()); dsyslog("CAM %d: didn't reply to QUERY", SlotNumber()); } return false; } void cCamSlot::StartDecrypting(void) { SendCaPmt(CPCI_OK_DESCRAMBLING); } void cCamSlot::StopDecrypting(void) { cMutexLock MutexLock(&mutex); if (mtdHandler) { mtdHandler->StopDecrypting(); return; } if (caProgramList.Count()) { caProgramList.Clear(); if (!dynamic_cast(this) || !MasterSlot()->IsDecrypting()) SendCaPmt(CPCI_NOT_SELECTED); } } bool cCamSlot::IsDecrypting(void) { cMutexLock MutexLock(&mutex); if (mtdHandler) return mtdHandler->IsDecrypting(); if (caProgramList.Count()) { for (cCiCaProgramData *p = caProgramList.First(); p; p = caProgramList.Next(p)) { if (p->modified) return true; // any modifications need to be processed before we can assume it's no longer decrypting for (cCiCaPidData *q = p->pidList.First(); q; q = p->pidList.Next(q)) { if (q->active) return true; } } } return false; } uchar *cCamSlot::Decrypt(uchar *Data, int &Count) { if (Data) Count = TS_SIZE; return Data; } bool cCamSlot::TsPostProcess(uchar *Data) { return tc[1] ? tc[1]->TsPostProcess(Data) : false; } bool cCamSlot::Inject(uchar *Data, int Count) { return true; } void cCamSlot::InjectEit(int Sid) { cEitGenerator Eit(Sid); Inject(Eit.Data(), Eit.Length()); } // --- cCamSlots ------------------------------------------------------------- cCamSlots CamSlots; int cCamSlots::NumReadyMasterSlots(void) { int n = 0; for (cCamSlot *CamSlot = CamSlots.First(); CamSlot; CamSlot = CamSlots.Next(CamSlot)) { if (CamSlot->IsMasterSlot() && CamSlot->ModuleStatus() == msReady) n++; } return n; } bool cCamSlots::WaitForAllCamSlotsReady(int Timeout) { bool ready = true; for (time_t t0 = time(NULL); time(NULL) - t0 < Timeout; ) { ready = true; for (cCamSlot *CamSlot = CamSlots.First(); CamSlot; CamSlot = CamSlots.Next(CamSlot)) { if (!CamSlot->Ready()) { ready = false; cCondWait::SleepMs(100); } } if (ready) break; } for (cCamSlot *CamSlot = CamSlots.First(); CamSlot; CamSlot = CamSlots.Next(CamSlot)) dsyslog("CAM %d: %sready, %s", CamSlot->SlotNumber(), CamSlot->Ready() ? "" : "not ", CamSlot->IsMasterSlot() ? *cString::sprintf("master (%s)", CamSlot->GetCamName() ? CamSlot->GetCamName() : "empty") : *cString::sprintf("slave of CAM %d", CamSlot->MasterSlotNumber())); return ready; } // --- cChannelCamRelation --------------------------------------------------- #define CAM_CHECKED_TIMEOUT 15 // seconds before a CAM that has been checked for a particular channel will be checked again class cChannelCamRelation : public cListObject { private: tChannelID channelID; uint32_t camSlotsChecked; uint32_t camSlotsDecrypt; time_t lastChecked; public: cChannelCamRelation(tChannelID ChannelID); bool TimedOut(void); tChannelID ChannelID(void) { return channelID; } bool CamChecked(int CamSlotNumber); bool CamDecrypt(int CamSlotNumber); void SetChecked(int CamSlotNumber); void SetDecrypt(int CamSlotNumber); void ClrChecked(int CamSlotNumber); void ClrDecrypt(int CamSlotNumber); }; cChannelCamRelation::cChannelCamRelation(tChannelID ChannelID) { channelID = ChannelID; camSlotsChecked = 0; camSlotsDecrypt = 0; lastChecked = 0; } bool cChannelCamRelation::TimedOut(void) { return !camSlotsDecrypt && time(NULL) - lastChecked > CAM_CHECKED_TIMEOUT; } bool cChannelCamRelation::CamChecked(int CamSlotNumber) { if (lastChecked && time(NULL) - lastChecked > CAM_CHECKED_TIMEOUT) { lastChecked = 0; camSlotsChecked = 0; } return camSlotsChecked & (1 << (CamSlotNumber - 1)); } bool cChannelCamRelation::CamDecrypt(int CamSlotNumber) { return camSlotsDecrypt & (1 << (CamSlotNumber - 1)); } void cChannelCamRelation::SetChecked(int CamSlotNumber) { camSlotsChecked |= (1 << (CamSlotNumber - 1)); lastChecked = time(NULL); ClrDecrypt(CamSlotNumber); } void cChannelCamRelation::SetDecrypt(int CamSlotNumber) { camSlotsDecrypt |= (1 << (CamSlotNumber - 1)); ClrChecked(CamSlotNumber); } void cChannelCamRelation::ClrChecked(int CamSlotNumber) { camSlotsChecked &= ~(1 << (CamSlotNumber - 1)); lastChecked = 0; } void cChannelCamRelation::ClrDecrypt(int CamSlotNumber) { camSlotsDecrypt &= ~(1 << (CamSlotNumber - 1)); } // --- cChannelCamRelations -------------------------------------------------- #define MAX_CAM_NUMBER 32 #define CHANNEL_CAM_RELATIONS_CLEANUP_INTERVAL 3600 // seconds between cleanups cChannelCamRelations ChannelCamRelations; cChannelCamRelations::cChannelCamRelations(void) { lastCleanup = time(NULL); } void cChannelCamRelations::Cleanup(void) { cMutexLock MutexLock(&mutex); if (time(NULL) - lastCleanup > CHANNEL_CAM_RELATIONS_CLEANUP_INTERVAL) { for (cChannelCamRelation *ccr = First(); ccr; ) { cChannelCamRelation *c = ccr; ccr = Next(ccr); if (c->TimedOut()) Del(c); } lastCleanup = time(NULL); } } cChannelCamRelation *cChannelCamRelations::GetEntry(tChannelID ChannelID) { cMutexLock MutexLock(&mutex); Cleanup(); for (cChannelCamRelation *ccr = First(); ccr; ccr = Next(ccr)) { if (ccr->ChannelID() == ChannelID) return ccr; } return NULL; } cChannelCamRelation *cChannelCamRelations::AddEntry(tChannelID ChannelID) { cMutexLock MutexLock(&mutex); cChannelCamRelation *ccr = GetEntry(ChannelID); if (!ccr) Add(ccr = new cChannelCamRelation(ChannelID)); return ccr; } void cChannelCamRelations::Reset(int CamSlotNumber) { cMutexLock MutexLock(&mutex); for (cChannelCamRelation *ccr = First(); ccr; ccr = Next(ccr)) { ccr->ClrChecked(CamSlotNumber); ccr->ClrDecrypt(CamSlotNumber); } } bool cChannelCamRelations::CamChecked(tChannelID ChannelID, int CamSlotNumber) { cMutexLock MutexLock(&mutex); cChannelCamRelation *ccr = GetEntry(ChannelID); return ccr ? ccr->CamChecked(CamSlotNumber) : false; } bool cChannelCamRelations::CamDecrypt(tChannelID ChannelID, int CamSlotNumber) { cMutexLock MutexLock(&mutex); cChannelCamRelation *ccr = GetEntry(ChannelID); return ccr ? ccr->CamDecrypt(CamSlotNumber) : false; } void cChannelCamRelations::SetChecked(tChannelID ChannelID, int CamSlotNumber) { cMutexLock MutexLock(&mutex); cChannelCamRelation *ccr = AddEntry(ChannelID); if (ccr) ccr->SetChecked(CamSlotNumber); } void cChannelCamRelations::SetDecrypt(tChannelID ChannelID, int CamSlotNumber) { cMutexLock MutexLock(&mutex); cChannelCamRelation *ccr = AddEntry(ChannelID); if (ccr) ccr->SetDecrypt(CamSlotNumber); } void cChannelCamRelations::ClrChecked(tChannelID ChannelID, int CamSlotNumber) { cMutexLock MutexLock(&mutex); cChannelCamRelation *ccr = GetEntry(ChannelID); if (ccr) ccr->ClrChecked(CamSlotNumber); } void cChannelCamRelations::ClrDecrypt(tChannelID ChannelID, int CamSlotNumber) { cMutexLock MutexLock(&mutex); cChannelCamRelation *ccr = GetEntry(ChannelID); if (ccr) ccr->ClrDecrypt(CamSlotNumber); } void cChannelCamRelations::Load(const char *FileName) { cMutexLock MutexLock(&mutex); fileName = FileName; if (access(fileName, R_OK) == 0) { dsyslog("loading %s", *fileName); if (FILE *f = fopen(fileName, "r")) { cReadLine ReadLine; char *s; while ((s = ReadLine.Read(f)) != NULL) { if (char *p = strchr(s, ' ')) { *p = 0; if (*++p) { tChannelID ChannelID = tChannelID::FromString(s); if (ChannelID.Valid()) { char *q; char *strtok_next; while ((q = strtok_r(p, " ", &strtok_next)) != NULL) { int CamSlotNumber = atoi(q); if (CamSlotNumber >= 1 && CamSlotNumber <= MAX_CAM_NUMBER) SetDecrypt(ChannelID, CamSlotNumber); p = NULL; } } } } } fclose(f); } else LOG_ERROR_STR(*fileName); } } void cChannelCamRelations::Save(void) { if (!*fileName) return; cMutexLock MutexLock(&mutex); struct stat st; if (stat(fileName, &st) == 0) { if ((st.st_mode & S_IWUSR) == 0) { dsyslog("not saving %s (file is read-only)", *fileName); return; } } dsyslog("saving %s", *fileName); cSafeFile f(fileName); if (f.Open()) { for (cChannelCamRelation *ccr = First(); ccr; ccr = Next(ccr)) { if (ccr->ChannelID().Valid()) { cString s; for (int i = 1; i <= MAX_CAM_NUMBER; i++) { if (ccr->CamDecrypt(i)) s = cString::sprintf("%s%s%d", *s ? *s : "", *s ? " " : "", i); } if (*s) fprintf(f, "%s %s\n", *ccr->ChannelID().ToString(), *s); } } f.Close(); } else LOG_ERROR_STR(*fileName); }