├── .gitignore ├── LICENSE ├── README.md ├── app ├── app.go ├── client_context.go ├── hterm_preferences.go ├── http_logger.go └── resource.go ├── example └── index.json ├── go.mod ├── go.sum ├── help.go ├── main.go └── resources ├── favicon.png ├── gotty.js └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | multi-gotty 2 | bindata 3 | builds 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Iwasaki Yudai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | ============================================================================= 24 | 25 | This software is built with following third party open source software. 26 | 27 | 28 | # golng/go 29 | 30 | Copyright (c) 2009 The Go Authors. All rights reserved. 31 | 32 | Redistribution and use in source and binary forms, with or without 33 | modification, are permitted provided that the following conditions are 34 | met: 35 | 36 | * Redistributions of source code must retain the above copyright 37 | notice, this list of conditions and the following disclaimer. 38 | * Redistributions in binary form must reproduce the above 39 | copyright notice, this list of conditions and the following disclaimer 40 | in the documentation and/or other materials provided with the 41 | distribution. 42 | * Neither the name of Google Inc. nor the names of its 43 | contributors may be used to endorse or promote products derived from 44 | this software without specific prior written permission. 45 | 46 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 47 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 48 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 49 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 50 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 51 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 52 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 53 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 54 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 55 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 56 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 57 | 58 | 59 | # libapps/hterm 60 | 61 | Copyright (c) 2014, Google Inc. All rights reserved. 62 | 63 | Redistribution and use in source and binary forms, with or without modification, 64 | are permitted provided that the following conditions are met: 65 | 66 | * Redistributions of source code must retain the above copyright notice, this 67 | list of conditions and the following disclaimer. 68 | 69 | * Redistributions in binary form must reproduce the above copyright notice, this 70 | list of conditions and the following disclaimer in the documentation and/or 71 | other materials provided with the distribution. 72 | 73 | * Neither the name of Google Inc. nor the names of its contributors may be used 74 | to endorse or promote products derived from this software without specific 75 | prior written permission. 76 | 77 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 78 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 79 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 80 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 81 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 82 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 83 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 84 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 85 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 86 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 87 | 88 | 89 | # odegangsta/cli 90 | 91 | Copyright (C) 2013 Jeremy Saenz 92 | All Rights Reserved. 93 | 94 | MIT LICENSE 95 | 96 | Permission is hereby granted, free of charge, to any person obtaining a copy of 97 | this software and associated documentation files (the "Software"), to deal in 98 | the Software without restriction, including without limitation the rights to 99 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 100 | the Software, and to permit persons to whom the Software is furnished to do so, 101 | subject to the following conditions: 102 | 103 | The above copyright notice and this permission notice shall be included in all 104 | copies or substantial portions of the Software. 105 | 106 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 107 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 108 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 109 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 110 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 111 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 112 | 113 | 114 | # jteeuwen/go-bindata 115 | 116 | This work is subject to the CC0 1.0 Universal (CC0 1.0) Public Domain Dedication 117 | license. Its contents can be found at: 118 | http://creativecommons.org/publicdomain/zero/1.0 119 | 120 | # elazarl/go-bindata-assetfs 121 | 122 | Copyright (c) 2014, Elazar Leibovich 123 | All rights reserved. 124 | 125 | Redistribution and use in source and binary forms, with or without 126 | modification, are permitted provided that the following conditions are met: 127 | 128 | * Redistributions of source code must retain the above copyright notice, this 129 | list of conditions and the following disclaimer. 130 | 131 | * Redistributions in binary form must reproduce the above copyright notice, 132 | this list of conditions and the following disclaimer in the documentation 133 | and/or other materials provided with the distribution. 134 | 135 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 136 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 137 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 138 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 139 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 140 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 141 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 142 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 143 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 144 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 145 | 146 | 147 | # gorilla/websocket 148 | 149 | Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. 150 | 151 | Redistribution and use in source and binary forms, with or without 152 | modification, are permitted provided that the following conditions are met: 153 | 154 | Redistributions of source code must retain the above copyright notice, this 155 | list of conditions and the following disclaimer. 156 | 157 | Redistributions in binary form must reproduce the above copyright notice, 158 | this list of conditions and the following disclaimer in the documentation 159 | and/or other materials provided with the distribution. 160 | 161 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 162 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 163 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 164 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 165 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 166 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 167 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 168 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 169 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 170 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 171 | 172 | 173 | # kr/pty 174 | 175 | Copyright (c) 2011 Keith Rarick 176 | 177 | Permission is hereby granted, free of charge, to any person 178 | obtaining a copy of this software and associated 179 | documentation files (the "Software"), to deal in the 180 | Software without restriction, including without limitation 181 | the rights to use, copy, modify, merge, publish, distribute, 182 | sublicense, and/or sell copies of the Software, and to 183 | permit persons to whom the Software is furnished to do so, 184 | subject to the following conditions: 185 | 186 | The above copyright notice and this permission notice shall 187 | be included in all copies or substantial portions of the 188 | Software. 189 | 190 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 191 | KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 192 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 193 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 194 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 195 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 196 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 197 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 198 | 199 | 200 | # krishnasrinivas/wetty 201 | 202 | The MIT License (MIT) 203 | 204 | Copyright (c) 2014 Krishna Srinivas 205 | 206 | Permission is hereby granted, free of charge, to any person obtaining a copy 207 | of this software and associated documentation files (the "Software"), to deal 208 | in the Software without restriction, including without limitation the rights 209 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 210 | copies of the Software, and to permit persons to whom the Software is 211 | furnished to do so, subject to the following conditions: 212 | 213 | The above copyright notice and this permission notice shall be included in all 214 | copies or substantial portions of the Software. 215 | 216 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 217 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 218 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 219 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 220 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 221 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 222 | SOFTWARE. 223 | 224 | 225 | # braintree/manners 226 | 227 | Copyright (c) 2014 Braintree, a division of PayPal, Inc. 228 | 229 | Permission is hereby granted, free of charge, to any person obtaining a copy 230 | of this software and associated documentation files (the "Software"), to deal 231 | in the Software without restriction, including without limitation the rights 232 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 233 | copies of the Software, and to permit persons to whom the Software is 234 | furnished to do so, subject to the following conditions: 235 | 236 | The above copyright notice and this permission notice shall be included in 237 | all copies or substantial portions of the Software. 238 | 239 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 240 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 241 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 242 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 243 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 244 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 245 | THE SOFTWARE. 246 | 247 | 248 | # fatih/camelcase 249 | 250 | The MIT License (MIT) 251 | 252 | Copyright (c) 2015 Fatih Arslan 253 | 254 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 255 | 256 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 257 | 258 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 259 | 260 | 261 | # fatih/structs 262 | 263 | The MIT License (MIT) 264 | 265 | Copyright (c) 2014 Fatih Arslan 266 | 267 | Permission is hereby granted, free of charge, to any person obtaining a copy 268 | of this software and associated documentation files (the "Software"), to deal 269 | in the Software without restriction, including without limitation the rights 270 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 271 | copies of the Software, and to permit persons to whom the Software is 272 | furnished to do so, subject to the following conditions: 273 | 274 | The above copyright notice and this permission notice shall be included in all 275 | copies or substantial portions of the Software. 276 | 277 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 278 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 279 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 280 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 281 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 282 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 283 | SOFTWARE. 284 | 285 | 286 | # hashicorp/hcl 287 | 288 | Mozilla Public License, version 2.0 289 | 290 | 1. Definitions 291 | 292 | 1.1. “Contributor” 293 | 294 | means each individual or legal entity that creates, contributes to the 295 | creation of, or owns Covered Software. 296 | 297 | 1.2. “Contributor Version” 298 | 299 | means the combination of the Contributions of others (if any) used by a 300 | Contributor and that particular Contributor’s Contribution. 301 | 302 | 1.3. “Contribution” 303 | 304 | means Covered Software of a particular Contributor. 305 | 306 | 1.4. “Covered Software” 307 | 308 | means Source Code Form to which the initial Contributor has attached the 309 | notice in Exhibit A, the Executable Form of such Source Code Form, and 310 | Modifications of such Source Code Form, in each case including portions 311 | thereof. 312 | 313 | 1.5. “Incompatible With Secondary Licenses” 314 | means 315 | 316 | a. that the initial Contributor has attached the notice described in 317 | Exhibit B to the Covered Software; or 318 | 319 | b. that the Covered Software was made available under the terms of version 320 | 1.1 or earlier of the License, but not also under the terms of a 321 | Secondary License. 322 | 323 | 1.6. “Executable Form” 324 | 325 | means any form of the work other than Source Code Form. 326 | 327 | 1.7. “Larger Work” 328 | 329 | means a work that combines Covered Software with other material, in a separate 330 | file or files, that is not Covered Software. 331 | 332 | 1.8. “License” 333 | 334 | means this document. 335 | 336 | 1.9. “Licensable” 337 | 338 | means having the right to grant, to the maximum extent possible, whether at the 339 | time of the initial grant or subsequently, any and all of the rights conveyed by 340 | this License. 341 | 342 | 1.10. “Modifications” 343 | 344 | means any of the following: 345 | 346 | a. any file in Source Code Form that results from an addition to, deletion 347 | from, or modification of the contents of Covered Software; or 348 | 349 | b. any new file in Source Code Form that contains any Covered Software. 350 | 351 | 1.11. “Patent Claims” of a Contributor 352 | 353 | means any patent claim(s), including without limitation, method, process, 354 | and apparatus claims, in any patent Licensable by such Contributor that 355 | would be infringed, but for the grant of the License, by the making, 356 | using, selling, offering for sale, having made, import, or transfer of 357 | either its Contributions or its Contributor Version. 358 | 359 | 1.12. “Secondary License” 360 | 361 | means either the GNU General Public License, Version 2.0, the GNU Lesser 362 | General Public License, Version 2.1, the GNU Affero General Public 363 | License, Version 3.0, or any later versions of those licenses. 364 | 365 | 1.13. “Source Code Form” 366 | 367 | means the form of the work preferred for making modifications. 368 | 369 | 1.14. “You” (or “Your”) 370 | 371 | means an individual or a legal entity exercising rights under this 372 | License. For legal entities, “You” includes any entity that controls, is 373 | controlled by, or is under common control with You. For purposes of this 374 | definition, “control” means (a) the power, direct or indirect, to cause 375 | the direction or management of such entity, whether by contract or 376 | otherwise, or (b) ownership of more than fifty percent (50%) of the 377 | outstanding shares or beneficial ownership of such entity. 378 | 379 | 380 | 2. License Grants and Conditions 381 | 382 | 2.1. Grants 383 | 384 | Each Contributor hereby grants You a world-wide, royalty-free, 385 | non-exclusive license: 386 | 387 | a. under intellectual property rights (other than patent or trademark) 388 | Licensable by such Contributor to use, reproduce, make available, 389 | modify, display, perform, distribute, and otherwise exploit its 390 | Contributions, either on an unmodified basis, with Modifications, or as 391 | part of a Larger Work; and 392 | 393 | b. under Patent Claims of such Contributor to make, use, sell, offer for 394 | sale, have made, import, and otherwise transfer either its Contributions 395 | or its Contributor Version. 396 | 397 | 2.2. Effective Date 398 | 399 | The licenses granted in Section 2.1 with respect to any Contribution become 400 | effective for each Contribution on the date the Contributor first distributes 401 | such Contribution. 402 | 403 | 2.3. Limitations on Grant Scope 404 | 405 | The licenses granted in this Section 2 are the only rights granted under this 406 | License. No additional rights or licenses will be implied from the distribution 407 | or licensing of Covered Software under this License. Notwithstanding Section 408 | 2.1(b) above, no patent license is granted by a Contributor: 409 | 410 | a. for any code that a Contributor has removed from Covered Software; or 411 | 412 | b. for infringements caused by: (i) Your and any other third party’s 413 | modifications of Covered Software, or (ii) the combination of its 414 | Contributions with other software (except as part of its Contributor 415 | Version); or 416 | 417 | c. under Patent Claims infringed by Covered Software in the absence of its 418 | Contributions. 419 | 420 | This License does not grant any rights in the trademarks, service marks, or 421 | logos of any Contributor (except as may be necessary to comply with the 422 | notice requirements in Section 3.4). 423 | 424 | 2.4. Subsequent Licenses 425 | 426 | No Contributor makes additional grants as a result of Your choice to 427 | distribute the Covered Software under a subsequent version of this License 428 | (see Section 10.2) or under the terms of a Secondary License (if permitted 429 | under the terms of Section 3.3). 430 | 431 | 2.5. Representation 432 | 433 | Each Contributor represents that the Contributor believes its Contributions 434 | are its original creation(s) or it has sufficient rights to grant the 435 | rights to its Contributions conveyed by this License. 436 | 437 | 2.6. Fair Use 438 | 439 | This License is not intended to limit any rights You have under applicable 440 | copyright doctrines of fair use, fair dealing, or other equivalents. 441 | 442 | 2.7. Conditions 443 | 444 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 445 | Section 2.1. 446 | 447 | 448 | 3. Responsibilities 449 | 450 | 3.1. Distribution of Source Form 451 | 452 | All distribution of Covered Software in Source Code Form, including any 453 | Modifications that You create or to which You contribute, must be under the 454 | terms of this License. You must inform recipients that the Source Code Form 455 | of the Covered Software is governed by the terms of this License, and how 456 | they can obtain a copy of this License. You may not attempt to alter or 457 | restrict the recipients’ rights in the Source Code Form. 458 | 459 | 3.2. Distribution of Executable Form 460 | 461 | If You distribute Covered Software in Executable Form then: 462 | 463 | a. such Covered Software must also be made available in Source Code Form, 464 | as described in Section 3.1, and You must inform recipients of the 465 | Executable Form how they can obtain a copy of such Source Code Form by 466 | reasonable means in a timely manner, at a charge no more than the cost 467 | of distribution to the recipient; and 468 | 469 | b. You may distribute such Executable Form under the terms of this License, 470 | or sublicense it under different terms, provided that the license for 471 | the Executable Form does not attempt to limit or alter the recipients’ 472 | rights in the Source Code Form under this License. 473 | 474 | 3.3. Distribution of a Larger Work 475 | 476 | You may create and distribute a Larger Work under terms of Your choice, 477 | provided that You also comply with the requirements of this License for the 478 | Covered Software. If the Larger Work is a combination of Covered Software 479 | with a work governed by one or more Secondary Licenses, and the Covered 480 | Software is not Incompatible With Secondary Licenses, this License permits 481 | You to additionally distribute such Covered Software under the terms of 482 | such Secondary License(s), so that the recipient of the Larger Work may, at 483 | their option, further distribute the Covered Software under the terms of 484 | either this License or such Secondary License(s). 485 | 486 | 3.4. Notices 487 | 488 | You may not remove or alter the substance of any license notices (including 489 | copyright notices, patent notices, disclaimers of warranty, or limitations 490 | of liability) contained within the Source Code Form of the Covered 491 | Software, except that You may alter any license notices to the extent 492 | required to remedy known factual inaccuracies. 493 | 494 | 3.5. Application of Additional Terms 495 | 496 | You may choose to offer, and to charge a fee for, warranty, support, 497 | indemnity or liability obligations to one or more recipients of Covered 498 | Software. However, You may do so only on Your own behalf, and not on behalf 499 | of any Contributor. You must make it absolutely clear that any such 500 | warranty, support, indemnity, or liability obligation is offered by You 501 | alone, and You hereby agree to indemnify every Contributor for any 502 | liability incurred by such Contributor as a result of warranty, support, 503 | indemnity or liability terms You offer. You may include additional 504 | disclaimers of warranty and limitations of liability specific to any 505 | jurisdiction. 506 | 507 | 4. Inability to Comply Due to Statute or Regulation 508 | 509 | If it is impossible for You to comply with any of the terms of this License 510 | with respect to some or all of the Covered Software due to statute, judicial 511 | order, or regulation then You must: (a) comply with the terms of this License 512 | to the maximum extent possible; and (b) describe the limitations and the code 513 | they affect. Such description must be placed in a text file included with all 514 | distributions of the Covered Software under this License. Except to the 515 | extent prohibited by statute or regulation, such description must be 516 | sufficiently detailed for a recipient of ordinary skill to be able to 517 | understand it. 518 | 519 | 5. Termination 520 | 521 | 5.1. The rights granted under this License will terminate automatically if You 522 | fail to comply with any of its terms. However, if You become compliant, 523 | then the rights granted under this License from a particular Contributor 524 | are reinstated (a) provisionally, unless and until such Contributor 525 | explicitly and finally terminates Your grants, and (b) on an ongoing basis, 526 | if such Contributor fails to notify You of the non-compliance by some 527 | reasonable means prior to 60 days after You have come back into compliance. 528 | Moreover, Your grants from a particular Contributor are reinstated on an 529 | ongoing basis if such Contributor notifies You of the non-compliance by 530 | some reasonable means, this is the first time You have received notice of 531 | non-compliance with this License from such Contributor, and You become 532 | compliant prior to 30 days after Your receipt of the notice. 533 | 534 | 5.2. If You initiate litigation against any entity by asserting a patent 535 | infringement claim (excluding declaratory judgment actions, counter-claims, 536 | and cross-claims) alleging that a Contributor Version directly or 537 | indirectly infringes any patent, then the rights granted to You by any and 538 | all Contributors for the Covered Software under Section 2.1 of this License 539 | shall terminate. 540 | 541 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 542 | license agreements (excluding distributors and resellers) which have been 543 | validly granted by You or Your distributors under this License prior to 544 | termination shall survive termination. 545 | 546 | 6. Disclaimer of Warranty 547 | 548 | Covered Software is provided under this License on an “as is” basis, without 549 | warranty of any kind, either expressed, implied, or statutory, including, 550 | without limitation, warranties that the Covered Software is free of defects, 551 | merchantable, fit for a particular purpose or non-infringing. The entire 552 | risk as to the quality and performance of the Covered Software is with You. 553 | Should any Covered Software prove defective in any respect, You (not any 554 | Contributor) assume the cost of any necessary servicing, repair, or 555 | correction. This disclaimer of warranty constitutes an essential part of this 556 | License. No use of any Covered Software is authorized under this License 557 | except under this disclaimer. 558 | 559 | 7. Limitation of Liability 560 | 561 | Under no circumstances and under no legal theory, whether tort (including 562 | negligence), contract, or otherwise, shall any Contributor, or anyone who 563 | distributes Covered Software as permitted above, be liable to You for any 564 | direct, indirect, special, incidental, or consequential damages of any 565 | character including, without limitation, damages for lost profits, loss of 566 | goodwill, work stoppage, computer failure or malfunction, or any and all 567 | other commercial damages or losses, even if such party shall have been 568 | informed of the possibility of such damages. This limitation of liability 569 | shall not apply to liability for death or personal injury resulting from such 570 | party’s negligence to the extent applicable law prohibits such limitation. 571 | Some jurisdictions do not allow the exclusion or limitation of incidental or 572 | consequential damages, so this exclusion and limitation may not apply to You. 573 | 574 | 8. Litigation 575 | 576 | Any litigation relating to this License may be brought only in the courts of 577 | a jurisdiction where the defendant maintains its principal place of business 578 | and such litigation shall be governed by laws of that jurisdiction, without 579 | reference to its conflict-of-law provisions. Nothing in this Section shall 580 | prevent a party’s ability to bring cross-claims or counter-claims. 581 | 582 | 9. Miscellaneous 583 | 584 | This License represents the complete agreement concerning the subject matter 585 | hereof. If any provision of this License is held to be unenforceable, such 586 | provision shall be reformed only to the extent necessary to make it 587 | enforceable. Any law or regulation which provides that the language of a 588 | contract shall be construed against the drafter shall not be used to construe 589 | this License against a Contributor. 590 | 591 | 592 | 10. Versions of the License 593 | 594 | 10.1. New Versions 595 | 596 | Mozilla Foundation is the license steward. Except as provided in Section 597 | 10.3, no one other than the license steward has the right to modify or 598 | publish new versions of this License. Each version will be given a 599 | distinguishing version number. 600 | 601 | 10.2. Effect of New Versions 602 | 603 | You may distribute the Covered Software under the terms of the version of 604 | the License under which You originally received the Covered Software, or 605 | under the terms of any subsequent version published by the license 606 | steward. 607 | 608 | 10.3. Modified Versions 609 | 610 | If you create software not governed by this License, and you want to 611 | create a new license for such software, you may create and use a modified 612 | version of this License if you rename the license and remove any 613 | references to the name of the license steward (except to note that such 614 | modified license differs from this License). 615 | 616 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 617 | If You choose to distribute Source Code Form that is Incompatible With 618 | Secondary Licenses under the terms of this version of the License, the 619 | notice described in Exhibit B of this License must be attached. 620 | 621 | Exhibit A - Source Code Form License Notice 622 | 623 | This Source Code Form is subject to the 624 | terms of the Mozilla Public License, v. 625 | 2.0. If a copy of the MPL was not 626 | distributed with this file, You can 627 | obtain one at 628 | http://mozilla.org/MPL/2.0/. 629 | 630 | If it is not possible or desirable to put the notice in a particular file, then 631 | You may include the notice in a location (such as a LICENSE file in a relevant 632 | directory) where a recipient would be likely to look for such a notice. 633 | 634 | You may add additional accurate notices of copyright ownership. 635 | 636 | Exhibit B - “Incompatible With Secondary Licenses” Notice 637 | 638 | This Source Code Form is “Incompatible 639 | With Secondary Licenses”, as defined by 640 | the Mozilla Public License, v. 2.0. 641 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multi-gotty 2 | 3 | This is a fork of [gotty](https://github.com/yudai/gotty/releases) that lets 4 | `gotty` serve multiple different terminal applications at the same time and 5 | lets you programmatically determine what terminal application `gotty` should serve by 6 | giving it a webserver to use. 7 | 8 | I just wrote it for myself to use and don't really plan on maintaining it for anyone 9 | else's use, but maybe it's an interesting example of how to modify `gotty` for 10 | a different use case. I hardcoded all of `gotty`'s command line arguments. 11 | 12 | ## the JSON format 13 | 14 | The `examples/` directory contains an example of the JSON format gotty expects 15 | from its server: 16 | 17 | ``` 18 | {"banana": ["htop"], "pomegranate": ["watch", "ls"], "shell": ["bash"]} 19 | ``` 20 | 21 | When I'm using this I actually programmatically generate the JSON file, the 22 | static JSON file is just to demonstrate the format. 23 | 24 | ### how to use it 25 | 26 | Start a server like this: 27 | ``` 28 | cd examples 29 | python3 -m http.server 30 | ``` 31 | 32 | Then start multi-gotty like this: 33 | 34 | ``` 35 | multi-gotty --port 7777 http://localhost:3000/index.json 36 | ``` 37 | 38 | Then visit http://localhost:7777/proxy/banana/ in your browser. You should get 39 | `htop`. http://localhost:7777/proxy/shell/ should give you a shell. 40 | 41 | -------------------------------------------------------------------------------- /app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net" 10 | "net/http" 11 | "os/exec" 12 | "strings" 13 | "sync" 14 | "sync/atomic" 15 | "text/template" 16 | "time" 17 | 18 | "github.com/braintree/manners" 19 | "github.com/elazarl/go-bindata-assetfs" 20 | "github.com/gorilla/websocket" 21 | "github.com/kr/pty" 22 | ) 23 | 24 | type InitMessage struct { 25 | Arguments string `json:"Arguments,omitempty"` 26 | AuthToken string `json:"AuthToken,omitempty"` 27 | } 28 | 29 | type App struct { 30 | commandServer string 31 | options *Options 32 | 33 | upgrader *websocket.Upgrader 34 | server *manners.GracefulServer 35 | 36 | titleTemplate *template.Template 37 | 38 | timer *time.Timer 39 | 40 | // clientContext writes concurrently 41 | // Use atomic operations. 42 | connections *int64 43 | } 44 | 45 | type Options struct { 46 | Address string `hcl:"address"` 47 | Port string `hcl:"port"` 48 | WSOrigin string `hcl:"allowed_origin"` 49 | PermitWrite bool `hcl:"permit_write"` 50 | EnableBasicAuth bool `hcl:"enable_basic_auth"` 51 | Credential string `hcl:"credential"` 52 | EnableRandomUrl bool `hcl:"enable_random_url"` 53 | RandomUrlLength int `hcl:"random_url_length"` 54 | IndexFile string `hcl:"index_file"` 55 | EnableTLS bool `hcl:"enable_tls"` 56 | TLSCrtFile string `hcl:"tls_crt_file"` 57 | TLSKeyFile string `hcl:"tls_key_file"` 58 | EnableTLSClientAuth bool `hcl:"enable_tls_client_auth"` 59 | TLSCACrtFile string `hcl:"tls_ca_crt_file"` 60 | TitleFormat string `hcl:"title_format"` 61 | EnableReconnect bool `hcl:"enable_reconnect"` 62 | ReconnectTime int `hcl:"reconnect_time"` 63 | MaxConnection int `hcl:"max_connection"` 64 | Once bool `hcl:"once"` 65 | Timeout int `hcl:"timeout"` 66 | PermitArguments bool `hcl:"permit_arguments"` 67 | CloseSignal int `hcl:"close_signal"` 68 | Preferences HtermPrefernces `hcl:"preferences"` 69 | RawPreferences map[string]interface{} `hcl:"preferences"` 70 | Width int `hcl:"width"` 71 | Height int `hcl:"height"` 72 | } 73 | 74 | var DefaultOptions = Options{ 75 | Address: "", 76 | Port: "8080", 77 | WSOrigin: "http://127.0.0.1", 78 | PermitWrite: false, 79 | EnableBasicAuth: false, 80 | Credential: "", 81 | EnableRandomUrl: false, 82 | RandomUrlLength: 8, 83 | IndexFile: "", 84 | EnableTLS: false, 85 | TLSCrtFile: "~/.gotty.crt", 86 | TLSKeyFile: "~/.gotty.key", 87 | EnableTLSClientAuth: false, 88 | TLSCACrtFile: "~/.gotty.ca.crt", 89 | TitleFormat: "GoTTY", 90 | EnableReconnect: false, 91 | ReconnectTime: 10, 92 | MaxConnection: 0, 93 | Once: false, 94 | CloseSignal: 1, // syscall.SIGHUP 95 | Preferences: HtermPrefernces{}, 96 | Width: 0, 97 | Height: 0, 98 | } 99 | 100 | var Version = "1.0.1" 101 | 102 | func New(commandServer string, options *Options) (*App, error) { 103 | titleTemplate, err := template.New("title").Parse(options.TitleFormat) 104 | if err != nil { 105 | return nil, errors.New("Title format string syntax error") 106 | } 107 | connections := int64(0) 108 | 109 | var originChecker func(r *http.Request) bool 110 | if options.WSOrigin != "" { 111 | originChecker = func(r *http.Request) bool { 112 | return r.Header.Get("Origin") == options.WSOrigin 113 | } 114 | } 115 | 116 | return &App{ 117 | options: options, 118 | commandServer: commandServer, 119 | 120 | upgrader: &websocket.Upgrader{ 121 | ReadBufferSize: 1024, 122 | WriteBufferSize: 1024, 123 | Subprotocols: []string{"gotty"}, 124 | CheckOrigin: originChecker, 125 | }, 126 | 127 | titleTemplate: titleTemplate, 128 | 129 | connections: &connections, 130 | }, nil 131 | } 132 | 133 | func (app *App) Run() error { 134 | endpoint := net.JoinHostPort(app.options.Address, app.options.Port) 135 | log.Printf("Server is starting at %s", endpoint) 136 | 137 | handler := http.HandlerFunc(app.handleRequest) 138 | siteHandler := wrapLogger(handler) 139 | app.server = app.makeServer(endpoint, &siteHandler) 140 | 141 | err := app.server.ListenAndServe() 142 | if err != nil { 143 | return err 144 | } 145 | 146 | log.Printf("Exiting...") 147 | 148 | return nil 149 | } 150 | 151 | func (app *App) makeServer(addr string, handler *http.Handler) *manners.GracefulServer { 152 | server := &http.Server{ 153 | Addr: addr, 154 | Handler: *handler, 155 | } 156 | return manners.NewWithServer(server) 157 | } 158 | 159 | func (app *App) restartTimer() { 160 | if app.options.Timeout > 0 { 161 | app.timer.Reset(time.Duration(app.options.Timeout) * time.Second) 162 | } 163 | } 164 | 165 | func (app *App) readMapping() map[string][]string { 166 | resp, err := http.Get(app.commandServer) 167 | if err != nil { 168 | log.Fatal(err) 169 | } 170 | defer resp.Body.Close() 171 | body, err := ioutil.ReadAll(resp.Body) 172 | if err != nil { 173 | log.Fatal(err) 174 | } 175 | var mapping map[string][]string 176 | json.Unmarshal(body, &mapping) 177 | return mapping 178 | } 179 | 180 | func (app *App) handleAuthToken(w http.ResponseWriter, r *http.Request) { 181 | w.Header().Set("Content-Type", "application/javascript") 182 | w.Write([]byte("var gotty_auth_token = '" + app.options.Credential + "';")) 183 | } 184 | 185 | func (app *App) handleWS(command []string, w http.ResponseWriter, r *http.Request) { 186 | connections := atomic.AddInt64(app.connections, 1) 187 | if int64(app.options.MaxConnection) != 0 { 188 | if connections > int64(app.options.MaxConnection) { 189 | log.Printf("Reached max connection: %d", app.options.MaxConnection) 190 | return 191 | } 192 | } 193 | log.Printf("New client connected: %s", r.RemoteAddr) 194 | 195 | if r.Method != "GET" { 196 | http.Error(w, "Method not allowed", 405) 197 | return 198 | } 199 | 200 | conn, err := app.upgrader.Upgrade(w, r, nil) 201 | if err != nil { 202 | log.Print("Failed to upgrade connection: " + err.Error()) 203 | return 204 | } 205 | 206 | _, stream, err := conn.ReadMessage() 207 | if err != nil { 208 | log.Print("Failed to authenticate websocket connection") 209 | conn.Close() 210 | return 211 | } 212 | var init InitMessage 213 | 214 | err = json.Unmarshal(stream, &init) 215 | if err != nil { 216 | log.Printf("Failed to parse init message %v", err) 217 | conn.Close() 218 | return 219 | } 220 | argv := command[1:] 221 | app.server.StartRoutine() 222 | 223 | cmd := exec.Command(command[0], argv...) 224 | ptyIo, err := pty.Start(cmd) 225 | if err != nil { 226 | log.Print("Failed to execute command", err) 227 | return 228 | } 229 | 230 | if app.options.MaxConnection != 0 { 231 | log.Printf("Command is running for client %s with PID %d (args=%q), connections: %d/%d", 232 | r.RemoteAddr, cmd.Process.Pid, strings.Join(argv, " "), connections, app.options.MaxConnection) 233 | } else { 234 | log.Printf("Command is running for client %s with PID %d (args=%q), connections: %d", 235 | r.RemoteAddr, cmd.Process.Pid, strings.Join(argv, " "), connections) 236 | } 237 | 238 | context := &clientContext{ 239 | app: app, 240 | request: r, 241 | connection: conn, 242 | command: cmd, 243 | pty: ptyIo, 244 | writeMutex: &sync.Mutex{}, 245 | } 246 | 247 | context.goHandleClient() 248 | } 249 | 250 | func (app *App) handleRequest(w http.ResponseWriter, r *http.Request) { 251 | path := r.URL.Path 252 | parts := strings.Split(path, "/") 253 | // TODO: this panics if the path doesn't have enough stuff in it 254 | // TODO: actually match on /proxy and don't do this strings.Split thing 255 | prefix := strings.Join(parts[:3], "/") 256 | // /proxy/ID/ 257 | if strings.HasSuffix(path, "/auth_token.js") { 258 | app.handleAuthToken(w, r) 259 | } else if strings.HasSuffix(path, "/ws") { 260 | id := parts[2] 261 | mapping := app.readMapping() 262 | if command, ok := mapping[id]; ok { 263 | app.handleWS(command, w, r) 264 | } else { 265 | w.WriteHeader(500) 266 | w.Write([]byte(fmt.Sprintf("didn't find proxy: %s", id))) 267 | } 268 | } else { 269 | handler := http.FileServer( 270 | &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, Prefix: "static"}, 271 | ) 272 | if app.options.IndexFile != "" { 273 | handler = http.FileServer(http.Dir(app.options.IndexFile)) 274 | } 275 | http.StripPrefix(prefix, handler).ServeHTTP(w, r) 276 | } 277 | } 278 | 279 | func (app *App) Exit() (firstCall bool) { 280 | if app.server != nil { 281 | firstCall = app.server.Close() 282 | if firstCall { 283 | log.Printf("Received Exit command, waiting for all clients to close sessions...") 284 | } 285 | return firstCall 286 | } 287 | return true 288 | } 289 | 290 | func wrapLogger(handler http.Handler) http.Handler { 291 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 292 | rw := &responseWrapper{w, 200} 293 | handler.ServeHTTP(rw, r) 294 | log.Printf("%s %d %s %s", r.RemoteAddr, rw.status, r.Method, r.URL.Path) 295 | }) 296 | } 297 | -------------------------------------------------------------------------------- /app/client_context.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | "sync" 13 | "sync/atomic" 14 | "syscall" 15 | "unsafe" 16 | 17 | "github.com/fatih/structs" 18 | "github.com/gorilla/websocket" 19 | ) 20 | 21 | type clientContext struct { 22 | app *App 23 | request *http.Request 24 | connection *websocket.Conn 25 | command *exec.Cmd 26 | pty *os.File 27 | writeMutex *sync.Mutex 28 | } 29 | 30 | const ( 31 | Input = '0' 32 | Ping = '1' 33 | ResizeTerminal = '2' 34 | ) 35 | 36 | const ( 37 | Output = '0' 38 | Pong = '1' 39 | SetWindowTitle = '2' 40 | SetPreferences = '3' 41 | SetReconnect = '4' 42 | ) 43 | 44 | type argResizeTerminal struct { 45 | Columns float64 46 | Rows float64 47 | } 48 | 49 | type ContextVars struct { 50 | Command string 51 | Pid int 52 | Hostname string 53 | RemoteAddr string 54 | } 55 | 56 | func (context *clientContext) goHandleClient() { 57 | exit := make(chan bool, 2) 58 | 59 | go func() { 60 | defer func() { exit <- true }() 61 | 62 | context.processSend() 63 | }() 64 | 65 | go func() { 66 | defer func() { exit <- true }() 67 | 68 | context.processReceive() 69 | }() 70 | 71 | go func() { 72 | defer context.app.server.FinishRoutine() 73 | defer func() { 74 | connections := atomic.AddInt64(context.app.connections, -1) 75 | 76 | if context.app.options.MaxConnection != 0 { 77 | log.Printf("Connection closed: %s, connections: %d/%d", 78 | context.request.RemoteAddr, connections, context.app.options.MaxConnection) 79 | } else { 80 | log.Printf("Connection closed: %s, connections: %d", 81 | context.request.RemoteAddr, connections) 82 | } 83 | 84 | if connections == 0 { 85 | context.app.restartTimer() 86 | } 87 | }() 88 | 89 | <-exit 90 | context.pty.Close() 91 | 92 | // Even if the PTY has been closed, 93 | // Read(0 in processSend() keeps blocking and the process doen't exit 94 | context.command.Process.Signal(syscall.Signal(context.app.options.CloseSignal)) 95 | 96 | context.command.Wait() 97 | context.connection.Close() 98 | }() 99 | } 100 | 101 | func (context *clientContext) processSend() { 102 | if err := context.sendInitialize(); err != nil { 103 | log.Printf(err.Error()) 104 | return 105 | } 106 | 107 | buf := make([]byte, 1024) 108 | 109 | for { 110 | size, err := context.pty.Read(buf) 111 | if err != nil { 112 | log.Printf("Command exited for: %s", context.request.RemoteAddr) 113 | return 114 | } 115 | safeMessage := base64.StdEncoding.EncodeToString([]byte(buf[:size])) 116 | if err = context.write(append([]byte{Output}, []byte(safeMessage)...)); err != nil { 117 | log.Printf(err.Error()) 118 | return 119 | } 120 | } 121 | } 122 | 123 | func (context *clientContext) write(data []byte) error { 124 | context.writeMutex.Lock() 125 | defer context.writeMutex.Unlock() 126 | return context.connection.WriteMessage(websocket.TextMessage, data) 127 | } 128 | 129 | func (context *clientContext) sendInitialize() error { 130 | hostname, _ := os.Hostname() 131 | titleVars := ContextVars{ 132 | Pid: context.command.Process.Pid, 133 | Hostname: hostname, 134 | RemoteAddr: context.request.RemoteAddr, 135 | } 136 | 137 | titleBuffer := new(bytes.Buffer) 138 | if err := context.app.titleTemplate.Execute(titleBuffer, titleVars); err != nil { 139 | return err 140 | } 141 | if err := context.write(append([]byte{SetWindowTitle}, titleBuffer.Bytes()...)); err != nil { 142 | return err 143 | } 144 | 145 | prefStruct := structs.New(context.app.options.Preferences) 146 | prefMap := prefStruct.Map() 147 | htermPrefs := make(map[string]interface{}) 148 | for key, value := range prefMap { 149 | rawKey := prefStruct.Field(key).Tag("hcl") 150 | if _, ok := context.app.options.RawPreferences[rawKey]; ok { 151 | htermPrefs[strings.Replace(rawKey, "_", "-", -1)] = value 152 | } 153 | } 154 | prefs, err := json.Marshal(htermPrefs) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | if err := context.write(append([]byte{SetPreferences}, prefs...)); err != nil { 160 | return err 161 | } 162 | if context.app.options.EnableReconnect { 163 | reconnect, _ := json.Marshal(context.app.options.ReconnectTime) 164 | if err := context.write(append([]byte{SetReconnect}, reconnect...)); err != nil { 165 | return err 166 | } 167 | } 168 | return nil 169 | } 170 | 171 | func (context *clientContext) processReceive() { 172 | for { 173 | _, data, err := context.connection.ReadMessage() 174 | if err != nil { 175 | log.Print(err.Error()) 176 | return 177 | } 178 | if len(data) == 0 { 179 | log.Print("An error has occured") 180 | return 181 | } 182 | 183 | switch data[0] { 184 | case Input: 185 | if !context.app.options.PermitWrite { 186 | break 187 | } 188 | 189 | _, err := context.pty.Write(data[1:]) 190 | if err != nil { 191 | return 192 | } 193 | 194 | case Ping: 195 | if err := context.write([]byte{Pong}); err != nil { 196 | log.Print(err.Error()) 197 | return 198 | } 199 | case ResizeTerminal: 200 | var args argResizeTerminal 201 | err = json.Unmarshal(data[1:], &args) 202 | if err != nil { 203 | log.Print("Malformed remote command") 204 | return 205 | } 206 | 207 | rows := uint16(context.app.options.Height) 208 | if rows == 0 { 209 | rows = uint16(args.Rows) 210 | } 211 | 212 | columns := uint16(context.app.options.Width) 213 | if columns == 0 { 214 | columns = uint16(args.Columns) 215 | } 216 | 217 | window := struct { 218 | row uint16 219 | col uint16 220 | x uint16 221 | y uint16 222 | }{ 223 | rows, 224 | columns, 225 | 0, 226 | 0, 227 | } 228 | syscall.Syscall( 229 | syscall.SYS_IOCTL, 230 | context.pty.Fd(), 231 | syscall.TIOCSWINSZ, 232 | uintptr(unsafe.Pointer(&window)), 233 | ) 234 | 235 | default: 236 | log.Print("Unknown message type") 237 | return 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /app/hterm_preferences.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | type HtermPrefernces struct { 4 | AltGrMode *string `hcl:"alt_gr_mode"` 5 | AltBackspaceIsMetaBackspace bool `hcl:"alt_backspace_is_meta_backspace"` 6 | AltIsMeta bool `hcl:"alt_is_meta"` 7 | AltSendsWhat string `hcl:"alt_sends_what"` 8 | AudibleBellSound string `hcl:"audible_bell_sound"` 9 | DesktopNotificationBell bool `hcl:"desktop_notification_bell"` 10 | BackgroundColor string `hcl:"background_color"` 11 | BackgroundImage string `hcl:"background_image"` 12 | BackgroundSize string `hcl:"background_size"` 13 | BackgroundPosition string `hcl:"background_position"` 14 | BackspaceSendsBackspace bool `hcl:"backspace_sends_backspace"` 15 | CharacterMapOverrides map[string]map[string]string `hcl:"character_map_overrides"` 16 | CloseOnExit bool `hcl:"close_on_exit"` 17 | CursorBlink bool `hcl:"cursor_blink"` 18 | CursorBlinkCycle [2]int `hcl:"cursor_blink_cycle"` 19 | CursorColor string `hcl:"cursor_color"` 20 | ColorPaletteOverrides []*string `hcl:"color_palette_overrides"` 21 | CopyOnSelect bool `hcl:"copy_on_select"` 22 | UseDefaultWindowCopy bool `hcl:"use_default_window_copy"` 23 | ClearSelectionAfterCopy bool `hcl:"clear_selection_after_copy"` 24 | CtrlPlusMinusZeroZoom bool `hcl:"ctrl_plus_minus_zero_zoom"` 25 | CtrlCCopy bool `hcl:"ctrl_c_copy"` 26 | CtrlVPaste bool `hcl:"ctrl_v_paste"` 27 | EastAsianAmbiguousAsTwoColumn bool `hcl:"east_asian_ambiguous_as_two_column"` 28 | Enable8BitControl *bool `hcl:"enable_8_bit_control"` 29 | EnableBold *bool `hcl:"enable_bold"` 30 | EnableBoldAsBright bool `hcl:"enable_bold_as_bright"` 31 | EnableClipboardNotice bool `hcl:"enable_clipboard_notice"` 32 | EnableClipboardWrite bool `hcl:"enable_clipboard_write"` 33 | EnableDec12 bool `hcl:"enable_dec12"` 34 | Environment map[string]string `hcl:"environment"` 35 | FontFamily string `hcl:"font_family"` 36 | FontSize int `hcl:"font_size"` 37 | FontSmoothing string `hcl:"font_smoothing"` 38 | ForegroundColor string `hcl:"foreground_color"` 39 | HomeKeysScroll bool `hcl:"home_keys_scroll"` 40 | Keybindings map[string]string `hcl:"keybindings"` 41 | MaxStringSequence int `hcl:"max_string_sequence"` 42 | MediaKeysAreFkeys bool `hcl:"media_keys_are_fkeys"` 43 | MetaSendsEscape bool `hcl:"meta_sends_escape"` 44 | MousePasteButton *int `hcl:"mouse_paste_button"` 45 | PageKeysScroll bool `hcl:"page_keys_scroll"` 46 | PassAltNumber *bool `hcl:"pass_alt_number"` 47 | PassCtrlNumber *bool `hcl:"pass_ctrl_number"` 48 | PassMetaNumber *bool `hcl:"pass_meta_number"` 49 | PassMetaV bool `hcl:"pass_meta_v"` 50 | ReceiveEncoding string `hcl:"receive_encoding"` 51 | ScrollOnKeystroke bool `hcl:"scroll_on_keystroke"` 52 | ScrollOnOutput bool `hcl:"scroll_on_output"` 53 | ScrollbarVisible bool `hcl:"scrollbar_visible"` 54 | ScrollWheelMoveMultiplier int `hcl:"scroll_wheel_move_multiplier"` 55 | SendEncoding string `hcl:"send_encoding"` 56 | ShiftInsertPaste bool `hcl:"shift_insert_paste"` 57 | UserCss string `hcl:"user_css"` 58 | } 59 | -------------------------------------------------------------------------------- /app/http_logger.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "net/http" 7 | ) 8 | 9 | type responseWrapper struct { 10 | http.ResponseWriter 11 | status int 12 | } 13 | 14 | func (w *responseWrapper) WriteHeader(status int) { 15 | w.status = status 16 | w.ResponseWriter.WriteHeader(status) 17 | } 18 | 19 | func (w *responseWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) { 20 | hj, _ := w.ResponseWriter.(http.Hijacker) 21 | w.status = http.StatusSwitchingProtocols 22 | return hj.Hijack() 23 | } 24 | -------------------------------------------------------------------------------- /example/index.json: -------------------------------------------------------------------------------- 1 | {"banana": ["htop"], "pomegranate": ["watch", "ls"], "shell": ["bash"]} 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jvns/multi-gotty 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/braintree/manners v0.0.0-20160418043613-82a8879fc5fd 7 | github.com/codegangsta/cli v1.19.1 8 | github.com/elazarl/go-bindata-assetfs v1.0.1 9 | github.com/fatih/structs v1.1.0 10 | github.com/gorilla/websocket v1.4.2 11 | github.com/hashicorp/go-multierror v1.1.0 // indirect 12 | github.com/kr/pty v1.1.8 13 | github.com/yudai/gotty v1.0.1 14 | github.com/yudai/hcl v0.0.0-20151013225006-5fa2393b3552 15 | github.com/yudai/umutex v0.0.0-20150817080136-18216d265c6b 16 | golang.org/x/sys v0.0.0-20210113131315-ba0562f347e0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/braintree/manners v0.0.0-20160418043613-82a8879fc5fd h1:ePesaBzdTmoMQjwqRCLP2jY+jjWMBpwws/LEQdt1fMM= 2 | github.com/braintree/manners v0.0.0-20160418043613-82a8879fc5fd/go.mod h1:TNehV1AhBwtT7Bd+rh8G6MoGDbBLNs/sKdk3nvr4Yzg= 3 | github.com/codegangsta/cli v1.19.1 h1:+wkU9+nidApJ051CVhVGnj5li64qOfLPz7eZMn2DPXw= 4 | github.com/codegangsta/cli v1.19.1/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= 5 | github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= 6 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 7 | github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw= 8 | github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= 9 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 10 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 11 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 12 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 13 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 14 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 15 | github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= 16 | github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= 17 | github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= 18 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 19 | github.com/yudai/gotty v1.0.1/go.mod h1:QBg0hL6VTVdqQk0qoBYk631EHLRH+XtR4wtbVi64UJ4= 20 | github.com/yudai/hcl v0.0.0-20151013225006-5fa2393b3552 h1:tjsK9T2IA3d2FFNxzDP7AJf+EXhyuPd7PB4Z2HrtAoc= 21 | github.com/yudai/hcl v0.0.0-20151013225006-5fa2393b3552/go.mod h1:hg0ZaCmQL3rze1cH8Fh2g0a9q8vQs0uN8ESpePEwSEw= 22 | github.com/yudai/umutex v0.0.0-20150817080136-18216d265c6b h1:5/txHOjeYQCspaoZzyqanb7On7ZBSndTanlfFfOIEiE= 23 | github.com/yudai/umutex v0.0.0-20150817080136-18216d265c6b/go.mod h1:OR9LtYACUuYfnQwp/brOYClaZwAo7CIJoaWTbcgqo2o= 24 | golang.org/x/sys v0.0.0-20210113131315-ba0562f347e0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var helpTemplate = `NAME: 4 | {{.Name}} - {{.Usage}} 5 | 6 | USAGE: 7 | {{.Name}} [options] [] 8 | 9 | VERSION: 10 | {{.Version}}{{if or .Author .Email}} 11 | 12 | AUTHOR:{{if .Author}} 13 | {{.Author}}{{if .Email}} - <{{.Email}}>{{end}}{{else}} 14 | {{.Email}}{{end}}{{end}} 15 | 16 | OPTIONS: 17 | {{range .Flags}}{{.}} 18 | {{end}} 19 | ` 20 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/codegangsta/cli" 10 | 11 | "github.com/jvns/multi-gotty/app" 12 | ) 13 | 14 | func main() { 15 | cmd := cli.NewApp() 16 | cmd.Version = app.Version 17 | cmd.Name = "multi-gotty" 18 | cmd.Usage = "Share many terminals as a web application" 19 | cmd.HideHelp = true 20 | 21 | cmd.Flags = []cli.Flag{ 22 | cli.StringFlag{ 23 | Name: "address", 24 | Value: "127.0.0.1", 25 | Usage: "ip address to listen on", 26 | }, 27 | cli.StringFlag{ 28 | Name: "port", 29 | Value: "8080", 30 | Usage: "port to listen on", 31 | }, 32 | cli.StringFlag{ 33 | Name: "index-dir", 34 | Value: "", 35 | Usage: "directory to serve statics from", 36 | }, 37 | cli.StringFlag{ 38 | Name: "ws-origin", 39 | Value: "", 40 | Usage: "directory to serve statics from", 41 | }, 42 | } 43 | 44 | cmd.Action = func(c *cli.Context) { 45 | options := app.DefaultOptions 46 | options.Address = c.String("address") 47 | options.Port = c.String("port") 48 | options.IndexFile = c.String("index-dir") 49 | options.WSOrigin = c.String("ws-origin") 50 | options.PermitWrite = true 51 | if len(c.Args()) != 1 { 52 | fmt.Println("Error: No command given.\n") 53 | cli.ShowAppHelp(c) 54 | exit(nil, 1) 55 | } 56 | commandServer := c.Args().Get(0) 57 | 58 | app, err := app.New(commandServer, &options) 59 | if err != nil { 60 | exit(err, 3) 61 | } 62 | 63 | registerSignals(app) 64 | 65 | err = app.Run() 66 | if err != nil { 67 | exit(err, 4) 68 | } 69 | } 70 | 71 | cli.AppHelpTemplate = helpTemplate 72 | 73 | cmd.Run(os.Args) 74 | } 75 | 76 | func exit(err error, code int) { 77 | if err != nil { 78 | fmt.Println(err) 79 | } 80 | os.Exit(code) 81 | } 82 | 83 | func registerSignals(app *app.App) { 84 | sigChan := make(chan os.Signal, 1) 85 | signal.Notify( 86 | sigChan, 87 | syscall.SIGINT, 88 | syscall.SIGTERM, 89 | ) 90 | 91 | go func() { 92 | for { 93 | s := <-sigChan 94 | switch s { 95 | case syscall.SIGINT, syscall.SIGTERM: 96 | if app.Exit() { 97 | fmt.Println("Send ^C to force exit.") 98 | } else { 99 | os.Exit(5) 100 | } 101 | } 102 | } 103 | }() 104 | } 105 | -------------------------------------------------------------------------------- /resources/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvns/multi-gotty/009470fbdab42141e9523f9a948be9a175d23de7/resources/favicon.png -------------------------------------------------------------------------------- /resources/gotty.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var httpsEnabled = window.location.protocol == "https:"; 3 | var args = window.location.search; 4 | var url = (httpsEnabled ? 'wss://' : 'ws://') + window.location.host + window.location.pathname + 'ws'; 5 | var protocols = ["gotty"]; 6 | var autoReconnect = -1; 7 | 8 | var openWs = function() { 9 | var ws = new WebSocket(url, protocols); 10 | 11 | var term; 12 | 13 | var pingTimer; 14 | 15 | ws.onopen = function(event) { 16 | ws.send(JSON.stringify({ Arguments: args, AuthToken: gotty_auth_token,})); 17 | pingTimer = setInterval(sendPing, 30 * 1000, ws); 18 | 19 | hterm.defaultStorage = new lib.Storage.Memory(); 20 | 21 | term = new hterm.Terminal(); 22 | 23 | term.getPrefs().set("send-encoding", "raw"); 24 | 25 | term.onTerminalReady = function() { 26 | var io = term.io.push(); 27 | 28 | io.onVTKeystroke = function(str) { 29 | ws.send("0" + str); 30 | }; 31 | 32 | io.sendString = io.onVTKeystroke; 33 | 34 | io.onTerminalResize = function(columns, rows) { 35 | ws.send( 36 | "2" + JSON.stringify( 37 | { 38 | columns: columns, 39 | rows: rows, 40 | } 41 | ) 42 | ) 43 | }; 44 | 45 | term.installKeyboard(); 46 | }; 47 | 48 | term.decorate(document.getElementById("terminal")); 49 | }; 50 | 51 | ws.onmessage = function(event) { 52 | data = event.data.slice(1); 53 | switch(event.data[0]) { 54 | case '0': 55 | term.io.writeUTF8(window.atob(data)); 56 | break; 57 | case '1': 58 | // pong 59 | break; 60 | case '2': 61 | term.setWindowTitle(data); 62 | break; 63 | case '3': 64 | preferences = JSON.parse(data); 65 | Object.keys(preferences).forEach(function(key) { 66 | console.log("Setting " + key + ": " + preferences[key]); 67 | term.getPrefs().set(key, preferences[key]); 68 | }); 69 | break; 70 | case '4': 71 | autoReconnect = JSON.parse(data); 72 | console.log("Enabling reconnect: " + autoReconnect + " seconds") 73 | break; 74 | } 75 | }; 76 | 77 | ws.onclose = function(event) { 78 | if (term) { 79 | term.uninstallKeyboard(); 80 | term.io.showOverlay("Connection Closed", null); 81 | } 82 | clearInterval(pingTimer); 83 | if (autoReconnect > 0) { 84 | setTimeout(openWs, autoReconnect * 1000); 85 | } 86 | }; 87 | } 88 | 89 | 90 | var sendPing = function(ws) { 91 | ws.send("1"); 92 | } 93 | 94 | openWs(); 95 | })() 96 | -------------------------------------------------------------------------------- /resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GoTTY 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | --------------------------------------------------------------------------------