#include "proxy.h"
#include "client.h"
#include "server.h"
#include "helper.h"
#include "tmpbuffer.h"
#include "ldap.h"
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdarg.h>
#include <sys/socket.h>
#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;
// Internal
BOOL hasUser;
} attr_t;
typedef struct
{
unsigned long clientMessageId;
unsigned long serverMessageId;
epoll_client_t *client;
attr_t attr;
} pending_t;
static pending_t _pendingRequest[MAXPENDING];
static int _pendingCount = 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_memberOf, 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_memberof, s_distinguishedname, s_loginshell;
// 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;
// Other
static struct string str_ADUSER, str_ADUSERDN;
// HACK
static BOOL isInt(struct string *value, int start)
{
size_t i;
for (i = start; i < value->l; ++i) {
if (value->s[i] < '0' || value->s[i] > '9') return FALSE;
}
return TRUE;
}
static void fixUnNumeric(struct string *value)
{
if (value == NULL || value->l < 2) return;
if (value->s[0] != 's') return;
if (!isInt(value, 1)) return;
value->s++;
value->l--;
}
static void fixNumeric(struct string *value)
{
size_t i;
if (value == NULL || value->l < 1 || value->l > 18) return;
if (!isInt(value, 0)) return;
char *buf = tmpbuffer_get();
buf[0] = 's';
for (i = 0; i < value->l; ++i) {
buf[i+1] = value->s[i];
}
value->s = buf;
value->l++;
}
// 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(memberOf);
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(memberof);
SETSTR(distinguishedname);
// 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;
}
void proxy_removeClient(epoll_client_t * const client)
{
int i, lastValid = -1;
for (i = 0; i < _pendingCount; ++i) {
if (_pendingRequest[i].client == client) {
plog(DEBUG_TRACE, "RemoveClient success %p", client);
_pendingRequest[i].client = NULL;
}
else if (_pendingRequest[i].client != NULL) lastValid = i;
}
_pendingCount = lastValid + 1;
if (client->fixedServer != NULL) {
client->fixedServer->kill = TRUE;
shutdown(client->fixedServer->fd, SHUT_RDWR);
client->fixedServer->fixedClient = NULL;
}
}
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:
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)
{
for (int i = 0; i < _pendingCount; ++i) {
if (_pendingRequest[i].client == NULL) {
memset(&_pendingRequest[i], 0, sizeof(pending_t));
_pendingRequest[i].client = client;
return &_pendingRequest[i];
}
}
if (_pendingCount < MAXPENDING) {
memset(&_pendingRequest[_pendingCount], 0, sizeof(pending_t));
_pendingRequest[_pendingCount].client = client;
return &_pendingRequest[_pendingCount++];
}
return NULL;
}
/*
static void pref(int spaces, char prefix)
{
for (int i = 0; i < spaces; ++i) putchar(' ');
putchar(prefix);
putchar(' ');
}
*/
static inline int equals(struct string *a, struct string *b)
{
if (a->l != b->l) return 0;
if (a->s == b->s) return 1;
return strncmp(a->s, b->s, a->l) == 0;
}
/**
* b MUST be in lowercase already.
*/
static inline int iequals(const struct string * const a, const struct string * const b)
{
if (a->l != b->l) return 0;
if (a->s == b->s) return 1;
for (size_t i = 0; i < a->l; ++i) {
if (tolower(a->s[i]) != b->s[i]) return 0;
}
return 1;
}
// ---- 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;
}
}
elifSETATTR(gidnumber, gidNumber);
elifSETATTR(gecos, gecos);
elifSETATTR(realaccount, realAccount);
elifSETATTR(loginshell, loginShell);
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) {
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 s[0-9]+, we assume that it's a numeric account name in AD, as a workaround
if (value == NULL) return TRUE;
fixUnNumeric(value);
////// ###################
} 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 (value == NULL) return TRUE;
if (value != NULL && !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_memberof);
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(memberof)
|| S_EQ(distinguishedname))) {
// 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;
BOOL wasNumeric = FALSE;
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 &&);
elifDEL(mail);
elifDELATTR(cn, cn);
elifDEL(memberof);
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;
if (username->l > 1 && username->s[0] == 's' && isInt(username, 1)) wasNumeric = TRUE;
}
pal = &(*pal)->next;
}
if (username != NULL) {
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 (wasNumeric) user++; // From here on, user is the real AD/ldap username, no leading 's'
if (attr->homeMount && server->homeTemplate[0] != '\0') {
ADDATTR(homeMount, server->homeTemplate, user, user, user, user, user, user);
}
// Do this here so user++ 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)) {
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)
{
helper_printpal(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);
return ret;
}
// Forward
if (_debugLevel >= DEBUG_TRACE) {
printf("[Client] Search request (original): ");
helper_printfilter(req.filter);
}
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);
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("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) {
// Failed to forward.. TODO: Fail client
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)) || isInt(&name, 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.");
}
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);
}
return FALSE;
}