001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2009-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 */
017
018package org.forgerock.opendj.examples;
019
020import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool;
021import static org.forgerock.opendj.ldap.LDAPListener.*;
022import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
023
024import java.io.IOException;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Set;
028
029import org.forgerock.opendj.ldap.Attribute;
030import org.forgerock.opendj.ldap.AttributeDescription;
031import org.forgerock.opendj.ldap.Attributes;
032import org.forgerock.opendj.ldap.ConnectionFactory;
033import org.forgerock.opendj.ldap.Connections;
034import org.forgerock.opendj.ldap.DN;
035import org.forgerock.opendj.ldap.Filter;
036import org.forgerock.opendj.ldap.IntermediateResponseHandler;
037import org.forgerock.opendj.ldap.LDAPClientContext;
038import org.forgerock.opendj.ldap.LDAPConnectionFactory;
039import org.forgerock.opendj.ldap.LDAPListener;
040import org.forgerock.opendj.ldap.LdapException;
041import org.forgerock.opendj.ldap.LdapResultHandler;
042import org.forgerock.opendj.ldap.Modification;
043import org.forgerock.opendj.ldap.RequestContext;
044import org.forgerock.opendj.ldap.RequestHandler;
045import org.forgerock.opendj.ldap.RequestHandlerFactory;
046import org.forgerock.opendj.ldap.SearchResultHandler;
047import org.forgerock.opendj.ldap.ServerConnectionFactory;
048import org.forgerock.opendj.ldap.controls.Control;
049import org.forgerock.opendj.ldap.requests.AddRequest;
050import org.forgerock.opendj.ldap.requests.BindRequest;
051import org.forgerock.opendj.ldap.requests.CompareRequest;
052import org.forgerock.opendj.ldap.requests.DeleteRequest;
053import org.forgerock.opendj.ldap.requests.ExtendedRequest;
054import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
055import org.forgerock.opendj.ldap.requests.ModifyRequest;
056import org.forgerock.opendj.ldap.requests.Requests;
057import org.forgerock.opendj.ldap.requests.SearchRequest;
058import org.forgerock.opendj.ldap.responses.BindResult;
059import org.forgerock.opendj.ldap.responses.CompareResult;
060import org.forgerock.opendj.ldap.responses.ExtendedResult;
061import org.forgerock.opendj.ldap.responses.Result;
062import org.forgerock.opendj.ldap.responses.SearchResultEntry;
063import org.forgerock.opendj.ldap.responses.SearchResultReference;
064import org.forgerock.opendj.ldap.schema.AttributeType;
065import org.forgerock.util.Options;
066
067import com.forgerock.reactive.ServerConnectionFactoryAdapter;
068
069/**
070 * This example is based on the {@link Proxy}. This example does no load
071 * balancing, but instead rewrites attribute descriptions and DN suffixes in
072 * requests to and responses from a directory server using hard coded
073 * configuration.
074 * <ul>
075 * <li>It transforms DNs ending in {@code o=example} on the client side to end
076 * in {@code dc=example,dc=com} on the server side and vice versa.
077 * <li>It transforms the attribute description {@code fullname} on the client
078 * side to {@code cn} on the server side and vice versa.
079 * </ul>
080 *
081 * This example has a number of restrictions.
082 * <ul>
083 * <li>It does not support SSL connections.
084 * <li>It does not support StartTLS.
085 * <li>It does not support Abandon or Cancel requests.
086 * <li>It has very basic authentication and authorization support.
087 * <li>It does not rewrite bind DNs.
088 * <li>It uses proxied authorization, so if you use OpenDJ directory server, you
089 * must set the {@code proxied-auth} privilege for the proxy user.
090 * <li>It does not touch matched DNs in results.
091 * <li>It does not rewrite attributes with options in search result entries.
092 * <li>It does not touch search result references.
093 * </ul>
094 * This example takes the following command line parameters:
095 *
096 * <pre>
097 *  {@code <localAddress> <localPort> <proxyDN> <proxyPassword> <serverAddress> <serverPort>}
098 * </pre>
099 *
100 * If you have imported the users from <a
101 * href="http://opendj.forgerock.org/Example.ldif">Example.ldif</a>, then you
102 * can set {@code proxyUserDN} to {@code cn=My App,ou=Apps,dc=example,dc=com}
103 * and {@code proxyUserPassword} to {@code password}.
104 */
105public final class RewriterProxy {
106    private static final class Rewriter implements RequestHandler<RequestContext> {
107
108        /** This example hard codes the attribute... */
109        private static final String CLIENT_ATTRIBUTE = "fullname";
110        private static final String SERVER_ATTRIBUTE = "cn";
111
112        /** ...and DN rewriting configuration. */
113        private static final String CLIENT_SUFFIX = "o=example";
114        private static final String SERVER_SUFFIX = "dc=example,dc=com";
115
116        private final AttributeDescription clientAttributeDescription = AttributeDescription
117                .valueOf(CLIENT_ATTRIBUTE);
118        private final AttributeDescription serverAttributeDescription = AttributeDescription
119                .valueOf(SERVER_ATTRIBUTE);
120
121        /** Next request handler in the chain. */
122        private final RequestHandler<RequestContext> nextHandler;
123
124        private Rewriter(final RequestHandler<RequestContext> nextHandler) {
125            this.nextHandler = nextHandler;
126        }
127
128        @Override
129        public void handleAdd(final RequestContext requestContext, final AddRequest request,
130                final IntermediateResponseHandler intermediateResponseHandler,
131                final LdapResultHandler<Result> resultHandler) {
132            nextHandler.handleAdd(requestContext, rewrite(request), intermediateResponseHandler,
133                    resultHandler);
134        }
135
136        @Override
137        public void handleBind(final RequestContext requestContext, final int version,
138                final BindRequest request,
139                final IntermediateResponseHandler intermediateResponseHandler,
140                final LdapResultHandler<BindResult> resultHandler) {
141            nextHandler.handleBind(requestContext, version, rewrite(request),
142                    intermediateResponseHandler, resultHandler);
143        }
144
145        @Override
146        public void handleCompare(final RequestContext requestContext,
147                final CompareRequest request,
148                final IntermediateResponseHandler intermediateResponseHandler,
149                final LdapResultHandler<CompareResult> resultHandler) {
150            nextHandler.handleCompare(requestContext, rewrite(request),
151                    intermediateResponseHandler, resultHandler);
152        }
153
154        @Override
155        public void handleDelete(final RequestContext requestContext, final DeleteRequest request,
156                final IntermediateResponseHandler intermediateResponseHandler,
157                final LdapResultHandler<Result> resultHandler) {
158            nextHandler.handleDelete(requestContext, rewrite(request), intermediateResponseHandler,
159                    resultHandler);
160        }
161
162        @Override
163        public <R extends ExtendedResult> void handleExtendedRequest(
164                final RequestContext requestContext, final ExtendedRequest<R> request,
165                final IntermediateResponseHandler intermediateResponseHandler,
166                final LdapResultHandler<R> resultHandler) {
167            nextHandler.handleExtendedRequest(requestContext, rewrite(request),
168                    intermediateResponseHandler, resultHandler);
169        }
170
171        @Override
172        public void handleModify(final RequestContext requestContext, final ModifyRequest request,
173                final IntermediateResponseHandler intermediateResponseHandler,
174                final LdapResultHandler<Result> resultHandler) {
175            nextHandler.handleModify(requestContext, rewrite(request), intermediateResponseHandler,
176                    resultHandler);
177        }
178
179        @Override
180        public void handleModifyDN(final RequestContext requestContext,
181                final ModifyDNRequest request,
182                final IntermediateResponseHandler intermediateResponseHandler,
183                final LdapResultHandler<Result> resultHandler) {
184            nextHandler.handleModifyDN(requestContext, rewrite(request),
185                    intermediateResponseHandler, resultHandler);
186        }
187
188        @Override
189        public void handleSearch(final RequestContext requestContext, final SearchRequest request,
190            final IntermediateResponseHandler intermediateResponseHandler,
191            final SearchResultHandler entryHandler, final LdapResultHandler<Result> resultHandler) {
192            nextHandler.handleSearch(requestContext, rewrite(request), intermediateResponseHandler,
193                new SearchResultHandler() {
194                    @Override
195                    public boolean handleReference(SearchResultReference reference) {
196                        return entryHandler.handleReference(reference);
197                    }
198
199                    @Override
200                    public boolean handleEntry(SearchResultEntry entry) {
201                        return entryHandler.handleEntry(rewrite(entry));
202                    }
203                }, resultHandler);
204        }
205
206        private AddRequest rewrite(final AddRequest request) {
207            // Transform the client DN into a server DN.
208            final AddRequest rewrittenRequest = Requests.copyOfAddRequest(request);
209            rewrittenRequest.setName(request.getName().toString().replace(CLIENT_SUFFIX,
210                    SERVER_SUFFIX));
211            /*
212             * Transform the client attribute names into server attribute names,
213             * fullname;lang-fr ==> cn;lang-fr.
214             */
215            for (final Attribute a : request.getAllAttributes(clientAttributeDescription)) {
216                if (a != null) {
217                    final String ad =
218                            a.getAttributeDescriptionAsString().replaceFirst(
219                                    CLIENT_ATTRIBUTE, SERVER_ATTRIBUTE);
220                    final Attribute serverAttr =
221                            Attributes.renameAttribute(a, AttributeDescription.valueOf(ad));
222                    rewrittenRequest.addAttribute(serverAttr);
223                    rewrittenRequest.removeAttribute(a.getAttributeDescription());
224                }
225            }
226            return rewrittenRequest;
227        }
228
229        private BindRequest rewrite(final BindRequest request) {
230            // TODO: Transform client DN into server DN.
231            return request;
232        }
233
234        private CompareRequest rewrite(final CompareRequest request) {
235            /*
236             * Transform the client attribute name into a server attribute name,
237             * fullname;lang-fr ==> cn;lang-fr.
238             */
239            final String ad = request.getAttributeDescription().toString();
240            if (ad.toLowerCase().startsWith(CLIENT_ATTRIBUTE.toLowerCase())) {
241                final String serverAttrDesc =
242                        ad.replaceFirst(CLIENT_ATTRIBUTE, SERVER_ATTRIBUTE);
243                request.setAttributeDescription(AttributeDescription.valueOf(serverAttrDesc));
244            }
245
246            // Transform the client DN into a server DN.
247            return request
248                    .setName(request.getName().toString().replace(CLIENT_SUFFIX, SERVER_SUFFIX));
249        }
250
251        private DeleteRequest rewrite(final DeleteRequest request) {
252            // Transform the client DN into a server DN.
253            return request
254                    .setName(request.getName().toString().replace(CLIENT_SUFFIX, SERVER_SUFFIX));
255        }
256
257        private <S extends ExtendedResult> ExtendedRequest<S> rewrite(
258                final ExtendedRequest<S> request) {
259            // TODO: Transform password modify, etc.
260            return request;
261        }
262
263        private ModifyDNRequest rewrite(final ModifyDNRequest request) {
264            // Transform the client DNs into server DNs.
265            if (request.getNewSuperior() != null) {
266                return request.setName(
267                        request.getName().toString().replace(CLIENT_SUFFIX, SERVER_SUFFIX))
268                        .setNewSuperior(
269                                request.getNewSuperior().toString().replace(CLIENT_SUFFIX,
270                                        SERVER_SUFFIX));
271            } else {
272                return request.setName(request.getName().toString().replace(CLIENT_SUFFIX,
273                        SERVER_SUFFIX));
274            }
275        }
276
277        private ModifyRequest rewrite(final ModifyRequest request) {
278            // Transform the client DN into a server DN.
279            final ModifyRequest rewrittenRequest =
280                    Requests.newModifyRequest(request.getName().toString().replace(CLIENT_SUFFIX,
281                            SERVER_SUFFIX));
282
283            /*
284             * Transform the client attribute names into server attribute names,
285             * fullname;lang-fr ==> cn;lang-fr.
286             */
287            final List<Modification> mods = request.getModifications();
288            for (final Modification mod : mods) {
289                final Attribute a = mod.getAttribute();
290                final AttributeDescription ad = a.getAttributeDescription();
291                final AttributeType at = ad.getAttributeType();
292
293                if (at.equals(clientAttributeDescription.getAttributeType())) {
294                    final AttributeDescription serverAttrDesc =
295                            AttributeDescription.valueOf(ad.toString().replaceFirst(
296                                    CLIENT_ATTRIBUTE, SERVER_ATTRIBUTE));
297                    rewrittenRequest.addModification(new Modification(mod.getModificationType(),
298                            Attributes.renameAttribute(a, serverAttrDesc)));
299                } else {
300                    rewrittenRequest.addModification(mod);
301                }
302            }
303            for (final Control control : request.getControls()) {
304                rewrittenRequest.addControl(control);
305            }
306
307            return rewrittenRequest;
308        }
309
310        private SearchRequest rewrite(final SearchRequest request) {
311            /*
312             * Transform the client attribute names to a server attribute names,
313             * fullname;lang-fr ==> cn;lang-fr.
314             */
315            final String[] a = new String[request.getAttributes().size()];
316            int count = 0;
317            for (final String attrName : request.getAttributes()) {
318                if (attrName.toLowerCase().startsWith(CLIENT_ATTRIBUTE.toLowerCase())) {
319                    a[count] =
320                            attrName.replaceFirst(CLIENT_ATTRIBUTE, SERVER_ATTRIBUTE);
321                } else {
322                    a[count] = attrName;
323                }
324                ++count;
325            }
326
327            /*
328             * Rewrite the baseDN, and rewrite the Filter in dangerously lazy
329             * fashion. All the filter rewrite does is a string replace, so if
330             * the client attribute name appears in the value part of the AVA,
331             * this implementation will not work.
332             */
333            return Requests.newSearchRequest(DN.valueOf(request.getName().toString().replace(
334                    CLIENT_SUFFIX, SERVER_SUFFIX)), request.getScope(), Filter.valueOf(request
335                    .getFilter().toString().replace(CLIENT_ATTRIBUTE,
336                            SERVER_ATTRIBUTE)), a);
337        }
338
339        private SearchResultEntry rewrite(final SearchResultEntry entry) {
340            // Replace server attributes with client attributes.
341            final Set<Attribute> attrsToAdd = new HashSet<>();
342            final Set<AttributeDescription> attrsToRemove = new HashSet<>();
343
344            for (final Attribute a : entry.getAllAttributes(serverAttributeDescription)) {
345                final AttributeDescription ad = a.getAttributeDescription();
346                final AttributeType at = ad.getAttributeType();
347                if (at.equals(serverAttributeDescription.getAttributeType())) {
348                    final AttributeDescription clientAttrDesc =
349                            AttributeDescription.valueOf(ad.toString().replaceFirst(
350                                    SERVER_ATTRIBUTE, CLIENT_ATTRIBUTE));
351                    attrsToAdd.add(Attributes.renameAttribute(a, clientAttrDesc));
352                    attrsToRemove.add(ad);
353                }
354            }
355
356            if (!attrsToAdd.isEmpty() && !attrsToRemove.isEmpty()) {
357                for (final Attribute a : attrsToAdd) {
358                    entry.addAttribute(a);
359                }
360                for (final AttributeDescription ad : attrsToRemove) {
361                    entry.removeAttribute(ad);
362                }
363            }
364
365            // Transform the server DN suffix into a client DN suffix.
366            return entry.setName(entry.getName().toString().replace(SERVER_SUFFIX, CLIENT_SUFFIX));
367
368        }
369
370    }
371
372    /**
373     * Main method.
374     *
375     * @param args
376     *            The command line arguments: local address, local port, proxy
377     *            user DN, proxy user password, server address, server port
378     */
379    public static void main(final String[] args) {
380        if (args.length != 6) {
381            System.err.println("Usage:" + "\tlocalAddress localPort proxyDN proxyPassword "
382                    + "serverAddress serverPort");
383            System.exit(1);
384        }
385
386        final String localAddress = args[0];
387        final int localPort = Integer.parseInt(args[1]);
388        final String proxyDN = args[2];
389        final String proxyPassword = args[3];
390        final String remoteAddress = args[4];
391        final int remotePort = Integer.parseInt(args[5]);
392
393        // Create connection factories.
394        final Options factoryOptions = Options.defaultOptions()
395                                   .set(LDAPConnectionFactory.AUTHN_BIND_REQUEST,
396                                        newSimpleBindRequest(proxyDN, proxyPassword.toCharArray()));
397        final ConnectionFactory factory = newCachedConnectionPool(new LDAPConnectionFactory(remoteAddress,
398                                                                                            remotePort,
399                                                                                            factoryOptions));
400        final ConnectionFactory bindFactory = newCachedConnectionPool(new LDAPConnectionFactory(remoteAddress,
401                                                                                                remotePort));
402
403        /*
404         * Create a server connection adapter which will create a new proxy
405         * backend for each inbound client connection. This is required because
406         * we need to maintain authorization state between client requests. The
407         * proxy bound will be wrapped in a rewriter in order to transform
408         * inbound requests and their responses.
409         */
410        final RequestHandlerFactory<LDAPClientContext, RequestContext> proxyFactory =
411                new RequestHandlerFactory<LDAPClientContext, RequestContext>() {
412                    @Override
413                    public Rewriter handleAccept(final LDAPClientContext clientContext) throws LdapException {
414                        return new Rewriter(new ProxyBackend(factory, bindFactory));
415                    }
416                };
417        final ServerConnectionFactory<LDAPClientContext, Integer> connectionHandler =
418                Connections.newServerConnectionFactory(proxyFactory);
419
420        // Create listener.
421        final Options listenerOptions = Options.defaultOptions().set(CONNECT_MAX_BACKLOG, 4096);
422        LDAPListener listener = null;
423        try {
424            listener = new LDAPListener(localAddress, localPort, new ServerConnectionFactoryAdapter(
425                    listenerOptions.get(LDAP_DECODE_OPTIONS), connectionHandler), listenerOptions);
426            System.out.println("Press any key to stop the server...");
427            System.in.read();
428        } catch (final IOException e) {
429            System.out.println("Error listening on " + localAddress + ":" + localPort);
430            e.printStackTrace();
431        } finally {
432            if (listener != null) {
433                listener.close();
434            }
435        }
436    }
437
438    private RewriterProxy() {
439        // Not used.
440    }
441}