Giter Site home page Giter Site logo

shibboleth-multi-context-broker's People

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

shibboleth-multi-context-broker's Issues

IDMS returning a context not handled by the MCB will throw a NPE

When the IDMS returns a context that is not configured in the MCB, it throws a NPE trying to resolve the context:

23:23:37.708 - TRACE [edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet:562] - Looking up method for context name = [http://id.i
ncommon.org/assurance/bronze]
23:23:37.719 - ERROR [edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet:346] - Exception calling submodule.
java.lang.NullPointerException: null
at edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet.showMethods(MCBLoginServlet.java:563) [multi-context-broker-1.1.4.j
ar:na]
at edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet.performAuthentication(MCBLoginServlet.java:266) [multi-context-brok
er-1.1.4.jar:na]
at edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet.service(MCBLoginServlet.java:158) [multi-context-broker-1.1.4.jar:n
a]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:717) [tomcat6-servlet-2.5-api-6.0.24.jar:na]

No prompt for stronger authentication if initial context "good enough" for first SP

Subject: Re: [shib-assure] 1.2.2 testing -- good news and, possibly bad news?
Date: Fri, 13 Feb 2015 08:37:04 -0800
From: David Walker [email protected]
To: [email protected]

Keith,

Chiming in... I also remember discussion of this issue. I think the general principle is that the configured initial authentication context should be handled separate from the incoming request from the SP, making behavior for the first SP and the second SP the same (the second SP exhibiting the correct behavior here). I looked at our Github issue list, and I don't see this one there, however, so I'll add it.

...

David

On 02/12/2015 10:26 AM, Wessel, Keith wrote:
...
However, and I don’t know if this has been tackled yet, we still have the problem with the initial context being “good enough” and the MCB stopping there. To reiterate this issue:

Configure the IDP to have Password and Duo. Configure password as the only initial context since one can’t Duo auth until we know their principal.

With no session, go to an SP that accepts DUO then Password, in that order.

MCB prompts for password, user successfully authenticates.

Rather than giving the option of stepping up to Duo or even requiring it, user gets sent back to SP with Password.

If the SP described above is the 2nd SP the user visits in the session and the user already has satisfied Password from their 1st SP authentication, the MCB will allow for stepping up to Duo or possibly require it depending on configuration. It’s a different user experience, and it provides for functionality (stepping up) different than the 1st scenario above.

I recall agreeing that the scenario should be the same whether the session already existed or was newly created. It’s possible this was already fixed and I’m missing a configuration item. Can someone chime in here and help me out?

Keith

404 when JSP login page incorrectly specified

When using the JAAS login submodule with JSP pages, the page must be qualified with a leading slash. When it's not, the container cannot find the page and you end up with a 404 response.

UIInfo null language nullpointerexception

When our metadata is read MCB throws the following exception due to a missing null pointer check.

16:28:03.842 - ERROR [edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet:602] - Exception calling submodule.
java.lang.NullPointerException: null
at edu.internet2.middleware.assurance.mcb.authn.provider.ui.IDPUIHandler.getServiceLogo(IDPUIHandler.java:190) ~[IDPUIHandler.class:na]
at edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet.doVelocity(MCBLoginServlet.java:692) [MCBLoginServlet.class:na]
at edu.uchicago.identity.mcb.authn.provider.duo.DuoLoginSubmodule.displayLogin(DuoLoginSubmodule.java:102) ~[DuoLoginSubmodule.class:2.1.0]
at edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet.showMethods(MCBLoginServlet.java:594) [MCBLoginServlet.class:na]
at edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet.service(MCBLoginServlet.java:138) [MCBLoginServlet.class:na]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:725) [servlet-api.jar:na]

Example UIInfo
<md:EntityDescriptor entityID="http://www.workday.com/fau3">
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
md:Extensionsmdui:UIInfo<mdui:DisplayName xml:lang="en">FAU Workday - fau3 - P3/mdui:DisplayName<mdui:Description xml:lang="en">Workday is designed to work the way we work today and enables nimble processes for the data driven organization. Log in to manage your, information, request time off, access pay stubs, purchases, and other related tasks. Users without an FAUNet ID may use the following link to login <a href="https://impl.workday.com/fau3/login.flex?redirect=n&quot;&quot;&gt;directly to Workday</a>. /mdui:Description<mdui:InformationURL xml:lang="en">http://www.workday.com/mdui:InformationURL<mdui:Logo width="159" height="62">https://images.workday.com/shared/wd-logo.gif/mdui:Logo/mdui:UIInfo/md:Extensions
md:KeyDescriptor
ds:KeyInfo
ds:X509Data

maxFailures value of -1 is not honored

If one sets in multi-context-broker.xml

-1

then instead of an "unlimited number of login failures" as indicated by the documentation the IdP will send the user back to the SP with a SAML error after the first login failure.

I believe this is because of this if() statement in MCBLoginServlet.java:

if (principal.getFailedCount() >= mcbConfig.getMaxFailures())

With maxFailures set to -1 that test will always be true after the first login failure.

Something like

if ((principal.getFailedCount() >= mcbConfig.getMaxFailures()) && (mcbConfig.getMaxFailures() != -1))

appears to fix the issue.

Step-up Authn broken

  1. user initially authN at password context
  2. return to SP
  3. user comes back & SP requests Duo
  4. user doesn't get prompted for duo, MCB returns to SP.

See Steven Carmody's thread on shib-assure.

SSO failure when login page refreshed

-------- Forwarded Message --------
Subject: MCB bug on failed logins?
Date: Tue, 28 Apr 2015 18:45:54 +0000
From: Ho, PeiQuan [email protected]
Reply-To: Shib Users [email protected]
To: Shib Users [email protected]

Hi,

            I’m running some testing on the IDP with MCB.  I noticed that when I’m directed to the IDP login page and I simply don’t login and just refresh the page, the MCB regards this as an SSO session and tries to query LDAP  using a null user.  This fails of course.  But it also increments the failed login count.    The odd thing is in the logs, it shows that there was actually no previous session found, but the MCB still thinks it is SSO.  Is this the place to be reporting possible MCB bugs?

BTW, here’s what I see in the logs when I refresh the login page.

14:33:19.675 - INFO [Shibboleth-Access:73] - 20150428T183319Z|130.64.204.178|:443|/profile/SAML2/Redirect/SSO|
14:33:19.744 - DEBUG [edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginHandler:92] - MCBConfiguration bean = [edu.internet2.middleware.assurance.mcb.authn.provider.MCBConfiguration@67c04226]
14:33:19.745 - DEBUG [edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginHandler:106] - Relying party = []
14:33:19.745 - TRACE [edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginHandler:248] - No session found. Previous Session Support setting = [true]
14:33:19.745 - DEBUG [edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginHandler:280] - Redirecting to https:///idp/Authn/MCB
14:33:19.805 - TRACE [edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet:119] - =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
14:33:19.805 - DEBUG [edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet:120] - Request received from [130.64.204.178]
14:33:19.805 - DEBUG [edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet:128] - principal = [{MCBUsernamePrincipal}[principal]]
14:33:19.806 - DEBUG [edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet:137] - Relying party = []
14:33:19.806 - DEBUG [edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet:170] - Performing authentication for request.
14:33:19.806 - DEBUG [edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet:207] - Found 2nd leg of authentication, performing authentication.
14:33:19.807 - DEBUG [edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet:806] - Getting requested contexts for relying party = []
14:33:19.807 - DEBUG [edu.internet2.middleware.assurance.mcb.authn.provider.JAASLoginSubmodule:244] - Attempting to authenticate user null
14:33:19.819 - TRACE [edu.vt.middleware.ldap.jaas.LdapLoginModule:144] - Begin initialize

.

.

.

Thanks,

-PQ

Direct access to login servlet

I'm using version 1.2.1 of the MCB and if somebody directly accesses the login servlet, MCBLoginServlet.java, it raises an exception and code 500 is returned by Apache httpd. In IdP without MCB there is a possibility to check for a direct access and return a proper message without returning 500 status.
<%
StorageService storageService = HttpServletHelper.getStorageService(application);
LoginContext loginContext = HttpServletHelper.getLoginContext(storageService, application,request);
if (loginContext == null) {
%>

To fix this issue I added "try" block, line 130, in the "service" method of MCBLoginServlet class and that solved the problem.

try { application = this.getServletContext();
loginContext = (LoginContext)HttpServletHelper.getLoginContext(HttpServletHelper.getStorageService(application), application, request);
entityDescriptor = HttpServletHelper.getRelyingPartyMetadata(loginContext.getRelyingPartyId(),
HttpServletHelper.getRelyingPartyConfigurationManager(application));
entityID = entityDescriptor.getEntityID();
log.debug("Relying party = [{}]", entityID);
} catch (Exception e) {
log.error("Can't find entityID of the SP");
}

Is the original behavior intentional or is it an overlooked error?

Passwords are handled differently in the MCB than in the default IdP code

We have had reports from several users who are unable to login to our Shibboleth server after implementing the MCB plugin to the IdP. After severals days of troubleshooting, I lucked into the solution when one of our help desk staff (against policy!) added the user's password to their support ticket. The password has a space at the end, which is allowed by our password policy (it's actually a passphrase policy, which allows spaces). I replicated the conditions for this user by taking a test account and attempting to login with a passphrase that has no whitespace at either end and again after changing to a passphrase with a space character at the end. The second test failed, which led me to look at the source for both the IdP and the MCB.

The default behavior of the IdP is to pass the user's password as-is to the authentication source, but the MCB runs it through the DatatypeHelper.safeTrimOrNullString() method first, which strips whitespace off of the beginning or end of the password.

Luckily we are in the middle of the Summer, but as the Fall starts we will probably run into many more users with this issue.

Please remove the call to safeTrimOrNullString() so the MCB follows the identical behavior of the "normal" IdP code.

$UILogo displays an openSAML reference instead of the URL

Tested on 1.1.3, 1.1.4, and 1.2.3.

Using the $UILogo variable in the jaaslogin.vm template causes the following output in the generated HTML:

<img src="org.opensaml.samlext.saml2mdui.impl.LogoImpl@1d0a2dab" />

The jaaslogin.vm template looks like this:

<div class="column zero">
     #if ($UILogo)
       <img src="$UILogo" />
     #end

     <p><strong>
       Login to
       #if ($UIName)
         $UIName
       #else
         $UIEntityID
       #end
       </strong>
     </p>

     #if ($UIDescription)
     <p> $UIDescription </p>
     #end

     #if ($UIPrivacyURL)
       <p><em><a href="$UIPrivacyURL" target="_blank">Privacy Notice</a></em></p>
     #end
     #if ($UIInfoURL)
       <p><em><a href="$UIInfoURL" target="_blank">More Information</a></em></p>
     #end

     <br/>
   </div>

Add support for defaultAuthenticationMethod

Shibboleth relying party configuration allows a default authentication method to be specified per relying party. Make use of the default value when a RP does not send a method (context) with their request.

Still outstanding is whether to use the default method when the RP does send a value. If so, does it get added to the beginning or the end of the list?

$uiName should pull from more than the mdui element

The idpui:displayName tag in the IDP's taglib support has more sources for an SP's display name than just the mdui element. This is useful when metadata contains a serviceName tag or you want to populate the element with the SP's hostname if no display name is set. Specifics here: https://wiki.shibboleth.net/confluence/display/SHIB2/IdPAuthUserPassLoginPage#IdPAuthUserPassLoginPage-ServiceProviderName

This would be a useful addition to the $uiName variable made available in Velocity.

getInboundMessageTransport not available within the requestContext when using a ScriptedAttribute

The requestContext available to the ScriptedAttribute javascript normally contains the method "getInboundMessageTransport()", which can be used to retrieve the peer's IP address. This method does not appear to be mapped to the requestContext during attribute resolution when attribute resolution is handled by the MCB. The call to getInboundMessageTransport() always returns null.

Example code:

logger.debug("Inbound transport peer address: {}", requestContext.getInboundMessageTransport().getPeerAddress());

When not using the MCB, the expected result is:

2015-05-27 13:45:58.939 - DEBUG [edu.internet2.middleware.shibboleth.resolver.Script.validAuthContexts:-1] - Inbound transport peer address: 10.9.2.5

When using the MCB, the actual result is:

2015-05-27 12:49:43.800 - ERROR [edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.attributeDefinition.ScriptedAttributeDefinition:136] - 1D5EB29135FDC4F092D53E8C16B6CFDA - ScriptletAttributeDefinition validAuthContexts unable to execute script javax.script.ScriptException: sun.org.mozilla.javascript.internal.EcmaError: TypeError: Cannot call method "getPeerAddress" of null (<Unknown Source>#117) in <Unknown Source> at line number 117

Login.jsp not working

We configured the MCB latest distribution (1.2.5) into a Shibboleth IdP 2.4.4, along with the Duo plugin for it. We configured the initial context login page to be a JSP (login.jsp). Then we set up a test to force Duo authentication. The MCB gets control, sends the user to the login page correctly for initial authentication, but is then just giving control back to the IdP and not running the 2nd leg of authentication with Duo. With the exact same config and test, just changing the login page config to be the sample Velocity template for such, our test works as expected, going on to Duo after the login is completed. So a bug has been introduced at some point in the last few MCB releases that has broken support for a JSP-based login page. (Separately shared an idp-process log with Paul Hethmon illustrating the failure.)

NPE in JAAS if login session incomplete

Might just be a special case of issue #3.

To reproduce:

  1. Visit an SP (symplicity, google, local wiki)
  2. (Get redirected to https://login.carleton.edu/idp/Authn/MCB)
  3. Do not submit form. Repeat steps 1 and 2.

Expected results:

Blank login form, no error.

Actual results:

Login form is shown, and actually works, but is preceded by ugly backtrace.

Problem appears to go away if I remove

#if ($loginFailed != "")
    <p>$loginFailed</p>
#end

from templates/jaaslogin.vm.

java.lang.NullPointerException at edu.internet2.middleware.assurance.mcb.authn.provider.JAASLoginSubmodule$SimpleCallbackHandler.handle(JAASLoginSubmodule.java:313) at javax.security.auth.login.LoginContext$SecureCallbackHandler$1.run(LoginContext.java:947) at javax.security.auth.login.LoginContext$SecureCallbackHandler$1.run(LoginContext.java:944) at java.security.AccessController.doPrivileged(Native Method) at javax.security.auth.login.LoginContext$SecureCallbackHandler.handle(LoginContext.java:943) at edu.vt.middleware.ldap.jaas.AbstractLoginModule.getCredentials(AbstractLoginModule.java:429) at edu.vt.middleware.ldap.jaas.LdapLoginModule.login(LdapLoginModule.java:98) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at javax.security.auth.login.LoginContext.invoke(LoginContext.java:762) at javax.security.auth.login.LoginContext.access$000(LoginContext.java:203) at javax.security.auth.login.LoginContext$4.run(LoginContext.java:690) at javax.security.auth.login.LoginContext$4.run(LoginContext.java:688) at java.security.AccessController.doPrivileged(Native Method) at javax.security.auth.login.LoginContext.invokePriv(LoginContext.java:687) at javax.security.auth.login.LoginContext.login(LoginContext.java:595) at edu.internet2.middleware.assurance.mcb.authn.provider.JAASLoginSubmodule.authenticateUser(JAASLoginSubmodule.java:244) at edu.internet2.middleware.assurance.mcb.authn.provider.JAASLoginSubmodule.processLogin(JAASLoginSubmodule.java:168) at edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet.performAuthentication(MCBLoginServlet.java:209) at edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet.service(MCBLoginServlet.java:158) at javax.servlet.http.HttpServlet.service(HttpServlet.java:717) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) at edu.internet2.middleware.shibboleth.idp.util.NoCacheFilter.doFilter(NoCacheFilter.java:50) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) at edu.internet2.middleware.shibboleth.idp.session.IdPSessionFilter.doFilter(IdPSessionFilter.java:87) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) at edu.internet2.middleware.shibboleth.common.log.SLF4JMDCCleanupFilter.doFilter(SLF4JMDCCleanupFilter.java:52) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:127) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:298) at org.apache.coyote.ajp.AjpAprProcessor.process(AjpAprProcessor.java:429) at org.apache.coyote.ajp.AjpAprProtocol$AjpConnectionHandler.process(AjpAprProtocol.java:384) at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1556) at java.lang.Thread.run(Thread.java:745)

Wrong context reported when SP requests multiple allowable contexts

If a user has authenticated previously at a context level of "bronze", then the new SP requests either "silver" or "bronze" (in that specific order), the MCB will return "silver" even though the user has not completed that level of authentication.

The change/fix is to force upgraded authentication in this scenario. A new option "showSatisfiedContexts" was added to control the behavior of whether already satisfied contexts will be shown in the selection list. The default behavior is false, to not show already satisfied contexts/methods in the list. Only the other levels would be shown.

Alleged NPE associated with Microsoft OneNote

I will try to reproduce locally. A remote user claims that she always gets an error hitting the IdP if and only if she uses IE11 with the Microsoft OneNote 2013 browser plug-in enabled.

Apr 29, 2014 5:54:49 PM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet MCBLoginServlet threw exception
java.lang.NullPointerException
at edu.internet2.middleware.assurance.mcb.authn.provider.MCBLoginServlet.service(MCBLoginServlet.java:127)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at edu.internet2.middleware.shibboleth.idp.util.NoCacheFilter.doFilter(NoCacheFilter.java:50)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at edu.internet2.middleware.shibboleth.idp.session.IdPSessionFilter.doFilter(IdPSessionFilter.java:87)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at edu.internet2.middleware.shibboleth.common.log.SLF4JMDCCleanupFilter.doFilter(SLF4JMDCCleanupFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:127)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:298)
at org.apache.coyote.ajp.AjpAprProcessor.process(AjpAprProcessor.java:429)
at org.apache.coyote.ajp.AjpAprProtocol$AjpConnectionHandler.process(AjpAprProtocol.java:384)
at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1556)
at java.lang.Thread.run(Thread.java:745)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.