#include "proxy.h" #include "client.h" #include "server.h" #include "helper.h" #include "tmpbuffer.h" #include "ldap.h" #include "uidmap.h" #include "lstring.h" #include #include #include #include #include #include #define MAXPENDING 200 #define MAX(a,b) ((a) > (b) ? (a) : (b)) typedef struct { BOOL homeDirectory; BOOL homeMount; BOOL gidNumber; BOOL gecos; BOOL loginShell; BOOL uid; BOOL uidNumber; BOOL cn; BOOL realAccount; BOOL bogusFieldName42; BOOL bogusFieldName43; // Internal BOOL hasUser; } attr_t; /* * The bogus fields can be used to map two arbitrary additional fields * to the uid when searching for a user. It's currently used to handle * sssd being smart and doing lookups for krbPrincipalName and mail * when the authenticating user's name contains '@' * For this reason the sssd.conf contains (in the ldap section): * ldap_user_email = bogusFieldName42 * ldap_user_principal = bogusFieldName43 */ typedef struct { unsigned long clientMessageId; unsigned long serverMessageId; epoll_client_t *client; attr_t attr; time_t started; } pending_t; static pending_t _pendingRequest[MAXPENDING]; static int _pendingCount = 0; static int _problemCount = 0; // Attributes static struct string s_uid, s_sAMAccountName, s_objectSid, s_homeMount, s_memberUid, s_realAccount; static struct string s_objectClass, s_homeDirectory, s_gidNumber, s_gecos, s_cn, s_dn; static struct string s_loginShell, s_uidNumber, s_mail, s_objectCategory, s_distinguishedName; // Some again in lowercase static struct string s_samaccountname, s_objectsid, s_homemount, s_memberuid, s_realaccount, s_objectclass; static struct string s_homedirectory, s_gidnumber, s_uidnumber, s_distinguishedname, s_loginshell; static struct string s_msmqsigncertificates; // Values static struct string s_shadowAccount, s_posixAccount, s_posixGroup, s_3, s_1001, s_user, s_member; // Feature query static struct string s_namingContexts, s_supportedControl, s_supportedExtension, s_supportedFeatures, s_supportedLDAPVersion, s_lastUSN, s_highestCommittedUSN; static struct string s_bogusFieldName42, s_bogusFieldName43; static struct string s_bogusfieldname42, s_bogusfieldname43; // Other static struct string str_ADUSER, str_ADUSERDN; // HACK - we pefix usernames that start with something other than a-z_ with "_x_" // This is for requests - remove the prefix that the client added static void fixUnNumeric(struct string *value) { if (value == NULL) return; if (value->s[0] >= '0' && value->s[0] <= '9') { // This is a query from the client for an actually numeric ID - this should not // happen, so let's break the request to make sure it doesn't yield anything char *n = tmpbuffer_get(); size_t len = 0; n[len++] = 'f'; n[len++] = 'a'; n[len++] = 'k'; n[len++] = 'e'; for (size_t i = 0; i < value->l && i < 30; ++i) { n[len++] = value->s[i] + (value->s[i] < 64 ? 10 : -10); } value->s = n; value->l = len; return; } // Look for actual expected prefix and remove if (value->l < 4) return; if (value->s[0] != '_' || value->s[1] != 'x' || value->s[2] != '_') return; value->s += 3; value->l -= 3; } // For server responses, if they're numeric, add the prefix static void fixNumeric(struct string *value) { size_t i; if (value == NULL || value->l < 1 || value->l > 27) return; if (value->s[0] < '0' || value->s[0] > '9') return; char *buf = tmpbuffer_get(); buf[0] = '_'; buf[1] = 'x'; buf[2] = '_'; for (i = 0; i < value->l; ++i) { buf[i+3] = value->s[i]; } value->s = buf; value->l += 3; } // END HACK // static BOOL proxy_clientBindRequest(epoll_client_t *client, const unsigned long messageId, const size_t offset, const size_t maxLen); static BOOL proxy_serverBindResponse(epoll_server_t *server, const unsigned long messageId, const size_t offset, const size_t maxLen); static BOOL proxy_clientSearchRequest(epoll_client_t *client, const unsigned long messageId, const size_t offset, const size_t maxLen); static BOOL proxy_serverSearchResult(epoll_server_t *server, const unsigned long messageId, const unsigned long type, const size_t offset, const size_t maxLen); static BOOL proxy_localFeatureReply(epoll_client_t *client, const unsigned long messageId); static BOOL proxy_localSearchRequest(epoll_client_t *client, const unsigned long messageId, const struct SearchRequest *req); // #define SETSTR(x) s_ ## x.s = #x; s_ ## x.l = strlen( #x ) void proxy_init() { static int done = 0; if (done) return; done = 1; // Set up some strings we frequently use SETSTR(shadowAccount); SETSTR(posixAccount); SETSTR(posixGroup); SETSTR(user); SETSTR(uid); SETSTR(sAMAccountName); SETSTR(objectSid); SETSTR(objectClass); SETSTR(objectclass); SETSTR(homeDirectory); SETSTR(homeMount); SETSTR(gidNumber); SETSTR(gecos); SETSTR(loginShell); SETSTR(uidNumber); SETSTR(mail); SETSTR(objectCategory); SETSTR(distinguishedName); SETSTR(cn); SETSTR(dn); SETSTR(member); SETSTR(memberUid); SETSTR(realAccount); SETSTR(namingContexts); SETSTR(supportedControl); SETSTR(supportedExtension); SETSTR(supportedFeatures); SETSTR(supportedLDAPVersion); SETSTR(lastUSN); SETSTR(highestCommittedUSN); SETSTR(1001); SETSTR(3); SETSTR(samaccountname); SETSTR(objectsid); SETSTR(homemount); SETSTR(memberuid); SETSTR(realaccount); SETSTR(homedirectory); SETSTR(gidnumber); SETSTR(uidnumber); SETSTR(distinguishedname); SETSTR(bogusFieldName42); SETSTR(bogusFieldName43); SETSTR(bogusfieldname42); SETSTR(bogusfieldname43); // TODO: configurable str_ADUSER.s = "ad_user"; str_ADUSER.l = strlen(str_ADUSER.s); str_ADUSERDN.s = "cn=ad_user,ou=groups,dc=sausageface,dc=de"; str_ADUSERDN.l = strlen(str_ADUSERDN.s); } #undef SETSTR /** * Initialize default mapping of attributes for those which weren't * set in the config file. For LDAP, this is mostly a 1:1 mapping, * for AD, we pull some stunts. */ void proxy_initDefaultMap(server_t *server) { attr_map_t *m = &server->map; if (server->plainLdap) { if (m->homemount.l == 0) m->homemount = s_homemount; if (m->localhome.l == 0) m->localhome = s_homedirectory; if (m->posixAccount.l == 0) m->posixAccount = s_posixAccount; if (m->shadowAccount.l == 0) m->shadowAccount = s_shadowAccount; if (m->uid.l == 0) m->uid = s_uid; if (m->uidnumber.l == 0) m->uidnumber = s_uidnumber; } else { if (m->homemount.l == 0) m->homemount = s_homedirectory; if (m->localhome.l == 0) m->localhome = s_supportedLDAPVersion; // Unused, use something long and unlikely if (m->posixAccount.l == 0) m->posixAccount = s_user; if (m->shadowAccount.l == 0) m->shadowAccount = s_user; if (m->uid.l == 0) m->uid = s_samaccountname; if (m->uidnumber.l == 0) m->uidnumber = s_objectsid; } } static void proxy_killClient(epoll_client_t *client) { client->kill = TRUE; if (client->ssl) { SSL_shutdown(client->ssl); } else { shutdown(client->fd, SHUT_RDWR); } } BOOL proxy_fromClient(epoll_client_t *client, const size_t maxLen) { unsigned long messageId, op; size_t len; const size_t res = scan_ldapmessage(client->readBuffer, client->readBuffer + maxLen, &messageId, &op, &len); if (res == 0) return FALSE; plog(DEBUG_TRACE, "[C] scan_ldapmessage: Consumed %d, remaining length %d, id %lu, op %lu", (int)res, (int)len, messageId, op); // TODO: Caching switch (op) { case BindRequest: return proxy_clientBindRequest(client, messageId, res, maxLen); case SearchRequest: return proxy_clientSearchRequest(client, messageId, res, maxLen); case UnbindRequest: proxy_killClient(client); return TRUE; } return TRUE; } /** * Client notifies us about how it's disconnecting */ void proxy_removeClient(epoll_client_t * const client) { int i, lastValid = -1; BOOL problem = FALSE; for (i = 0; i < _pendingCount; ++i) { if (_pendingRequest[i].client == client) { plog(DEBUG_TRACE, "RemoveClient success %p", client); _pendingRequest[i].client = NULL; if (_autoRestart && _pendingRequest[i].started + 10 < time(NULL)) { plog(DEBUG_WARNING, "Client has pending request, age %d", (int)(time(NULL) - _pendingRequest[i].started)); problem = TRUE; } } else if (_pendingRequest[i].client != NULL) { lastValid = i; } } if (problem && ++_problemCount > 5) { bail("Too many unanswered requests, bailing out"); } _pendingCount = lastValid + 1; if (client->fixedServer != NULL) { client->fixedServer->kill = TRUE; shutdown(client->fixedServer->fd, SHUT_RDWR); client->fixedServer->fixedClient = NULL; } } /** * Server is closing the connection, get rid of according */ void proxy_removeServer(epoll_server_t * const server) { if (server->fixedClient != NULL) { proxy_killClient(server->fixedClient); server->fixedClient->fixedServer = NULL; } } static void hexdump(epoll_server_t *server, const size_t start, const size_t maxLen) { for (size_t i = start; i < maxLen; ++i) { const uint8_t c = server->readBuffer[i]; if (c >= 32 && c <= 126) { putchar(c); } else { printf("[%X]", (int)c); } } putchar('\n'); } static pending_t* proxy_getPendingFromServer(unsigned long serverMessageId) { for (int i = 0; i < _pendingCount; ++i) { if (_pendingRequest[i].client == NULL) continue; if (_pendingRequest[i].serverMessageId == serverMessageId) return &_pendingRequest[i]; } return NULL; } BOOL proxy_fromServer(epoll_server_t *server, const size_t maxLen) { unsigned long messageId, op; size_t len; const size_t res = scan_ldapmessage(server->readBuffer, server->readBuffer + maxLen, &messageId, &op, &len); if (res == 0) { return FALSE; } plog(DEBUG_TRACE, "[Server] scan_ldapmessage: Consumed %d, remaining length %d, id %lu, op %lu", (int)res, (int)len, messageId, op); switch (op) { case BindResponse: return proxy_serverBindResponse(server, messageId, res, maxLen); case SearchResultEntry: case SearchResultDone: if (_autoRestart && _problemCount > 0) { --_problemCount; } return proxy_serverSearchResult(server, messageId, op, res, maxLen); case SearchResultReference: // Just ignore these for now //scan_ldapstring(server->readBuffer + res,const char* max,struct string* s); return TRUE; } pending_t *pending = proxy_getPendingFromServer(messageId); plog(DEBUG_WARNING, "[Server] Unsupported op in reply: %lu; dropping pending %p.", op, pending); if (_debugLevel >= DEBUG_VERBOSE) { printf("[Server] Message content was: "); hexdump(server, 0, maxLen); } if (pending != NULL && pending->client != NULL) { proxy_killClient(pending->client); pending->client = NULL; } return TRUE; // See if we can recover by skipping this message... } // static pending_t* proxy_getFreePendingSlot(epoll_client_t *client) { time_t oldest = 0; int i; for (i = 0; i < _pendingCount; ++i) { if (_pendingRequest[i].client == NULL) break; if (oldest == 0 || oldest > _pendingRequest[i].started) { oldest = _pendingRequest[i].started; } } if (i == _pendingCount) { if (_pendingCount >= MAXPENDING) { if (_autoRestart && oldest + 30 < time(NULL)) { bail("Bailing out: Request queue full and auto-restart enabled!"); } return NULL; } i = _pendingCount++; } memset(&_pendingRequest[i], 0, sizeof(pending_t)); _pendingRequest[i].client = client; _pendingRequest[i].started = time(NULL); return &_pendingRequest[i]; } /* static void pref(int spaces, char prefix) { for (int i = 0; i < spaces; ++i) putchar(' '); putchar(prefix); putchar(' '); } */ // ---- client to AD replacements //#define PREF(...) do { pref(spaces, prefix); printf(__VA_ARGS__); } while (0) static BOOL request_isUserFilter(struct Filter *filter); static BOOL request_isServerCheck(struct AttributeDescriptionList* adl); static BOOL request_getGroupFilter(struct Filter *filter, struct string *wantedGroupName, uint32_t *wantedGroupId, BOOL *wantsMember); static void request_replaceFilter(server_t *server, struct Filter **filter, const BOOL negated); static void request_replaceAdl(server_t *server, struct AttributeDescriptionList **adl, attr_t *attr); static BOOL request_replaceAttribute(server_t *server, struct string *attribute, struct string *value, attr_t *attr, const BOOL negated); /** * Checks whether the filter contains attributes that indicate this is a search for a user. * Expects pre-replacement attribute names. */ static BOOL request_isUserFilter(struct Filter *filter) { for (; filter != NULL; filter = filter->next) { switch (filter->type) { case NOT: case AND: case OR: if (filter->x != NULL && request_isUserFilter(filter->x)) return TRUE; break; case EQUAL: case GREATEQUAL: case LESSEQUAL: case APPROX: if (iequals(&filter->ava.desc, &s_objectclass) && (equals(&filter->ava.value, &s_posixAccount) || equals(&filter->ava.value, &s_shadowAccount))) { return TRUE; } if (iequals(&filter->ava.desc, &s_uid) || iequals(&filter->ava.desc, &s_uidnumber)) { return TRUE; } break; default: break; } } return FALSE; } static BOOL request_isServerCheck(struct AttributeDescriptionList* adl) { int counter = 0; for (; adl != NULL; adl = adl->next) { if (equals(&adl->a, &s_namingContexts)) counter++; if (equals(&adl->a, &s_supportedControl)) counter++; if (equals(&adl->a, &s_supportedExtension)) counter++; if (equals(&adl->a, &s_supportedLDAPVersion)) counter++; if (equals(&adl->a, &s_supportedFeatures)) counter++; if (equals(&adl->a, &s_lastUSN)) counter++; if (equals(&adl->a, &s_highestCommittedUSN)) counter++; if (counter > 3) return TRUE; } return FALSE; } /** * This is REALLY cheap. It doesn't really look at the logic operators in the filter as we assume that pam_ldap * or nss_ldap etc. won't do anything fancy like "!(objectClass=groupAccount)", just simple AND and OR combined * with EQUAL. * You could actually get real group memberships using the memberOf attributes of the user, and do additional * queries for these groups, but for that to make any sense you'd also have to implement useful permission * handling.... So as all we really want is authentication and optionally mounting a home directory, we pretend * there is just one group with id 1001 and name ad_user. * This function will try to figure out if the given filter is a lookup for a group name or group id. */ static BOOL request_getGroupFilter(struct Filter *filter, struct string *wantedGroupName, uint32_t *wantedGroupId, BOOL *wantsMember) { BOOL retval = FALSE; for (; filter != NULL; filter = filter->next) { switch (filter->type) { case AND: case OR: if (filter->x != NULL && request_getGroupFilter(filter->x, wantedGroupName, wantedGroupId, wantsMember)) retval = TRUE; break; case EQUAL: case APPROX: if (iequals(&filter->ava.desc, &s_objectclass) && equals(&filter->ava.value, &s_posixGroup)) { retval = TRUE; } else if (iequals(&filter->ava.desc, &s_dn) && iequals(&filter->ava.value, &str_ADUSERDN)) { *wantedGroupName = str_ADUSER; } else if (iequals(&filter->ava.desc, &s_gidnumber)) { *wantedGroupId = 0; // Should we check for a valid number? I don't see how it would hurt not doing so... for (size_t i = 0; i < filter->ava.value.l; ++i) *wantedGroupId = (*wantedGroupId * 10) + (filter->ava.value.s[i] - '0'); } else if (iequals(&filter->ava.desc, &s_cn)) { *wantedGroupName = filter->ava.value; } else if (iequals(&filter->ava.desc, &s_member) || iequals(&filter->ava.desc, &s_memberuid)) { *wantsMember = TRUE; } break; default: break; } } return retval; } /** * Gets passed original filter from client. */ static void request_replaceFilter(server_t *server, struct Filter **filter, BOOL negated) { while (*filter != NULL) { BOOL del = FALSE; switch ((*filter)->type) { case NOT: request_replaceFilter(server, &(*filter)->x, !negated); if ((*filter)->x == NULL) { del = TRUE; } break; case AND: case OR: #ifdef FILTER_COLLAPSING // This collapses nested filters of same type, which was suspected to cause issues // with certain LDAP servers (which was not the case in the end) { struct Filter *f = (*filter)->x, **p = &(*filter)->x; while (f) { if (f->type == (*filter)->type) { struct Filter **l = &f->x; while (*l != NULL) l = &(*l)->next; *l = f->next; *p = f->x; f->x = NULL; f->next = NULL; free_ldapsearchfilter(f); f = *p; } else { p = &f->next; f = f->next; } } } #endif request_replaceFilter(server, &(*filter)->x, negated); if ((*filter)->x == NULL) { del = TRUE; } break; case PRESENT: case SUBSTRING: if (!request_replaceAttribute(server, &(*filter)->ava.desc, NULL, NULL, negated)) { del = TRUE; } break; case EQUAL: case GREATEQUAL: case LESSEQUAL: case APPROX: if (!request_replaceAttribute(server, &(*filter)->ava.desc, &(*filter)->ava.value, NULL, negated)) { del = TRUE; } break; default: break; } if (del) { struct Filter *old = *filter; *filter = (*filter)->next; old->next = NULL; free_ldapsearchfilter(old); } else { filter = &(*filter)->next; } } } #define elifSETATTR(MATCH,TOSET) else if (iequals(&(*adl)->a, &s_ ## MATCH)) attr->TOSET = TRUE, next = (*adl)->next, free(*adl), *adl = next static void request_replaceAdl(server_t *server, struct AttributeDescriptionList **adl, attr_t *attr) { while (*adl != NULL) { struct AttributeDescriptionList *next = NULL; if (attr == NULL) request_replaceAttribute(server, &(*adl)->a, NULL, NULL, FALSE); else if (iequals(&(*adl)->a, &s_homedirectory)) { attr->homeDirectory = TRUE; if (server->plainLdap) { (*adl)->a = server->map.localhome; } else { next = (*adl)->next; free(*adl); *adl = next; } } // Set simple flag for there elifSETATTR(gidnumber, gidNumber); elifSETATTR(gecos, gecos); elifSETATTR(realaccount, realAccount); elifSETATTR(loginshell, loginShell); // Further handling (uid, homeMount, objectClass, uidNumber) else request_replaceAttribute(server, &(*adl)->a, NULL, attr, FALSE); if (*adl == NULL) break; if (next == NULL) adl = &(*adl)->next; // If next is not NULL, we removed an entry, so we don't need to shift } if (!attr->hasUser) { if (attr->homeDirectory || attr->gecos || attr->homeMount || (server->genUidNumber && attr->uidNumber)) { struct AttributeDescriptionList *user = calloc(1, sizeof(struct AttributeDescriptionList)); user->a = server->map.uid; user->next = *adl; *adl = user; } } } /** * @return FALSE = delete attribute, TRUE = keep */ static BOOL request_replaceAttribute(server_t *server, struct string *attribute, struct string *value, attr_t *attr, const BOOL negated) { if (iequals(attribute, &s_uid)) { *attribute = server->map.uid; if (attr) attr->hasUser = TRUE; // If uid is of format _x_*, we assume that it's a numeric account name in AD, as a workaround if (value == NULL) return TRUE; if (server->fixNumeric) { fixUnNumeric(value); } ////// ################### } else if (iequals(attribute, &s_bogusfieldname42)) { if (attr) { attr->bogusFieldName42 = TRUE; return FALSE; } *attribute = server->map.uid; } else if (iequals(attribute, &s_bogusfieldname43)) { if (attr) { attr->bogusFieldName43 = TRUE; return FALSE; } *attribute = server->map.uid; } else if (iequals(attribute, &s_homemount)) { *attribute = server->map.homemount; if (attr != NULL) attr->homeMount = TRUE; } else if (iequals(attribute, &s_objectclass)) { if (value == NULL) return TRUE; if (equals(value, &s_shadowAccount)) *value = server->map.shadowAccount; else if (equals(value, &s_posixAccount)) *value = server->map.posixAccount; } else if (iequals(attribute, &s_uidnumber)) { *attribute = server->map.uidnumber; if (attr) attr->uidNumber = TRUE; if (value == NULL) return TRUE; if (server->genUidNumber && !(value->l == 1 && value->s[0] == '0')) { // We're managing uidNumbers on the proxy const struct string *name = uidmap_getNameForNumber(&server->uidmap, value); if (name != NULL) { // Mapped to uid, so query will return all the requested fields *attribute = server->map.uid; *value = *name; } else { // Nothing, make sure query doesn't match *value = s_uid; } return TRUE; } if (!negated && value->l == 1 && value->s[0] == '0') { // Saftey measure: Query for user with uidNumber == 0 - root; replace with something that // should never return anything *value = s_uid; } if (!server->plainLdap) { uint32_t tmp = 0; for (size_t i = 0; i < value->l; ++i) tmp = (tmp * 10) + (value->s[i] - '0'); memcpy(server->sid + (SIDLEN - 4), &tmp, 4); value->s = server->sid; value->l = SIDLEN; } } return TRUE; } #undef elifSETATTR // ----- Sanitize requested attributes by whitelist static void prependAdl(struct AttributeDescriptionList **adl, struct string *str) { struct AttributeDescriptionList *item = calloc(1, sizeof(struct AttributeDescriptionList)); item->a = *str; item->next = *adl; *adl = item; } /** * Add all the default attributes we're interested in in our context to the * given ADL from a client. Pre-relace/map, with names corresponding to * our expected names. */ static void request_addDefaultAttributes(struct AttributeDescriptionList **adl) { prependAdl(adl, &s_uid); prependAdl(adl, &s_homemount); prependAdl(adl, &s_memberuid); prependAdl(adl, &s_realaccount); prependAdl(adl, &s_objectclass); prependAdl(adl, &s_homedirectory); prependAdl(adl, &s_gidnumber); prependAdl(adl, &s_uidnumber); prependAdl(adl, &s_distinguishedname); } #define S_EQ(wat) iequals(str, &s_ ## wat) /** * Filter all requested attributes which are not on our whitelist. * Attribute names are pre-replacement, directly what the client sends us, before * mapping to scheme expected by server. */ static void request_filterRequestedAttributes(struct AttributeDescriptionList **adl) { while (*adl != NULL) { const struct string * const str = &(*adl)->a; if (!(S_EQ(uid) || S_EQ(homemount) || S_EQ(memberuid) || S_EQ(realaccount) || S_EQ(objectclass) || S_EQ(homedirectory) || S_EQ(gidnumber) || S_EQ(uidnumber) || S_EQ(distinguishedname) || S_EQ(bogusfieldname42) || S_EQ(bogusfieldname43))) { // Delete struct AttributeDescriptionList *next = (*adl)->next; free(*adl); *adl = next; } else { adl = &(*adl)->next; } } } #undef S_EQ // --------- AD to client replacements static void response_replacePal(server_t *server, struct PartialAttributeList **pal, attr_t *attr); static void response_replaceAdl(server_t *server, struct string *type, struct AttributeDescriptionList **adl, attr_t *attr); static void response_replaceAttribute(server_t *server, const struct string * const attribute, struct string * const value); static BOOL response_filterHomeDir(struct PartialAttributeList *pal); static BOOL response_filterLocalHomeDir(struct PartialAttributeList *pal); static struct PartialAttributeList* response_addPal(struct PartialAttributeList *pal, struct string *attribute, const char *format, ...); #define ADDATTR(x,...) do { if (attr->x) *pal = response_addPal(*pal, &s_ ## x, __VA_ARGS__); } while (0) #define elifDELATTR(MATCH,FIELD,...) else if (__VA_ARGS__ (iequals(&(*pal)->type, &s_ ## MATCH)) ) del = TRUE, attr->FIELD = TRUE #define elifDEL(MATCH) else if (iequals(&(*pal)->type, &s_ ## MATCH)) del = TRUE static void response_replacePal(server_t *server, struct PartialAttributeList **pal, attr_t *attr) { struct string *username = NULL; while (*pal != NULL) { BOOL del = FALSE; if (0) { } // Remove fields we don't want from AD/LDAP elifDELATTR(gidnumber, gidNumber); elifDELATTR(gecos, gecos); elifDELATTR(loginshell, loginShell); elifDELATTR(uidnumber, uidNumber, (!server->plainLdap || server->genUidNumber) &&); elifDEL(mail); elifDEL(msmqsigncertificates); elifDELATTR(cn, cn, !iequals(&server->map.uid, &s_cn) &&); else if (server->genUidNumber && iequals(&(*pal)->type, &server->map.uidnumber)) { del = TRUE; } else if (iequals(&(*pal)->type, &server->map.homemount)) { // homeDirectory is set in AD - it can either be a local path (in which case it's useless) // or a UNC path, which we can easily mount via mount.cifs if (!response_filterHomeDir(*pal)) { // Not UNC, ignore and generate later if possible del = TRUE; attr->homeMount = TRUE; } else { attr->homeMount = FALSE; (*pal)->type = s_homeMount; } } else if (iequals(&(*pal)->type, &server->map.localhome)) { // homeDirectory is set in LDAP - use if it's a local path if(response_filterLocalHomeDir(*pal)) { attr->homeDirectory = FALSE; (*pal)->type = s_homeDirectory; } else { del = TRUE; } } // Entry should be removed, free structs if (del) { struct PartialAttributeList *next = (*pal)->next; free_ldapadl((*pal)->values); free(*pal); *pal = next; continue; } response_replaceAdl(server, &(*pal)->type, &(*pal)->values, attr); // Fetch user name so we can add our fake fields later if (username == NULL && iequals(&(*pal)->type, &s_uid)) { username = &(*pal)->values->a; } pal = &(*pal)->next; } if (username != NULL) { if (server->genUidNumber && attr->uidNumber) { // Let's supply some uidNumber uint32_t num = uidmap_getNumberForName(&server->uidmap, username); if (num >= 2000) { ADDATTR(uidNumber, "%"PRIu32, num); } } char *user = tmpbuffer_get(); snprintf(user, TMPLEN, "%.*s", (int)username->l, username->s); if (attr->homeDirectory) { ADDATTR(homeDirectory, "/home/%s", user); } ADDATTR(gecos, "%s,,,", user); ADDATTR(cn, "%s", user); if (attr->bogusFieldName42) { ADDATTR(bogusFieldName42, "%s", user); } if (attr->bogusFieldName43) { ADDATTR(bogusFieldName43, "%s", user); } if (server->fixNumeric && strncmp(user, "_x_", 3) == 0) { user += 3; } if (attr->homeMount && server->homeTemplate[0] != '\0') { ADDATTR(homeMount, server->homeTemplate, user, user, user, user, user, user); } // Do this here so user += 3 will have been executed ADDATTR(realAccount, "%s", user); } ADDATTR(loginShell, "/bin/bash"); ADDATTR(gidNumber, "1001"); } static void response_replaceAdl(server_t *server, struct string *attribute, struct AttributeDescriptionList **adl, attr_t *attr) { if (iequals(attribute, &server->map.uid)) { *attribute = s_uid; } else if (iequals(attribute, &server->map.uidnumber)) { *attribute = s_uidNumber; } while (*adl != NULL) { struct AttributeDescriptionList *next = NULL; // Maybe delete entries here later if (next != NULL) { free(*adl); *adl = next; continue; } response_replaceAttribute(server, attribute, &(*adl)->a); adl = &(*adl)->next; // If next is not NULL, we removed an entry, so we don't need to shift } } static void response_replaceAttribute(server_t *server, const struct string * const attribute, struct string * const value) { if (value == NULL) return; // Attributes already remapped here! if (iequals(attribute, &s_uid)) { if (server->fixNumeric) { fixNumeric(value); } } else if (iequals(attribute, &s_uidnumber)) { if (!server->plainLdap) { plog(DEBUG_TRACE, "Replacing uidnumber from objectsid len=%d", (int)value->l); // If this is AD, this must have been objectSid before - convert SID to number if (value->l != SIDLEN) return; // It we don't have the servers SID base yet and we see a valid one, store it if (server->sid[0] == 0 && value->s[0] == 1 && value->s[1] == 5 && value->s[7] == 5) { memcpy(server->sid, value->s, SIDLEN - 4); } int tmp; memcpy(&tmp, value->s + (value->l - 4), 4); // We know value->s is in our receive buffer and there are 28 bytes available, so we reuse the buffer value->l = snprintf((char*)value->s, value->l, "%u", tmp); } } else if (iequals(attribute, &s_objectclass)) { if (equals(value, &server->map.posixAccount)) { *value = s_posixAccount; } else if (equals(value, &server->map.shadowAccount)) { *value = s_shadowAccount; } } } /** * Remove every attribute except one that looks like a local home directory path. */ static BOOL response_filterHomeDir(struct PartialAttributeList *pal) { for (struct AttributeDescriptionList *adl = pal->values; adl != NULL; adl = pal->values /* sic */) { if (adl->a.s != NULL) { if (adl->a.l > 0 && adl->a.s[0] == '\\') { for (size_t i = 0; i < adl->a.l; ++i) if (adl->a.s[i] == '\\') *((char*)adl->a.s + i) = '/'; } if (adl->a.l > 2 && adl->a.s[0] == '/' && adl->a.s[1] == '/') { free_ldapadl(adl->next); adl->next = NULL; pal->type = s_homeMount; return TRUE; } } pal->values = adl->next; free(adl); } return FALSE; } /** * Remove every attribute except one that looks like a UNC path. */ static BOOL response_filterLocalHomeDir(struct PartialAttributeList *pal) { for (struct AttributeDescriptionList *adl = pal->values; adl != NULL; adl = pal->values /* sic */) { if (adl->a.s != NULL) { if (adl->a.l > 0 && adl->a.s[0] == '/') { free_ldapadl(adl->next); adl->next = NULL; pal->type = s_homeDirectory; return TRUE; } } pal->values = adl->next; free(adl); } return FALSE; } static struct PartialAttributeList* response_addPal(struct PartialAttributeList *pal, struct string *attribute, const char *format, ...) { struct PartialAttributeList *next = malloc(sizeof(struct PartialAttributeList)); va_list args; next->next = pal; next->type = *attribute; next->values = malloc(sizeof(struct AttributeDescriptionList)); va_start(args, format); tmpbuffer_formatva(&next->values->a, format, args); va_end(args); next->values->next = NULL; return next; } // ----- static BOOL proxy_clientSearchRequest(epoll_client_t *client, const unsigned long messageId, const size_t offset, const size_t maxLen) { struct SearchRequest req; const size_t res = scan_ldapsearchrequest(client->readBuffer + offset, client->readBuffer + maxLen, &req); if (res == 0) return FALSE; if (req.scope == baseObject && request_isServerCheck(req.attributes)) { const BOOL ret = proxy_localFeatureReply(client, messageId); free_ldapsearchrequest(&req); return ret; } server_t *server = server_getFromBase(&req.baseObject); if (server == NULL) { plog(DEBUG_WARNING, "[Client] Invalid search request: baseObj '%.*s' unknown (scope %d).", (int)req.baseObject.l, req.baseObject.s, (int)req.scope); return FALSE; } plog(DEBUG_TRACE, "scan_ldapsearchrequest: baseObj: %.*s, scope: %d, derefAliases: %d", (int)req.baseObject.l, req.baseObject.s, req.scope, req.derefAliases); // Try to figure out if this is a lookup for a user/multiple users, or something else (eg. group) if (!request_isUserFilter(req.filter)) { // Handle locally if (_debugLevel >= DEBUG_VERBOSE) { printf("[Client] Search request (handling local): "); helper_printfilter(req.filter); } const BOOL ret = proxy_localSearchRequest(client, messageId, &req); free_ldapsearchrequest(&req); plog(DEBUG_TRACE, "[Client] Result: %d\n", (int)ret); return ret; } // Forward if (_debugLevel >= DEBUG_TRACE) { printf("[Client] Search request (original): "); helper_printfilter(req.filter); printf("[Client] Wanted attributes: "); helper_printal(req.attributes); } if (req.sizeLimit == 0 || req.sizeLimit > 20) req.sizeLimit = 20; // TODO: Magic value pending_t *pending = proxy_getFreePendingSlot(client); if (pending == NULL) { plog(DEBUG_WARNING, "Cannot handle incoming client request; too many pending requests on the wire."); free_ldapsearchrequest(&req); client_searchResultError(client, messageId, busy, "I'm busy!"); return FALSE; } // In case of anonymous bind: Narrow down list of what the user can see to protect private data if (client->fixedServer == NULL) { request_filterRequestedAttributes(&req.attributes); } if (req.attributes != NULL) { request_replaceAdl(server, &req.attributes, &pending->attr); } // Might have become NULL in the meantime if (req.attributes == NULL) { if (client->fixedServer == NULL) { request_addDefaultAttributes(&req.attributes); request_replaceAdl(server, &req.attributes, &pending->attr); } memset(&pending->attr, -1, sizeof(pending->attr)); } request_replaceFilter(server, &req.filter, FALSE); if (_debugLevel >= DEBUG_TRACE) { printf("[Client] Search request (translated): "); helper_printfilter(req.filter); printf("[Client] Wanted attributes: "); helper_printal(req.attributes); } pending->clientMessageId = messageId; if (client->fixedServer == NULL) { pending->serverMessageId = server_searchRequest(server, &req); } else { pending->serverMessageId = server_searchRequestOnConnection(client->fixedServer, &req); } if (pending->serverMessageId == 0) { client_searchResultError(client, messageId, unavailable, "Cannot contact upstream server."); plog(DEBUG_WARNING, "Failed to forward a search request to server."); pending->client = NULL; } free_ldapsearchrequest(&req); // if (pending->client == NULL) return FALSE; return TRUE; } static BOOL proxy_serverSearchResult(epoll_server_t *server, const unsigned long messageId, const unsigned long type, const size_t offset, const size_t maxLen) { static char *bodyBuffer = NULL; if (bodyBuffer == NULL) bodyBuffer = malloc(MAXMSGLEN); pending_t *pending = proxy_getPendingFromServer(messageId); if (pending == NULL) { plog(DEBUG_WARNING, "[Server] Received response with unknown messageId %lu, ignoring...", messageId); return TRUE; } const char *body; size_t bodyLen; if (type == SearchResultDone) { // Just forward with new header bodyLen = maxLen - offset; body = server->readBuffer + offset; plog(DEBUG_TRACE, "[Server] SRDONE"); // pending->client will be released at end of function } else { // Transform reply struct SearchResultEntry sre; const size_t res = scan_ldapsearchresultentry(server->readBuffer + offset, server->readBuffer + maxLen, &sre); if (res == 0) return FALSE; if (_debugLevel >= DEBUG_TRACE) { printf("SearchResultEntry:\n"); helper_printpal(sre.attributes); } response_replacePal(server->serverData, &sre.attributes, &pending->attr); if (_debugLevel >= DEBUG_TRACE) { helper_printpal(sre.attributes); } bodyLen = fmt_ldapsearchresultentry(NULL, &sre); if (bodyLen == 0) { plog(DEBUG_WARNING, "Error formatting ldapsearchresultentry after transformation."); free_ldapsearchresultentry(&sre); return FALSE; } if (bodyLen > MAXMSGLEN) { plog(DEBUG_WARNING, "ldapsearchresultentry too large after transformation."); free_ldapsearchresultentry(&sre); return FALSE; } fmt_ldapsearchresultentry(bodyBuffer, &sre); free_ldapsearchresultentry(&sre); if (_debugLevel >= DEBUG_TRACE) { const size_t res = scan_ldapsearchresultentry(bodyBuffer, bodyBuffer + bodyLen, &sre); if (res != 0) { helper_printpal(sre.attributes); free_ldapsearchresultentry(&sre); } } body = bodyBuffer; } // Build header and fire away const size_t headerLen = fmt_ldapmessage(NULL, pending->clientMessageId, type, bodyLen); char buffer[headerLen]; fmt_ldapmessage(buffer, pending->clientMessageId, type, bodyLen); client_send(pending->client, buffer, headerLen, TRUE); client_send(pending->client, body, bodyLen, FALSE); if (type == SearchResultDone) pending->client = NULL; // Release pending return TRUE; } static BOOL proxy_clientBindRequest(epoll_client_t *client, const unsigned long messageId, const size_t offset, const size_t maxLen) { unsigned long version, method; struct string name, password; char buffer[800]; char *bufoff = buffer + 100; size_t bodyLen; const size_t res = scan_ldapbindrequest(client->readBuffer + offset, client->readBuffer + maxLen, &version, &name, &method); if (res == 0) return FALSE; // Parsing request failed if (method != 0) { // Other than simple bind - currently not supported plog(DEBUG_WARNING, "[Client] Unsupported bind method: %lu", method); bodyLen = fmt_ldapbindresponse(bufoff, authMethodNotSupported, "", "SIMPLE only", ""); } else { // Simple bind :-) password.l = 0; const size_t res2 = scan_ldapstring(client->readBuffer + offset + res, client->readBuffer + maxLen, &password); plog(DEBUG_TRACE, "[Client] scan_ldapbindrequest: Consumed %d, version %lu, method %lu, name '%.*s'", (int)(res + res2), version, method, (int)name.l, name.s); if (name.l == 0 && password.l == 0) { // Anonymous bind used for "normal" lookups plog(DEBUG_VERBOSE, "[Client] Anonymous bind accepted"); bodyLen = fmt_ldapbindresponse(bufoff, success, "", "main screen turn on", ""); } else { BOOL incorrect = FALSE; server_t *server = server_getFromBase(&name); if (server == NULL || (incorrect = (strncmp(password.s, "\x08\x0a\x0d\x7fINCORRECT", 13) == 0)) || (server->fixNumeric && isdigit(name.s[0]))) { // The INCORRECT part is some weird thing I saw pam_ldap do - probably to identify misconfigured // LDAP servers/accounts that will accept any password - save the round trip to AD and deny if (!incorrect) plog(DEBUG_WARNING, "[Client] Numeric account or invalid binddn for %.*s", (int)name.l, name.s); bodyLen = fmt_ldapbindresponse(bufoff, invalidCredentials, "", "invalid credentials", ""); } else { // Seems to be an actual bind - forward to AD - TODO: SASL (DIGEST-MD5? Something?) // TODO: Handle DN, but should not be needed... fixUnNumeric(&name); pending_t *pending = proxy_getFreePendingSlot(client); epoll_server_t *con; const unsigned long smid = server_tryUserBind(server, &name, &password, &con); if (pending == NULL || smid == 0) { // Busy if (pending != NULL) pending->client = NULL; plog(DEBUG_WARNING, "[Client] Too many pending requests, or cannot connect to AD for bind."); bodyLen = fmt_ldapbindresponse(bufoff, busy, "", "can't handle it", ""); } else { // Request queued, client needs to wait plog(DEBUG_INFO, "[Client] Forwarding bind to AD/LDAP for user %.*s (#%lu)", (int)name.l, name.s, smid); pending->clientMessageId = messageId; pending->serverMessageId = smid; con->fixedClient = client; client->fixedServer = con; return TRUE; } } } } const size_t headerLen = fmt_ldapmessage(NULL, messageId, BindResponse, bodyLen); if (headerLen > 100) return FALSE; // Too long - don't care fmt_ldapmessage(bufoff - headerLen, messageId, BindResponse, bodyLen); return client_send(client, bufoff - headerLen, bodyLen + headerLen, FALSE); } static BOOL proxy_serverBindResponse(epoll_server_t *server, const unsigned long messageId, const size_t offset, const size_t maxLen) { unsigned long result; struct string binddn, error, refer; const size_t res = scan_ldapbindresponse(server->readBuffer + offset, server->readBuffer + maxLen, &result, &binddn, &error, &refer); if (res == 0) return FALSE; // Parsing request failed plog(DEBUG_TRACE, "[Server] scan_ldapbindresponse: Consumed %d, result: %lu, binddn: %.*s, error: %.*s, referral: %.*s", (int)res, result, (int)binddn.l, binddn.s, (int)error.l, error.s, (int)refer.l, refer.s); server->bound = (result == success); if (server->bound) { plog(DEBUG_VERBOSE, "[Server] Accepted credentials (#%lu)", messageId); } else { plog(DEBUG_INFO, "[Server] Wrong credentials (#%lu)", messageId); } if (messageId <= 1) return TRUE; // Was a forwarded auth pending_t *pending = proxy_getPendingFromServer(messageId); if (pending == NULL) return FALSE; const size_t headerLen = fmt_ldapmessage(NULL, pending->clientMessageId, BindResponse, res); char buffer[headerLen]; fmt_ldapmessage(buffer, pending->clientMessageId, BindResponse, res); client_send(pending->client, buffer, headerLen, TRUE); client_send(pending->client, server->readBuffer + offset, res, FALSE); pending->client = NULL; return server->bound; // Return FALSE here so server.c will kill off this server connection } // ---- Local handling ---- static void prependPal(struct SearchResultEntry *dest, struct PartialAttributeList *pal, struct AttributeDescriptionList *adl, struct string *key, struct string *value) { memset(pal, 0, sizeof *pal); memset(adl, 0, sizeof *adl); pal->next = dest->attributes; dest->attributes = pal; pal->type = *key; pal->values = adl; adl->a = *value; } static BOOL proxy_localFeatureReply(epoll_client_t *client, const unsigned long messageId) { plog(DEBUG_VERBOSE, "[Proxy] Sending static feature request reply to client."); struct SearchResultEntry sre; struct PartialAttributeList vers; struct AttributeDescriptionList versVal; memset(&sre, 0, sizeof(sre)); sre.objectName.l = 0; prependPal(&sre, &vers, &versVal, &s_supportedLDAPVersion, &s_3); // Build reply const size_t bodyLen = fmt_ldapsearchresultentry(NULL, &sre); const size_t headerLen = fmt_ldapmessage(NULL, messageId, SearchResultEntry, bodyLen); const size_t doneLen = fmt_ldapsearchresultdone(NULL, success, "", "", ""); const size_t doneHeaderLen = fmt_ldapmessage(NULL, messageId, SearchResultDone, doneLen); size_t len = MAX(bodyLen + headerLen, doneLen + doneHeaderLen); char buffer[len]; fmt_ldapmessage(buffer, messageId, SearchResultEntry, bodyLen); fmt_ldapsearchresultentry(buffer + headerLen, &sre); client_send(client, buffer, headerLen + bodyLen, TRUE); fmt_ldapmessage(buffer, messageId, SearchResultDone, doneLen); fmt_ldapsearchresultdone(buffer + doneHeaderLen, success, "", "", ""); return client_send(client, buffer, doneHeaderLen + doneLen, FALSE); } static BOOL proxy_localSearchRequest(epoll_client_t *client, const unsigned long messageId, const struct SearchRequest *req) { struct string name; uint32_t number = 2; BOOL wantsMember = FALSE; name.l = 0; if (request_getGroupFilter(req->filter, &name, &number, &wantsMember)) { // Request for group (by number or by name)? if (number == 2 && name.l == 0 && !wantsMember) { // posixGroup requested, but neither gidNumber nor cn requested, so it must be "list all" number = 1001; name.l = 1; } else if (!iequals(&name, &str_ADUSER)) { // We know only one group... name.l = 0; } if (number == 1001 || name.l != 0) { // At least one of them was set plog(DEBUG_VERBOSE, "[Proxy] Sending static group membership to client."); struct SearchResultEntry sre; struct PartialAttributeList gidNumber, cn, objectClass; struct AttributeDescriptionList gidNumberVal, cnVal, objectClassVal; memset(&sre, 0, sizeof(sre)); sre.objectName = str_ADUSERDN; prependPal(&sre, &cn, &cnVal, &s_cn, &str_ADUSER); prependPal(&sre, &gidNumber, &gidNumberVal, &s_gidNumber, &s_1001); prependPal(&sre, &objectClass, &objectClassVal, &s_objectClass, &s_posixGroup); // Build reply const size_t bodyLen = fmt_ldapsearchresultentry(NULL, &sre); const size_t headerLen = fmt_ldapmessage(NULL, messageId, SearchResultEntry, bodyLen); char buffer[headerLen + bodyLen]; fmt_ldapmessage(buffer, messageId, SearchResultEntry, bodyLen); fmt_ldapsearchresultentry(buffer + headerLen, &sre); client_send(client, buffer, headerLen + bodyLen, TRUE); } else { plog(DEBUG_VERBOSE, "[Proxy] Sending empty posixGroup search result to client."); } } // Always send SearchResultDone here, so if we have no match above it results in // an empty reply. const size_t doneLen = fmt_ldapsearchresultdone(NULL, success, "", "", ""); const size_t doneHeaderLen = fmt_ldapmessage(NULL, messageId, SearchResultDone, doneLen); char buffer[doneLen + doneHeaderLen]; fmt_ldapsearchresultdone(buffer + doneHeaderLen, success, "", "", ""); fmt_ldapmessage(buffer, messageId, SearchResultDone, doneLen); return client_send(client, buffer, doneHeaderLen + doneLen, FALSE); }