Coder Social home page Coder Social logo

scala-ssh's Introduction

scala-ssh is a Scala library providing remote shell access via SSH. It builds on SSHJ to provide the following features:

  • Remote execution of one or more shell commands
  • Access to stdin, stdout, stderr and exitcode of remote shell commands
  • Authentication via password, public key or agent
  • Host key verification via known_hosts file or explicit fingerprint
  • Convenient configuration of remote host properties via config file, resource or directly in code
  • Scala-idiomatic API
The latest scala-ssh artifacts on Maven Central https://travis-ci.org/sirthias/scala-ssh.svg?branch=master uses badges

Installation

The latest release is 0.11.1 and is built against Scala 2.12, 2.13 and 3. It is available from Maven Central. If you use SBT you can pull in the scala-ssh artifacts with:

libraryDependencies += "com.decodified" %% "scala-ssh" % "0.11.1"

SSHJ uses SLF4J for logging, so you might want to also add logback to your dependencies:

libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"

Usage

The highest-level API element provided by scala-ssh is the SSH object. You use it like this:

SSH("example.com") { client =>
  for {
    result <- client.exec("ls -a")
  } println("Result:\n" + result.stdOutAsString())
}

This establishes an SSH connection to host example.com and gives you an SshClient instance that you can use to execute one or more commands on the host. SSH.apply has a second (optional) parameter of type HostConfigProvider, which is essentially a function returning a HostConfig instance for a given hostname. A HostConfig looks like this:

case class HostConfig(
  login: SshLogin,
  hostName: String = "",
  port: Int = 22,
  connectTimeout: Option[Int] = None,
  connectionTimeout: Option[Int] = None,
  commandTimeout: Option[Int] = None,
  enableCompression: Boolean = false,
  hostKeyVerifier: HostKeyVerifier = ...,
  sshjConfig: Config = ...
)

It provides all the details required for properly establishing an SSH connection. If you don't provide an explicit HostConfigProvider the default one will be used. For every hostname you pass to the SSH.apply method this default HostConfigProvider expects a file ~/.scala-ssh/{hostname}, which contains the properties of a HostConfig in a simple config file format (see below for details). The HostResourceConfig object gives you alternative HostConfigProvider implementations that read the host config from classpath resources.

If the file ~/.scala-ssh/{hostname} (or the classpath resource {hostname}) doesn't exist scala-ssh looks for more general files (or resources) in the following way:

  1. As long as the first segment of the host name (up to the first .) contains one or more digits replace the rightmost of these with X and look for a respectively named file or resource. Repeat until no digits left.
  2. Drop all characters up to (and including) the first . from the host name and look for a respectively named file or resource.
  3. Repeat from 1. as long as there are characters left.

This means that for a host with name node42.tier1.example.com the following locations (either under ~/.scala-ssh/ or the classpath, depending on the HostConfigProvider) are tried:

  1. node42.tier1.example.com
  2. node4X.tier1.example.com
  3. nodeXX.tier1.example.com
  4. tier1.example.com
  5. tierX.example.com
  6. example.com
  7. com

Host Config File Format

A host config file is a UTF8-encoded text file containing key = value pairs, one per line. Blank lines and lines starting with a # character are ignored. This is an example file:

# simple password-based config
login-type = password
username = bob
password = 123
command-timeout = 5000
enable-compression = yes

These key are defined:

login-type
required, can be either password or keyfile
host-name
optional, if not given the name of the config file is assumed to be the hostname
port
optional, the default value is 22
username
required
password
required for login-type password, ignored otherwise
keyfile
optionally specifies the location of the user keyfile to use with login-type keyfile, if not given the default files ~/.ssh/id_ed25519, ~/.ssh/id_rsa and ~/.ssh/id_dsa are tried, if the filename starts with a + the file is searched in addition to the default locations, if the filename starts with classpath: it is interpreted as the name of a classpath resource holding the private key, ignored for login-type password
passphrase
optionally specifies the passphrase for the keyfile, if not given the keyfile is assumed to be unencrypted, ignored for login-type password
connect-timeout
optionally specifies the number of milli-seconds that a connection request has to succeed in before triggering a timeout error, default value is 'no timeout'
connection-timeout
optionally specifies the number of milli-seconds that an idle connection is held open before being closed due due to idleness, default value is 'no timeout'
command-timeout
optionally specifies the number of milli-seconds that a pending response to an issued command is waited for before triggering a timeout error, default value is 'no timeout'
enable-compression
optionally adds zlib compression to preferred compression algorithms, there is no guarantee that it will be successfully negotiatied, requires jzlib on the classpath (see 'installation' chapter) above, default is 'no'
fingerprint
optionally specifies the fingerprint of the public host key to verify in standard SSH format (e.g. 4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21), if not given the standard ~/.ssh/known_hosts or ~/.ssh/known_hosts2 files will be searched for a matching entry, fingerprint verification can be entirely disabled by setting fingerprint = any

Troubleshooting

Java Cryptography Extension Policy Files

To use this library it might be necessary that you install the Java Cryptography Extension Policy Files from the JDK additional downloads section. Make sure they are installed, especially if you encounter exceptions like this:

net.schmizz.sshj.common.SSHRuntimeException: null
at net.schmizz.sshj.common.Buffer.readPublicKey(Buffer.java:432) ~[sshj-0.12.0.jar:na] at net.schmizz.sshj.transport.kex.AbstractDHG.next(AbstractDHG.java:108) ~[sshj-0.12.0.jar:na] at net.schmizz.sshj.transport.KeyExchanger.handle(KeyExchanger.java:352) ~[sshj-0.12.0.jar:na] at net.schmizz.sshj.transport.TransportImpl.handle(TransportImpl.java:487) ~[sshj-0.12.0.jar:na] at net.schmizz.sshj.transport.Decoder.decode(Decoder.java:107) ~[sshj-0.12.0.jar:na] at net.schmizz.sshj.transport.Decoder.received(Decoder.java:175) ~[sshj-0.12.0.jar:na] at net.schmizz.sshj.transport.Reader.run(Reader.java:61) ~[sshj-0.12.0.jar:na]
Caused by: java.security.GeneralSecurityException: java.security.spec.InvalidKeySpecException: key spec not recognised
at net.schmizz.sshj.common.KeyType$3.readPubKeyFromBuffer(KeyType.java:146) ~[sshj-0.12.0.jar:na] at net.schmizz.sshj.common.Buffer.readPublicKey(Buffer.java:430) ~[sshj-0.12.0.jar:na] ... 6 common frames omitted
Caused by: java.security.spec.InvalidKeySpecException: key spec not recognised
at org.bouncycastle.jcajce.provider.asymmetric.util.BaseKeyFactorySpi.engineGeneratePublic(Unknown Source) ~[bcprov-jdk15on-1.52.jar:1.52.0] at org.bouncycastle.jcajce.provider.asymmetric.ec.KeyFactorySpi.engineGeneratePublic(Unknown Source) ~[bcprov-jdk15on-1.52.jar:1.52.0] at java.security.KeyFactory.generatePublic(KeyFactory.java:334) ~[na:1.8.0_05] at net.schmizz.sshj.common.KeyType$3.readPubKeyFromBuffer(KeyType.java:144) ~[sshj-0.12.0.jar:na] ... 7 common frames omitted

Running tests locally

Some of the tests needs a running SSH daemon and expects particular structure of ~/.scala-ssh. In case you want to run those tests locally there is prepared structure to run dockerized sbt and sshd. Running dockerized structure should work just by itself:

` scripts/local/dockerized-sbt.sh # and then in sbt: test `

Having sbt as a separate step make it easier to run changes iteratively in dockerized sbt.

License

scala-ssh is licensed under APL 2.0.

Patch Policy

Feedback and contributions to the project, no matter what kind, are always very welcome. However, patches can only be accepted from their original author. Along with any patches, please state that the patch is your original work and that you license the work to the scala-ssh project under the project’s open source license.

scala-ssh's People

Contributors

bplommer avatar danosipov avatar hairyfotr avatar johansja avatar jzt avatar laughedelic avatar matsluni avatar note avatar philcali avatar pjfanning avatar razie avatar readmecritic avatar sethtisue avatar sirthias avatar stefri avatar topu avatar will-sargent-eero avatar xuwei-k avatar zaneli avatar

Stargazers

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

Watchers

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

scala-ssh's Issues

CommandResult's lazy vals get null value when remote command throwing a "SEGV" as exitSignal

the lazy vals(exitCode,exitErrorMessage, i/o streams etc.) gives null instead of Option for the channel as result of a remote command causes "Segmentation fault" or "Floating point exception" where the exception stored in exitSignal
it's easy to reproduce, just compile following c code and run it's binary with exec and then access the exitCode, it throws java.lang.NullPointerException

int main(int argc, char *argv[]){
int x;
x = 100/0;
}

Suppress login banner

Is there any way to suppress banner on login?

Something like -o LogLevel=error or -q option for OpenSSH Client.

I'm trying to parse command output from some remote hosts and sometimes there is unexpected banners.

For example, I want to check OS version on remote host by lsb_release -d command. And I no need to have banners in output.

import com.decodified.scalassh.SSH
import com.decodified.scalassh.HostConfig

val hostname: String = ???
val hostConfig: HostConfig = ???

SSH(hostname, hostConfig) { client =>
   client.exec("lsb_release -d").map { res: CommandResult =>
      val errStr = res.stdErrAsString().trim
      val outStr = res.stdOutAsString().trim   // I have banner in outStr, but I don't want it. Expected only output of "lsb_release"
      ...
   }
}

client.download not working

Hi, I'm trying to use the download api but it does not work.

SSH(hostname, Sshconfig){ client : SshClient =>

    val dstPath = s"somepath" + Path(remoteFileName).name
    client.download(remoteFileName, dstPath) match {
      case Success(value) => value
      case Failure(exception) =>  throw new IllegalArgumentException(exception)
    }
  }

I get:
19/09/26 10:20:10 INFO SshClient: Authenticating to host:22 using user ...
19/09/26 10:20:10 INFO SshClient: Closing connection to host:22 ...
19/09/26 10:20:10 INFO TransportImpl: Disconnected - BY_APPLICATION

at the end no file is at the destination.
If I use exec command it works.

Please release 0.7.1

The unreleased 0.7.1 depends on the new name of sshj (com.hierynomus instead of net.schmizz), the first release (0.11) of which fixes:

hierynomus/sshj#54

The 0.7.0 depends on net.schmizz.sshj and there is no way to override this to use a later version with the fix since the name is different.

So, please either make 0.7.1 or a snapshot available on Maven. Thanks!

"SSHException: No provider available for PKCS8 key file" running basic example

Hi, I'm getting the above exception from sshj when trying to use key based authentication (full exception printed below) using a slightly modified version of the example in the Readme. I did some initial research and found in a source comment in sshj that this typically indicates that bouncy castle is not on the classpath. I've currently got this deployed using the one-jar sbt plugin and have verified that it does in fact include the bouncy castle library and have even added it manually when executing the jar using the -cp flag. I've also double checked to make sure that the key file is correct.

Code sample I'm running:

import com.decodified.scalassh._
import org.bouncycastle.jce.provider._

object SshClient extends App {

    SSH("myhost.net") { client =>
        client.exec("ls -a") match { 
            case Right(result) => println("Result: " + result.stdOutAsString())
            case Left(error) => println("Error: " + error)
        }
    }

}

With the following config (~/.scala-ssh/myhost.net):

login-type = keyfile
username = myhost-user
keyfile = ~/.ssh/ec2/myhostkey

SBT dependencies:

libraryDependencies ++= Seq (
    "com.decodified" % "scala-ssh" % "0.6.2",
    "org.bouncycastle" % "bcprov-jdk16" % "1.46",
    "com.jcraft" % "jzlib" % "1.1.1",
    "ch.qos.logback" % "logback-classic" % "1.0.3"
)

Running this application results in the following output:

14:52:51.240 [main] INFO  com.decodified.scalassh.SshClient - Authenticating to myhost.net:22 using PublicKeyLogin(sparcloud-boss,None,List(/Users/bgilbert/.ssh/ec2/myhostkey)) ...
Error: Could not authenticate (with keyfile) to myhost.net:22 due to net.schmizz.sshj.common.SSHException: No provider available for PKCS8 key file
14:52:51.245 [main] INFO  com.decodified.scalassh.SshClient - Closing connection to myhost.net:22 ...

Any ideas as to what the problem could be? Am I maybe missing something basic?

Thanks very much in advance!

the only example does not work

$ scalac -classpath lib/scala-ssh_2.9.1-0.5.0.jar ScalaSsh.scala 
scalassh_t.scala:6: error: missing arguments for method stdOutAsString in class CommandResult;
follow this method with `_' if you want to treat it as a partially applied function
      println("Result:\n" + result.stdOutAsString)
                                   ^
one error found

$ cat ScalaSsh.scala 
import com.decodified.scalassh._

object ScalaSsh extends App {
  SSH("example.com") { client =>
    client.exec("ls -a").right.map { result =>
      println("Result:\n" + result.stdOutAsString)
    }
  }
}

$

executing sudo commands

Any tips on trying to run sudo commands via exec?

I just get back sudo: sorry, you must have a tty to run sudo

Whats the equivalent way to get this library to connect like w/ Force pseudo-tty allocation mode such as 'ssh -t'

Missing tests for SFTP client

I ran into some issues with the SFTP client (getting Error: SFTP client failed despite successful SFTP interaction), and wanted to validate this against a reference implementation - but only SCP test cases are present in the ClientSpec.

Test cases for SFTP will allow me to validate the reference behavior and tell me how to correctly use the client.

Long running command does not come back and does not stream

I tried this

SSH("192.168.1.5") {
  client =>
    val result = client.exec("find .").right.get
    val stream = new SequenceInputStream(result.stdOutStream, result.stdErrStream)
    val buff = new BufferedSource(stream)
    buff.getLines().foreach { println }
}

And I do not get what I expect (the results of the find, and it works if I do something smaller, like "ls ~/"). Instead I get a bunch of the following in the log:

21:55:25.621 [reader] DEBUG n.s.s.c.channel.Window$Remote - Increasing by 2097152 up to 2097152
21:55:25.621 [reader] DEBUG net.schmizz.concurrent.Promise - Setting <<chan#0 / chanreq for exec>> to SOME
21:55:25.622 [ScalaTest-main-running-SSHTest] DEBUG net.schmizz.concurrent.Promise - Awaiting <<chan#0 / close>>
21:55:25.633 [reader] DEBUG n.s.s.c.channel.Window$Local - Consuming by 4096 down to 2093056
21:55:25.636 [reader] DEBUG n.s.s.c.channel.Window$Local - Consuming by 4096 down to 2088960
21:55:25.637 [reader] DEBUG n.s.s.c.channel.Window$Local - Consuming by 4096 down to 2084864
21:55:25.640 [reader] DEBUG n.s.s.c.channel.Window$Local - Consuming by 4096 down to 2080768
21:55:25.642 [reader] DEBUG n.s.s.c.channel.Window$Local - Consuming by 4096 down to 2076672
21:55:25.644 [reader] DEBUG n.s.s.c.channel.Window$Local - Consuming by 4096 down to 2072576
21:55:25.645 [reader] DEBUG n.s.s.c.channel.Window$Local - Consuming by 4096 down to 2068480
21:55:25.647 [reader] DEBUG n.s.s.c.channel.Window$Local - Consuming by 4096 down to 2064384

And so on...

It's almost like reading from the streams is not really reading from them.

Detaching a process

I have not found any way to run a detached process over SSH. I am trying to use the library to run a monitoring service that may need to ssh into a remote machine and restart a long-lived process that may be hung.

Is there some way to run a forked process in SSH, so that the monitoring application does not remain stuck?

Programmatic configuration example using embedded ssh key information

I am happily using scala-ssh with a configuration file, as per the docs. This means that the following files are required:

~/.scala-ssh/domain.tld
~/.scala-ssh/known_hosts
~/.ssh/id_{r|d}sa

I would like to dispense with all of these configuration files and configure scala-ssh programmatically. Could you elaborate on how to do this? The following clue was not enough to get me going:

sshjConfig: Config = ...

My goal is to be able to create a single executable jar with my app, including any ssh key that might be required. BTW, I would be happy to put the key file in src/main/resources but the loader did not find it there. It would also be fine to hard-code the contents of the key as a text string somehow. Please don't lecture me about security; the resulting portable executable would be physically guarded with care :)

Thanks,

Mike

how to chain logins?

i need to log in to one ssh, and from it i need to log to another. how do i pass the username and password for the seond login? when i pass "; " after the second login command, it just grinds and time out eventually with nothing.

Stuck at "Closing connection" state after upload file

SSH("host", h) { client =>
        client.upload(file.getAbsolutePath, file.getName)
}

Here's output log.

11:09:44.452 [main] INFO  net.schmizz.sshj.common.SecurityUtils - BouncyCastle registration succeeded
11:09:44.490 [main] WARN  net.schmizz.sshj.DefaultConfig - Disabling high-strength ciphers: cipher strengths apparently limited by JCE policy
11:09:44.509 [main] INFO  com.decodified.scalassh.package$RichSshClient - Connecting to myhost.com:22 ...
11:09:44.557 [main] INFO  net.schmizz.sshj.transport.TransportImpl - Client identity string: SSH-2.0-SSHJ_0_9_2
11:09:44.606 [main] INFO  net.schmizz.sshj.transport.TransportImpl - Server identity string: SSH-2.0-OpenSSH_6.6.1p1 Ubuntu-2ubuntu2
11:09:44.826 [main] INFO  com.decodified.scalassh.package$RichSshClient - Authenticating to myhost.com:22 using jerry.cm ...
11:09:45.326 [main] INFO  net.schmizz.sshj.connection.channel.direct.SessionChannel - Will request to exec `scp -t -r -p 'dummy.txt'`
11:09:45.473 [main] INFO  net.schmizz.sshj.common.StreamCopier - 0.0048828125 KiB transferred in 0.0 seconds (Infinity KiB/s)
11:09:45.653 [main] INFO  com.decodified.scalassh.SshClient - Closing connection to myhost.com:22 ...

The application doesn't close. It will be closed automatically if I just use client.exec. Should I close my application manually?

scala-ssh execute sometimes hangs even though the remote command succeeds

I've got two virtually identical scripts that I'm running on a remote server, both of which succeed and complete on the remote server, but one of which hangs (that is, its scala-ssh session only terminates after command timeout). I'm executing them via this scala code:

def remoteRequest(req: SlackRequest): Either[Throwable, SlackResponse] = for {
    server <- findServer(req)
    studyOpt <- findStudy(req, server.id)
    commandInput <- getCommandInput(req)
    commandResult <- {
      val domain = studyOpt.map(_.domain).getOrElse("")
      SSH(server.host, HostResourceConfig()) {
        client =>
          for {
            result <- {
              val command = Command(s"bash -s -- $domain ${req.args.mkString(" ")}", commandInput)
              client.exec(command)
            }
          } yield result
      }.toEither
    }
    response <- processRemoteResult(req, commandResult)
  } yield response

Here's the host resource config:

# scala-ssh host config file
login-type = password
username = exxxxxxxxxxr
password = blahblahblah
fingerprint = any
command-timeout = 30000

I've attached two scripts, along with their logs. The lock_user.sh.txt script runs successfully on the remote server and scala-ssh returns normally after the command finishes. The unlock_user.sh.txt script also runs successfully on the remote server but scala-ssh hangs and only terminates when the command timeout is triggered. I've verified the unlock script succeeds, both by running it manually via the command line and because the db update succeeds.

lock_user.sh.txt
lock-ok.log
unlock_user.sh.txt
unlock-fail.log

Can anyone explain what's going on here? I can't tell what's different between these two cases. I've also changed the order of their execution, to no avail (that is, same result...lock always succeeds, unlock always hangs until timeout). I've got other scripts, some of which work and some of which hang even though all succeed in executing and terminating normally on the remote server, so I don't think this has anything to do with the scripts themselves.

Thanks for any insights.

None of the configured keyfiles exists: ~/.ssh/id_rsa, ~/.ssh/id_dsa

Default key locations specified in SshLogin.scala are ~/.ssh/id_rsa, ~/.ssh/id_dsa but the tilde is a shell specific expansion and not applied, resulting in the following exception when using the default key locations:

java.lang.RuntimeException: None of the configured keyfiles exists: ~/.ssh/id_rsa, ~/.ssh/id_dsa 

Replacing the tilde with System.getProperty("user.home") would be better.

0.10.0 & scala 2.13.1 is not working...

[info] SecureCopyTest:
[info] SecureCopyTest *** ABORTED ***
[info] java.lang.NoSuchMethodError: net.schmizz.sshj.xfer.LoggingTransferListener: method ()V not found
[info] at com.decodified.scalassh.ScpTransferable.defaultListener(ScpTransferable.scala:52)
[info] at com.decodified.scalassh.ScpTransferable.upload$default$3(ScpTransferable.scala:46)
[info] at net.uptel.sim.scp.services.ScpService.$anonfun$uploadFile$1(ScpService.scala:34)
[info] at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
[info] at cats.effect.internals.IORunLoop$.step(IORunLoop.scala:185)
[info] at cats.effect.IO.unsafeRunTimed(IO.scala:320)
[info] at cats.effect.IO.unsafeRunSync(IO.scala:239)
[info] at net.uptel.sim.scp.services.ScpService.$anonfun$uploadFiles$1(ScpService.scala:51)
[info] at cats.effect.internals.IORunLoop$.liftedTree1$1(IORunLoop.scala:95)
[info] at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:95)
[info] ...

Proxy support

How can I connect to a host by means of an HTTP proxy?

Accessing stdout, stderr etc.

Hi,

is there any example in streaming the resulting output instead of just getting back the result in one go?

It would be great if the readme or the wiki could be updated with a few use cases.

Thanks,

Christian

Commands hang forever when reading stdin

Hello, if I execute "cat > /tmp/foo.txt" with the command input "a a a a". The command hang forever and never return.

How do you signal the EOT/EOF aka Control-D?

PublicKeyLogin can't work


    val sshLogin = PublicKeyLogin("sjk")
    val conf = new HostConfig(sshLogin, "127.0.0.1")
    val client = new SshClient(conf)

    sshClientToRichClient(client).fileTransfer(f => {
      println("***************************************")
      f.upload(srcFile, dstFile)
    })

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.