diff --git a/gns3server/static/web-ui/3rdpartylicenses.txt b/gns3server/static/web-ui/3rdpartylicenses.txt deleted file mode 100644 index bafec8ced..000000000 --- a/gns3server/static/web-ui/3rdpartylicenses.txt +++ /dev/null @@ -1,2604 +0,0 @@ -@angular/animations -MIT - -@angular/cdk -MIT -The MIT License - -Copyright (c) 2022 Google LLC. - -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: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -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. - - -@angular/common -MIT - -@angular/compiler -MIT - -@angular/core -MIT - -@angular/forms -MIT - -@angular/material -MIT -The MIT License - -Copyright (c) 2022 Google LLC. - -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: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -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. - - -@angular/platform-browser -MIT - -@angular/router -MIT - -@babel/runtime -MIT -MIT License - -Copyright (c) 2014-present Sebastian McKenzie and other contributors - -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: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -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. - - -@mattlewis92/dom-autoscroller -MIT -The MIT License (MIT) - -Copyright (c) 2016 Quentin Engles - -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: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -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. - - -@sentry/browser -BSD-3-Clause -BSD 3-Clause License - -Copyright (c) 2019, Sentry -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -@sentry/core -BSD-3-Clause -BSD 3-Clause License - -Copyright (c) 2019, Sentry -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -@sentry/hub -BSD-3-Clause -BSD 3-Clause License - -Copyright (c) 2019, Sentry -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -@sentry/minimal -BSD-3-Clause -BSD 3-Clause License - -Copyright (c) 2019, Sentry -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -@sentry/types -BSD-3-Clause -BSD 3-Clause License - -Copyright (c) 2019, Sentry -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -@sentry/utils -BSD-3-Clause -BSD 3-Clause License - -Copyright (c) 2019, Sentry -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -angular-draggable-droppable -MIT -The MIT License (MIT) - -Copyright (c) 2018 Matt Lewis - -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: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -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. - - -angular-resizable-element -MIT -The MIT License (MIT) - -Copyright (c) 2016 Matt Lewis - -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: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -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. - - -bootstrap -MIT -The MIT License (MIT) - -Copyright (c) 2011-2023 The Bootstrap Authors - -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: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -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. - - -css-tree -MIT -Copyright (C) 2016-2022 by Roman Dvornov - -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: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -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. - - -d3-array -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-axis -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-brush -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-chord -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-collection -BSD-3-Clause -Copyright 2010-2016, Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-color -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-contour -BSD-3-Clause -Copyright 2012-2017 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-dispatch -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-drag -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-dsv -BSD-3-Clause -Copyright 2013-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-ease -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -Copyright 2001 Robert Penner -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-force -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-format -BSD-3-Clause -Copyright 2010-2015 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-geo -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -This license applies to GeographicLib, versions 1.12 and later. - -Copyright (c) 2008-2012, Charles Karney - -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: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -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. - - -d3-hierarchy -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-interpolate -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-ng2-service -MIT -The MIT License (MIT) - -Copyright (c) 2018 Thomas Wanzek - -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: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -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. - - -d3-path -BSD-3-Clause -Copyright 2015-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-polygon -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-quadtree -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-random -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-scale -BSD-3-Clause -Copyright 2010-2015 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-scale-chromatic -BSD-3-Clause -Copyright 2010-2018 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -Apache-Style Software License for ColorBrewer software and ColorBrewer Color -Schemes - -Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State -University. - -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed -under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, either express or implied. See the License for the -specific language governing permissions and limitations under the License. - - -d3-selection -BSD-3-Clause -Copyright (c) 2010-2018, Michael Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* The name Michael Bostock may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-selection-multi -BSD-3-Clause -Copyright (c) 2010-2016, Michael Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* The name Michael Bostock may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-shape -BSD-3-Clause -Copyright 2010-2015 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-time -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-time-format -BSD-3-Clause -Copyright 2010-2017 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-timer -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-transition -BSD-3-Clause -Copyright (c) 2010-2015, Michael Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* The name Michael Bostock may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -TERMS OF USE - EASING EQUATIONS - -Open source under the BSD License. - -Copyright 2001 Robert Penner -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -- Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -- Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -- Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -d3-voronoi -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -Copyright (C) 2010-2013 Raymond Hill -https://github.com/gorhill/Javascript-Voronoi - -Licensed under The MIT License -http://en.wikipedia.org/wiki/MIT_License - -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: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -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. - - -d3-zoom -BSD-3-Clause -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -ipaddr.js -MIT -Copyright (C) 2011-2017 whitequark - -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: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -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. - - -marked -MIT -# License information - -## Contribution License Agreement - -If you contribute code to this project, you are implicitly allowing your code -to be distributed under the MIT license. You are also implicitly verifying that -all code is your original work. `` - -## Marked - -Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/) -Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/) - -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: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -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. - -## Markdown - -Copyright © 2004, John Gruber -http://daringfireball.net/ -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. - - -mousetrap -Apache-2.0 WITH LLVM-exception - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - ---- Exceptions to the Apache 2.0 License ---- - -As an exception, if, as a result of your compiling your source code, portions -of this Software are embedded into an Object form of such source code, you -may redistribute such embedded portions in such Object form without complying -with the conditions of Sections 4(a), 4(b) and 4(d) of the License. - -In addition, if you combine or link compiled forms of this Software with -software that is licensed under the GPLv2 ("Combined Software") and if a -court of competent jurisdiction determines that the patent provision (Section -3), the indemnity provision (Section 9) or other Section of the License -conflicts with the conditions of the GPLv2, you may retroactively and -prospectively choose to deem waived or otherwise exclude such Section(s) of -the License, but only in their entirety and only with respect to the Combined -Software. - - -ng-circle-progress -MIT - -ng2-file-upload -MIT - -ngx-childprocess -MIT - -ngx-device-detector -MIT - -ngx-electron -MIT - -notosans-fontface -Apache-2.0 - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -rxjs -Apache-2.0 - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - -rxjs-compat -Apache-2.0 - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - -save-svg-as-png -MIT -The MIT License (MIT) - -Copyright (c) 2014 Eric Shull - -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: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -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. - - - -source-map-js -BSD-3-Clause - -Copyright (c) 2009-2011, Mozilla Foundation and contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the names of the Mozilla Foundation nor the names of project - contributors may be used to endorse or promote products derived from this - software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -spark-md5 -(WTFPL OR MIT) - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 - - Copyright (C) 2015 André Cruz - - Everyone is permitted to copy and distribute verbatim or modified - copies of this license document, and changing it is allowed as long - as the name is changed. - - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. You just DO WHAT THE FUCK YOU WANT TO. - - -svg-crowbar -MIT -Copyright (c) 2013 The New York Times - -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: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -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. - - -tslib -0BSD -Copyright (c) Microsoft Corporation. - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. - -uuid -MIT -The MIT License (MIT) - -Copyright (c) 2010-2020 Robert Kieffer and other contributors - -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: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -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. - - -xterm -MIT -Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js) -Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com) -Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/) - -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: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -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. - - -xterm-addon-attach -MIT -Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js) - -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: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -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. - - -xterm-addon-fit -MIT -Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js) - -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: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -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. - - -zone.js -MIT -The MIT License - -Copyright (c) 2010-2022 Google LLC. https://angular.io/license - -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: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -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. diff --git a/gns3server/static/web-ui/465.92c7ab880f2504d3.js b/gns3server/static/web-ui/465.92c7ab880f2504d3.js deleted file mode 100644 index afd76868c..000000000 --- a/gns3server/static/web-ui/465.92c7ab880f2504d3.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgns3_web_ui=self.webpackChunkgns3_web_ui||[]).push([[465],{2465:function(R,u,l){l.r(u),l.d(u,{TopologySummaryComponent:function(){return p}});var f=l(5671),v=l(3144),t=l(5e3),y=l(5366),C=l(9971),x=l(5542),S=l(6215),T=l(4068),g=l(9808),b=l(2072),E=l(4834),O=l(4107),d=l(508),_=l(3251);function P(s,n){if(1&s){var e=t.EpF();t.TgZ(0,"div",2),t.NdJ("mousemove",function(a){t.CHM(e);var i=t.oxw();return t.KtG(i.dragWidget(a))},!1,t.evT)("mouseup",function(){t.CHM(e);var a=t.oxw();return t.KtG(a.toggleDragging(!1))},!1,t.evT),t.qZA()}}function M(s,n){1&s&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",29),t.qZA())}function w(s,n){1&s&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",30),t.qZA())}function Z(s,n){1&s&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",31),t.qZA())}function F(s,n){if(1&s&&(t.TgZ(0,"div"),t._uU(1),t.qZA()),2&s){var e=t.oxw().$implicit;t.xp6(1),t.lnq(" ",e.console_type,"://",e.console_host,":",e.console," ")}}function D(s,n){1&s&&(t.TgZ(0,"div"),t._uU(1," none "),t.qZA())}function I(s,n){if(1&s&&(t.TgZ(0,"div",25)(1,"div"),t.YNc(2,M,2,0,"svg",26),t.YNc(3,w,2,0,"svg",26),t.YNc(4,Z,2,0,"svg",26),t._uU(5),t.qZA(),t.YNc(6,F,2,3,"div",27),t.YNc(7,D,2,0,"div",27),t.qZA()),2&s){var e=n.$implicit;t.xp6(2),t.Q6J("ngIf","started"===e.status),t.xp6(1),t.Q6J("ngIf","suspended"===e.status),t.xp6(1),t.Q6J("ngIf","stopped"===e.status),t.xp6(1),t.hij(" ",e.name," "),t.xp6(1),t.Q6J("ngIf",null!=e.console&&null!=e.console&&"none"!=e.console_type),t.xp6(1),t.Q6J("ngIf",null===e.console||void 0===e.console||"none"===e.console_type)}}function k(s,n){1&s&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",29),t.qZA())}function A(s,n){1&s&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",31),t.qZA())}function N(s,n){if(1&s&&(t.TgZ(0,"div",25)(1,"div"),t.YNc(2,k,2,0,"svg",26),t.YNc(3,A,2,0,"svg",26),t._uU(4),t.qZA(),t.TgZ(5,"div"),t._uU(6),t.qZA(),t.TgZ(7,"div"),t._uU(8),t.qZA()()),2&s){var e=n.$implicit,o=t.oxw(2);t.xp6(2),t.Q6J("ngIf",e.connected),t.xp6(1),t.Q6J("ngIf",!e.connected),t.xp6(1),t.hij(" ",e.name," "),t.xp6(2),t.hij(" ",e.host," "),t.xp6(2),t.hij(" ",o.controller.location," ")}}var U=function(n){return{lightTheme:n}},W=function(){return{right:!0,left:!0,bottom:!0,top:!0}};function K(s,n){if(1&s){var e=t.EpF();t.TgZ(0,"div",3),t.NdJ("mousedown",function(){t.CHM(e);var i=t.oxw();return t.KtG(i.toggleDragging(!0))})("resizeStart",function(){t.CHM(e);var i=t.oxw();return t.KtG(i.toggleDragging(!1))})("resizeEnd",function(i){t.CHM(e);var r=t.oxw();return t.KtG(r.onResizeEnd(i))}),t.TgZ(1,"div",4)(2,"mat-tab-group")(3,"mat-tab",5),t.NdJ("click",function(){t.CHM(e);var i=t.oxw();return t.KtG(i.toggleTopologyVisibility(!0))}),t.TgZ(4,"div",6)(5,"div",7)(6,"mat-select",8)(7,"mat-optgroup",9)(8,"mat-option",10),t.NdJ("onSelectionChange",function(){t.CHM(e);var i=t.oxw();return t.KtG(i.applyStatusFilter("started"))}),t._uU(9,"started"),t.qZA(),t.TgZ(10,"mat-option",11),t.NdJ("onSelectionChange",function(){t.CHM(e);var i=t.oxw();return t.KtG(i.applyStatusFilter("suspended"))}),t._uU(11,"suspended"),t.qZA(),t.TgZ(12,"mat-option",12),t.NdJ("onSelectionChange",function(){t.CHM(e);var i=t.oxw();return t.KtG(i.applyStatusFilter("stopped"))}),t._uU(13,"stopped"),t.qZA()(),t.TgZ(14,"mat-optgroup",13)(15,"mat-option",14),t.NdJ("onSelectionChange",function(){t.CHM(e);var i=t.oxw();return t.KtG(i.applyCaptureFilter("capture"))}),t._uU(16,"active capture(s)"),t.qZA(),t.TgZ(17,"mat-option",15),t.NdJ("onSelectionChange",function(){t.CHM(e);var i=t.oxw();return t.KtG(i.applyCaptureFilter("packet"))}),t._uU(18,"active packet captures"),t.qZA()()()(),t.TgZ(19,"div",16)(20,"mat-select",17),t.NdJ("selectionChange",function(){t.CHM(e);var i=t.oxw();return t.KtG(i.setSortingOrder())})("valueChange",function(i){t.CHM(e);var r=t.oxw();return t.KtG(r.sortingOrder=i)}),t.TgZ(21,"mat-option",18),t._uU(22,"sort by name ascending"),t.qZA(),t.TgZ(23,"mat-option",19),t._uU(24,"sort by name descending"),t.qZA()()(),t._UZ(25,"mat-divider",20),t.TgZ(26,"div",21),t.YNc(27,I,8,6,"div",22),t.qZA()()(),t.TgZ(28,"mat-tab",23),t.NdJ("click",function(){t.CHM(e);var i=t.oxw();return t.KtG(i.toggleTopologyVisibility(!1))}),t.TgZ(29,"div",6)(30,"div",24),t.YNc(31,N,9,5,"div",22),t.qZA()()()()()()}if(2&s){var o=t.oxw();t.Q6J("ngStyle",o.style)("ngClass",t.VKq(9,U,o.isLightThemeEnabled))("validateResize",o.validate)("resizeEdges",t.DdM(11,W))("enableGhostResize",!0),t.xp6(20),t.Q6J("value",o.sortingOrder),t.xp6(6),t.Q6J("ngStyle",o.styleInside),t.xp6(1),t.Q6J("ngForOf",o.filteredNodes),t.xp6(4),t.Q6J("ngForOf",o.computes)}}var p=function(){function s(n,e,o,a,i){(0,f.Z)(this,s),this.nodesDataSource=n,this.projectService=e,this.computeService=o,this.linksDataSource=a,this.themeService=i,this.closeTopologySummary=new t.vpe,this.style={},this.styleInside={height:"280px"},this.subscriptions=[],this.nodes=[],this.filteredNodes=[],this.sortingOrder="asc",this.startedStatusFilterEnabled=!1,this.suspendedStatusFilterEnabled=!1,this.stoppedStatusFilterEnabled=!1,this.captureFilterEnabled=!1,this.packetFilterEnabled=!1,this.computes=[],this.isTopologyVisible=!0,this.isDraggingEnabled=!1,this.isLightThemeEnabled=!1}return(0,v.Z)(s,[{key:"ngOnInit",value:function(){var e=this;"light"===this.themeService.getActualTheme()?this.isLightThemeEnabled=!0:this.isLightThemeEnabled=!1,this.subscriptions.push(this.nodesDataSource.changes.subscribe(function(o){e.nodes=o,e.nodes.forEach(function(a){("0.0.0.0"===a.console_host||"0:0:0:0:0:0:0:0"===a.console_host||"::"===a.console_host)&&(a.console_host=e.controller.host)}),"asc"===e.sortingOrder?e.filteredNodes=o.sort(e.compareAsc):e.filteredNodes=o.sort(e.compareDesc)})),this.projectService.getStatistics(this.controller,this.project.project_id).subscribe(function(o){e.projectsStatistics=o}),this.computeService.getComputes(this.controller).subscribe(function(o){e.computes=o}),this.revertPosition()}},{key:"revertPosition",value:function(){var e=localStorage.getItem("leftPosition"),o=localStorage.getItem("rightPosition"),a=localStorage.getItem("topPosition"),i=localStorage.getItem("widthOfWidget"),r=localStorage.getItem("heightOfWidget");this.style=a?{position:"fixed",left:"".concat(+e,"px"),right:"".concat(+o,"px"),top:"".concat(+a,"px"),width:"".concat(+i,"px"),height:"".concat(+r,"px")}:{top:"60px",right:"0px",width:"320px",height:"400px"}}},{key:"toggleDragging",value:function(e){this.isDraggingEnabled=e}},{key:"dragWidget",value:function(e){var o=Number(e.movementX),a=Number(e.movementY),i=Number(this.style.width.split("px")[0]),r=Number(this.style.height.split("px")[0]),c=Number(this.style.top.split("px")[0])+a;if(this.style.left){var m=Number(this.style.left.split("px")[0])+o;this.style={position:"fixed",left:"".concat(m,"px"),top:"".concat(c,"px"),width:"".concat(i,"px"),height:"".concat(r,"px")},localStorage.setItem("leftPosition",m.toString()),localStorage.setItem("topPosition",c.toString()),localStorage.setItem("widthOfWidget",i.toString()),localStorage.setItem("heightOfWidget",r.toString())}else{var h=Number(this.style.right.split("px")[0])-o;this.style={position:"fixed",right:"".concat(h,"px"),top:"".concat(c,"px"),width:"".concat(i,"px"),height:"".concat(r,"px")},localStorage.setItem("rightPosition",h.toString()),localStorage.setItem("topPosition",c.toString()),localStorage.setItem("widthOfWidget",i.toString()),localStorage.setItem("heightOfWidget",r.toString())}}},{key:"validate",value:function(e){return!(e.rectangle.width&&e.rectangle.height&&(e.rectangle.width<290||e.rectangle.height<260))}},{key:"onResizeEnd",value:function(e){this.style={position:"fixed",left:"".concat(e.rectangle.left,"px"),top:"".concat(e.rectangle.top,"px"),width:"".concat(e.rectangle.width,"px"),height:"".concat(e.rectangle.height,"px")},this.styleInside={height:"".concat(e.rectangle.height-120,"px")}}},{key:"toggleTopologyVisibility",value:function(e){this.isTopologyVisible=e,this.revertPosition()}},{key:"compareAsc",value:function(e,o){return e.name fix for platforms and network adapters - -GNS3 Web UI 2020.2.0-beta.2 - -What's New -- Drag & drop to add new nodes on topology -- Option to minimize/maximize and hide console widget -- Ability to add IOS templates -- Node names in HTTP console tabs -- Default settings for templates -- Support for adding IOS images -- Node dialog updated -- Messages with description in toasts -- Adding interfaces to cloud nodes -- Changes in notification box mechanism (once per day option) -- Additional tooltips added -- Copy/paste options in console (only Chrome full support) -- More details for server failed connections - -Bug Fixes -- Fix for console icons -- Fix for creating ethernet switches and hubs -- Fix for opening console from context menu -- Qemu configurator now works properly -- Fixes in snap to grid option -- Symbols preview now works correctly -- Error messages in preferences should be displayed -- Default values for New Ethernet devices in configurator -- Fix for wrong adapter types in Qemu -- Fix for fit in view option on Firefox -- Fix for navigation errors - -GNS3 Web UI 2020.2.0-beta.1 - -What's New -- Support for suspended status added -- Suport for 404 page -- Actions for group of nodes added -- Updating packages -- Button to close project added -- Opening ads in new window -- New dialog for adding nodes -- Option to import config -- Support for light theme added - -Bug Fixes -- Fix for navigating to project that doesn't exist -- Fix for AdButler errors -- Fix for screenshot issue -- Proper centering of icons -- Fix for adding custom symbols -- Fix for return command in console -- Fix for deleting links -- Fix for duplicating any node type -- Fix for console errors on servers page -- Fix for console errors on projects page - -GNS3 Web UI 2019.2.0 v10 - -What's New -- Qemu image configurator -- Custom console for particular node -- Option to connect console to all nodes -- Option to start Winpcap -- Filtering devices with packet filters on topology summary -- Filtering devices with captures on topology summary -- View options taken from map configuration -- Servers summary widget -- Ability to lock single item on the map -- Editing & import & export config files -- Context menu for inserted drawings -- Ability to drag topology summary & servers summary & console widgets -- Ability to resize topology summary & servers summary & console widgets -- Option to show the grid -- Option to snap to grid -- Usage instructions available from context menu -- Errors & warnings visible as notifications -- Fit in view options -- Support for global variables -- Support for layers -- Extending template preferences - -Bug Fixes -- Input validation in styles editor -- Fix for saving map as image -- Removing errors with uncorrect subscriptions - -GNS3 Web UI 2019.2.0 v9 - -What's New -- Editing interface labels on double click -- Support for keyboard shortcuts -- Menu extended with option to delete currently opened project, export & import project -- Possibility to save current state of project -- Ability to duplicate project from projects page -- Node information dialog available from context menu -- Topology summary widget on map view -- Improvements in dialog styles - -Bug Fixes -- Removing issues with opening console -- Context menu now is correctly placed -- Text validation in dialogs -- Removing errors with creating WebSockets +- Test environment stability improvements (Vitest isolation) +- Hardcoded color protection mechanism with multi-layer validation +- Symbol loading with JWT authentication +- Custom adapters support for template configuration +- Compute status real-time updates via WebSocket +- Curve drawing tool improvements +- Window boundary constraint service for better UX +- Console devices panel (v2.0.0) with improved device management diff --git a/gns3server/static/web-ui/assets/fonts/material-symbols/material-symbols-outlined.woff2 b/gns3server/static/web-ui/assets/fonts/material-symbols/material-symbols-outlined.woff2 new file mode 100644 index 000000000..18572482a Binary files /dev/null and b/gns3server/static/web-ui/assets/fonts/material-symbols/material-symbols-outlined.woff2 differ diff --git a/gns3server/static/web-ui/assets/fonts/material-symbols/material-symbols-rounded.woff2 b/gns3server/static/web-ui/assets/fonts/material-symbols/material-symbols-rounded.woff2 new file mode 100644 index 000000000..d265e2c9f Binary files /dev/null and b/gns3server/static/web-ui/assets/fonts/material-symbols/material-symbols-rounded.woff2 differ diff --git a/gns3server/static/web-ui/assets/fonts/material-symbols/material-symbols-sharp.woff2 b/gns3server/static/web-ui/assets/fonts/material-symbols/material-symbols-sharp.woff2 new file mode 100644 index 000000000..dbc4783bb Binary files /dev/null and b/gns3server/static/web-ui/assets/fonts/material-symbols/material-symbols-sharp.woff2 differ diff --git a/gns3server/static/web-ui/assets/github-icon.svg b/gns3server/static/web-ui/assets/github-icon.svg new file mode 100644 index 000000000..5d720c892 --- /dev/null +++ b/gns3server/static/web-ui/assets/github-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/gns3server/static/web-ui/assets/gns3-loading-logo.svg b/gns3server/static/web-ui/assets/gns3-loading-logo.svg new file mode 100644 index 000000000..c5f547e2c --- /dev/null +++ b/gns3server/static/web-ui/assets/gns3-loading-logo.svg @@ -0,0 +1,53 @@ + + diff --git a/gns3server/static/web-ui/assets/gns3_icon.svg b/gns3server/static/web-ui/assets/gns3_icon.svg index 2a9b63d48..fe9d50958 100644 --- a/gns3server/static/web-ui/assets/gns3_icon.svg +++ b/gns3server/static/web-ui/assets/gns3_icon.svg @@ -17,8 +17,8 @@ enable-background="new 0 0 792 612" xml:space="preserve" inkscape:version="0.92.2 (30a58d1, 2017-08-07)" - width="100%" - height="100%" + + sodipodi:docname="gns3_icon.svg" inkscape:export-filename="/home/grossmj/Downloads/gns3_icon_128x128.png" inkscape:export-xdpi="45" diff --git a/gns3server/static/web-ui/assets/vnc-console/index.html b/gns3server/static/web-ui/assets/vnc-console/index.html new file mode 100644 index 000000000..890e01b1e --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/index.html @@ -0,0 +1,779 @@ + + + + + + + VNC Console + + + +
+ + +
+
+
Initializing VNC Console...
+
+
+ + + + + +
+ + 00:00 +
+ + + + + + +
+

Clipboard

+ +
+ + +
+
+ +
Connecting...
+ +
+

Connection Failed

+

+
+ + +
+
+ + + + + diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/base64.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/base64.js new file mode 100644 index 000000000..3d7842376 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/base64.js @@ -0,0 +1,103 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js + +import * as Log from './util/logging.js'; + +export default { + /* Convert data (an array of integers) to a Base64 string. */ + toBase64Table: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''), + base64Pad: '=', + + encode(data) { + 'use strict'; + let result = ''; + const length = data.length; + const lengthpad = length % 3; + // Convert every three bytes to 4 ascii characters. + + for (let i = 0; i < length - 2; i += 3) { + result += this.toBase64Table[data[i] >> 2]; + result += this.toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += this.toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)]; + result += this.toBase64Table[data[i + 2] & 0x3f]; + } + + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. + const j = length - lengthpad; + if (lengthpad === 2) { + result += this.toBase64Table[data[j] >> 2]; + result += this.toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)]; + result += this.toBase64Table[(data[j + 1] & 0x0f) << 2]; + result += this.toBase64Table[64]; + } else if (lengthpad === 1) { + result += this.toBase64Table[data[j] >> 2]; + result += this.toBase64Table[(data[j] & 0x03) << 4]; + result += this.toBase64Table[64]; + result += this.toBase64Table[64]; + } + + return result; + }, + + /* Convert Base64 data to a string */ + /* eslint-disable comma-spacing */ + toBinaryTable: [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + -1, -1, -1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, -1, -1, -1, -1, -1, + ], + /* eslint-enable comma-spacing */ + + decode(data, offset = 0) { + let dataLength = data.indexOf('=') - offset; + if (dataLength < 0) { + dataLength = data.length - offset; + } + + /* Every four characters is 3 resulting numbers */ + const resultLength = (dataLength >> 2) * 3 + Math.floor((dataLength % 4) / 1.5); + const result = new Array(resultLength); + + // Convert one by one. + + let leftbits = 0; // number of bits decoded, but yet to be appended + let leftdata = 0; // bits decoded, but yet to be appended + for (let idx = 0, i = offset; i < data.length; i++) { + const c = this.toBinaryTable[data.charCodeAt(i) & 0x7f]; + const padding = data.charAt(i) === this.base64Pad; + // Skip illegal characters and whitespace + if (c === -1) { + Log.Error('Illegal character code ' + data.charCodeAt(i) + ' at position ' + i); + continue; + } + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) { + result[idx++] = (leftdata >> leftbits) & 0xff; + } + leftdata &= (1 << leftbits) - 1; + } + } + + // If there are any bits left, the base64 string was corrupted + if (leftbits) { + const err = new Error('Corrupted base64 string'); + err.name = 'Base64-Error'; + throw err; + } + + return result; + }, +}; /* End of Base64 namespace */ diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/copyrect.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/copyrect.js new file mode 100644 index 000000000..4f29a08b5 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/copyrect.js @@ -0,0 +1,27 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +export default class CopyRectDecoder { + decodeRect(x, y, width, height, sock, display, depth) { + if (sock.rQwait('COPYRECT', 4)) { + return false; + } + + let deltaX = sock.rQshift16(); + let deltaY = sock.rQshift16(); + + if (width === 0 || height === 0) { + return true; + } + + display.copyImage(deltaX, deltaY, x, y, width, height); + + return true; + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/hextile.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/hextile.js new file mode 100644 index 000000000..8d087a063 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/hextile.js @@ -0,0 +1,199 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +import * as Log from '../util/logging.js'; + +export default class HextileDecoder { + constructor() { + this._tiles = 0; + this._lastsubencoding = 0; + this._tileBuffer = new Uint8Array(16 * 16 * 4); + } + + decodeRect(x, y, width, height, sock, display, depth) { + if (this._tiles === 0) { + this._tilesX = Math.ceil(width / 16); + this._tilesY = Math.ceil(height / 16); + this._totalTiles = this._tilesX * this._tilesY; + this._tiles = this._totalTiles; + } + + while (this._tiles > 0) { + let bytes = 1; + + if (sock.rQwait('HEXTILE', bytes)) { + return false; + } + + let rQ = sock.rQ; + let rQi = sock.rQi; + + let subencoding = rQ[rQi]; // Peek + if (subencoding > 30) { + // Raw + throw new Error('Illegal hextile subencoding (subencoding: ' + subencoding + ')'); + } + + const currTile = this._totalTiles - this._tiles; + const tileX = currTile % this._tilesX; + const tileY = Math.floor(currTile / this._tilesX); + const tx = x + tileX * 16; + const ty = y + tileY * 16; + const tw = Math.min(16, x + width - tx); + const th = Math.min(16, y + height - ty); + + // Figure out how much we are expecting + if (subencoding & 0x01) { + // Raw + bytes += tw * th * 4; + } else { + if (subencoding & 0x02) { + // Background + bytes += 4; + } + if (subencoding & 0x04) { + // Foreground + bytes += 4; + } + if (subencoding & 0x08) { + // AnySubrects + bytes++; // Since we aren't shifting it off + + if (sock.rQwait('HEXTILE', bytes)) { + return false; + } + + let subrects = rQ[rQi + bytes - 1]; // Peek + if (subencoding & 0x10) { + // SubrectsColoured + bytes += subrects * (4 + 2); + } else { + bytes += subrects * 2; + } + } + } + + if (sock.rQwait('HEXTILE', bytes)) { + return false; + } + + // We know the encoding and have a whole tile + rQi++; + if (subencoding === 0) { + if (this._lastsubencoding & 0x01) { + // Weird: ignore blanks are RAW + Log.Debug(' Ignoring blank after RAW'); + } else { + display.fillRect(tx, ty, tw, th, this._background); + } + } else if (subencoding & 0x01) { + // Raw + let pixels = tw * th; + // Max sure the image is fully opaque + for (let i = 0; i < pixels; i++) { + rQ[rQi + i * 4 + 3] = 255; + } + display.blitImage(tx, ty, tw, th, rQ, rQi); + rQi += bytes - 1; + } else { + if (subencoding & 0x02) { + // Background + this._background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + rQi += 4; + } + if (subencoding & 0x04) { + // Foreground + this._foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + rQi += 4; + } + + this._startTile(tx, ty, tw, th, this._background); + if (subencoding & 0x08) { + // AnySubrects + let subrects = rQ[rQi]; + rQi++; + + for (let s = 0; s < subrects; s++) { + let color; + if (subencoding & 0x10) { + // SubrectsColoured + color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + rQi += 4; + } else { + color = this._foreground; + } + const xy = rQ[rQi]; + rQi++; + const sx = xy >> 4; + const sy = xy & 0x0f; + + const wh = rQ[rQi]; + rQi++; + const sw = (wh >> 4) + 1; + const sh = (wh & 0x0f) + 1; + + this._subTile(sx, sy, sw, sh, color); + } + } + this._finishTile(display); + } + sock.rQi = rQi; + this._lastsubencoding = subencoding; + this._tiles--; + } + + return true; + } + + // start updating a tile + _startTile(x, y, width, height, color) { + this._tileX = x; + this._tileY = y; + this._tileW = width; + this._tileH = height; + + const red = color[0]; + const green = color[1]; + const blue = color[2]; + + const data = this._tileBuffer; + for (let i = 0; i < width * height * 4; i += 4) { + data[i] = red; + data[i + 1] = green; + data[i + 2] = blue; + data[i + 3] = 255; + } + } + + // update sub-rectangle of the current tile + _subTile(x, y, w, h, color) { + const red = color[0]; + const green = color[1]; + const blue = color[2]; + const xend = x + w; + const yend = y + h; + + const data = this._tileBuffer; + const width = this._tileW; + for (let j = y; j < yend; j++) { + for (let i = x; i < xend; i++) { + const p = (i + j * width) * 4; + data[p] = red; + data[p + 1] = green; + data[p + 2] = blue; + data[p + 3] = 255; + } + } + } + + // draw the current tile to the screen + _finishTile(display) { + display.blitImage(this._tileX, this._tileY, this._tileW, this._tileH, this._tileBuffer, 0); + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/jpeg.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/jpeg.js new file mode 100644 index 000000000..9741ca39b --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/jpeg.js @@ -0,0 +1,134 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +export default class JPEGDecoder { + constructor() { + // RealVNC will reuse the quantization tables + // and Huffman tables, so we need to cache them. + this._quantTables = []; + this._huffmanTables = []; + this._cachedQuantTables = []; + this._cachedHuffmanTables = []; + + this._jpegLength = 0; + this._segments = []; + } + + decodeRect(x, y, width, height, sock, display, depth) { + // A rect of JPEG encodings is simply a JPEG file + if (!this._parseJPEG(sock.rQslice(0))) { + return false; + } + const data = sock.rQshiftBytes(this._jpegLength); + if (this._quantTables.length != 0 && this._huffmanTables.length != 0) { + // If there are quantization tables and Huffman tables in the JPEG + // image, we can directly render it. + display.imageRect(x, y, width, height, 'image/jpeg', data); + return true; + } else { + // Otherwise we need to insert cached tables. + const sofIndex = this._segments.findIndex((x) => x[1] == 0xc0 || x[1] == 0xc2); + if (sofIndex == -1) { + throw new Error('Illegal JPEG image without SOF'); + } + let segments = this._segments.slice(0, sofIndex); + segments = segments.concat(this._quantTables.length ? this._quantTables : this._cachedQuantTables); + segments.push(this._segments[sofIndex]); + segments = segments.concat( + this._huffmanTables.length ? this._huffmanTables : this._cachedHuffmanTables, + this._segments.slice(sofIndex + 1) + ); + let length = 0; + for (let i = 0; i < segments.length; i++) { + length += segments[i].length; + } + const data = new Uint8Array(length); + length = 0; + for (let i = 0; i < segments.length; i++) { + data.set(segments[i], length); + length += segments[i].length; + } + display.imageRect(x, y, width, height, 'image/jpeg', data); + return true; + } + } + + _parseJPEG(buffer) { + if (this._quantTables.length != 0) { + this._cachedQuantTables = this._quantTables; + } + if (this._huffmanTables.length != 0) { + this._cachedHuffmanTables = this._huffmanTables; + } + this._quantTables = []; + this._huffmanTables = []; + this._segments = []; + let i = 0; + let bufferLength = buffer.length; + while (true) { + let j = i; + if (j + 2 > bufferLength) { + return false; + } + if (buffer[j] != 0xff) { + throw new Error('Illegal JPEG marker received (byte: ' + buffer[j] + ')'); + } + const type = buffer[j + 1]; + j += 2; + if (type == 0xd9) { + this._jpegLength = j; + this._segments.push(buffer.slice(i, j)); + return true; + } else if (type == 0xda) { + // start of scan + let hasFoundEndOfScan = false; + for (let k = j + 3; k + 1 < bufferLength; k++) { + if (buffer[k] == 0xff && buffer[k + 1] != 0x00 && !(buffer[k + 1] >= 0xd0 && buffer[k + 1] <= 0xd7)) { + j = k; + hasFoundEndOfScan = true; + break; + } + } + if (!hasFoundEndOfScan) { + return false; + } + this._segments.push(buffer.slice(i, j)); + i = j; + continue; + } else if ((type >= 0xd0 && type < 0xd9) || type == 0x01) { + // No length after marker + this._segments.push(buffer.slice(i, j)); + i = j; + continue; + } + if (j + 2 > bufferLength) { + return false; + } + const length = (buffer[j] << 8) + buffer[j + 1] - 2; + if (length < 0) { + throw new Error('Illegal JPEG length received (length: ' + length + ')'); + } + j += 2; + if (j + length > bufferLength) { + return false; + } + j += length; + const segment = buffer.slice(i, j); + if (type == 0xc4) { + // Huffman tables + this._huffmanTables.push(segment); + } else if (type == 0xdb) { + // Quantization tables + this._quantTables.push(segment); + } + this._segments.push(segment); + i = j; + } + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/raw.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/raw.js new file mode 100644 index 000000000..e9505f4f3 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/raw.js @@ -0,0 +1,65 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +export default class RawDecoder { + constructor() { + this._lines = 0; + } + + decodeRect(x, y, width, height, sock, display, depth) { + if (width === 0 || height === 0) { + return true; + } + + if (this._lines === 0) { + this._lines = height; + } + + const pixelSize = depth == 8 ? 1 : 4; + const bytesPerLine = width * pixelSize; + + if (sock.rQwait('RAW', bytesPerLine)) { + return false; + } + + const curY = y + (height - this._lines); + const currHeight = Math.min(this._lines, Math.floor(sock.rQlen / bytesPerLine)); + const pixels = width * currHeight; + + let data = sock.rQ; + let index = sock.rQi; + + // Convert data if needed + if (depth == 8) { + const newdata = new Uint8Array(pixels * 4); + for (let i = 0; i < pixels; i++) { + newdata[i * 4 + 0] = (((data[index + i] >> 0) & 0x3) * 255) / 3; + newdata[i * 4 + 1] = (((data[index + i] >> 2) & 0x3) * 255) / 3; + newdata[i * 4 + 2] = (((data[index + i] >> 4) & 0x3) * 255) / 3; + newdata[i * 4 + 3] = 255; + } + data = newdata; + index = 0; + } + + // Max sure the image is fully opaque + for (let i = 0; i < pixels; i++) { + data[index + i * 4 + 3] = 255; + } + + display.blitImage(x, curY, width, currHeight, data, index); + sock.rQskipBytes(currHeight * bytesPerLine); + this._lines -= currHeight; + if (this._lines > 0) { + return false; + } + + return true; + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/rre.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/rre.js new file mode 100644 index 000000000..f1684afdd --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/rre.js @@ -0,0 +1,44 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +export default class RREDecoder { + constructor() { + this._subrects = 0; + } + + decodeRect(x, y, width, height, sock, display, depth) { + if (this._subrects === 0) { + if (sock.rQwait('RRE', 4 + 4)) { + return false; + } + + this._subrects = sock.rQshift32(); + + let color = sock.rQshiftBytes(4); // Background + display.fillRect(x, y, width, height, color); + } + + while (this._subrects > 0) { + if (sock.rQwait('RRE', 4 + 8)) { + return false; + } + + let color = sock.rQshiftBytes(4); + let sx = sock.rQshift16(); + let sy = sock.rQshift16(); + let swidth = sock.rQshift16(); + let sheight = sock.rQshift16(); + display.fillRect(x + sx, y + sy, swidth, sheight, color); + + this._subrects--; + } + + return true; + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/tight.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/tight.js new file mode 100644 index 000000000..61f98cfa5 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/tight.js @@ -0,0 +1,321 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca) + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +import * as Log from '../util/logging.js'; +import Inflator from '../inflator.js'; + +export default class TightDecoder { + constructor() { + this._ctl = null; + this._filter = null; + this._numColors = 0; + this._palette = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) + this._len = 0; + + this._zlibs = []; + for (let i = 0; i < 4; i++) { + this._zlibs[i] = new Inflator(); + } + } + + decodeRect(x, y, width, height, sock, display, depth) { + if (this._ctl === null) { + if (sock.rQwait('TIGHT compression-control', 1)) { + return false; + } + + this._ctl = sock.rQshift8(); + + // Reset streams if the server requests it + for (let i = 0; i < 4; i++) { + if ((this._ctl >> i) & 1) { + this._zlibs[i].reset(); + Log.Info('Reset zlib stream ' + i); + } + } + + // Figure out filter + this._ctl = this._ctl >> 4; + } + + let ret; + + if (this._ctl === 0x08) { + ret = this._fillRect(x, y, width, height, sock, display, depth); + } else if (this._ctl === 0x09) { + ret = this._jpegRect(x, y, width, height, sock, display, depth); + } else if (this._ctl === 0x0a) { + ret = this._pngRect(x, y, width, height, sock, display, depth); + } else if ((this._ctl & 0x08) == 0) { + ret = this._basicRect(this._ctl, x, y, width, height, sock, display, depth); + } else { + throw new Error('Illegal tight compression received (ctl: ' + this._ctl + ')'); + } + + if (ret) { + this._ctl = null; + } + + return ret; + } + + _fillRect(x, y, width, height, sock, display, depth) { + if (sock.rQwait('TIGHT', 3)) { + return false; + } + + const rQi = sock.rQi; + const rQ = sock.rQ; + + display.fillRect(x, y, width, height, [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false); + sock.rQskipBytes(3); + + return true; + } + + _jpegRect(x, y, width, height, sock, display, depth) { + let data = this._readData(sock); + if (data === null) { + return false; + } + + display.imageRect(x, y, width, height, 'image/jpeg', data); + + return true; + } + + _pngRect(x, y, width, height, sock, display, depth) { + throw new Error('PNG received in standard Tight rect'); + } + + _basicRect(ctl, x, y, width, height, sock, display, depth) { + if (this._filter === null) { + if (ctl & 0x4) { + if (sock.rQwait('TIGHT', 1)) { + return false; + } + + this._filter = sock.rQshift8(); + } else { + // Implicit CopyFilter + this._filter = 0; + } + } + + let streamId = ctl & 0x3; + + let ret; + + switch (this._filter) { + case 0: // CopyFilter + ret = this._copyFilter(streamId, x, y, width, height, sock, display, depth); + break; + case 1: // PaletteFilter + ret = this._paletteFilter(streamId, x, y, width, height, sock, display, depth); + break; + case 2: // GradientFilter + ret = this._gradientFilter(streamId, x, y, width, height, sock, display, depth); + break; + default: + throw new Error('Illegal tight filter received (ctl: ' + this._filter + ')'); + } + + if (ret) { + this._filter = null; + } + + return ret; + } + + _copyFilter(streamId, x, y, width, height, sock, display, depth) { + const uncompressedSize = width * height * 3; + let data; + + if (uncompressedSize === 0) { + return true; + } + + if (uncompressedSize < 12) { + if (sock.rQwait('TIGHT', uncompressedSize)) { + return false; + } + + data = sock.rQshiftBytes(uncompressedSize); + } else { + data = this._readData(sock); + if (data === null) { + return false; + } + + this._zlibs[streamId].setInput(data); + data = this._zlibs[streamId].inflate(uncompressedSize); + this._zlibs[streamId].setInput(null); + } + + let rgbx = new Uint8Array(width * height * 4); + for (let i = 0, j = 0; i < width * height * 4; i += 4, j += 3) { + rgbx[i] = data[j]; + rgbx[i + 1] = data[j + 1]; + rgbx[i + 2] = data[j + 2]; + rgbx[i + 3] = 255; // Alpha + } + + display.blitImage(x, y, width, height, rgbx, 0, false); + + return true; + } + + _paletteFilter(streamId, x, y, width, height, sock, display, depth) { + if (this._numColors === 0) { + if (sock.rQwait('TIGHT palette', 1)) { + return false; + } + + const numColors = sock.rQpeek8() + 1; + const paletteSize = numColors * 3; + + if (sock.rQwait('TIGHT palette', 1 + paletteSize)) { + return false; + } + + this._numColors = numColors; + sock.rQskipBytes(1); + + sock.rQshiftTo(this._palette, paletteSize); + } + + const bpp = this._numColors <= 2 ? 1 : 8; + const rowSize = Math.floor((width * bpp + 7) / 8); + const uncompressedSize = rowSize * height; + + let data; + + if (uncompressedSize === 0) { + return true; + } + + if (uncompressedSize < 12) { + if (sock.rQwait('TIGHT', uncompressedSize)) { + return false; + } + + data = sock.rQshiftBytes(uncompressedSize); + } else { + data = this._readData(sock); + if (data === null) { + return false; + } + + this._zlibs[streamId].setInput(data); + data = this._zlibs[streamId].inflate(uncompressedSize); + this._zlibs[streamId].setInput(null); + } + + // Convert indexed (palette based) image data to RGB + if (this._numColors == 2) { + this._monoRect(x, y, width, height, data, this._palette, display); + } else { + this._paletteRect(x, y, width, height, data, this._palette, display); + } + + this._numColors = 0; + + return true; + } + + _monoRect(x, y, width, height, data, palette, display) { + // Convert indexed (palette based) image data to RGB + // TODO: reduce number of calculations inside loop + const dest = this._getScratchBuffer(width * height * 4); + const w = Math.floor((width + 7) / 8); + const w1 = Math.floor(width / 8); + + for (let y = 0; y < height; y++) { + let dp, sp, x; + for (x = 0; x < w1; x++) { + for (let b = 7; b >= 0; b--) { + dp = (y * width + x * 8 + 7 - b) * 4; + sp = ((data[y * w + x] >> b) & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + dest[dp + 3] = 255; + } + } + + for (let b = 7; b >= 8 - (width % 8); b--) { + dp = (y * width + x * 8 + 7 - b) * 4; + sp = ((data[y * w + x] >> b) & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + dest[dp + 3] = 255; + } + } + + display.blitImage(x, y, width, height, dest, 0, false); + } + + _paletteRect(x, y, width, height, data, palette, display) { + // Convert indexed (palette based) image data to RGB + const dest = this._getScratchBuffer(width * height * 4); + const total = width * height * 4; + for (let i = 0, j = 0; i < total; i += 4, j++) { + const sp = data[j] * 3; + dest[i] = palette[sp]; + dest[i + 1] = palette[sp + 1]; + dest[i + 2] = palette[sp + 2]; + dest[i + 3] = 255; + } + + display.blitImage(x, y, width, height, dest, 0, false); + } + + _gradientFilter(streamId, x, y, width, height, sock, display, depth) { + throw new Error('Gradient filter not implemented'); + } + + _readData(sock) { + if (this._len === 0) { + if (sock.rQwait('TIGHT', 3)) { + return null; + } + + let byte; + + byte = sock.rQshift8(); + this._len = byte & 0x7f; + if (byte & 0x80) { + byte = sock.rQshift8(); + this._len |= (byte & 0x7f) << 7; + if (byte & 0x80) { + byte = sock.rQshift8(); + this._len |= byte << 14; + } + } + } + + if (sock.rQwait('TIGHT', this._len)) { + return null; + } + + let data = sock.rQshiftBytes(this._len); + this._len = 0; + + return data; + } + + _getScratchBuffer(size) { + if (!this._scratchBuffer || this._scratchBuffer.length < size) { + this._scratchBuffer = new Uint8Array(size); + } + return this._scratchBuffer; + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/tightpng.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/tightpng.js new file mode 100644 index 000000000..002184a67 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/tightpng.js @@ -0,0 +1,27 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +import TightDecoder from './tight.js'; + +export default class TightPNGDecoder extends TightDecoder { + _pngRect(x, y, width, height, sock, display, depth) { + let data = this._readData(sock); + if (data === null) { + return false; + } + + display.imageRect(x, y, width, height, 'image/png', data); + + return true; + } + + _basicRect(ctl, x, y, width, height, sock, display, depth) { + throw new Error('BasicCompression received in TightPNG rect'); + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/zrle.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/zrle.js new file mode 100644 index 000000000..43bfcee6a --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/decoders/zrle.js @@ -0,0 +1,187 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2021 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +import Inflate from '../inflator.js'; + +const ZRLE_TILE_WIDTH = 64; +const ZRLE_TILE_HEIGHT = 64; + +export default class ZRLEDecoder { + constructor() { + this._length = 0; + this._inflator = new Inflate(); + + this._pixelBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4); + this._tileBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4); + } + + decodeRect(x, y, width, height, sock, display, depth) { + if (this._length === 0) { + if (sock.rQwait('ZLib data length', 4)) { + return false; + } + this._length = sock.rQshift32(); + } + if (sock.rQwait('Zlib data', this._length)) { + return false; + } + + const data = sock.rQshiftBytes(this._length); + + this._inflator.setInput(data); + + for (let ty = y; ty < y + height; ty += ZRLE_TILE_HEIGHT) { + let th = Math.min(ZRLE_TILE_HEIGHT, y + height - ty); + + for (let tx = x; tx < x + width; tx += ZRLE_TILE_WIDTH) { + let tw = Math.min(ZRLE_TILE_WIDTH, x + width - tx); + + const tileSize = tw * th; + const subencoding = this._inflator.inflate(1)[0]; + if (subencoding === 0) { + // raw data + const data = this._readPixels(tileSize); + display.blitImage(tx, ty, tw, th, data, 0, false); + } else if (subencoding === 1) { + // solid + const background = this._readPixels(1); + display.fillRect(tx, ty, tw, th, [background[0], background[1], background[2]]); + } else if (subencoding >= 2 && subencoding <= 16) { + const data = this._decodePaletteTile(subencoding, tileSize, tw, th); + display.blitImage(tx, ty, tw, th, data, 0, false); + } else if (subencoding === 128) { + const data = this._decodeRLETile(tileSize); + display.blitImage(tx, ty, tw, th, data, 0, false); + } else if (subencoding >= 130 && subencoding <= 255) { + const data = this._decodeRLEPaletteTile(subencoding - 128, tileSize); + display.blitImage(tx, ty, tw, th, data, 0, false); + } else { + throw new Error('Unknown subencoding: ' + subencoding); + } + } + } + this._length = 0; + return true; + } + + _getBitsPerPixelInPalette(paletteSize) { + if (paletteSize <= 2) { + return 1; + } else if (paletteSize <= 4) { + return 2; + } else if (paletteSize <= 16) { + return 4; + } + } + + _readPixels(pixels) { + let data = this._pixelBuffer; + const buffer = this._inflator.inflate(3 * pixels); + for (let i = 0, j = 0; i < pixels * 4; i += 4, j += 3) { + data[i] = buffer[j]; + data[i + 1] = buffer[j + 1]; + data[i + 2] = buffer[j + 2]; + data[i + 3] = 255; // Add the Alpha + } + return data; + } + + _decodePaletteTile(paletteSize, tileSize, tilew, tileh) { + const data = this._tileBuffer; + const palette = this._readPixels(paletteSize); + const bitsPerPixel = this._getBitsPerPixelInPalette(paletteSize); + const mask = (1 << bitsPerPixel) - 1; + + let offset = 0; + let encoded = this._inflator.inflate(1)[0]; + + for (let y = 0; y < tileh; y++) { + let shift = 8 - bitsPerPixel; + for (let x = 0; x < tilew; x++) { + if (shift < 0) { + shift = 8 - bitsPerPixel; + encoded = this._inflator.inflate(1)[0]; + } + let indexInPalette = (encoded >> shift) & mask; + + data[offset] = palette[indexInPalette * 4]; + data[offset + 1] = palette[indexInPalette * 4 + 1]; + data[offset + 2] = palette[indexInPalette * 4 + 2]; + data[offset + 3] = palette[indexInPalette * 4 + 3]; + offset += 4; + shift -= bitsPerPixel; + } + if (shift < 8 - bitsPerPixel && y < tileh - 1) { + encoded = this._inflator.inflate(1)[0]; + } + } + return data; + } + + _decodeRLETile(tileSize) { + const data = this._tileBuffer; + let i = 0; + while (i < tileSize) { + const pixel = this._readPixels(1); + const length = this._readRLELength(); + for (let j = 0; j < length; j++) { + data[i * 4] = pixel[0]; + data[i * 4 + 1] = pixel[1]; + data[i * 4 + 2] = pixel[2]; + data[i * 4 + 3] = pixel[3]; + i++; + } + } + return data; + } + + _decodeRLEPaletteTile(paletteSize, tileSize) { + const data = this._tileBuffer; + + // palette + const palette = this._readPixels(paletteSize); + + let offset = 0; + while (offset < tileSize) { + let indexInPalette = this._inflator.inflate(1)[0]; + let length = 1; + if (indexInPalette >= 128) { + indexInPalette -= 128; + length = this._readRLELength(); + } + if (indexInPalette > paletteSize) { + throw new Error('Too big index in palette: ' + indexInPalette + ', palette size: ' + paletteSize); + } + if (offset + length > tileSize) { + throw new Error( + 'Too big rle length in palette mode: ' + length + ', allowed length is: ' + (tileSize - offset) + ); + } + + for (let j = 0; j < length; j++) { + data[offset * 4] = palette[indexInPalette * 4]; + data[offset * 4 + 1] = palette[indexInPalette * 4 + 1]; + data[offset * 4 + 2] = palette[indexInPalette * 4 + 2]; + data[offset * 4 + 3] = palette[indexInPalette * 4 + 3]; + offset++; + } + } + return data; + } + + _readRLELength() { + let length = 0; + let current = 0; + do { + current = this._inflator.inflate(1)[0]; + length += current; + } while (current === 255); + return length + 1; + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/deflator.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/deflator.js new file mode 100644 index 000000000..c191c32ea --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/deflator.js @@ -0,0 +1,84 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2020 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +import { deflateInit, deflate } from '../vendor/pako/lib/zlib/deflate.js'; +import { Z_FULL_FLUSH } from '../vendor/pako/lib/zlib/deflate.js'; +import ZStream from '../vendor/pako/lib/zlib/zstream.js'; + +export default class Deflator { + constructor() { + this.strm = new ZStream(); + this.chunkSize = 1024 * 10 * 10; + this.outputBuffer = new Uint8Array(this.chunkSize); + this.windowBits = 5; + + deflateInit(this.strm, this.windowBits); + } + + deflate(inData) { + /* eslint-disable camelcase */ + this.strm.input = inData; + this.strm.avail_in = this.strm.input.length; + this.strm.next_in = 0; + this.strm.output = this.outputBuffer; + this.strm.avail_out = this.chunkSize; + this.strm.next_out = 0; + /* eslint-enable camelcase */ + + let lastRet = deflate(this.strm, Z_FULL_FLUSH); + let outData = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out); + + if (lastRet < 0) { + throw new Error('zlib deflate failed'); + } + + if (this.strm.avail_in > 0) { + // Read chunks until done + + let chunks = [outData]; + let totalLen = outData.length; + do { + /* eslint-disable camelcase */ + this.strm.output = new Uint8Array(this.chunkSize); + this.strm.next_out = 0; + this.strm.avail_out = this.chunkSize; + /* eslint-enable camelcase */ + + lastRet = deflate(this.strm, Z_FULL_FLUSH); + + if (lastRet < 0) { + throw new Error('zlib deflate failed'); + } + + let chunk = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out); + totalLen += chunk.length; + chunks.push(chunk); + } while (this.strm.avail_in > 0); + + // Combine chunks into a single data + + let newData = new Uint8Array(totalLen); + let offset = 0; + + for (let i = 0; i < chunks.length; i++) { + newData.set(chunks[i], offset); + offset += chunks[i].length; + } + + outData = newData; + } + + /* eslint-disable camelcase */ + this.strm.input = null; + this.strm.avail_in = 0; + this.strm.next_in = 0; + /* eslint-enable camelcase */ + + return outData; + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/des.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/des.js new file mode 100644 index 000000000..0d8ca9bc9 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/des.js @@ -0,0 +1,810 @@ +/* + * Ported from Flashlight VNC ActionScript implementation: + * http://www.wizhelp.com/flashlight-vnc/ + * + * Full attribution follows: + * + * ------------------------------------------------------------------------- + * + * This DES class has been extracted from package Acme.Crypto for use in VNC. + * The unnecessary odd parity code has been removed. + * + * These changes are: + * Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + + * DesCipher - the DES encryption method + * + * The meat of this code is by Dave Zimmerman , and is: + * + * Copyright (c) 1996 Widget Workshop, Inc. All Rights Reserved. + * + * Permission to use, copy, modify, and distribute this software + * and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and + * without fee is hereby granted, provided that this copyright notice is kept + * intact. + * + * WIDGET WORKSHOP MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY + * OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WIDGET WORKSHOP SHALL NOT BE LIABLE + * FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. + * + * THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE + * CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE + * PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT + * NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE + * SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE + * SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE + * PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES"). WIDGET WORKSHOP + * SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR + * HIGH RISK ACTIVITIES. + * + * + * The rest is: + * + * Copyright (C) 1996 by Jef Poskanzer . All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * Visit the ACME Labs Java page for up-to-date versions of this and other + * fine Java utilities: http://www.acme.com/java/ + */ + +/* eslint-disable comma-spacing */ + +// Tables, permutations, S-boxes, etc. +const PC2 = [ + 13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9, 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, 40, 51, 30, 36, 46, 54, 29, + 39, 50, 44, 32, 47, 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31, + ], + totrot = [1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28]; + +const z = 0x0; +let a, b, c, d, e, f; +a = 1 << 16; +b = 1 << 24; +c = a | b; +d = 1 << 2; +e = 1 << 10; +f = d | e; +const SP1 = [ + c | e, + z | z, + a | z, + c | f, + c | d, + a | f, + z | d, + a | z, + z | e, + c | e, + c | f, + z | e, + b | f, + c | d, + b | z, + z | d, + z | f, + b | e, + b | e, + a | e, + a | e, + c | z, + c | z, + b | f, + a | d, + b | d, + b | d, + a | d, + z | z, + z | f, + a | f, + b | z, + a | z, + c | f, + z | d, + c | z, + c | e, + b | z, + b | z, + z | e, + c | d, + a | z, + a | e, + b | d, + z | e, + z | d, + b | f, + a | f, + c | f, + a | d, + c | z, + b | f, + b | d, + z | f, + a | f, + c | e, + z | f, + b | e, + b | e, + z | z, + a | d, + a | e, + z | z, + c | d, +]; +a = 1 << 20; +b = 1 << 31; +c = a | b; +d = 1 << 5; +e = 1 << 15; +f = d | e; +const SP2 = [ + c | f, + b | e, + z | e, + a | f, + a | z, + z | d, + c | d, + b | f, + b | d, + c | f, + c | e, + b | z, + b | e, + a | z, + z | d, + c | d, + a | e, + a | d, + b | f, + z | z, + b | z, + z | e, + a | f, + c | z, + a | d, + b | d, + z | z, + a | e, + z | f, + c | e, + c | z, + z | f, + z | z, + a | f, + c | d, + a | z, + b | f, + c | z, + c | e, + z | e, + c | z, + b | e, + z | d, + c | f, + a | f, + z | d, + z | e, + b | z, + z | f, + c | e, + a | z, + b | d, + a | d, + b | f, + b | d, + a | d, + a | e, + z | z, + b | e, + z | f, + b | z, + c | d, + c | f, + a | e, +]; +a = 1 << 17; +b = 1 << 27; +c = a | b; +d = 1 << 3; +e = 1 << 9; +f = d | e; +const SP3 = [ + z | f, + c | e, + z | z, + c | d, + b | e, + z | z, + a | f, + b | e, + a | d, + b | d, + b | d, + a | z, + c | f, + a | d, + c | z, + z | f, + b | z, + z | d, + c | e, + z | e, + a | e, + c | z, + c | d, + a | f, + b | f, + a | e, + a | z, + b | f, + z | d, + c | f, + z | e, + b | z, + c | e, + b | z, + a | d, + z | f, + a | z, + c | e, + b | e, + z | z, + z | e, + a | d, + c | f, + b | e, + b | d, + z | e, + z | z, + c | d, + b | f, + a | z, + b | z, + c | f, + z | d, + a | f, + a | e, + b | d, + c | z, + b | f, + z | f, + c | z, + a | f, + z | d, + c | d, + a | e, +]; +a = 1 << 13; +b = 1 << 23; +c = a | b; +d = 1 << 0; +e = 1 << 7; +f = d | e; +const SP4 = [ + c | d, + a | f, + a | f, + z | e, + c | e, + b | f, + b | d, + a | d, + z | z, + c | z, + c | z, + c | f, + z | f, + z | z, + b | e, + b | d, + z | d, + a | z, + b | z, + c | d, + z | e, + b | z, + a | d, + a | e, + b | f, + z | d, + a | e, + b | e, + a | z, + c | e, + c | f, + z | f, + b | e, + b | d, + c | z, + c | f, + z | f, + z | z, + z | z, + c | z, + a | e, + b | e, + b | f, + z | d, + c | d, + a | f, + a | f, + z | e, + c | f, + z | f, + z | d, + a | z, + b | d, + a | d, + c | e, + b | f, + a | d, + a | e, + b | z, + c | d, + z | e, + b | z, + a | z, + c | e, +]; +a = 1 << 25; +b = 1 << 30; +c = a | b; +d = 1 << 8; +e = 1 << 19; +f = d | e; +const SP5 = [ + z | d, + a | f, + a | e, + c | d, + z | e, + z | d, + b | z, + a | e, + b | f, + z | e, + a | d, + b | f, + c | d, + c | e, + z | f, + b | z, + a | z, + b | e, + b | e, + z | z, + b | d, + c | f, + c | f, + a | d, + c | e, + b | d, + z | z, + c | z, + a | f, + a | z, + c | z, + z | f, + z | e, + c | d, + z | d, + a | z, + b | z, + a | e, + c | d, + b | f, + a | d, + b | z, + c | e, + a | f, + b | f, + z | d, + a | z, + c | e, + c | f, + z | f, + c | z, + c | f, + a | e, + z | z, + b | e, + c | z, + z | f, + a | d, + b | d, + z | e, + z | z, + b | e, + a | f, + b | d, +]; +a = 1 << 22; +b = 1 << 29; +c = a | b; +d = 1 << 4; +e = 1 << 14; +f = d | e; +const SP6 = [ + b | d, + c | z, + z | e, + c | f, + c | z, + z | d, + c | f, + a | z, + b | e, + a | f, + a | z, + b | d, + a | d, + b | e, + b | z, + z | f, + z | z, + a | d, + b | f, + z | e, + a | e, + b | f, + z | d, + c | d, + c | d, + z | z, + a | f, + c | e, + z | f, + a | e, + c | e, + b | z, + b | e, + z | d, + c | d, + a | e, + c | f, + a | z, + z | f, + b | d, + a | z, + b | e, + b | z, + z | f, + b | d, + c | f, + a | e, + c | z, + a | f, + c | e, + z | z, + c | d, + z | d, + z | e, + c | z, + a | f, + z | e, + a | d, + b | f, + z | z, + c | e, + b | z, + a | d, + b | f, +]; +a = 1 << 21; +b = 1 << 26; +c = a | b; +d = 1 << 1; +e = 1 << 11; +f = d | e; +const SP7 = [ + a | z, + c | d, + b | f, + z | z, + z | e, + b | f, + a | f, + c | e, + c | f, + a | z, + z | z, + b | d, + z | d, + b | z, + c | d, + z | f, + b | e, + a | f, + a | d, + b | e, + b | d, + c | z, + c | e, + a | d, + c | z, + z | e, + z | f, + c | f, + a | e, + z | d, + b | z, + a | e, + b | z, + a | e, + a | z, + b | f, + b | f, + c | d, + c | d, + z | d, + a | d, + b | z, + b | e, + a | z, + c | e, + z | f, + a | f, + c | e, + z | f, + b | d, + c | f, + c | z, + a | e, + z | z, + z | d, + c | f, + z | z, + a | f, + c | z, + z | e, + b | d, + b | e, + z | e, + a | d, +]; +a = 1 << 18; +b = 1 << 28; +c = a | b; +d = 1 << 6; +e = 1 << 12; +f = d | e; +const SP8 = [ + b | f, + z | e, + a | z, + c | f, + b | z, + b | f, + z | d, + b | z, + a | d, + c | z, + c | f, + a | e, + c | e, + a | f, + z | e, + z | d, + c | z, + b | d, + b | e, + z | f, + a | e, + a | d, + c | d, + c | e, + z | f, + z | z, + z | z, + c | d, + b | d, + b | e, + a | f, + a | z, + a | f, + a | z, + c | e, + z | e, + z | d, + c | d, + z | e, + a | f, + b | e, + z | d, + b | d, + c | z, + c | d, + b | z, + a | z, + b | f, + z | z, + c | f, + a | d, + b | d, + c | z, + b | e, + b | f, + z | z, + c | f, + a | e, + a | e, + z | f, + z | f, + a | d, + b | z, + c | e, +]; + +/* eslint-enable comma-spacing */ + +export default class DES { + constructor(password) { + this.keys = []; + + // Set the key. + const pc1m = [], + pcr = [], + kn = []; + + for (let j = 0, l = 56; j < 56; ++j, l -= 8) { + l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1 + const m = l & 0x7; + pc1m[j] = (password[l >>> 3] & (1 << m)) !== 0 ? 1 : 0; + } + + for (let i = 0; i < 16; ++i) { + const m = i << 1; + const n = m + 1; + kn[m] = kn[n] = 0; + for (let o = 28; o < 59; o += 28) { + for (let j = o - 28; j < o; ++j) { + const l = j + totrot[i]; + pcr[j] = l < o ? pc1m[l] : pc1m[l - 28]; + } + } + for (let j = 0; j < 24; ++j) { + if (pcr[PC2[j]] !== 0) { + kn[m] |= 1 << (23 - j); + } + if (pcr[PC2[j + 24]] !== 0) { + kn[n] |= 1 << (23 - j); + } + } + } + + // cookey + for (let i = 0, rawi = 0, KnLi = 0; i < 16; ++i) { + const raw0 = kn[rawi++]; + const raw1 = kn[rawi++]; + this.keys[KnLi] = (raw0 & 0x00fc0000) << 6; + this.keys[KnLi] |= (raw0 & 0x00000fc0) << 10; + this.keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10; + this.keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6; + ++KnLi; + this.keys[KnLi] = (raw0 & 0x0003f000) << 12; + this.keys[KnLi] |= (raw0 & 0x0000003f) << 16; + this.keys[KnLi] |= (raw1 & 0x0003f000) >>> 4; + this.keys[KnLi] |= raw1 & 0x0000003f; + ++KnLi; + } + } + + // Encrypt 8 bytes of text + enc8(text) { + const b = text.slice(); + let i = 0, + l, + r, + x; // left, right, accumulator + + // Squash 8 bytes to 2 ints + l = (b[i++] << 24) | (b[i++] << 16) | (b[i++] << 8) | b[i++]; + r = (b[i++] << 24) | (b[i++] << 16) | (b[i++] << 8) | b[i++]; + + x = ((l >>> 4) ^ r) & 0x0f0f0f0f; + r ^= x; + l ^= x << 4; + x = ((l >>> 16) ^ r) & 0x0000ffff; + r ^= x; + l ^= x << 16; + x = ((r >>> 2) ^ l) & 0x33333333; + l ^= x; + r ^= x << 2; + x = ((r >>> 8) ^ l) & 0x00ff00ff; + l ^= x; + r ^= x << 8; + r = (r << 1) | ((r >>> 31) & 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 1) | ((l >>> 31) & 1); + + for (let i = 0, keysi = 0; i < 8; ++i) { + x = (r << 28) | (r >>> 4); + x ^= this.keys[keysi++]; + let fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = r ^ this.keys[keysi++]; + fval |= SP8[x & 0x3f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + l ^= fval; + x = (l << 28) | (l >>> 4); + x ^= this.keys[keysi++]; + fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = l ^ this.keys[keysi++]; + fval |= SP8[x & 0x0000003f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + r ^= fval; + } + + r = (r << 31) | (r >>> 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 31) | (l >>> 1); + x = ((l >>> 8) ^ r) & 0x00ff00ff; + r ^= x; + l ^= x << 8; + x = ((l >>> 2) ^ r) & 0x33333333; + r ^= x; + l ^= x << 2; + x = ((r >>> 16) ^ l) & 0x0000ffff; + l ^= x; + r ^= x << 16; + x = ((r >>> 4) ^ l) & 0x0f0f0f0f; + l ^= x; + r ^= x << 4; + + // Spread ints to bytes + x = [r, l]; + for (i = 0; i < 8; i++) { + b[i] = (x[i >>> 2] >>> (8 * (3 - (i % 4)))) % 256; + if (b[i] < 0) { + b[i] += 256; + } // unsigned + } + return b; + } + + // Encrypt 16 bytes of text using passwd as key + encrypt(t) { + return this.enc8(t.slice(0, 8)).concat(this.enc8(t.slice(8, 16))); + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/display.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/display.js new file mode 100644 index 000000000..f24bb35b0 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/display.js @@ -0,0 +1,521 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +import * as Log from './util/logging.js'; +import Base64 from './base64.js'; +import { toSigned32bit } from './util/int.js'; + +export default class Display { + constructor(target) { + this._drawCtx = null; + + this._renderQ = []; // queue drawing actions for in-oder rendering + this._flushing = false; + + // the full frame buffer (logical canvas) size + this._fbWidth = 0; + this._fbHeight = 0; + + this._prevDrawStyle = ''; + + Log.Debug('>> Display.constructor'); + + // The visible canvas + this._target = target; + + if (!this._target) { + throw new Error('Target must be set'); + } + + if (typeof this._target === 'string') { + throw new Error('target must be a DOM element'); + } + + if (!this._target.getContext) { + throw new Error('no getContext method'); + } + + this._targetCtx = this._target.getContext('2d'); + + // the visible canvas viewport (i.e. what actually gets seen) + this._viewportLoc = { x: 0, y: 0, w: this._target.width, h: this._target.height }; + + // The hidden canvas, where we do the actual rendering + this._backbuffer = document.createElement('canvas'); + this._drawCtx = this._backbuffer.getContext('2d'); + + this._damageBounds = { left: 0, top: 0, right: this._backbuffer.width, bottom: this._backbuffer.height }; + + Log.Debug('User Agent: ' + navigator.userAgent); + + Log.Debug('<< Display.constructor'); + + // ===== PROPERTIES ===== + + this._scale = 1.0; + this._clipViewport = false; + + // ===== EVENT HANDLERS ===== + + this.onflush = () => {}; // A flush request has finished + } + + // ===== PROPERTIES ===== + + get scale() { + return this._scale; + } + set scale(scale) { + this._rescale(scale); + } + + get clipViewport() { + return this._clipViewport; + } + set clipViewport(viewport) { + this._clipViewport = viewport; + // May need to readjust the viewport dimensions + const vp = this._viewportLoc; + this.viewportChangeSize(vp.w, vp.h); + this.viewportChangePos(0, 0); + } + + get width() { + return this._fbWidth; + } + + get height() { + return this._fbHeight; + } + + // ===== PUBLIC METHODS ===== + + viewportChangePos(deltaX, deltaY) { + const vp = this._viewportLoc; + deltaX = Math.floor(deltaX); + deltaY = Math.floor(deltaY); + + if (!this._clipViewport) { + deltaX = -vp.w; // clamped later of out of bounds + deltaY = -vp.h; + } + + const vx2 = vp.x + vp.w - 1; + const vy2 = vp.y + vp.h - 1; + + // Position change + + if (deltaX < 0 && vp.x + deltaX < 0) { + deltaX = -vp.x; + } + if (vx2 + deltaX >= this._fbWidth) { + deltaX -= vx2 + deltaX - this._fbWidth + 1; + } + + if (vp.y + deltaY < 0) { + deltaY = -vp.y; + } + if (vy2 + deltaY >= this._fbHeight) { + deltaY -= vy2 + deltaY - this._fbHeight + 1; + } + + if (deltaX === 0 && deltaY === 0) { + return; + } + Log.Debug('viewportChange deltaX: ' + deltaX + ', deltaY: ' + deltaY); + + vp.x += deltaX; + vp.y += deltaY; + + this._damage(vp.x, vp.y, vp.w, vp.h); + + this.flip(); + } + + viewportChangeSize(width, height) { + if (!this._clipViewport || typeof width === 'undefined' || typeof height === 'undefined') { + Log.Debug('Setting viewport to full display region'); + width = this._fbWidth; + height = this._fbHeight; + } + + width = Math.floor(width); + height = Math.floor(height); + + if (width > this._fbWidth) { + width = this._fbWidth; + } + if (height > this._fbHeight) { + height = this._fbHeight; + } + + const vp = this._viewportLoc; + if (vp.w !== width || vp.h !== height) { + vp.w = width; + vp.h = height; + + const canvas = this._target; + canvas.width = width; + canvas.height = height; + + // The position might need to be updated if we've grown + this.viewportChangePos(0, 0); + + this._damage(vp.x, vp.y, vp.w, vp.h); + this.flip(); + + // Update the visible size of the target canvas + this._rescale(this._scale); + } + } + + absX(x) { + if (this._scale === 0) { + return 0; + } + return toSigned32bit(x / this._scale + this._viewportLoc.x); + } + + absY(y) { + if (this._scale === 0) { + return 0; + } + return toSigned32bit(y / this._scale + this._viewportLoc.y); + } + + resize(width, height) { + this._prevDrawStyle = ''; + + this._fbWidth = width; + this._fbHeight = height; + + const canvas = this._backbuffer; + if (canvas.width !== width || canvas.height !== height) { + // We have to save the canvas data since changing the size will clear it + let saveImg = null; + if (canvas.width > 0 && canvas.height > 0) { + saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height); + } + + if (canvas.width !== width) { + canvas.width = width; + } + if (canvas.height !== height) { + canvas.height = height; + } + + if (saveImg) { + this._drawCtx.putImageData(saveImg, 0, 0); + } + } + + // Readjust the viewport as it may be incorrectly sized + // and positioned + const vp = this._viewportLoc; + this.viewportChangeSize(vp.w, vp.h); + this.viewportChangePos(0, 0); + } + + getImageData() { + return this._drawCtx.getImageData(0, 0, this.width, this.height); + } + + toDataURL(type, encoderOptions) { + return this._backbuffer.toDataURL(type, encoderOptions); + } + + toBlob(callback, type, quality) { + return this._backbuffer.toBlob(callback, type, quality); + } + + // Track what parts of the visible canvas that need updating + _damage(x, y, w, h) { + if (x < this._damageBounds.left) { + this._damageBounds.left = x; + } + if (y < this._damageBounds.top) { + this._damageBounds.top = y; + } + if (x + w > this._damageBounds.right) { + this._damageBounds.right = x + w; + } + if (y + h > this._damageBounds.bottom) { + this._damageBounds.bottom = y + h; + } + } + + // Update the visible canvas with the contents of the + // rendering canvas + flip(fromQueue) { + if (this._renderQ.length !== 0 && !fromQueue) { + this._renderQPush({ + type: 'flip', + }); + } else { + let x = this._damageBounds.left; + let y = this._damageBounds.top; + let w = this._damageBounds.right - x; + let h = this._damageBounds.bottom - y; + + let vx = x - this._viewportLoc.x; + let vy = y - this._viewportLoc.y; + + if (vx < 0) { + w += vx; + x -= vx; + vx = 0; + } + if (vy < 0) { + h += vy; + y -= vy; + vy = 0; + } + + if (vx + w > this._viewportLoc.w) { + w = this._viewportLoc.w - vx; + } + if (vy + h > this._viewportLoc.h) { + h = this._viewportLoc.h - vy; + } + + if (w > 0 && h > 0) { + // FIXME: We may need to disable image smoothing here + // as well (see copyImage()), but we haven't + // noticed any problem yet. + this._targetCtx.drawImage(this._backbuffer, x, y, w, h, vx, vy, w, h); + } + + this._damageBounds.left = this._damageBounds.top = 65535; + this._damageBounds.right = this._damageBounds.bottom = 0; + } + } + + pending() { + return this._renderQ.length > 0; + } + + flush() { + if (this._renderQ.length === 0) { + this.onflush(); + } else { + this._flushing = true; + } + } + + fillRect(x, y, width, height, color, fromQueue) { + if (this._renderQ.length !== 0 && !fromQueue) { + this._renderQPush({ + type: 'fill', + x: x, + y: y, + width: width, + height: height, + color: color, + }); + } else { + this._setFillColor(color); + this._drawCtx.fillRect(x, y, width, height); + this._damage(x, y, width, height); + } + } + + copyImage(oldX, oldY, newX, newY, w, h, fromQueue) { + if (this._renderQ.length !== 0 && !fromQueue) { + this._renderQPush({ + type: 'copy', + oldX: oldX, + oldY: oldY, + x: newX, + y: newY, + width: w, + height: h, + }); + } else { + // Due to this bug among others [1] we need to disable the image-smoothing to + // avoid getting a blur effect when copying data. + // + // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719 + // + // We need to set these every time since all properties are reset + // when the the size is changed + this._drawCtx.mozImageSmoothingEnabled = false; + this._drawCtx.webkitImageSmoothingEnabled = false; + this._drawCtx.msImageSmoothingEnabled = false; + this._drawCtx.imageSmoothingEnabled = false; + + this._drawCtx.drawImage(this._backbuffer, oldX, oldY, w, h, newX, newY, w, h); + this._damage(newX, newY, w, h); + } + } + + imageRect(x, y, width, height, mime, arr) { + /* The internal logic cannot handle empty images, so bail early */ + if (width === 0 || height === 0) { + return; + } + + const img = new Image(); + img.src = 'data: ' + mime + ';base64,' + Base64.encode(arr); + + this._renderQPush({ + type: 'img', + img: img, + x: x, + y: y, + width: width, + height: height, + }); + } + + blitImage(x, y, width, height, arr, offset, fromQueue) { + if (this._renderQ.length !== 0 && !fromQueue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + const newArr = new Uint8Array(width * height * 4); + newArr.set(new Uint8Array(arr.buffer, 0, newArr.length)); + this._renderQPush({ + type: 'blit', + data: newArr, + x: x, + y: y, + width: width, + height: height, + }); + } else { + // NB(directxman12): arr must be an Type Array view + let data = new Uint8ClampedArray(arr.buffer, arr.byteOffset + offset, width * height * 4); + let img = new ImageData(data, width, height); + this._drawCtx.putImageData(img, x, y); + this._damage(x, y, width, height); + } + } + + drawImage(img, x, y) { + this._drawCtx.drawImage(img, x, y); + this._damage(x, y, img.width, img.height); + } + + autoscale(containerWidth, containerHeight) { + let scaleRatio; + + if (containerWidth === 0 || containerHeight === 0) { + scaleRatio = 0; + } else { + const vp = this._viewportLoc; + const targetAspectRatio = containerWidth / containerHeight; + const fbAspectRatio = vp.w / vp.h; + + if (fbAspectRatio >= targetAspectRatio) { + scaleRatio = containerWidth / vp.w; + } else { + scaleRatio = containerHeight / vp.h; + } + } + + this._rescale(scaleRatio); + } + + // ===== PRIVATE METHODS ===== + + _rescale(factor) { + this._scale = factor; + const vp = this._viewportLoc; + + // NB(directxman12): If you set the width directly, or set the + // style width to a number, the canvas is cleared. + // However, if you set the style width to a string + // ('NNNpx'), the canvas is scaled without clearing. + const width = factor * vp.w + 'px'; + const height = factor * vp.h + 'px'; + + if (this._target.style.width !== width || this._target.style.height !== height) { + this._target.style.width = width; + this._target.style.height = height; + } + } + + _setFillColor(color) { + const newStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')'; + if (newStyle !== this._prevDrawStyle) { + this._drawCtx.fillStyle = newStyle; + this._prevDrawStyle = newStyle; + } + } + + _renderQPush(action) { + this._renderQ.push(action); + if (this._renderQ.length === 1) { + // If this can be rendered immediately it will be, otherwise + // the scanner will wait for the relevant event + this._scanRenderQ(); + } + } + + _resumeRenderQ() { + // "this" is the object that is ready, not the + // display object + this.removeEventListener('load', this._noVNCDisplay._resumeRenderQ); + this._noVNCDisplay._scanRenderQ(); + } + + _scanRenderQ() { + let ready = true; + while (ready && this._renderQ.length > 0) { + const a = this._renderQ[0]; + switch (a.type) { + case 'flip': + this.flip(true); + break; + case 'copy': + this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true); + break; + case 'fill': + this.fillRect(a.x, a.y, a.width, a.height, a.color, true); + break; + case 'blit': + this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true); + break; + case 'img': + if (a.img.complete) { + if (a.img.width !== a.width || a.img.height !== a.height) { + Log.Error( + 'Decoded image has incorrect dimensions. Got ' + + a.img.width + + 'x' + + a.img.height + + '. Expected ' + + a.width + + 'x' + + a.height + + '.' + ); + return; + } + this.drawImage(a.img, a.x, a.y); + } else { + a.img._noVNCDisplay = this; + a.img.addEventListener('load', this._resumeRenderQ); + // We need to wait for this image to 'load' + // to keep things in-order + ready = false; + } + break; + } + + if (ready) { + this._renderQ.shift(); + } + } + + if (this._renderQ.length === 0 && this._flushing) { + this._flushing = false; + this.onflush(); + } + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/encodings.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/encodings.js new file mode 100644 index 000000000..4fe083967 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/encodings.js @@ -0,0 +1,57 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +export const encodings = { + encodingRaw: 0, + encodingCopyRect: 1, + encodingRRE: 2, + encodingHextile: 5, + encodingTight: 7, + encodingZRLE: 16, + encodingTightPNG: -260, + encodingJPEG: 21, + + pseudoEncodingQualityLevel9: -23, + pseudoEncodingQualityLevel0: -32, + pseudoEncodingDesktopSize: -223, + pseudoEncodingLastRect: -224, + pseudoEncodingCursor: -239, + pseudoEncodingQEMUExtendedKeyEvent: -258, + pseudoEncodingDesktopName: -307, + pseudoEncodingExtendedDesktopSize: -308, + pseudoEncodingXvp: -309, + pseudoEncodingFence: -312, + pseudoEncodingContinuousUpdates: -313, + pseudoEncodingCompressLevel9: -247, + pseudoEncodingCompressLevel0: -256, + pseudoEncodingVMwareCursor: 0x574d5664, + pseudoEncodingExtendedClipboard: 0xc0a1e5ce, +}; + +export function encodingName(num) { + switch (num) { + case encodings.encodingRaw: + return 'Raw'; + case encodings.encodingCopyRect: + return 'CopyRect'; + case encodings.encodingRRE: + return 'RRE'; + case encodings.encodingHextile: + return 'Hextile'; + case encodings.encodingTight: + return 'Tight'; + case encodings.encodingZRLE: + return 'ZRLE'; + case encodings.encodingTightPNG: + return 'TightPNG'; + case encodings.encodingJPEG: + return 'JPEG'; + default: + return '[unknown encoding ' + num + ']'; + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/inflator.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/inflator.js new file mode 100644 index 000000000..91b0f5dd0 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/inflator.js @@ -0,0 +1,66 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2020 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +import { inflateInit, inflate, inflateReset } from '../vendor/pako/lib/zlib/inflate.js'; +import ZStream from '../vendor/pako/lib/zlib/zstream.js'; + +export default class Inflate { + constructor() { + this.strm = new ZStream(); + this.chunkSize = 1024 * 10 * 10; + this.strm.output = new Uint8Array(this.chunkSize); + this.windowBits = 5; + + inflateInit(this.strm, this.windowBits); + } + + setInput(data) { + if (!data) { + //FIXME: flush remaining data. + /* eslint-disable camelcase */ + this.strm.input = null; + this.strm.avail_in = 0; + this.strm.next_in = 0; + } else { + this.strm.input = data; + this.strm.avail_in = this.strm.input.length; + this.strm.next_in = 0; + /* eslint-enable camelcase */ + } + } + + inflate(expected) { + // resize our output buffer if it's too small + // (we could just use multiple chunks, but that would cause an extra + // allocation each time to flatten the chunks) + if (expected > this.chunkSize) { + this.chunkSize = expected; + this.strm.output = new Uint8Array(this.chunkSize); + } + + /* eslint-disable camelcase */ + this.strm.next_out = 0; + this.strm.avail_out = expected; + /* eslint-enable camelcase */ + + let ret = inflate(this.strm, 0); // Flush argument not used. + if (ret < 0) { + throw new Error('zlib inflate failed'); + } + + if (this.strm.next_out != expected) { + throw new Error('Incomplete zlib block'); + } + + return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out); + } + + reset() { + inflateReset(this.strm); + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/domkeytable.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/domkeytable.js new file mode 100644 index 000000000..778f337f2 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/domkeytable.js @@ -0,0 +1,311 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +import KeyTable from './keysym.js'; + +/* + * Mapping between HTML key values and VNC/X11 keysyms for "special" + * keys that cannot be handled via their Unicode codepoint. + * + * See https://www.w3.org/TR/uievents-key/ for possible values. + */ + +const DOMKeyTable = {}; + +function addStandard(key, standard) { + if (standard === undefined) throw new Error('Undefined keysym for key "' + key + '"'); + if (key in DOMKeyTable) throw new Error('Duplicate entry for key "' + key + '"'); + DOMKeyTable[key] = [standard, standard, standard, standard]; +} + +function addLeftRight(key, left, right) { + if (left === undefined) throw new Error('Undefined keysym for key "' + key + '"'); + if (right === undefined) throw new Error('Undefined keysym for key "' + key + '"'); + if (key in DOMKeyTable) throw new Error('Duplicate entry for key "' + key + '"'); + DOMKeyTable[key] = [left, left, right, left]; +} + +function addNumpad(key, standard, numpad) { + if (standard === undefined) throw new Error('Undefined keysym for key "' + key + '"'); + if (numpad === undefined) throw new Error('Undefined keysym for key "' + key + '"'); + if (key in DOMKeyTable) throw new Error('Duplicate entry for key "' + key + '"'); + DOMKeyTable[key] = [standard, standard, standard, numpad]; +} + +// 3.2. Modifier Keys + +addLeftRight('Alt', KeyTable.XK_Alt_L, KeyTable.XK_Alt_R); +addStandard('AltGraph', KeyTable.XK_ISO_Level3_Shift); +addStandard('CapsLock', KeyTable.XK_Caps_Lock); +addLeftRight('Control', KeyTable.XK_Control_L, KeyTable.XK_Control_R); +// - Fn +// - FnLock +addLeftRight('Meta', KeyTable.XK_Super_L, KeyTable.XK_Super_R); +addStandard('NumLock', KeyTable.XK_Num_Lock); +addStandard('ScrollLock', KeyTable.XK_Scroll_Lock); +addLeftRight('Shift', KeyTable.XK_Shift_L, KeyTable.XK_Shift_R); +// - Symbol +// - SymbolLock +// - Hyper +// - Super + +// 3.3. Whitespace Keys + +addNumpad('Enter', KeyTable.XK_Return, KeyTable.XK_KP_Enter); +addStandard('Tab', KeyTable.XK_Tab); +addNumpad(' ', KeyTable.XK_space, KeyTable.XK_KP_Space); + +// 3.4. Navigation Keys + +addNumpad('ArrowDown', KeyTable.XK_Down, KeyTable.XK_KP_Down); +addNumpad('ArrowLeft', KeyTable.XK_Left, KeyTable.XK_KP_Left); +addNumpad('ArrowRight', KeyTable.XK_Right, KeyTable.XK_KP_Right); +addNumpad('ArrowUp', KeyTable.XK_Up, KeyTable.XK_KP_Up); +addNumpad('End', KeyTable.XK_End, KeyTable.XK_KP_End); +addNumpad('Home', KeyTable.XK_Home, KeyTable.XK_KP_Home); +addNumpad('PageDown', KeyTable.XK_Next, KeyTable.XK_KP_Next); +addNumpad('PageUp', KeyTable.XK_Prior, KeyTable.XK_KP_Prior); + +// 3.5. Editing Keys + +addStandard('Backspace', KeyTable.XK_BackSpace); +// Browsers send "Clear" for the numpad 5 without NumLock because +// Windows uses VK_Clear for that key. But Unix expects KP_Begin for +// that scenario. +addNumpad('Clear', KeyTable.XK_Clear, KeyTable.XK_KP_Begin); +addStandard('Copy', KeyTable.XF86XK_Copy); +// - CrSel +addStandard('Cut', KeyTable.XF86XK_Cut); +addNumpad('Delete', KeyTable.XK_Delete, KeyTable.XK_KP_Delete); +// - EraseEof +// - ExSel +addNumpad('Insert', KeyTable.XK_Insert, KeyTable.XK_KP_Insert); +addStandard('Paste', KeyTable.XF86XK_Paste); +addStandard('Redo', KeyTable.XK_Redo); +addStandard('Undo', KeyTable.XK_Undo); + +// 3.6. UI Keys + +// - Accept +// - Again (could just be XK_Redo) +// - Attn +addStandard('Cancel', KeyTable.XK_Cancel); +addStandard('ContextMenu', KeyTable.XK_Menu); +addStandard('Escape', KeyTable.XK_Escape); +addStandard('Execute', KeyTable.XK_Execute); +addStandard('Find', KeyTable.XK_Find); +addStandard('Help', KeyTable.XK_Help); +addStandard('Pause', KeyTable.XK_Pause); +// - Play +// - Props +addStandard('Select', KeyTable.XK_Select); +addStandard('ZoomIn', KeyTable.XF86XK_ZoomIn); +addStandard('ZoomOut', KeyTable.XF86XK_ZoomOut); + +// 3.7. Device Keys + +addStandard('BrightnessDown', KeyTable.XF86XK_MonBrightnessDown); +addStandard('BrightnessUp', KeyTable.XF86XK_MonBrightnessUp); +addStandard('Eject', KeyTable.XF86XK_Eject); +addStandard('LogOff', KeyTable.XF86XK_LogOff); +addStandard('Power', KeyTable.XF86XK_PowerOff); +addStandard('PowerOff', KeyTable.XF86XK_PowerDown); +addStandard('PrintScreen', KeyTable.XK_Print); +addStandard('Hibernate', KeyTable.XF86XK_Hibernate); +addStandard('Standby', KeyTable.XF86XK_Standby); +addStandard('WakeUp', KeyTable.XF86XK_WakeUp); + +// 3.8. IME and Composition Keys + +addStandard('AllCandidates', KeyTable.XK_MultipleCandidate); +addStandard('Alphanumeric', KeyTable.XK_Eisu_toggle); +addStandard('CodeInput', KeyTable.XK_Codeinput); +addStandard('Compose', KeyTable.XK_Multi_key); +addStandard('Convert', KeyTable.XK_Henkan); +// - Dead +// - FinalMode +addStandard('GroupFirst', KeyTable.XK_ISO_First_Group); +addStandard('GroupLast', KeyTable.XK_ISO_Last_Group); +addStandard('GroupNext', KeyTable.XK_ISO_Next_Group); +addStandard('GroupPrevious', KeyTable.XK_ISO_Prev_Group); +// - ModeChange (XK_Mode_switch is often used for AltGr) +// - NextCandidate +addStandard('NonConvert', KeyTable.XK_Muhenkan); +addStandard('PreviousCandidate', KeyTable.XK_PreviousCandidate); +// - Process +addStandard('SingleCandidate', KeyTable.XK_SingleCandidate); +addStandard('HangulMode', KeyTable.XK_Hangul); +addStandard('HanjaMode', KeyTable.XK_Hangul_Hanja); +addStandard('JunjaMode', KeyTable.XK_Hangul_Jeonja); +addStandard('Eisu', KeyTable.XK_Eisu_toggle); +addStandard('Hankaku', KeyTable.XK_Hankaku); +addStandard('Hiragana', KeyTable.XK_Hiragana); +addStandard('HiraganaKatakana', KeyTable.XK_Hiragana_Katakana); +addStandard('KanaMode', KeyTable.XK_Kana_Shift); // could also be _Kana_Lock +addStandard('KanjiMode', KeyTable.XK_Kanji); +addStandard('Katakana', KeyTable.XK_Katakana); +addStandard('Romaji', KeyTable.XK_Romaji); +addStandard('Zenkaku', KeyTable.XK_Zenkaku); +addStandard('ZenkakuHankaku', KeyTable.XK_Zenkaku_Hankaku); + +// 3.9. General-Purpose Function Keys + +addStandard('F1', KeyTable.XK_F1); +addStandard('F2', KeyTable.XK_F2); +addStandard('F3', KeyTable.XK_F3); +addStandard('F4', KeyTable.XK_F4); +addStandard('F5', KeyTable.XK_F5); +addStandard('F6', KeyTable.XK_F6); +addStandard('F7', KeyTable.XK_F7); +addStandard('F8', KeyTable.XK_F8); +addStandard('F9', KeyTable.XK_F9); +addStandard('F10', KeyTable.XK_F10); +addStandard('F11', KeyTable.XK_F11); +addStandard('F12', KeyTable.XK_F12); +addStandard('F13', KeyTable.XK_F13); +addStandard('F14', KeyTable.XK_F14); +addStandard('F15', KeyTable.XK_F15); +addStandard('F16', KeyTable.XK_F16); +addStandard('F17', KeyTable.XK_F17); +addStandard('F18', KeyTable.XK_F18); +addStandard('F19', KeyTable.XK_F19); +addStandard('F20', KeyTable.XK_F20); +addStandard('F21', KeyTable.XK_F21); +addStandard('F22', KeyTable.XK_F22); +addStandard('F23', KeyTable.XK_F23); +addStandard('F24', KeyTable.XK_F24); +addStandard('F25', KeyTable.XK_F25); +addStandard('F26', KeyTable.XK_F26); +addStandard('F27', KeyTable.XK_F27); +addStandard('F28', KeyTable.XK_F28); +addStandard('F29', KeyTable.XK_F29); +addStandard('F30', KeyTable.XK_F30); +addStandard('F31', KeyTable.XK_F31); +addStandard('F32', KeyTable.XK_F32); +addStandard('F33', KeyTable.XK_F33); +addStandard('F34', KeyTable.XK_F34); +addStandard('F35', KeyTable.XK_F35); +// - Soft1... + +// 3.10. Multimedia Keys + +// - ChannelDown +// - ChannelUp +addStandard('Close', KeyTable.XF86XK_Close); +addStandard('MailForward', KeyTable.XF86XK_MailForward); +addStandard('MailReply', KeyTable.XF86XK_Reply); +addStandard('MailSend', KeyTable.XF86XK_Send); +// - MediaClose +addStandard('MediaFastForward', KeyTable.XF86XK_AudioForward); +addStandard('MediaPause', KeyTable.XF86XK_AudioPause); +addStandard('MediaPlay', KeyTable.XF86XK_AudioPlay); +// - MediaPlayPause +addStandard('MediaRecord', KeyTable.XF86XK_AudioRecord); +addStandard('MediaRewind', KeyTable.XF86XK_AudioRewind); +addStandard('MediaStop', KeyTable.XF86XK_AudioStop); +addStandard('MediaTrackNext', KeyTable.XF86XK_AudioNext); +addStandard('MediaTrackPrevious', KeyTable.XF86XK_AudioPrev); +addStandard('New', KeyTable.XF86XK_New); +addStandard('Open', KeyTable.XF86XK_Open); +addStandard('Print', KeyTable.XK_Print); +addStandard('Save', KeyTable.XF86XK_Save); +addStandard('SpellCheck', KeyTable.XF86XK_Spell); + +// 3.11. Multimedia Numpad Keys + +// - Key11 +// - Key12 + +// 3.12. Audio Keys + +// - AudioBalanceLeft +// - AudioBalanceRight +// - AudioBassBoostDown +// - AudioBassBoostToggle +// - AudioBassBoostUp +// - AudioFaderFront +// - AudioFaderRear +// - AudioSurroundModeNext +// - AudioTrebleDown +// - AudioTrebleUp +addStandard('AudioVolumeDown', KeyTable.XF86XK_AudioLowerVolume); +addStandard('AudioVolumeUp', KeyTable.XF86XK_AudioRaiseVolume); +addStandard('AudioVolumeMute', KeyTable.XF86XK_AudioMute); +// - MicrophoneToggle +// - MicrophoneVolumeDown +// - MicrophoneVolumeUp +addStandard('MicrophoneVolumeMute', KeyTable.XF86XK_AudioMicMute); + +// 3.13. Speech Keys + +// - SpeechCorrectionList +// - SpeechInputToggle + +// 3.14. Application Keys + +addStandard('LaunchApplication1', KeyTable.XF86XK_MyComputer); +addStandard('LaunchApplication2', KeyTable.XF86XK_Calculator); +addStandard('LaunchCalendar', KeyTable.XF86XK_Calendar); +// - LaunchContacts +addStandard('LaunchMail', KeyTable.XF86XK_Mail); +addStandard('LaunchMediaPlayer', KeyTable.XF86XK_AudioMedia); +addStandard('LaunchMusicPlayer', KeyTable.XF86XK_Music); +addStandard('LaunchPhone', KeyTable.XF86XK_Phone); +addStandard('LaunchScreenSaver', KeyTable.XF86XK_ScreenSaver); +addStandard('LaunchSpreadsheet', KeyTable.XF86XK_Excel); +addStandard('LaunchWebBrowser', KeyTable.XF86XK_WWW); +addStandard('LaunchWebCam', KeyTable.XF86XK_WebCam); +addStandard('LaunchWordProcessor', KeyTable.XF86XK_Word); + +// 3.15. Browser Keys + +addStandard('BrowserBack', KeyTable.XF86XK_Back); +addStandard('BrowserFavorites', KeyTable.XF86XK_Favorites); +addStandard('BrowserForward', KeyTable.XF86XK_Forward); +addStandard('BrowserHome', KeyTable.XF86XK_HomePage); +addStandard('BrowserRefresh', KeyTable.XF86XK_Refresh); +addStandard('BrowserSearch', KeyTable.XF86XK_Search); +addStandard('BrowserStop', KeyTable.XF86XK_Stop); + +// 3.16. Mobile Phone Keys + +// - A whole bunch... + +// 3.17. TV Keys + +// - A whole bunch... + +// 3.18. Media Controller Keys + +// - A whole bunch... +addStandard('Dimmer', KeyTable.XF86XK_BrightnessAdjust); +addStandard('MediaAudioTrack', KeyTable.XF86XK_AudioCycleTrack); +addStandard('RandomToggle', KeyTable.XF86XK_AudioRandomPlay); +addStandard('SplitScreenToggle', KeyTable.XF86XK_SplitScreen); +addStandard('Subtitle', KeyTable.XF86XK_Subtitle); +addStandard('VideoModeNext', KeyTable.XF86XK_Next_VMode); + +// Extra: Numpad + +addNumpad('=', KeyTable.XK_equal, KeyTable.XK_KP_Equal); +addNumpad('+', KeyTable.XK_plus, KeyTable.XK_KP_Add); +addNumpad('-', KeyTable.XK_minus, KeyTable.XK_KP_Subtract); +addNumpad('*', KeyTable.XK_asterisk, KeyTable.XK_KP_Multiply); +addNumpad('/', KeyTable.XK_slash, KeyTable.XK_KP_Divide); +addNumpad('.', KeyTable.XK_period, KeyTable.XK_KP_Decimal); +addNumpad(',', KeyTable.XK_comma, KeyTable.XK_KP_Separator); +addNumpad('0', KeyTable.XK_0, KeyTable.XK_KP_0); +addNumpad('1', KeyTable.XK_1, KeyTable.XK_KP_1); +addNumpad('2', KeyTable.XK_2, KeyTable.XK_KP_2); +addNumpad('3', KeyTable.XK_3, KeyTable.XK_KP_3); +addNumpad('4', KeyTable.XK_4, KeyTable.XK_KP_4); +addNumpad('5', KeyTable.XK_5, KeyTable.XK_KP_5); +addNumpad('6', KeyTable.XK_6, KeyTable.XK_KP_6); +addNumpad('7', KeyTable.XK_7, KeyTable.XK_KP_7); +addNumpad('8', KeyTable.XK_8, KeyTable.XK_KP_8); +addNumpad('9', KeyTable.XK_9, KeyTable.XK_KP_9); + +export default DOMKeyTable; diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/fixedkeys.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/fixedkeys.js new file mode 100644 index 000000000..680cb912f --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/fixedkeys.js @@ -0,0 +1,128 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +/* + * Fallback mapping between HTML key codes (physical keys) and + * HTML key values. This only works for keys that don't vary + * between layouts. We also omit those who manage fine by mapping the + * Unicode representation. + * + * See https://www.w3.org/TR/uievents-code/ for possible codes. + * See https://www.w3.org/TR/uievents-key/ for possible values. + */ + +/* eslint-disable key-spacing */ + +export default { + // 3.1.1.1. Writing System Keys + + Backspace: 'Backspace', + + // 3.1.1.2. Functional Keys + + AltLeft: 'Alt', + AltRight: 'Alt', // This could also be 'AltGraph' + CapsLock: 'CapsLock', + ContextMenu: 'ContextMenu', + ControlLeft: 'Control', + ControlRight: 'Control', + Enter: 'Enter', + MetaLeft: 'Meta', + MetaRight: 'Meta', + ShiftLeft: 'Shift', + ShiftRight: 'Shift', + Tab: 'Tab', + // FIXME: Japanese/Korean keys + + // 3.1.2. Control Pad Section + + Delete: 'Delete', + End: 'End', + Help: 'Help', + Home: 'Home', + Insert: 'Insert', + PageDown: 'PageDown', + PageUp: 'PageUp', + + // 3.1.3. Arrow Pad Section + + ArrowDown: 'ArrowDown', + ArrowLeft: 'ArrowLeft', + ArrowRight: 'ArrowRight', + ArrowUp: 'ArrowUp', + + // 3.1.4. Numpad Section + + NumLock: 'NumLock', + NumpadBackspace: 'Backspace', + NumpadClear: 'Clear', + + // 3.1.5. Function Section + + Escape: 'Escape', + F1: 'F1', + F2: 'F2', + F3: 'F3', + F4: 'F4', + F5: 'F5', + F6: 'F6', + F7: 'F7', + F8: 'F8', + F9: 'F9', + F10: 'F10', + F11: 'F11', + F12: 'F12', + F13: 'F13', + F14: 'F14', + F15: 'F15', + F16: 'F16', + F17: 'F17', + F18: 'F18', + F19: 'F19', + F20: 'F20', + F21: 'F21', + F22: 'F22', + F23: 'F23', + F24: 'F24', + F25: 'F25', + F26: 'F26', + F27: 'F27', + F28: 'F28', + F29: 'F29', + F30: 'F30', + F31: 'F31', + F32: 'F32', + F33: 'F33', + F34: 'F34', + F35: 'F35', + PrintScreen: 'PrintScreen', + ScrollLock: 'ScrollLock', + Pause: 'Pause', + + // 3.1.6. Media Keys + + BrowserBack: 'BrowserBack', + BrowserFavorites: 'BrowserFavorites', + BrowserForward: 'BrowserForward', + BrowserHome: 'BrowserHome', + BrowserRefresh: 'BrowserRefresh', + BrowserSearch: 'BrowserSearch', + BrowserStop: 'BrowserStop', + Eject: 'Eject', + LaunchApp1: 'LaunchMyComputer', + LaunchApp2: 'LaunchCalendar', + LaunchMail: 'LaunchMail', + MediaPlayPause: 'MediaPlay', + MediaStop: 'MediaStop', + MediaTrackNext: 'MediaTrackNext', + MediaTrackPrevious: 'MediaTrackPrevious', + Power: 'Power', + Sleep: 'Sleep', + AudioVolumeDown: 'AudioVolumeDown', + AudioVolumeMute: 'AudioVolumeMute', + AudioVolumeUp: 'AudioVolumeUp', + WakeUp: 'WakeUp', +}; diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/gesturehandler.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/gesturehandler.js new file mode 100644 index 000000000..05e943719 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/gesturehandler.js @@ -0,0 +1,546 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2020 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +const GH_NOGESTURE = 0; +const GH_ONETAP = 1; +const GH_TWOTAP = 2; +const GH_THREETAP = 4; +const GH_DRAG = 8; +const GH_LONGPRESS = 16; +const GH_TWODRAG = 32; +const GH_PINCH = 64; + +const GH_INITSTATE = 127; + +const GH_MOVE_THRESHOLD = 50; +const GH_ANGLE_THRESHOLD = 90; // Degrees + +// Timeout when waiting for gestures (ms) +const GH_MULTITOUCH_TIMEOUT = 250; + +// Maximum time between press and release for a tap (ms) +const GH_TAP_TIMEOUT = 1000; + +// Timeout when waiting for longpress (ms) +const GH_LONGPRESS_TIMEOUT = 1000; + +// Timeout when waiting to decide between PINCH and TWODRAG (ms) +const GH_TWOTOUCH_TIMEOUT = 50; + +export default class GestureHandler { + constructor() { + this._target = null; + + this._state = GH_INITSTATE; + + this._tracked = []; + this._ignored = []; + + this._waitingRelease = false; + this._releaseStart = 0.0; + + this._longpressTimeoutId = null; + this._twoTouchTimeoutId = null; + + this._boundEventHandler = this._eventHandler.bind(this); + } + + attach(target) { + this.detach(); + + this._target = target; + this._target.addEventListener('touchstart', this._boundEventHandler); + this._target.addEventListener('touchmove', this._boundEventHandler); + this._target.addEventListener('touchend', this._boundEventHandler); + this._target.addEventListener('touchcancel', this._boundEventHandler); + } + + detach() { + if (!this._target) { + return; + } + + this._stopLongpressTimeout(); + this._stopTwoTouchTimeout(); + + this._target.removeEventListener('touchstart', this._boundEventHandler); + this._target.removeEventListener('touchmove', this._boundEventHandler); + this._target.removeEventListener('touchend', this._boundEventHandler); + this._target.removeEventListener('touchcancel', this._boundEventHandler); + this._target = null; + } + + _eventHandler(e) { + let fn; + + e.stopPropagation(); + e.preventDefault(); + + switch (e.type) { + case 'touchstart': + fn = this._touchStart; + break; + case 'touchmove': + fn = this._touchMove; + break; + case 'touchend': + case 'touchcancel': + fn = this._touchEnd; + break; + } + + for (let i = 0; i < e.changedTouches.length; i++) { + let touch = e.changedTouches[i]; + fn.call(this, touch.identifier, touch.clientX, touch.clientY); + } + } + + _touchStart(id, x, y) { + // Ignore any new touches if there is already an active gesture, + // or we're in a cleanup state + if (this._hasDetectedGesture() || this._state === GH_NOGESTURE) { + this._ignored.push(id); + return; + } + + // Did it take too long between touches that we should no longer + // consider this a single gesture? + if (this._tracked.length > 0 && Date.now() - this._tracked[0].started > GH_MULTITOUCH_TIMEOUT) { + this._state = GH_NOGESTURE; + this._ignored.push(id); + return; + } + + // If we're waiting for fingers to release then we should no longer + // recognize new touches + if (this._waitingRelease) { + this._state = GH_NOGESTURE; + this._ignored.push(id); + return; + } + + this._tracked.push({ + id: id, + started: Date.now(), + active: true, + firstX: x, + firstY: y, + lastX: x, + lastY: y, + angle: 0, + }); + + switch (this._tracked.length) { + case 1: + this._startLongpressTimeout(); + break; + + case 2: + this._state &= ~(GH_ONETAP | GH_DRAG | GH_LONGPRESS); + this._stopLongpressTimeout(); + break; + + case 3: + this._state &= ~(GH_TWOTAP | GH_TWODRAG | GH_PINCH); + break; + + default: + this._state = GH_NOGESTURE; + } + } + + _touchMove(id, x, y) { + let touch = this._tracked.find((t) => t.id === id); + + // If this is an update for a touch we're not tracking, ignore it + if (touch === undefined) { + return; + } + + // Update the touches last position with the event coordinates + touch.lastX = x; + touch.lastY = y; + + let deltaX = x - touch.firstX; + let deltaY = y - touch.firstY; + + // Update angle when the touch has moved + if (touch.firstX !== touch.lastX || touch.firstY !== touch.lastY) { + touch.angle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI; + } + + if (!this._hasDetectedGesture()) { + // Ignore moves smaller than the minimum threshold + if (Math.hypot(deltaX, deltaY) < GH_MOVE_THRESHOLD) { + return; + } + + // Can't be a tap or long press as we've seen movement + this._state &= ~(GH_ONETAP | GH_TWOTAP | GH_THREETAP | GH_LONGPRESS); + this._stopLongpressTimeout(); + + if (this._tracked.length !== 1) { + this._state &= ~GH_DRAG; + } + if (this._tracked.length !== 2) { + this._state &= ~(GH_TWODRAG | GH_PINCH); + } + + // We need to figure out which of our different two touch gestures + // this might be + if (this._tracked.length === 2) { + // The other touch is the one where the id doesn't match + let prevTouch = this._tracked.find((t) => t.id !== id); + + // How far the previous touch point has moved since start + let prevDeltaMove = Math.hypot(prevTouch.firstX - prevTouch.lastX, prevTouch.firstY - prevTouch.lastY); + + // We know that the current touch moved far enough, + // but unless both touches moved further than their + // threshold we don't want to disqualify any gestures + if (prevDeltaMove > GH_MOVE_THRESHOLD) { + // The angle difference between the direction of the touch points + let deltaAngle = Math.abs(touch.angle - prevTouch.angle); + deltaAngle = Math.abs(((deltaAngle + 180) % 360) - 180); + + // PINCH or TWODRAG can be eliminated depending on the angle + if (deltaAngle > GH_ANGLE_THRESHOLD) { + this._state &= ~GH_TWODRAG; + } else { + this._state &= ~GH_PINCH; + } + + if (this._isTwoTouchTimeoutRunning()) { + this._stopTwoTouchTimeout(); + } + } else if (!this._isTwoTouchTimeoutRunning()) { + // We can't determine the gesture right now, let's + // wait and see if more events are on their way + this._startTwoTouchTimeout(); + } + } + + if (!this._hasDetectedGesture()) { + return; + } + + this._pushEvent('gesturestart'); + } + + this._pushEvent('gesturemove'); + } + + _touchEnd(id, x, y) { + // Check if this is an ignored touch + if (this._ignored.indexOf(id) !== -1) { + // Remove this touch from ignored + this._ignored.splice(this._ignored.indexOf(id), 1); + + // And reset the state if there are no more touches + if (this._ignored.length === 0 && this._tracked.length === 0) { + this._state = GH_INITSTATE; + this._waitingRelease = false; + } + return; + } + + // We got a touchend before the timer triggered, + // this cannot result in a gesture anymore. + if (!this._hasDetectedGesture() && this._isTwoTouchTimeoutRunning()) { + this._stopTwoTouchTimeout(); + this._state = GH_NOGESTURE; + } + + // Some gestures don't trigger until a touch is released + if (!this._hasDetectedGesture()) { + // Can't be a gesture that relies on movement + this._state &= ~(GH_DRAG | GH_TWODRAG | GH_PINCH); + // Or something that relies on more time + this._state &= ~GH_LONGPRESS; + this._stopLongpressTimeout(); + + if (!this._waitingRelease) { + this._releaseStart = Date.now(); + this._waitingRelease = true; + + // Can't be a tap that requires more touches than we current have + switch (this._tracked.length) { + case 1: + this._state &= ~(GH_TWOTAP | GH_THREETAP); + break; + + case 2: + this._state &= ~(GH_ONETAP | GH_THREETAP); + break; + } + } + } + + // Waiting for all touches to release? (i.e. some tap) + if (this._waitingRelease) { + // Were all touches released at roughly the same time? + if (Date.now() - this._releaseStart > GH_MULTITOUCH_TIMEOUT) { + this._state = GH_NOGESTURE; + } + + // Did too long time pass between press and release? + if (this._tracked.some((t) => Date.now() - t.started > GH_TAP_TIMEOUT)) { + this._state = GH_NOGESTURE; + } + + let touch = this._tracked.find((t) => t.id === id); + touch.active = false; + + // Are we still waiting for more releases? + if (this._hasDetectedGesture()) { + this._pushEvent('gesturestart'); + } else { + // Have we reached a dead end? + if (this._state !== GH_NOGESTURE) { + return; + } + } + } + + if (this._hasDetectedGesture()) { + this._pushEvent('gestureend'); + } + + // Ignore any remaining touches until they are ended + for (let i = 0; i < this._tracked.length; i++) { + if (this._tracked[i].active) { + this._ignored.push(this._tracked[i].id); + } + } + this._tracked = []; + + this._state = GH_NOGESTURE; + + // Remove this touch from ignored if it's in there + if (this._ignored.indexOf(id) !== -1) { + this._ignored.splice(this._ignored.indexOf(id), 1); + } + + // We reset the state if ignored is empty + if (this._ignored.length === 0) { + this._state = GH_INITSTATE; + this._waitingRelease = false; + } + } + + _hasDetectedGesture() { + if (this._state === GH_NOGESTURE) { + return false; + } + // Check to see if the bitmask value is a power of 2 + // (i.e. only one bit set). If it is, we have a state. + if (this._state & (this._state - 1)) { + return false; + } + + // For taps we also need to have all touches released + // before we've fully detected the gesture + if (this._state & (GH_ONETAP | GH_TWOTAP | GH_THREETAP)) { + if (this._tracked.some((t) => t.active)) { + return false; + } + } + + return true; + } + + _startLongpressTimeout() { + this._stopLongpressTimeout(); + this._longpressTimeoutId = setTimeout(() => this._longpressTimeout(), GH_LONGPRESS_TIMEOUT); + } + + _stopLongpressTimeout() { + clearTimeout(this._longpressTimeoutId); + this._longpressTimeoutId = null; + } + + _longpressTimeout() { + if (this._hasDetectedGesture()) { + throw new Error('A longpress gesture failed, conflict with a different gesture'); + } + + this._state = GH_LONGPRESS; + this._pushEvent('gesturestart'); + } + + _startTwoTouchTimeout() { + this._stopTwoTouchTimeout(); + this._twoTouchTimeoutId = setTimeout(() => this._twoTouchTimeout(), GH_TWOTOUCH_TIMEOUT); + } + + _stopTwoTouchTimeout() { + clearTimeout(this._twoTouchTimeoutId); + this._twoTouchTimeoutId = null; + } + + _isTwoTouchTimeoutRunning() { + return this._twoTouchTimeoutId !== null; + } + + _twoTouchTimeout() { + if (this._tracked.length === 0) { + throw new Error('A pinch or two drag gesture failed, no tracked touches'); + } + + // How far each touch point has moved since start + let avgM = this._getAverageMovement(); + let avgMoveH = Math.abs(avgM.x); + let avgMoveV = Math.abs(avgM.y); + + // The difference in the distance between where + // the touch points started and where they are now + let avgD = this._getAverageDistance(); + let deltaTouchDistance = Math.abs(Math.hypot(avgD.first.x, avgD.first.y) - Math.hypot(avgD.last.x, avgD.last.y)); + + if (avgMoveV < deltaTouchDistance && avgMoveH < deltaTouchDistance) { + this._state = GH_PINCH; + } else { + this._state = GH_TWODRAG; + } + + this._pushEvent('gesturestart'); + this._pushEvent('gesturemove'); + } + + _pushEvent(type) { + let detail = { type: this._stateToGesture(this._state) }; + + // For most gesture events the current (average) position is the + // most useful + let avg = this._getPosition(); + let pos = avg.last; + + // However we have a slight distance to detect gestures, so for the + // first gesture event we want to use the first positions we saw + if (type === 'gesturestart') { + pos = avg.first; + } + + // For these gestures, we always want the event coordinates + // to be where the gesture began, not the current touch location. + switch (this._state) { + case GH_TWODRAG: + case GH_PINCH: + pos = avg.first; + break; + } + + detail['clientX'] = pos.x; + detail['clientY'] = pos.y; + + // FIXME: other coordinates? + + // Some gestures also have a magnitude + if (this._state === GH_PINCH) { + let distance = this._getAverageDistance(); + if (type === 'gesturestart') { + detail['magnitudeX'] = distance.first.x; + detail['magnitudeY'] = distance.first.y; + } else { + detail['magnitudeX'] = distance.last.x; + detail['magnitudeY'] = distance.last.y; + } + } else if (this._state === GH_TWODRAG) { + if (type === 'gesturestart') { + detail['magnitudeX'] = 0.0; + detail['magnitudeY'] = 0.0; + } else { + let movement = this._getAverageMovement(); + detail['magnitudeX'] = movement.x; + detail['magnitudeY'] = movement.y; + } + } + + let gev = new CustomEvent(type, { detail: detail }); + this._target.dispatchEvent(gev); + } + + _stateToGesture(state) { + switch (state) { + case GH_ONETAP: + return 'onetap'; + case GH_TWOTAP: + return 'twotap'; + case GH_THREETAP: + return 'threetap'; + case GH_DRAG: + return 'drag'; + case GH_LONGPRESS: + return 'longpress'; + case GH_TWODRAG: + return 'twodrag'; + case GH_PINCH: + return 'pinch'; + } + + throw new Error('Unknown gesture state: ' + state); + } + + _getPosition() { + if (this._tracked.length === 0) { + throw new Error('Failed to get gesture position, no tracked touches'); + } + + let size = this._tracked.length; + let fx = 0, + fy = 0, + lx = 0, + ly = 0; + + for (let i = 0; i < this._tracked.length; i++) { + fx += this._tracked[i].firstX; + fy += this._tracked[i].firstY; + lx += this._tracked[i].lastX; + ly += this._tracked[i].lastY; + } + + return { first: { x: fx / size, y: fy / size }, last: { x: lx / size, y: ly / size } }; + } + + _getAverageMovement() { + if (this._tracked.length === 0) { + throw new Error('Failed to get gesture movement, no tracked touches'); + } + + let totalH, totalV; + totalH = totalV = 0; + let size = this._tracked.length; + + for (let i = 0; i < this._tracked.length; i++) { + totalH += this._tracked[i].lastX - this._tracked[i].firstX; + totalV += this._tracked[i].lastY - this._tracked[i].firstY; + } + + return { x: totalH / size, y: totalV / size }; + } + + _getAverageDistance() { + if (this._tracked.length === 0) { + throw new Error('Failed to get gesture distance, no tracked touches'); + } + + // Distance between the first and last tracked touches + + let first = this._tracked[0]; + let last = this._tracked[this._tracked.length - 1]; + + let fdx = Math.abs(last.firstX - first.firstX); + let fdy = Math.abs(last.firstY - first.firstY); + + let ldx = Math.abs(last.lastX - first.lastX); + let ldy = Math.abs(last.lastY - first.lastY); + + return { first: { x: fdx, y: fdy }, last: { x: ldx, y: ldy } }; + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/keyboard.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/keyboard.js new file mode 100644 index 000000000..125151522 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/keyboard.js @@ -0,0 +1,278 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +import * as Log from '../util/logging.js'; +import { stopEvent } from '../util/events.js'; +import * as KeyboardUtil from './util.js'; +import KeyTable from './keysym.js'; +import * as browser from '../util/browser.js'; + +// +// Keyboard event handler +// + +export default class Keyboard { + constructor(target) { + this._target = target || null; + + this._keyDownList = {}; // List of depressed keys + // (even if they are happy) + this._altGrArmed = false; // Windows AltGr detection + + // keep these here so we can refer to them later + this._eventHandlers = { + keyup: this._handleKeyUp.bind(this), + keydown: this._handleKeyDown.bind(this), + blur: this._allKeysUp.bind(this), + }; + + // ===== EVENT HANDLERS ===== + + this.onkeyevent = () => {}; // Handler for key press/release + } + + // ===== PRIVATE METHODS ===== + + _sendKeyEvent(keysym, code, down) { + if (down) { + this._keyDownList[code] = keysym; + } else { + // Do we really think this key is down? + if (!(code in this._keyDownList)) { + return; + } + delete this._keyDownList[code]; + } + + Log.Debug('onkeyevent ' + (down ? 'down' : 'up') + ', keysym: ' + keysym, ', code: ' + code); + this.onkeyevent(keysym, code, down); + } + + _getKeyCode(e) { + const code = KeyboardUtil.getKeycode(e); + if (code !== 'Unidentified') { + return code; + } + + // Unstable, but we don't have anything else to go on + if (e.keyCode) { + // 229 is used for composition events + if (e.keyCode !== 229) { + return 'Platform' + e.keyCode; + } + } + + // A precursor to the final DOM3 standard. Unfortunately it + // is not layout independent, so it is as bad as using keyCode + if (e.keyIdentifier) { + // Non-character key? + if (e.keyIdentifier.substr(0, 2) !== 'U+') { + return e.keyIdentifier; + } + + const codepoint = parseInt(e.keyIdentifier.substr(2), 16); + const char = String.fromCharCode(codepoint).toUpperCase(); + + return 'Platform' + char.charCodeAt(); + } + + return 'Unidentified'; + } + + _handleKeyDown(e) { + const code = this._getKeyCode(e); + let keysym = KeyboardUtil.getKeysym(e); + + // Windows doesn't have a proper AltGr, but handles it using + // fake Ctrl+Alt. However the remote end might not be Windows, + // so we need to merge those in to a single AltGr event. We + // detect this case by seeing the two key events directly after + // each other with a very short time between them (<50ms). + if (this._altGrArmed) { + this._altGrArmed = false; + clearTimeout(this._altGrTimeout); + + if (code === 'AltRight' && e.timeStamp - this._altGrCtrlTime < 50) { + // FIXME: We fail to detect this if either Ctrl key is + // first manually pressed as Windows then no + // longer sends the fake Ctrl down event. It + // does however happily send real Ctrl events + // even when AltGr is already down. Some + // browsers detect this for us though and set the + // key to "AltGraph". + keysym = KeyTable.XK_ISO_Level3_Shift; + } else { + this._sendKeyEvent(KeyTable.XK_Control_L, 'ControlLeft', true); + } + } + + // We cannot handle keys we cannot track, but we also need + // to deal with virtual keyboards which omit key info + if (code === 'Unidentified') { + if (keysym) { + // If it's a virtual keyboard then it should be + // sufficient to just send press and release right + // after each other + this._sendKeyEvent(keysym, code, true); + this._sendKeyEvent(keysym, code, false); + } + + stopEvent(e); + return; + } + + // Alt behaves more like AltGraph on macOS, so shuffle the + // keys around a bit to make things more sane for the remote + // server. This method is used by RealVNC and TigerVNC (and + // possibly others). + if (browser.isMac() || browser.isIOS()) { + switch (keysym) { + case KeyTable.XK_Super_L: + keysym = KeyTable.XK_Alt_L; + break; + case KeyTable.XK_Super_R: + keysym = KeyTable.XK_Super_L; + break; + case KeyTable.XK_Alt_L: + keysym = KeyTable.XK_Mode_switch; + break; + case KeyTable.XK_Alt_R: + keysym = KeyTable.XK_ISO_Level3_Shift; + break; + } + } + + // Is this key already pressed? If so, then we must use the + // same keysym or we'll confuse the server + if (code in this._keyDownList) { + keysym = this._keyDownList[code]; + } + + // macOS doesn't send proper key releases if a key is pressed + // while meta is held down + if ((browser.isMac() || browser.isIOS()) && e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight') { + this._sendKeyEvent(keysym, code, true); + this._sendKeyEvent(keysym, code, false); + stopEvent(e); + return; + } + + // macOS doesn't send proper key events for modifiers, only + // state change events. That gets extra confusing for CapsLock + // which toggles on each press, but not on release. So pretend + // it was a quick press and release of the button. + if ((browser.isMac() || browser.isIOS()) && code === 'CapsLock') { + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); + stopEvent(e); + return; + } + + // Windows doesn't send proper key releases for a bunch of + // Japanese IM keys so we have to fake the release right away + const jpBadKeys = [ + KeyTable.XK_Zenkaku_Hankaku, + KeyTable.XK_Eisu_toggle, + KeyTable.XK_Katakana, + KeyTable.XK_Hiragana, + KeyTable.XK_Romaji, + ]; + if (browser.isWindows() && jpBadKeys.includes(keysym)) { + this._sendKeyEvent(keysym, code, true); + this._sendKeyEvent(keysym, code, false); + stopEvent(e); + return; + } + + stopEvent(e); + + // Possible start of AltGr sequence? (see above) + if (code === 'ControlLeft' && browser.isWindows() && !('ControlLeft' in this._keyDownList)) { + this._altGrArmed = true; + this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100); + this._altGrCtrlTime = e.timeStamp; + return; + } + + this._sendKeyEvent(keysym, code, true); + } + + _handleKeyUp(e) { + stopEvent(e); + + const code = this._getKeyCode(e); + + // We can't get a release in the middle of an AltGr sequence, so + // abort that detection + if (this._altGrArmed) { + this._altGrArmed = false; + clearTimeout(this._altGrTimeout); + this._sendKeyEvent(KeyTable.XK_Control_L, 'ControlLeft', true); + } + + // See comment in _handleKeyDown() + if ((browser.isMac() || browser.isIOS()) && code === 'CapsLock') { + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); + return; + } + + this._sendKeyEvent(this._keyDownList[code], code, false); + + // Windows has a rather nasty bug where it won't send key + // release events for a Shift button if the other Shift is still + // pressed + if (browser.isWindows() && (code === 'ShiftLeft' || code === 'ShiftRight')) { + if ('ShiftRight' in this._keyDownList) { + this._sendKeyEvent(this._keyDownList['ShiftRight'], 'ShiftRight', false); + } + if ('ShiftLeft' in this._keyDownList) { + this._sendKeyEvent(this._keyDownList['ShiftLeft'], 'ShiftLeft', false); + } + } + } + + _handleAltGrTimeout() { + this._altGrArmed = false; + clearTimeout(this._altGrTimeout); + this._sendKeyEvent(KeyTable.XK_Control_L, 'ControlLeft', true); + } + + _allKeysUp() { + Log.Debug('>> Keyboard.allKeysUp'); + for (let code in this._keyDownList) { + this._sendKeyEvent(this._keyDownList[code], code, false); + } + Log.Debug('<< Keyboard.allKeysUp'); + } + + // ===== PUBLIC METHODS ===== + + grab() { + //Log.Debug(">> Keyboard.grab"); + + this._target.addEventListener('keydown', this._eventHandlers.keydown); + this._target.addEventListener('keyup', this._eventHandlers.keyup); + + // Release (key up) if window loses focus + window.addEventListener('blur', this._eventHandlers.blur); + + //Log.Debug("<< Keyboard.grab"); + } + + ungrab() { + //Log.Debug(">> Keyboard.ungrab"); + + this._target.removeEventListener('keydown', this._eventHandlers.keydown); + this._target.removeEventListener('keyup', this._eventHandlers.keyup); + window.removeEventListener('blur', this._eventHandlers.blur); + + // Release (key up) all keys that are in a down state + this._allKeysUp(); + + //Log.Debug(">> Keyboard.ungrab"); + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/keysym.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/keysym.js new file mode 100644 index 000000000..98b61e982 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/keysym.js @@ -0,0 +1,615 @@ +/* eslint-disable key-spacing */ + +export default { + XK_VoidSymbol: 0xffffff /* Void symbol */, + + XK_BackSpace: 0xff08 /* Back space, back char */, + XK_Tab: 0xff09, + XK_Linefeed: 0xff0a /* Linefeed, LF */, + XK_Clear: 0xff0b, + XK_Return: 0xff0d /* Return, enter */, + XK_Pause: 0xff13 /* Pause, hold */, + XK_Scroll_Lock: 0xff14, + XK_Sys_Req: 0xff15, + XK_Escape: 0xff1b, + XK_Delete: 0xffff /* Delete, rubout */, + + /* International & multi-key character composition */ + + XK_Multi_key: 0xff20 /* Multi-key character compose */, + XK_Codeinput: 0xff37, + XK_SingleCandidate: 0xff3c, + XK_MultipleCandidate: 0xff3d, + XK_PreviousCandidate: 0xff3e, + + /* Japanese keyboard support */ + + XK_Kanji: 0xff21 /* Kanji, Kanji convert */, + XK_Muhenkan: 0xff22 /* Cancel Conversion */, + XK_Henkan_Mode: 0xff23 /* Start/Stop Conversion */, + XK_Henkan: 0xff23 /* Alias for Henkan_Mode */, + XK_Romaji: 0xff24 /* to Romaji */, + XK_Hiragana: 0xff25 /* to Hiragana */, + XK_Katakana: 0xff26 /* to Katakana */, + XK_Hiragana_Katakana: 0xff27 /* Hiragana/Katakana toggle */, + XK_Zenkaku: 0xff28 /* to Zenkaku */, + XK_Hankaku: 0xff29 /* to Hankaku */, + XK_Zenkaku_Hankaku: 0xff2a /* Zenkaku/Hankaku toggle */, + XK_Touroku: 0xff2b /* Add to Dictionary */, + XK_Massyo: 0xff2c /* Delete from Dictionary */, + XK_Kana_Lock: 0xff2d /* Kana Lock */, + XK_Kana_Shift: 0xff2e /* Kana Shift */, + XK_Eisu_Shift: 0xff2f /* Alphanumeric Shift */, + XK_Eisu_toggle: 0xff30 /* Alphanumeric toggle */, + XK_Kanji_Bangou: 0xff37 /* Codeinput */, + XK_Zen_Koho: 0xff3d /* Multiple/All Candidate(s) */, + XK_Mae_Koho: 0xff3e /* Previous Candidate */, + + /* Cursor control & motion */ + + XK_Home: 0xff50, + XK_Left: 0xff51 /* Move left, left arrow */, + XK_Up: 0xff52 /* Move up, up arrow */, + XK_Right: 0xff53 /* Move right, right arrow */, + XK_Down: 0xff54 /* Move down, down arrow */, + XK_Prior: 0xff55 /* Prior, previous */, + XK_Page_Up: 0xff55, + XK_Next: 0xff56 /* Next */, + XK_Page_Down: 0xff56, + XK_End: 0xff57 /* EOL */, + XK_Begin: 0xff58 /* BOL */, + + /* Misc functions */ + + XK_Select: 0xff60 /* Select, mark */, + XK_Print: 0xff61, + XK_Execute: 0xff62 /* Execute, run, do */, + XK_Insert: 0xff63 /* Insert, insert here */, + XK_Undo: 0xff65, + XK_Redo: 0xff66 /* Redo, again */, + XK_Menu: 0xff67, + XK_Find: 0xff68 /* Find, search */, + XK_Cancel: 0xff69 /* Cancel, stop, abort, exit */, + XK_Help: 0xff6a /* Help */, + XK_Break: 0xff6b, + XK_Mode_switch: 0xff7e /* Character set switch */, + XK_script_switch: 0xff7e /* Alias for mode_switch */, + XK_Num_Lock: 0xff7f, + + /* Keypad functions, keypad numbers cleverly chosen to map to ASCII */ + + XK_KP_Space: 0xff80 /* Space */, + XK_KP_Tab: 0xff89, + XK_KP_Enter: 0xff8d /* Enter */, + XK_KP_F1: 0xff91 /* PF1, KP_A, ... */, + XK_KP_F2: 0xff92, + XK_KP_F3: 0xff93, + XK_KP_F4: 0xff94, + XK_KP_Home: 0xff95, + XK_KP_Left: 0xff96, + XK_KP_Up: 0xff97, + XK_KP_Right: 0xff98, + XK_KP_Down: 0xff99, + XK_KP_Prior: 0xff9a, + XK_KP_Page_Up: 0xff9a, + XK_KP_Next: 0xff9b, + XK_KP_Page_Down: 0xff9b, + XK_KP_End: 0xff9c, + XK_KP_Begin: 0xff9d, + XK_KP_Insert: 0xff9e, + XK_KP_Delete: 0xff9f, + XK_KP_Equal: 0xffbd /* Equals */, + XK_KP_Multiply: 0xffaa, + XK_KP_Add: 0xffab, + XK_KP_Separator: 0xffac /* Separator, often comma */, + XK_KP_Subtract: 0xffad, + XK_KP_Decimal: 0xffae, + XK_KP_Divide: 0xffaf, + + XK_KP_0: 0xffb0, + XK_KP_1: 0xffb1, + XK_KP_2: 0xffb2, + XK_KP_3: 0xffb3, + XK_KP_4: 0xffb4, + XK_KP_5: 0xffb5, + XK_KP_6: 0xffb6, + XK_KP_7: 0xffb7, + XK_KP_8: 0xffb8, + XK_KP_9: 0xffb9, + + /* + * Auxiliary functions; note the duplicate definitions for left and right + * function keys; Sun keyboards and a few other manufacturers have such + * function key groups on the left and/or right sides of the keyboard. + * We've not found a keyboard with more than 35 function keys total. + */ + + XK_F1: 0xffbe, + XK_F2: 0xffbf, + XK_F3: 0xffc0, + XK_F4: 0xffc1, + XK_F5: 0xffc2, + XK_F6: 0xffc3, + XK_F7: 0xffc4, + XK_F8: 0xffc5, + XK_F9: 0xffc6, + XK_F10: 0xffc7, + XK_F11: 0xffc8, + XK_L1: 0xffc8, + XK_F12: 0xffc9, + XK_L2: 0xffc9, + XK_F13: 0xffca, + XK_L3: 0xffca, + XK_F14: 0xffcb, + XK_L4: 0xffcb, + XK_F15: 0xffcc, + XK_L5: 0xffcc, + XK_F16: 0xffcd, + XK_L6: 0xffcd, + XK_F17: 0xffce, + XK_L7: 0xffce, + XK_F18: 0xffcf, + XK_L8: 0xffcf, + XK_F19: 0xffd0, + XK_L9: 0xffd0, + XK_F20: 0xffd1, + XK_L10: 0xffd1, + XK_F21: 0xffd2, + XK_R1: 0xffd2, + XK_F22: 0xffd3, + XK_R2: 0xffd3, + XK_F23: 0xffd4, + XK_R3: 0xffd4, + XK_F24: 0xffd5, + XK_R4: 0xffd5, + XK_F25: 0xffd6, + XK_R5: 0xffd6, + XK_F26: 0xffd7, + XK_R6: 0xffd7, + XK_F27: 0xffd8, + XK_R7: 0xffd8, + XK_F28: 0xffd9, + XK_R8: 0xffd9, + XK_F29: 0xffda, + XK_R9: 0xffda, + XK_F30: 0xffdb, + XK_R10: 0xffdb, + XK_F31: 0xffdc, + XK_R11: 0xffdc, + XK_F32: 0xffdd, + XK_R12: 0xffdd, + XK_F33: 0xffde, + XK_R13: 0xffde, + XK_F34: 0xffdf, + XK_R14: 0xffdf, + XK_F35: 0xffe0, + XK_R15: 0xffe0, + + /* Modifiers */ + + XK_Shift_L: 0xffe1 /* Left shift */, + XK_Shift_R: 0xffe2 /* Right shift */, + XK_Control_L: 0xffe3 /* Left control */, + XK_Control_R: 0xffe4 /* Right control */, + XK_Caps_Lock: 0xffe5 /* Caps lock */, + XK_Shift_Lock: 0xffe6 /* Shift lock */, + + XK_Meta_L: 0xffe7 /* Left meta */, + XK_Meta_R: 0xffe8 /* Right meta */, + XK_Alt_L: 0xffe9 /* Left alt */, + XK_Alt_R: 0xffea /* Right alt */, + XK_Super_L: 0xffeb /* Left super */, + XK_Super_R: 0xffec /* Right super */, + XK_Hyper_L: 0xffed /* Left hyper */, + XK_Hyper_R: 0xffee /* Right hyper */, + + /* + * Keyboard (XKB) Extension function and modifier keys + * (from Appendix C of "The X Keyboard Extension: Protocol Specification") + * Byte 3 = 0xfe + */ + + XK_ISO_Level3_Shift: 0xfe03 /* AltGr */, + XK_ISO_Next_Group: 0xfe08, + XK_ISO_Prev_Group: 0xfe0a, + XK_ISO_First_Group: 0xfe0c, + XK_ISO_Last_Group: 0xfe0e, + + /* + * Latin 1 + * (ISO/IEC 8859-1: Unicode U+0020..U+00FF) + * Byte 3: 0 + */ + + XK_space: 0x0020 /* U+0020 SPACE */, + XK_exclam: 0x0021 /* U+0021 EXCLAMATION MARK */, + XK_quotedbl: 0x0022 /* U+0022 QUOTATION MARK */, + XK_numbersign: 0x0023 /* U+0023 NUMBER SIGN */, + XK_dollar: 0x0024 /* U+0024 DOLLAR SIGN */, + XK_percent: 0x0025 /* U+0025 PERCENT SIGN */, + XK_ampersand: 0x0026 /* U+0026 AMPERSAND */, + XK_apostrophe: 0x0027 /* U+0027 APOSTROPHE */, + XK_quoteright: 0x0027 /* deprecated */, + XK_parenleft: 0x0028 /* U+0028 LEFT PARENTHESIS */, + XK_parenright: 0x0029 /* U+0029 RIGHT PARENTHESIS */, + XK_asterisk: 0x002a /* U+002A ASTERISK */, + XK_plus: 0x002b /* U+002B PLUS SIGN */, + XK_comma: 0x002c /* U+002C COMMA */, + XK_minus: 0x002d /* U+002D HYPHEN-MINUS */, + XK_period: 0x002e /* U+002E FULL STOP */, + XK_slash: 0x002f /* U+002F SOLIDUS */, + XK_0: 0x0030 /* U+0030 DIGIT ZERO */, + XK_1: 0x0031 /* U+0031 DIGIT ONE */, + XK_2: 0x0032 /* U+0032 DIGIT TWO */, + XK_3: 0x0033 /* U+0033 DIGIT THREE */, + XK_4: 0x0034 /* U+0034 DIGIT FOUR */, + XK_5: 0x0035 /* U+0035 DIGIT FIVE */, + XK_6: 0x0036 /* U+0036 DIGIT SIX */, + XK_7: 0x0037 /* U+0037 DIGIT SEVEN */, + XK_8: 0x0038 /* U+0038 DIGIT EIGHT */, + XK_9: 0x0039 /* U+0039 DIGIT NINE */, + XK_colon: 0x003a /* U+003A COLON */, + XK_semicolon: 0x003b /* U+003B SEMICOLON */, + XK_less: 0x003c /* U+003C LESS-THAN SIGN */, + XK_equal: 0x003d /* U+003D EQUALS SIGN */, + XK_greater: 0x003e /* U+003E GREATER-THAN SIGN */, + XK_question: 0x003f /* U+003F QUESTION MARK */, + XK_at: 0x0040 /* U+0040 COMMERCIAL AT */, + XK_A: 0x0041 /* U+0041 LATIN CAPITAL LETTER A */, + XK_B: 0x0042 /* U+0042 LATIN CAPITAL LETTER B */, + XK_C: 0x0043 /* U+0043 LATIN CAPITAL LETTER C */, + XK_D: 0x0044 /* U+0044 LATIN CAPITAL LETTER D */, + XK_E: 0x0045 /* U+0045 LATIN CAPITAL LETTER E */, + XK_F: 0x0046 /* U+0046 LATIN CAPITAL LETTER F */, + XK_G: 0x0047 /* U+0047 LATIN CAPITAL LETTER G */, + XK_H: 0x0048 /* U+0048 LATIN CAPITAL LETTER H */, + XK_I: 0x0049 /* U+0049 LATIN CAPITAL LETTER I */, + XK_J: 0x004a /* U+004A LATIN CAPITAL LETTER J */, + XK_K: 0x004b /* U+004B LATIN CAPITAL LETTER K */, + XK_L: 0x004c /* U+004C LATIN CAPITAL LETTER L */, + XK_M: 0x004d /* U+004D LATIN CAPITAL LETTER M */, + XK_N: 0x004e /* U+004E LATIN CAPITAL LETTER N */, + XK_O: 0x004f /* U+004F LATIN CAPITAL LETTER O */, + XK_P: 0x0050 /* U+0050 LATIN CAPITAL LETTER P */, + XK_Q: 0x0051 /* U+0051 LATIN CAPITAL LETTER Q */, + XK_R: 0x0052 /* U+0052 LATIN CAPITAL LETTER R */, + XK_S: 0x0053 /* U+0053 LATIN CAPITAL LETTER S */, + XK_T: 0x0054 /* U+0054 LATIN CAPITAL LETTER T */, + XK_U: 0x0055 /* U+0055 LATIN CAPITAL LETTER U */, + XK_V: 0x0056 /* U+0056 LATIN CAPITAL LETTER V */, + XK_W: 0x0057 /* U+0057 LATIN CAPITAL LETTER W */, + XK_X: 0x0058 /* U+0058 LATIN CAPITAL LETTER X */, + XK_Y: 0x0059 /* U+0059 LATIN CAPITAL LETTER Y */, + XK_Z: 0x005a /* U+005A LATIN CAPITAL LETTER Z */, + XK_bracketleft: 0x005b /* U+005B LEFT SQUARE BRACKET */, + XK_backslash: 0x005c /* U+005C REVERSE SOLIDUS */, + XK_bracketright: 0x005d /* U+005D RIGHT SQUARE BRACKET */, + XK_asciicircum: 0x005e /* U+005E CIRCUMFLEX ACCENT */, + XK_underscore: 0x005f /* U+005F LOW LINE */, + XK_grave: 0x0060 /* U+0060 GRAVE ACCENT */, + XK_quoteleft: 0x0060 /* deprecated */, + XK_a: 0x0061 /* U+0061 LATIN SMALL LETTER A */, + XK_b: 0x0062 /* U+0062 LATIN SMALL LETTER B */, + XK_c: 0x0063 /* U+0063 LATIN SMALL LETTER C */, + XK_d: 0x0064 /* U+0064 LATIN SMALL LETTER D */, + XK_e: 0x0065 /* U+0065 LATIN SMALL LETTER E */, + XK_f: 0x0066 /* U+0066 LATIN SMALL LETTER F */, + XK_g: 0x0067 /* U+0067 LATIN SMALL LETTER G */, + XK_h: 0x0068 /* U+0068 LATIN SMALL LETTER H */, + XK_i: 0x0069 /* U+0069 LATIN SMALL LETTER I */, + XK_j: 0x006a /* U+006A LATIN SMALL LETTER J */, + XK_k: 0x006b /* U+006B LATIN SMALL LETTER K */, + XK_l: 0x006c /* U+006C LATIN SMALL LETTER L */, + XK_m: 0x006d /* U+006D LATIN SMALL LETTER M */, + XK_n: 0x006e /* U+006E LATIN SMALL LETTER N */, + XK_o: 0x006f /* U+006F LATIN SMALL LETTER O */, + XK_p: 0x0070 /* U+0070 LATIN SMALL LETTER P */, + XK_q: 0x0071 /* U+0071 LATIN SMALL LETTER Q */, + XK_r: 0x0072 /* U+0072 LATIN SMALL LETTER R */, + XK_s: 0x0073 /* U+0073 LATIN SMALL LETTER S */, + XK_t: 0x0074 /* U+0074 LATIN SMALL LETTER T */, + XK_u: 0x0075 /* U+0075 LATIN SMALL LETTER U */, + XK_v: 0x0076 /* U+0076 LATIN SMALL LETTER V */, + XK_w: 0x0077 /* U+0077 LATIN SMALL LETTER W */, + XK_x: 0x0078 /* U+0078 LATIN SMALL LETTER X */, + XK_y: 0x0079 /* U+0079 LATIN SMALL LETTER Y */, + XK_z: 0x007a /* U+007A LATIN SMALL LETTER Z */, + XK_braceleft: 0x007b /* U+007B LEFT CURLY BRACKET */, + XK_bar: 0x007c /* U+007C VERTICAL LINE */, + XK_braceright: 0x007d /* U+007D RIGHT CURLY BRACKET */, + XK_asciitilde: 0x007e /* U+007E TILDE */, + + XK_nobreakspace: 0x00a0 /* U+00A0 NO-BREAK SPACE */, + XK_exclamdown: 0x00a1 /* U+00A1 INVERTED EXCLAMATION MARK */, + XK_cent: 0x00a2 /* U+00A2 CENT SIGN */, + XK_sterling: 0x00a3 /* U+00A3 POUND SIGN */, + XK_currency: 0x00a4 /* U+00A4 CURRENCY SIGN */, + XK_yen: 0x00a5 /* U+00A5 YEN SIGN */, + XK_brokenbar: 0x00a6 /* U+00A6 BROKEN BAR */, + XK_section: 0x00a7 /* U+00A7 SECTION SIGN */, + XK_diaeresis: 0x00a8 /* U+00A8 DIAERESIS */, + XK_copyright: 0x00a9 /* U+00A9 COPYRIGHT SIGN */, + XK_ordfeminine: 0x00aa /* U+00AA FEMININE ORDINAL INDICATOR */, + XK_guillemotleft: 0x00ab /* U+00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK */, + XK_notsign: 0x00ac /* U+00AC NOT SIGN */, + XK_hyphen: 0x00ad /* U+00AD SOFT HYPHEN */, + XK_registered: 0x00ae /* U+00AE REGISTERED SIGN */, + XK_macron: 0x00af /* U+00AF MACRON */, + XK_degree: 0x00b0 /* U+00B0 DEGREE SIGN */, + XK_plusminus: 0x00b1 /* U+00B1 PLUS-MINUS SIGN */, + XK_twosuperior: 0x00b2 /* U+00B2 SUPERSCRIPT TWO */, + XK_threesuperior: 0x00b3 /* U+00B3 SUPERSCRIPT THREE */, + XK_acute: 0x00b4 /* U+00B4 ACUTE ACCENT */, + XK_mu: 0x00b5 /* U+00B5 MICRO SIGN */, + XK_paragraph: 0x00b6 /* U+00B6 PILCROW SIGN */, + XK_periodcentered: 0x00b7 /* U+00B7 MIDDLE DOT */, + XK_cedilla: 0x00b8 /* U+00B8 CEDILLA */, + XK_onesuperior: 0x00b9 /* U+00B9 SUPERSCRIPT ONE */, + XK_masculine: 0x00ba /* U+00BA MASCULINE ORDINAL INDICATOR */, + XK_guillemotright: 0x00bb /* U+00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK */, + XK_onequarter: 0x00bc /* U+00BC VULGAR FRACTION ONE QUARTER */, + XK_onehalf: 0x00bd /* U+00BD VULGAR FRACTION ONE HALF */, + XK_threequarters: 0x00be /* U+00BE VULGAR FRACTION THREE QUARTERS */, + XK_questiondown: 0x00bf /* U+00BF INVERTED QUESTION MARK */, + XK_Agrave: 0x00c0 /* U+00C0 LATIN CAPITAL LETTER A WITH GRAVE */, + XK_Aacute: 0x00c1 /* U+00C1 LATIN CAPITAL LETTER A WITH ACUTE */, + XK_Acircumflex: 0x00c2 /* U+00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX */, + XK_Atilde: 0x00c3 /* U+00C3 LATIN CAPITAL LETTER A WITH TILDE */, + XK_Adiaeresis: 0x00c4 /* U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS */, + XK_Aring: 0x00c5 /* U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE */, + XK_AE: 0x00c6 /* U+00C6 LATIN CAPITAL LETTER AE */, + XK_Ccedilla: 0x00c7 /* U+00C7 LATIN CAPITAL LETTER C WITH CEDILLA */, + XK_Egrave: 0x00c8 /* U+00C8 LATIN CAPITAL LETTER E WITH GRAVE */, + XK_Eacute: 0x00c9 /* U+00C9 LATIN CAPITAL LETTER E WITH ACUTE */, + XK_Ecircumflex: 0x00ca /* U+00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX */, + XK_Ediaeresis: 0x00cb /* U+00CB LATIN CAPITAL LETTER E WITH DIAERESIS */, + XK_Igrave: 0x00cc /* U+00CC LATIN CAPITAL LETTER I WITH GRAVE */, + XK_Iacute: 0x00cd /* U+00CD LATIN CAPITAL LETTER I WITH ACUTE */, + XK_Icircumflex: 0x00ce /* U+00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX */, + XK_Idiaeresis: 0x00cf /* U+00CF LATIN CAPITAL LETTER I WITH DIAERESIS */, + XK_ETH: 0x00d0 /* U+00D0 LATIN CAPITAL LETTER ETH */, + XK_Eth: 0x00d0 /* deprecated */, + XK_Ntilde: 0x00d1 /* U+00D1 LATIN CAPITAL LETTER N WITH TILDE */, + XK_Ograve: 0x00d2 /* U+00D2 LATIN CAPITAL LETTER O WITH GRAVE */, + XK_Oacute: 0x00d3 /* U+00D3 LATIN CAPITAL LETTER O WITH ACUTE */, + XK_Ocircumflex: 0x00d4 /* U+00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX */, + XK_Otilde: 0x00d5 /* U+00D5 LATIN CAPITAL LETTER O WITH TILDE */, + XK_Odiaeresis: 0x00d6 /* U+00D6 LATIN CAPITAL LETTER O WITH DIAERESIS */, + XK_multiply: 0x00d7 /* U+00D7 MULTIPLICATION SIGN */, + XK_Oslash: 0x00d8 /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */, + XK_Ooblique: 0x00d8 /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */, + XK_Ugrave: 0x00d9 /* U+00D9 LATIN CAPITAL LETTER U WITH GRAVE */, + XK_Uacute: 0x00da /* U+00DA LATIN CAPITAL LETTER U WITH ACUTE */, + XK_Ucircumflex: 0x00db /* U+00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX */, + XK_Udiaeresis: 0x00dc /* U+00DC LATIN CAPITAL LETTER U WITH DIAERESIS */, + XK_Yacute: 0x00dd /* U+00DD LATIN CAPITAL LETTER Y WITH ACUTE */, + XK_THORN: 0x00de /* U+00DE LATIN CAPITAL LETTER THORN */, + XK_Thorn: 0x00de /* deprecated */, + XK_ssharp: 0x00df /* U+00DF LATIN SMALL LETTER SHARP S */, + XK_agrave: 0x00e0 /* U+00E0 LATIN SMALL LETTER A WITH GRAVE */, + XK_aacute: 0x00e1 /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */, + XK_acircumflex: 0x00e2 /* U+00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX */, + XK_atilde: 0x00e3 /* U+00E3 LATIN SMALL LETTER A WITH TILDE */, + XK_adiaeresis: 0x00e4 /* U+00E4 LATIN SMALL LETTER A WITH DIAERESIS */, + XK_aring: 0x00e5 /* U+00E5 LATIN SMALL LETTER A WITH RING ABOVE */, + XK_ae: 0x00e6 /* U+00E6 LATIN SMALL LETTER AE */, + XK_ccedilla: 0x00e7 /* U+00E7 LATIN SMALL LETTER C WITH CEDILLA */, + XK_egrave: 0x00e8 /* U+00E8 LATIN SMALL LETTER E WITH GRAVE */, + XK_eacute: 0x00e9 /* U+00E9 LATIN SMALL LETTER E WITH ACUTE */, + XK_ecircumflex: 0x00ea /* U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX */, + XK_ediaeresis: 0x00eb /* U+00EB LATIN SMALL LETTER E WITH DIAERESIS */, + XK_igrave: 0x00ec /* U+00EC LATIN SMALL LETTER I WITH GRAVE */, + XK_iacute: 0x00ed /* U+00ED LATIN SMALL LETTER I WITH ACUTE */, + XK_icircumflex: 0x00ee /* U+00EE LATIN SMALL LETTER I WITH CIRCUMFLEX */, + XK_idiaeresis: 0x00ef /* U+00EF LATIN SMALL LETTER I WITH DIAERESIS */, + XK_eth: 0x00f0 /* U+00F0 LATIN SMALL LETTER ETH */, + XK_ntilde: 0x00f1 /* U+00F1 LATIN SMALL LETTER N WITH TILDE */, + XK_ograve: 0x00f2 /* U+00F2 LATIN SMALL LETTER O WITH GRAVE */, + XK_oacute: 0x00f3 /* U+00F3 LATIN SMALL LETTER O WITH ACUTE */, + XK_ocircumflex: 0x00f4 /* U+00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX */, + XK_otilde: 0x00f5 /* U+00F5 LATIN SMALL LETTER O WITH TILDE */, + XK_odiaeresis: 0x00f6 /* U+00F6 LATIN SMALL LETTER O WITH DIAERESIS */, + XK_division: 0x00f7 /* U+00F7 DIVISION SIGN */, + XK_oslash: 0x00f8 /* U+00F8 LATIN SMALL LETTER O WITH STROKE */, + XK_ooblique: 0x00f8 /* U+00F8 LATIN SMALL LETTER O WITH STROKE */, + XK_ugrave: 0x00f9 /* U+00F9 LATIN SMALL LETTER U WITH GRAVE */, + XK_uacute: 0x00fa /* U+00FA LATIN SMALL LETTER U WITH ACUTE */, + XK_ucircumflex: 0x00fb /* U+00FB LATIN SMALL LETTER U WITH CIRCUMFLEX */, + XK_udiaeresis: 0x00fc /* U+00FC LATIN SMALL LETTER U WITH DIAERESIS */, + XK_yacute: 0x00fd /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */, + XK_thorn: 0x00fe /* U+00FE LATIN SMALL LETTER THORN */, + XK_ydiaeresis: 0x00ff /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */, + + /* + * Korean + * Byte 3 = 0x0e + */ + + XK_Hangul: 0xff31 /* Hangul start/stop(toggle) */, + XK_Hangul_Hanja: 0xff34 /* Start Hangul->Hanja Conversion */, + XK_Hangul_Jeonja: 0xff38 /* Jeonja mode */, + + /* + * XFree86 vendor specific keysyms. + * + * The XFree86 keysym range is 0x10080001 - 0x1008FFFF. + */ + + XF86XK_ModeLock: 0x1008ff01, + XF86XK_MonBrightnessUp: 0x1008ff02, + XF86XK_MonBrightnessDown: 0x1008ff03, + XF86XK_KbdLightOnOff: 0x1008ff04, + XF86XK_KbdBrightnessUp: 0x1008ff05, + XF86XK_KbdBrightnessDown: 0x1008ff06, + XF86XK_Standby: 0x1008ff10, + XF86XK_AudioLowerVolume: 0x1008ff11, + XF86XK_AudioMute: 0x1008ff12, + XF86XK_AudioRaiseVolume: 0x1008ff13, + XF86XK_AudioPlay: 0x1008ff14, + XF86XK_AudioStop: 0x1008ff15, + XF86XK_AudioPrev: 0x1008ff16, + XF86XK_AudioNext: 0x1008ff17, + XF86XK_HomePage: 0x1008ff18, + XF86XK_Mail: 0x1008ff19, + XF86XK_Start: 0x1008ff1a, + XF86XK_Search: 0x1008ff1b, + XF86XK_AudioRecord: 0x1008ff1c, + XF86XK_Calculator: 0x1008ff1d, + XF86XK_Memo: 0x1008ff1e, + XF86XK_ToDoList: 0x1008ff1f, + XF86XK_Calendar: 0x1008ff20, + XF86XK_PowerDown: 0x1008ff21, + XF86XK_ContrastAdjust: 0x1008ff22, + XF86XK_RockerUp: 0x1008ff23, + XF86XK_RockerDown: 0x1008ff24, + XF86XK_RockerEnter: 0x1008ff25, + XF86XK_Back: 0x1008ff26, + XF86XK_Forward: 0x1008ff27, + XF86XK_Stop: 0x1008ff28, + XF86XK_Refresh: 0x1008ff29, + XF86XK_PowerOff: 0x1008ff2a, + XF86XK_WakeUp: 0x1008ff2b, + XF86XK_Eject: 0x1008ff2c, + XF86XK_ScreenSaver: 0x1008ff2d, + XF86XK_WWW: 0x1008ff2e, + XF86XK_Sleep: 0x1008ff2f, + XF86XK_Favorites: 0x1008ff30, + XF86XK_AudioPause: 0x1008ff31, + XF86XK_AudioMedia: 0x1008ff32, + XF86XK_MyComputer: 0x1008ff33, + XF86XK_VendorHome: 0x1008ff34, + XF86XK_LightBulb: 0x1008ff35, + XF86XK_Shop: 0x1008ff36, + XF86XK_History: 0x1008ff37, + XF86XK_OpenURL: 0x1008ff38, + XF86XK_AddFavorite: 0x1008ff39, + XF86XK_HotLinks: 0x1008ff3a, + XF86XK_BrightnessAdjust: 0x1008ff3b, + XF86XK_Finance: 0x1008ff3c, + XF86XK_Community: 0x1008ff3d, + XF86XK_AudioRewind: 0x1008ff3e, + XF86XK_BackForward: 0x1008ff3f, + XF86XK_Launch0: 0x1008ff40, + XF86XK_Launch1: 0x1008ff41, + XF86XK_Launch2: 0x1008ff42, + XF86XK_Launch3: 0x1008ff43, + XF86XK_Launch4: 0x1008ff44, + XF86XK_Launch5: 0x1008ff45, + XF86XK_Launch6: 0x1008ff46, + XF86XK_Launch7: 0x1008ff47, + XF86XK_Launch8: 0x1008ff48, + XF86XK_Launch9: 0x1008ff49, + XF86XK_LaunchA: 0x1008ff4a, + XF86XK_LaunchB: 0x1008ff4b, + XF86XK_LaunchC: 0x1008ff4c, + XF86XK_LaunchD: 0x1008ff4d, + XF86XK_LaunchE: 0x1008ff4e, + XF86XK_LaunchF: 0x1008ff4f, + XF86XK_ApplicationLeft: 0x1008ff50, + XF86XK_ApplicationRight: 0x1008ff51, + XF86XK_Book: 0x1008ff52, + XF86XK_CD: 0x1008ff53, + XF86XK_Calculater: 0x1008ff54, + XF86XK_Clear: 0x1008ff55, + XF86XK_Close: 0x1008ff56, + XF86XK_Copy: 0x1008ff57, + XF86XK_Cut: 0x1008ff58, + XF86XK_Display: 0x1008ff59, + XF86XK_DOS: 0x1008ff5a, + XF86XK_Documents: 0x1008ff5b, + XF86XK_Excel: 0x1008ff5c, + XF86XK_Explorer: 0x1008ff5d, + XF86XK_Game: 0x1008ff5e, + XF86XK_Go: 0x1008ff5f, + XF86XK_iTouch: 0x1008ff60, + XF86XK_LogOff: 0x1008ff61, + XF86XK_Market: 0x1008ff62, + XF86XK_Meeting: 0x1008ff63, + XF86XK_MenuKB: 0x1008ff65, + XF86XK_MenuPB: 0x1008ff66, + XF86XK_MySites: 0x1008ff67, + XF86XK_New: 0x1008ff68, + XF86XK_News: 0x1008ff69, + XF86XK_OfficeHome: 0x1008ff6a, + XF86XK_Open: 0x1008ff6b, + XF86XK_Option: 0x1008ff6c, + XF86XK_Paste: 0x1008ff6d, + XF86XK_Phone: 0x1008ff6e, + XF86XK_Q: 0x1008ff70, + XF86XK_Reply: 0x1008ff72, + XF86XK_Reload: 0x1008ff73, + XF86XK_RotateWindows: 0x1008ff74, + XF86XK_RotationPB: 0x1008ff75, + XF86XK_RotationKB: 0x1008ff76, + XF86XK_Save: 0x1008ff77, + XF86XK_ScrollUp: 0x1008ff78, + XF86XK_ScrollDown: 0x1008ff79, + XF86XK_ScrollClick: 0x1008ff7a, + XF86XK_Send: 0x1008ff7b, + XF86XK_Spell: 0x1008ff7c, + XF86XK_SplitScreen: 0x1008ff7d, + XF86XK_Support: 0x1008ff7e, + XF86XK_TaskPane: 0x1008ff7f, + XF86XK_Terminal: 0x1008ff80, + XF86XK_Tools: 0x1008ff81, + XF86XK_Travel: 0x1008ff82, + XF86XK_UserPB: 0x1008ff84, + XF86XK_User1KB: 0x1008ff85, + XF86XK_User2KB: 0x1008ff86, + XF86XK_Video: 0x1008ff87, + XF86XK_WheelButton: 0x1008ff88, + XF86XK_Word: 0x1008ff89, + XF86XK_Xfer: 0x1008ff8a, + XF86XK_ZoomIn: 0x1008ff8b, + XF86XK_ZoomOut: 0x1008ff8c, + XF86XK_Away: 0x1008ff8d, + XF86XK_Messenger: 0x1008ff8e, + XF86XK_WebCam: 0x1008ff8f, + XF86XK_MailForward: 0x1008ff90, + XF86XK_Pictures: 0x1008ff91, + XF86XK_Music: 0x1008ff92, + XF86XK_Battery: 0x1008ff93, + XF86XK_Bluetooth: 0x1008ff94, + XF86XK_WLAN: 0x1008ff95, + XF86XK_UWB: 0x1008ff96, + XF86XK_AudioForward: 0x1008ff97, + XF86XK_AudioRepeat: 0x1008ff98, + XF86XK_AudioRandomPlay: 0x1008ff99, + XF86XK_Subtitle: 0x1008ff9a, + XF86XK_AudioCycleTrack: 0x1008ff9b, + XF86XK_CycleAngle: 0x1008ff9c, + XF86XK_FrameBack: 0x1008ff9d, + XF86XK_FrameForward: 0x1008ff9e, + XF86XK_Time: 0x1008ff9f, + XF86XK_Select: 0x1008ffa0, + XF86XK_View: 0x1008ffa1, + XF86XK_TopMenu: 0x1008ffa2, + XF86XK_Red: 0x1008ffa3, + XF86XK_Green: 0x1008ffa4, + XF86XK_Yellow: 0x1008ffa5, + XF86XK_Blue: 0x1008ffa6, + XF86XK_Suspend: 0x1008ffa7, + XF86XK_Hibernate: 0x1008ffa8, + XF86XK_TouchpadToggle: 0x1008ffa9, + XF86XK_TouchpadOn: 0x1008ffb0, + XF86XK_TouchpadOff: 0x1008ffb1, + XF86XK_AudioMicMute: 0x1008ffb2, + XF86XK_Switch_VT_1: 0x1008fe01, + XF86XK_Switch_VT_2: 0x1008fe02, + XF86XK_Switch_VT_3: 0x1008fe03, + XF86XK_Switch_VT_4: 0x1008fe04, + XF86XK_Switch_VT_5: 0x1008fe05, + XF86XK_Switch_VT_6: 0x1008fe06, + XF86XK_Switch_VT_7: 0x1008fe07, + XF86XK_Switch_VT_8: 0x1008fe08, + XF86XK_Switch_VT_9: 0x1008fe09, + XF86XK_Switch_VT_10: 0x1008fe0a, + XF86XK_Switch_VT_11: 0x1008fe0b, + XF86XK_Switch_VT_12: 0x1008fe0c, + XF86XK_Ungrab: 0x1008fe20, + XF86XK_ClearGrab: 0x1008fe21, + XF86XK_Next_VMode: 0x1008fe22, + XF86XK_Prev_VMode: 0x1008fe23, + XF86XK_LogWindowTree: 0x1008fe24, + XF86XK_LogGrabInfo: 0x1008fe25, +}; diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/keysymdef.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/keysymdef.js new file mode 100644 index 000000000..d486f420a --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/keysymdef.js @@ -0,0 +1,688 @@ +/* + * Mapping from Unicode codepoints to X11/RFB keysyms + * + * This file was automatically generated from keysymdef.h + * DO NOT EDIT! + */ + +/* Functions at the bottom */ + +const codepoints = { + 0x0100: 0x03c0, // XK_Amacron + 0x0101: 0x03e0, // XK_amacron + 0x0102: 0x01c3, // XK_Abreve + 0x0103: 0x01e3, // XK_abreve + 0x0104: 0x01a1, // XK_Aogonek + 0x0105: 0x01b1, // XK_aogonek + 0x0106: 0x01c6, // XK_Cacute + 0x0107: 0x01e6, // XK_cacute + 0x0108: 0x02c6, // XK_Ccircumflex + 0x0109: 0x02e6, // XK_ccircumflex + 0x010a: 0x02c5, // XK_Cabovedot + 0x010b: 0x02e5, // XK_cabovedot + 0x010c: 0x01c8, // XK_Ccaron + 0x010d: 0x01e8, // XK_ccaron + 0x010e: 0x01cf, // XK_Dcaron + 0x010f: 0x01ef, // XK_dcaron + 0x0110: 0x01d0, // XK_Dstroke + 0x0111: 0x01f0, // XK_dstroke + 0x0112: 0x03aa, // XK_Emacron + 0x0113: 0x03ba, // XK_emacron + 0x0116: 0x03cc, // XK_Eabovedot + 0x0117: 0x03ec, // XK_eabovedot + 0x0118: 0x01ca, // XK_Eogonek + 0x0119: 0x01ea, // XK_eogonek + 0x011a: 0x01cc, // XK_Ecaron + 0x011b: 0x01ec, // XK_ecaron + 0x011c: 0x02d8, // XK_Gcircumflex + 0x011d: 0x02f8, // XK_gcircumflex + 0x011e: 0x02ab, // XK_Gbreve + 0x011f: 0x02bb, // XK_gbreve + 0x0120: 0x02d5, // XK_Gabovedot + 0x0121: 0x02f5, // XK_gabovedot + 0x0122: 0x03ab, // XK_Gcedilla + 0x0123: 0x03bb, // XK_gcedilla + 0x0124: 0x02a6, // XK_Hcircumflex + 0x0125: 0x02b6, // XK_hcircumflex + 0x0126: 0x02a1, // XK_Hstroke + 0x0127: 0x02b1, // XK_hstroke + 0x0128: 0x03a5, // XK_Itilde + 0x0129: 0x03b5, // XK_itilde + 0x012a: 0x03cf, // XK_Imacron + 0x012b: 0x03ef, // XK_imacron + 0x012e: 0x03c7, // XK_Iogonek + 0x012f: 0x03e7, // XK_iogonek + 0x0130: 0x02a9, // XK_Iabovedot + 0x0131: 0x02b9, // XK_idotless + 0x0134: 0x02ac, // XK_Jcircumflex + 0x0135: 0x02bc, // XK_jcircumflex + 0x0136: 0x03d3, // XK_Kcedilla + 0x0137: 0x03f3, // XK_kcedilla + 0x0138: 0x03a2, // XK_kra + 0x0139: 0x01c5, // XK_Lacute + 0x013a: 0x01e5, // XK_lacute + 0x013b: 0x03a6, // XK_Lcedilla + 0x013c: 0x03b6, // XK_lcedilla + 0x013d: 0x01a5, // XK_Lcaron + 0x013e: 0x01b5, // XK_lcaron + 0x0141: 0x01a3, // XK_Lstroke + 0x0142: 0x01b3, // XK_lstroke + 0x0143: 0x01d1, // XK_Nacute + 0x0144: 0x01f1, // XK_nacute + 0x0145: 0x03d1, // XK_Ncedilla + 0x0146: 0x03f1, // XK_ncedilla + 0x0147: 0x01d2, // XK_Ncaron + 0x0148: 0x01f2, // XK_ncaron + 0x014a: 0x03bd, // XK_ENG + 0x014b: 0x03bf, // XK_eng + 0x014c: 0x03d2, // XK_Omacron + 0x014d: 0x03f2, // XK_omacron + 0x0150: 0x01d5, // XK_Odoubleacute + 0x0151: 0x01f5, // XK_odoubleacute + 0x0152: 0x13bc, // XK_OE + 0x0153: 0x13bd, // XK_oe + 0x0154: 0x01c0, // XK_Racute + 0x0155: 0x01e0, // XK_racute + 0x0156: 0x03a3, // XK_Rcedilla + 0x0157: 0x03b3, // XK_rcedilla + 0x0158: 0x01d8, // XK_Rcaron + 0x0159: 0x01f8, // XK_rcaron + 0x015a: 0x01a6, // XK_Sacute + 0x015b: 0x01b6, // XK_sacute + 0x015c: 0x02de, // XK_Scircumflex + 0x015d: 0x02fe, // XK_scircumflex + 0x015e: 0x01aa, // XK_Scedilla + 0x015f: 0x01ba, // XK_scedilla + 0x0160: 0x01a9, // XK_Scaron + 0x0161: 0x01b9, // XK_scaron + 0x0162: 0x01de, // XK_Tcedilla + 0x0163: 0x01fe, // XK_tcedilla + 0x0164: 0x01ab, // XK_Tcaron + 0x0165: 0x01bb, // XK_tcaron + 0x0166: 0x03ac, // XK_Tslash + 0x0167: 0x03bc, // XK_tslash + 0x0168: 0x03dd, // XK_Utilde + 0x0169: 0x03fd, // XK_utilde + 0x016a: 0x03de, // XK_Umacron + 0x016b: 0x03fe, // XK_umacron + 0x016c: 0x02dd, // XK_Ubreve + 0x016d: 0x02fd, // XK_ubreve + 0x016e: 0x01d9, // XK_Uring + 0x016f: 0x01f9, // XK_uring + 0x0170: 0x01db, // XK_Udoubleacute + 0x0171: 0x01fb, // XK_udoubleacute + 0x0172: 0x03d9, // XK_Uogonek + 0x0173: 0x03f9, // XK_uogonek + 0x0178: 0x13be, // XK_Ydiaeresis + 0x0179: 0x01ac, // XK_Zacute + 0x017a: 0x01bc, // XK_zacute + 0x017b: 0x01af, // XK_Zabovedot + 0x017c: 0x01bf, // XK_zabovedot + 0x017d: 0x01ae, // XK_Zcaron + 0x017e: 0x01be, // XK_zcaron + 0x0192: 0x08f6, // XK_function + 0x01d2: 0x10001d1, // XK_Ocaron + 0x02c7: 0x01b7, // XK_caron + 0x02d8: 0x01a2, // XK_breve + 0x02d9: 0x01ff, // XK_abovedot + 0x02db: 0x01b2, // XK_ogonek + 0x02dd: 0x01bd, // XK_doubleacute + 0x0385: 0x07ae, // XK_Greek_accentdieresis + 0x0386: 0x07a1, // XK_Greek_ALPHAaccent + 0x0388: 0x07a2, // XK_Greek_EPSILONaccent + 0x0389: 0x07a3, // XK_Greek_ETAaccent + 0x038a: 0x07a4, // XK_Greek_IOTAaccent + 0x038c: 0x07a7, // XK_Greek_OMICRONaccent + 0x038e: 0x07a8, // XK_Greek_UPSILONaccent + 0x038f: 0x07ab, // XK_Greek_OMEGAaccent + 0x0390: 0x07b6, // XK_Greek_iotaaccentdieresis + 0x0391: 0x07c1, // XK_Greek_ALPHA + 0x0392: 0x07c2, // XK_Greek_BETA + 0x0393: 0x07c3, // XK_Greek_GAMMA + 0x0394: 0x07c4, // XK_Greek_DELTA + 0x0395: 0x07c5, // XK_Greek_EPSILON + 0x0396: 0x07c6, // XK_Greek_ZETA + 0x0397: 0x07c7, // XK_Greek_ETA + 0x0398: 0x07c8, // XK_Greek_THETA + 0x0399: 0x07c9, // XK_Greek_IOTA + 0x039a: 0x07ca, // XK_Greek_KAPPA + 0x039b: 0x07cb, // XK_Greek_LAMDA + 0x039c: 0x07cc, // XK_Greek_MU + 0x039d: 0x07cd, // XK_Greek_NU + 0x039e: 0x07ce, // XK_Greek_XI + 0x039f: 0x07cf, // XK_Greek_OMICRON + 0x03a0: 0x07d0, // XK_Greek_PI + 0x03a1: 0x07d1, // XK_Greek_RHO + 0x03a3: 0x07d2, // XK_Greek_SIGMA + 0x03a4: 0x07d4, // XK_Greek_TAU + 0x03a5: 0x07d5, // XK_Greek_UPSILON + 0x03a6: 0x07d6, // XK_Greek_PHI + 0x03a7: 0x07d7, // XK_Greek_CHI + 0x03a8: 0x07d8, // XK_Greek_PSI + 0x03a9: 0x07d9, // XK_Greek_OMEGA + 0x03aa: 0x07a5, // XK_Greek_IOTAdieresis + 0x03ab: 0x07a9, // XK_Greek_UPSILONdieresis + 0x03ac: 0x07b1, // XK_Greek_alphaaccent + 0x03ad: 0x07b2, // XK_Greek_epsilonaccent + 0x03ae: 0x07b3, // XK_Greek_etaaccent + 0x03af: 0x07b4, // XK_Greek_iotaaccent + 0x03b0: 0x07ba, // XK_Greek_upsilonaccentdieresis + 0x03b1: 0x07e1, // XK_Greek_alpha + 0x03b2: 0x07e2, // XK_Greek_beta + 0x03b3: 0x07e3, // XK_Greek_gamma + 0x03b4: 0x07e4, // XK_Greek_delta + 0x03b5: 0x07e5, // XK_Greek_epsilon + 0x03b6: 0x07e6, // XK_Greek_zeta + 0x03b7: 0x07e7, // XK_Greek_eta + 0x03b8: 0x07e8, // XK_Greek_theta + 0x03b9: 0x07e9, // XK_Greek_iota + 0x03ba: 0x07ea, // XK_Greek_kappa + 0x03bb: 0x07eb, // XK_Greek_lamda + 0x03bc: 0x07ec, // XK_Greek_mu + 0x03bd: 0x07ed, // XK_Greek_nu + 0x03be: 0x07ee, // XK_Greek_xi + 0x03bf: 0x07ef, // XK_Greek_omicron + 0x03c0: 0x07f0, // XK_Greek_pi + 0x03c1: 0x07f1, // XK_Greek_rho + 0x03c2: 0x07f3, // XK_Greek_finalsmallsigma + 0x03c3: 0x07f2, // XK_Greek_sigma + 0x03c4: 0x07f4, // XK_Greek_tau + 0x03c5: 0x07f5, // XK_Greek_upsilon + 0x03c6: 0x07f6, // XK_Greek_phi + 0x03c7: 0x07f7, // XK_Greek_chi + 0x03c8: 0x07f8, // XK_Greek_psi + 0x03c9: 0x07f9, // XK_Greek_omega + 0x03ca: 0x07b5, // XK_Greek_iotadieresis + 0x03cb: 0x07b9, // XK_Greek_upsilondieresis + 0x03cc: 0x07b7, // XK_Greek_omicronaccent + 0x03cd: 0x07b8, // XK_Greek_upsilonaccent + 0x03ce: 0x07bb, // XK_Greek_omegaaccent + 0x0401: 0x06b3, // XK_Cyrillic_IO + 0x0402: 0x06b1, // XK_Serbian_DJE + 0x0403: 0x06b2, // XK_Macedonia_GJE + 0x0404: 0x06b4, // XK_Ukrainian_IE + 0x0405: 0x06b5, // XK_Macedonia_DSE + 0x0406: 0x06b6, // XK_Ukrainian_I + 0x0407: 0x06b7, // XK_Ukrainian_YI + 0x0408: 0x06b8, // XK_Cyrillic_JE + 0x0409: 0x06b9, // XK_Cyrillic_LJE + 0x040a: 0x06ba, // XK_Cyrillic_NJE + 0x040b: 0x06bb, // XK_Serbian_TSHE + 0x040c: 0x06bc, // XK_Macedonia_KJE + 0x040e: 0x06be, // XK_Byelorussian_SHORTU + 0x040f: 0x06bf, // XK_Cyrillic_DZHE + 0x0410: 0x06e1, // XK_Cyrillic_A + 0x0411: 0x06e2, // XK_Cyrillic_BE + 0x0412: 0x06f7, // XK_Cyrillic_VE + 0x0413: 0x06e7, // XK_Cyrillic_GHE + 0x0414: 0x06e4, // XK_Cyrillic_DE + 0x0415: 0x06e5, // XK_Cyrillic_IE + 0x0416: 0x06f6, // XK_Cyrillic_ZHE + 0x0417: 0x06fa, // XK_Cyrillic_ZE + 0x0418: 0x06e9, // XK_Cyrillic_I + 0x0419: 0x06ea, // XK_Cyrillic_SHORTI + 0x041a: 0x06eb, // XK_Cyrillic_KA + 0x041b: 0x06ec, // XK_Cyrillic_EL + 0x041c: 0x06ed, // XK_Cyrillic_EM + 0x041d: 0x06ee, // XK_Cyrillic_EN + 0x041e: 0x06ef, // XK_Cyrillic_O + 0x041f: 0x06f0, // XK_Cyrillic_PE + 0x0420: 0x06f2, // XK_Cyrillic_ER + 0x0421: 0x06f3, // XK_Cyrillic_ES + 0x0422: 0x06f4, // XK_Cyrillic_TE + 0x0423: 0x06f5, // XK_Cyrillic_U + 0x0424: 0x06e6, // XK_Cyrillic_EF + 0x0425: 0x06e8, // XK_Cyrillic_HA + 0x0426: 0x06e3, // XK_Cyrillic_TSE + 0x0427: 0x06fe, // XK_Cyrillic_CHE + 0x0428: 0x06fb, // XK_Cyrillic_SHA + 0x0429: 0x06fd, // XK_Cyrillic_SHCHA + 0x042a: 0x06ff, // XK_Cyrillic_HARDSIGN + 0x042b: 0x06f9, // XK_Cyrillic_YERU + 0x042c: 0x06f8, // XK_Cyrillic_SOFTSIGN + 0x042d: 0x06fc, // XK_Cyrillic_E + 0x042e: 0x06e0, // XK_Cyrillic_YU + 0x042f: 0x06f1, // XK_Cyrillic_YA + 0x0430: 0x06c1, // XK_Cyrillic_a + 0x0431: 0x06c2, // XK_Cyrillic_be + 0x0432: 0x06d7, // XK_Cyrillic_ve + 0x0433: 0x06c7, // XK_Cyrillic_ghe + 0x0434: 0x06c4, // XK_Cyrillic_de + 0x0435: 0x06c5, // XK_Cyrillic_ie + 0x0436: 0x06d6, // XK_Cyrillic_zhe + 0x0437: 0x06da, // XK_Cyrillic_ze + 0x0438: 0x06c9, // XK_Cyrillic_i + 0x0439: 0x06ca, // XK_Cyrillic_shorti + 0x043a: 0x06cb, // XK_Cyrillic_ka + 0x043b: 0x06cc, // XK_Cyrillic_el + 0x043c: 0x06cd, // XK_Cyrillic_em + 0x043d: 0x06ce, // XK_Cyrillic_en + 0x043e: 0x06cf, // XK_Cyrillic_o + 0x043f: 0x06d0, // XK_Cyrillic_pe + 0x0440: 0x06d2, // XK_Cyrillic_er + 0x0441: 0x06d3, // XK_Cyrillic_es + 0x0442: 0x06d4, // XK_Cyrillic_te + 0x0443: 0x06d5, // XK_Cyrillic_u + 0x0444: 0x06c6, // XK_Cyrillic_ef + 0x0445: 0x06c8, // XK_Cyrillic_ha + 0x0446: 0x06c3, // XK_Cyrillic_tse + 0x0447: 0x06de, // XK_Cyrillic_che + 0x0448: 0x06db, // XK_Cyrillic_sha + 0x0449: 0x06dd, // XK_Cyrillic_shcha + 0x044a: 0x06df, // XK_Cyrillic_hardsign + 0x044b: 0x06d9, // XK_Cyrillic_yeru + 0x044c: 0x06d8, // XK_Cyrillic_softsign + 0x044d: 0x06dc, // XK_Cyrillic_e + 0x044e: 0x06c0, // XK_Cyrillic_yu + 0x044f: 0x06d1, // XK_Cyrillic_ya + 0x0451: 0x06a3, // XK_Cyrillic_io + 0x0452: 0x06a1, // XK_Serbian_dje + 0x0453: 0x06a2, // XK_Macedonia_gje + 0x0454: 0x06a4, // XK_Ukrainian_ie + 0x0455: 0x06a5, // XK_Macedonia_dse + 0x0456: 0x06a6, // XK_Ukrainian_i + 0x0457: 0x06a7, // XK_Ukrainian_yi + 0x0458: 0x06a8, // XK_Cyrillic_je + 0x0459: 0x06a9, // XK_Cyrillic_lje + 0x045a: 0x06aa, // XK_Cyrillic_nje + 0x045b: 0x06ab, // XK_Serbian_tshe + 0x045c: 0x06ac, // XK_Macedonia_kje + 0x045e: 0x06ae, // XK_Byelorussian_shortu + 0x045f: 0x06af, // XK_Cyrillic_dzhe + 0x0490: 0x06bd, // XK_Ukrainian_GHE_WITH_UPTURN + 0x0491: 0x06ad, // XK_Ukrainian_ghe_with_upturn + 0x05d0: 0x0ce0, // XK_hebrew_aleph + 0x05d1: 0x0ce1, // XK_hebrew_bet + 0x05d2: 0x0ce2, // XK_hebrew_gimel + 0x05d3: 0x0ce3, // XK_hebrew_dalet + 0x05d4: 0x0ce4, // XK_hebrew_he + 0x05d5: 0x0ce5, // XK_hebrew_waw + 0x05d6: 0x0ce6, // XK_hebrew_zain + 0x05d7: 0x0ce7, // XK_hebrew_chet + 0x05d8: 0x0ce8, // XK_hebrew_tet + 0x05d9: 0x0ce9, // XK_hebrew_yod + 0x05da: 0x0cea, // XK_hebrew_finalkaph + 0x05db: 0x0ceb, // XK_hebrew_kaph + 0x05dc: 0x0cec, // XK_hebrew_lamed + 0x05dd: 0x0ced, // XK_hebrew_finalmem + 0x05de: 0x0cee, // XK_hebrew_mem + 0x05df: 0x0cef, // XK_hebrew_finalnun + 0x05e0: 0x0cf0, // XK_hebrew_nun + 0x05e1: 0x0cf1, // XK_hebrew_samech + 0x05e2: 0x0cf2, // XK_hebrew_ayin + 0x05e3: 0x0cf3, // XK_hebrew_finalpe + 0x05e4: 0x0cf4, // XK_hebrew_pe + 0x05e5: 0x0cf5, // XK_hebrew_finalzade + 0x05e6: 0x0cf6, // XK_hebrew_zade + 0x05e7: 0x0cf7, // XK_hebrew_qoph + 0x05e8: 0x0cf8, // XK_hebrew_resh + 0x05e9: 0x0cf9, // XK_hebrew_shin + 0x05ea: 0x0cfa, // XK_hebrew_taw + 0x060c: 0x05ac, // XK_Arabic_comma + 0x061b: 0x05bb, // XK_Arabic_semicolon + 0x061f: 0x05bf, // XK_Arabic_question_mark + 0x0621: 0x05c1, // XK_Arabic_hamza + 0x0622: 0x05c2, // XK_Arabic_maddaonalef + 0x0623: 0x05c3, // XK_Arabic_hamzaonalef + 0x0624: 0x05c4, // XK_Arabic_hamzaonwaw + 0x0625: 0x05c5, // XK_Arabic_hamzaunderalef + 0x0626: 0x05c6, // XK_Arabic_hamzaonyeh + 0x0627: 0x05c7, // XK_Arabic_alef + 0x0628: 0x05c8, // XK_Arabic_beh + 0x0629: 0x05c9, // XK_Arabic_tehmarbuta + 0x062a: 0x05ca, // XK_Arabic_teh + 0x062b: 0x05cb, // XK_Arabic_theh + 0x062c: 0x05cc, // XK_Arabic_jeem + 0x062d: 0x05cd, // XK_Arabic_hah + 0x062e: 0x05ce, // XK_Arabic_khah + 0x062f: 0x05cf, // XK_Arabic_dal + 0x0630: 0x05d0, // XK_Arabic_thal + 0x0631: 0x05d1, // XK_Arabic_ra + 0x0632: 0x05d2, // XK_Arabic_zain + 0x0633: 0x05d3, // XK_Arabic_seen + 0x0634: 0x05d4, // XK_Arabic_sheen + 0x0635: 0x05d5, // XK_Arabic_sad + 0x0636: 0x05d6, // XK_Arabic_dad + 0x0637: 0x05d7, // XK_Arabic_tah + 0x0638: 0x05d8, // XK_Arabic_zah + 0x0639: 0x05d9, // XK_Arabic_ain + 0x063a: 0x05da, // XK_Arabic_ghain + 0x0640: 0x05e0, // XK_Arabic_tatweel + 0x0641: 0x05e1, // XK_Arabic_feh + 0x0642: 0x05e2, // XK_Arabic_qaf + 0x0643: 0x05e3, // XK_Arabic_kaf + 0x0644: 0x05e4, // XK_Arabic_lam + 0x0645: 0x05e5, // XK_Arabic_meem + 0x0646: 0x05e6, // XK_Arabic_noon + 0x0647: 0x05e7, // XK_Arabic_ha + 0x0648: 0x05e8, // XK_Arabic_waw + 0x0649: 0x05e9, // XK_Arabic_alefmaksura + 0x064a: 0x05ea, // XK_Arabic_yeh + 0x064b: 0x05eb, // XK_Arabic_fathatan + 0x064c: 0x05ec, // XK_Arabic_dammatan + 0x064d: 0x05ed, // XK_Arabic_kasratan + 0x064e: 0x05ee, // XK_Arabic_fatha + 0x064f: 0x05ef, // XK_Arabic_damma + 0x0650: 0x05f0, // XK_Arabic_kasra + 0x0651: 0x05f1, // XK_Arabic_shadda + 0x0652: 0x05f2, // XK_Arabic_sukun + 0x0e01: 0x0da1, // XK_Thai_kokai + 0x0e02: 0x0da2, // XK_Thai_khokhai + 0x0e03: 0x0da3, // XK_Thai_khokhuat + 0x0e04: 0x0da4, // XK_Thai_khokhwai + 0x0e05: 0x0da5, // XK_Thai_khokhon + 0x0e06: 0x0da6, // XK_Thai_khorakhang + 0x0e07: 0x0da7, // XK_Thai_ngongu + 0x0e08: 0x0da8, // XK_Thai_chochan + 0x0e09: 0x0da9, // XK_Thai_choching + 0x0e0a: 0x0daa, // XK_Thai_chochang + 0x0e0b: 0x0dab, // XK_Thai_soso + 0x0e0c: 0x0dac, // XK_Thai_chochoe + 0x0e0d: 0x0dad, // XK_Thai_yoying + 0x0e0e: 0x0dae, // XK_Thai_dochada + 0x0e0f: 0x0daf, // XK_Thai_topatak + 0x0e10: 0x0db0, // XK_Thai_thothan + 0x0e11: 0x0db1, // XK_Thai_thonangmontho + 0x0e12: 0x0db2, // XK_Thai_thophuthao + 0x0e13: 0x0db3, // XK_Thai_nonen + 0x0e14: 0x0db4, // XK_Thai_dodek + 0x0e15: 0x0db5, // XK_Thai_totao + 0x0e16: 0x0db6, // XK_Thai_thothung + 0x0e17: 0x0db7, // XK_Thai_thothahan + 0x0e18: 0x0db8, // XK_Thai_thothong + 0x0e19: 0x0db9, // XK_Thai_nonu + 0x0e1a: 0x0dba, // XK_Thai_bobaimai + 0x0e1b: 0x0dbb, // XK_Thai_popla + 0x0e1c: 0x0dbc, // XK_Thai_phophung + 0x0e1d: 0x0dbd, // XK_Thai_fofa + 0x0e1e: 0x0dbe, // XK_Thai_phophan + 0x0e1f: 0x0dbf, // XK_Thai_fofan + 0x0e20: 0x0dc0, // XK_Thai_phosamphao + 0x0e21: 0x0dc1, // XK_Thai_moma + 0x0e22: 0x0dc2, // XK_Thai_yoyak + 0x0e23: 0x0dc3, // XK_Thai_rorua + 0x0e24: 0x0dc4, // XK_Thai_ru + 0x0e25: 0x0dc5, // XK_Thai_loling + 0x0e26: 0x0dc6, // XK_Thai_lu + 0x0e27: 0x0dc7, // XK_Thai_wowaen + 0x0e28: 0x0dc8, // XK_Thai_sosala + 0x0e29: 0x0dc9, // XK_Thai_sorusi + 0x0e2a: 0x0dca, // XK_Thai_sosua + 0x0e2b: 0x0dcb, // XK_Thai_hohip + 0x0e2c: 0x0dcc, // XK_Thai_lochula + 0x0e2d: 0x0dcd, // XK_Thai_oang + 0x0e2e: 0x0dce, // XK_Thai_honokhuk + 0x0e2f: 0x0dcf, // XK_Thai_paiyannoi + 0x0e30: 0x0dd0, // XK_Thai_saraa + 0x0e31: 0x0dd1, // XK_Thai_maihanakat + 0x0e32: 0x0dd2, // XK_Thai_saraaa + 0x0e33: 0x0dd3, // XK_Thai_saraam + 0x0e34: 0x0dd4, // XK_Thai_sarai + 0x0e35: 0x0dd5, // XK_Thai_saraii + 0x0e36: 0x0dd6, // XK_Thai_saraue + 0x0e37: 0x0dd7, // XK_Thai_sarauee + 0x0e38: 0x0dd8, // XK_Thai_sarau + 0x0e39: 0x0dd9, // XK_Thai_sarauu + 0x0e3a: 0x0dda, // XK_Thai_phinthu + 0x0e3f: 0x0ddf, // XK_Thai_baht + 0x0e40: 0x0de0, // XK_Thai_sarae + 0x0e41: 0x0de1, // XK_Thai_saraae + 0x0e42: 0x0de2, // XK_Thai_sarao + 0x0e43: 0x0de3, // XK_Thai_saraaimaimuan + 0x0e44: 0x0de4, // XK_Thai_saraaimaimalai + 0x0e45: 0x0de5, // XK_Thai_lakkhangyao + 0x0e46: 0x0de6, // XK_Thai_maiyamok + 0x0e47: 0x0de7, // XK_Thai_maitaikhu + 0x0e48: 0x0de8, // XK_Thai_maiek + 0x0e49: 0x0de9, // XK_Thai_maitho + 0x0e4a: 0x0dea, // XK_Thai_maitri + 0x0e4b: 0x0deb, // XK_Thai_maichattawa + 0x0e4c: 0x0dec, // XK_Thai_thanthakhat + 0x0e4d: 0x0ded, // XK_Thai_nikhahit + 0x0e50: 0x0df0, // XK_Thai_leksun + 0x0e51: 0x0df1, // XK_Thai_leknung + 0x0e52: 0x0df2, // XK_Thai_leksong + 0x0e53: 0x0df3, // XK_Thai_leksam + 0x0e54: 0x0df4, // XK_Thai_leksi + 0x0e55: 0x0df5, // XK_Thai_lekha + 0x0e56: 0x0df6, // XK_Thai_lekhok + 0x0e57: 0x0df7, // XK_Thai_lekchet + 0x0e58: 0x0df8, // XK_Thai_lekpaet + 0x0e59: 0x0df9, // XK_Thai_lekkao + 0x2002: 0x0aa2, // XK_enspace + 0x2003: 0x0aa1, // XK_emspace + 0x2004: 0x0aa3, // XK_em3space + 0x2005: 0x0aa4, // XK_em4space + 0x2007: 0x0aa5, // XK_digitspace + 0x2008: 0x0aa6, // XK_punctspace + 0x2009: 0x0aa7, // XK_thinspace + 0x200a: 0x0aa8, // XK_hairspace + 0x2012: 0x0abb, // XK_figdash + 0x2013: 0x0aaa, // XK_endash + 0x2014: 0x0aa9, // XK_emdash + 0x2015: 0x07af, // XK_Greek_horizbar + 0x2017: 0x0cdf, // XK_hebrew_doublelowline + 0x2018: 0x0ad0, // XK_leftsinglequotemark + 0x2019: 0x0ad1, // XK_rightsinglequotemark + 0x201a: 0x0afd, // XK_singlelowquotemark + 0x201c: 0x0ad2, // XK_leftdoublequotemark + 0x201d: 0x0ad3, // XK_rightdoublequotemark + 0x201e: 0x0afe, // XK_doublelowquotemark + 0x2020: 0x0af1, // XK_dagger + 0x2021: 0x0af2, // XK_doubledagger + 0x2022: 0x0ae6, // XK_enfilledcircbullet + 0x2025: 0x0aaf, // XK_doubbaselinedot + 0x2026: 0x0aae, // XK_ellipsis + 0x2030: 0x0ad5, // XK_permille + 0x2032: 0x0ad6, // XK_minutes + 0x2033: 0x0ad7, // XK_seconds + 0x2038: 0x0afc, // XK_caret + 0x203e: 0x047e, // XK_overline + 0x20a9: 0x0eff, // XK_Korean_Won + 0x20ac: 0x20ac, // XK_EuroSign + 0x2105: 0x0ab8, // XK_careof + 0x2116: 0x06b0, // XK_numerosign + 0x2117: 0x0afb, // XK_phonographcopyright + 0x211e: 0x0ad4, // XK_prescription + 0x2122: 0x0ac9, // XK_trademark + 0x2153: 0x0ab0, // XK_onethird + 0x2154: 0x0ab1, // XK_twothirds + 0x2155: 0x0ab2, // XK_onefifth + 0x2156: 0x0ab3, // XK_twofifths + 0x2157: 0x0ab4, // XK_threefifths + 0x2158: 0x0ab5, // XK_fourfifths + 0x2159: 0x0ab6, // XK_onesixth + 0x215a: 0x0ab7, // XK_fivesixths + 0x215b: 0x0ac3, // XK_oneeighth + 0x215c: 0x0ac4, // XK_threeeighths + 0x215d: 0x0ac5, // XK_fiveeighths + 0x215e: 0x0ac6, // XK_seveneighths + 0x2190: 0x08fb, // XK_leftarrow + 0x2191: 0x08fc, // XK_uparrow + 0x2192: 0x08fd, // XK_rightarrow + 0x2193: 0x08fe, // XK_downarrow + 0x21d2: 0x08ce, // XK_implies + 0x21d4: 0x08cd, // XK_ifonlyif + 0x2202: 0x08ef, // XK_partialderivative + 0x2207: 0x08c5, // XK_nabla + 0x2218: 0x0bca, // XK_jot + 0x221a: 0x08d6, // XK_radical + 0x221d: 0x08c1, // XK_variation + 0x221e: 0x08c2, // XK_infinity + 0x2227: 0x08de, // XK_logicaland + 0x2228: 0x08df, // XK_logicalor + 0x2229: 0x08dc, // XK_intersection + 0x222a: 0x08dd, // XK_union + 0x222b: 0x08bf, // XK_integral + 0x2234: 0x08c0, // XK_therefore + 0x223c: 0x08c8, // XK_approximate + 0x2243: 0x08c9, // XK_similarequal + 0x2245: 0x1002248, // XK_approxeq + 0x2260: 0x08bd, // XK_notequal + 0x2261: 0x08cf, // XK_identical + 0x2264: 0x08bc, // XK_lessthanequal + 0x2265: 0x08be, // XK_greaterthanequal + 0x2282: 0x08da, // XK_includedin + 0x2283: 0x08db, // XK_includes + 0x22a2: 0x0bfc, // XK_righttack + 0x22a3: 0x0bdc, // XK_lefttack + 0x22a4: 0x0bc2, // XK_downtack + 0x22a5: 0x0bce, // XK_uptack + 0x2308: 0x0bd3, // XK_upstile + 0x230a: 0x0bc4, // XK_downstile + 0x2315: 0x0afa, // XK_telephonerecorder + 0x2320: 0x08a4, // XK_topintegral + 0x2321: 0x08a5, // XK_botintegral + 0x2395: 0x0bcc, // XK_quad + 0x239b: 0x08ab, // XK_topleftparens + 0x239d: 0x08ac, // XK_botleftparens + 0x239e: 0x08ad, // XK_toprightparens + 0x23a0: 0x08ae, // XK_botrightparens + 0x23a1: 0x08a7, // XK_topleftsqbracket + 0x23a3: 0x08a8, // XK_botleftsqbracket + 0x23a4: 0x08a9, // XK_toprightsqbracket + 0x23a6: 0x08aa, // XK_botrightsqbracket + 0x23a8: 0x08af, // XK_leftmiddlecurlybrace + 0x23ac: 0x08b0, // XK_rightmiddlecurlybrace + 0x23b7: 0x08a1, // XK_leftradical + 0x23ba: 0x09ef, // XK_horizlinescan1 + 0x23bb: 0x09f0, // XK_horizlinescan3 + 0x23bc: 0x09f2, // XK_horizlinescan7 + 0x23bd: 0x09f3, // XK_horizlinescan9 + 0x2409: 0x09e2, // XK_ht + 0x240a: 0x09e5, // XK_lf + 0x240b: 0x09e9, // XK_vt + 0x240c: 0x09e3, // XK_ff + 0x240d: 0x09e4, // XK_cr + 0x2423: 0x0aac, // XK_signifblank + 0x2424: 0x09e8, // XK_nl + 0x2500: 0x08a3, // XK_horizconnector + 0x2502: 0x08a6, // XK_vertconnector + 0x250c: 0x08a2, // XK_topleftradical + 0x2510: 0x09eb, // XK_uprightcorner + 0x2514: 0x09ed, // XK_lowleftcorner + 0x2518: 0x09ea, // XK_lowrightcorner + 0x251c: 0x09f4, // XK_leftt + 0x2524: 0x09f5, // XK_rightt + 0x252c: 0x09f7, // XK_topt + 0x2534: 0x09f6, // XK_bott + 0x253c: 0x09ee, // XK_crossinglines + 0x2592: 0x09e1, // XK_checkerboard + 0x25aa: 0x0ae7, // XK_enfilledsqbullet + 0x25ab: 0x0ae1, // XK_enopensquarebullet + 0x25ac: 0x0adb, // XK_filledrectbullet + 0x25ad: 0x0ae2, // XK_openrectbullet + 0x25ae: 0x0adf, // XK_emfilledrect + 0x25af: 0x0acf, // XK_emopenrectangle + 0x25b2: 0x0ae8, // XK_filledtribulletup + 0x25b3: 0x0ae3, // XK_opentribulletup + 0x25b6: 0x0add, // XK_filledrighttribullet + 0x25b7: 0x0acd, // XK_rightopentriangle + 0x25bc: 0x0ae9, // XK_filledtribulletdown + 0x25bd: 0x0ae4, // XK_opentribulletdown + 0x25c0: 0x0adc, // XK_filledlefttribullet + 0x25c1: 0x0acc, // XK_leftopentriangle + 0x25c6: 0x09e0, // XK_soliddiamond + 0x25cb: 0x0ace, // XK_emopencircle + 0x25cf: 0x0ade, // XK_emfilledcircle + 0x25e6: 0x0ae0, // XK_enopencircbullet + 0x2606: 0x0ae5, // XK_openstar + 0x260e: 0x0af9, // XK_telephone + 0x2613: 0x0aca, // XK_signaturemark + 0x261c: 0x0aea, // XK_leftpointer + 0x261e: 0x0aeb, // XK_rightpointer + 0x2640: 0x0af8, // XK_femalesymbol + 0x2642: 0x0af7, // XK_malesymbol + 0x2663: 0x0aec, // XK_club + 0x2665: 0x0aee, // XK_heart + 0x2666: 0x0aed, // XK_diamond + 0x266d: 0x0af6, // XK_musicalflat + 0x266f: 0x0af5, // XK_musicalsharp + 0x2713: 0x0af3, // XK_checkmark + 0x2717: 0x0af4, // XK_ballotcross + 0x271d: 0x0ad9, // XK_latincross + 0x2720: 0x0af0, // XK_maltesecross + 0x27e8: 0x0abc, // XK_leftanglebracket + 0x27e9: 0x0abe, // XK_rightanglebracket + 0x3001: 0x04a4, // XK_kana_comma + 0x3002: 0x04a1, // XK_kana_fullstop + 0x300c: 0x04a2, // XK_kana_openingbracket + 0x300d: 0x04a3, // XK_kana_closingbracket + 0x309b: 0x04de, // XK_voicedsound + 0x309c: 0x04df, // XK_semivoicedsound + 0x30a1: 0x04a7, // XK_kana_a + 0x30a2: 0x04b1, // XK_kana_A + 0x30a3: 0x04a8, // XK_kana_i + 0x30a4: 0x04b2, // XK_kana_I + 0x30a5: 0x04a9, // XK_kana_u + 0x30a6: 0x04b3, // XK_kana_U + 0x30a7: 0x04aa, // XK_kana_e + 0x30a8: 0x04b4, // XK_kana_E + 0x30a9: 0x04ab, // XK_kana_o + 0x30aa: 0x04b5, // XK_kana_O + 0x30ab: 0x04b6, // XK_kana_KA + 0x30ad: 0x04b7, // XK_kana_KI + 0x30af: 0x04b8, // XK_kana_KU + 0x30b1: 0x04b9, // XK_kana_KE + 0x30b3: 0x04ba, // XK_kana_KO + 0x30b5: 0x04bb, // XK_kana_SA + 0x30b7: 0x04bc, // XK_kana_SHI + 0x30b9: 0x04bd, // XK_kana_SU + 0x30bb: 0x04be, // XK_kana_SE + 0x30bd: 0x04bf, // XK_kana_SO + 0x30bf: 0x04c0, // XK_kana_TA + 0x30c1: 0x04c1, // XK_kana_CHI + 0x30c3: 0x04af, // XK_kana_tsu + 0x30c4: 0x04c2, // XK_kana_TSU + 0x30c6: 0x04c3, // XK_kana_TE + 0x30c8: 0x04c4, // XK_kana_TO + 0x30ca: 0x04c5, // XK_kana_NA + 0x30cb: 0x04c6, // XK_kana_NI + 0x30cc: 0x04c7, // XK_kana_NU + 0x30cd: 0x04c8, // XK_kana_NE + 0x30ce: 0x04c9, // XK_kana_NO + 0x30cf: 0x04ca, // XK_kana_HA + 0x30d2: 0x04cb, // XK_kana_HI + 0x30d5: 0x04cc, // XK_kana_FU + 0x30d8: 0x04cd, // XK_kana_HE + 0x30db: 0x04ce, // XK_kana_HO + 0x30de: 0x04cf, // XK_kana_MA + 0x30df: 0x04d0, // XK_kana_MI + 0x30e0: 0x04d1, // XK_kana_MU + 0x30e1: 0x04d2, // XK_kana_ME + 0x30e2: 0x04d3, // XK_kana_MO + 0x30e3: 0x04ac, // XK_kana_ya + 0x30e4: 0x04d4, // XK_kana_YA + 0x30e5: 0x04ad, // XK_kana_yu + 0x30e6: 0x04d5, // XK_kana_YU + 0x30e7: 0x04ae, // XK_kana_yo + 0x30e8: 0x04d6, // XK_kana_YO + 0x30e9: 0x04d7, // XK_kana_RA + 0x30ea: 0x04d8, // XK_kana_RI + 0x30eb: 0x04d9, // XK_kana_RU + 0x30ec: 0x04da, // XK_kana_RE + 0x30ed: 0x04db, // XK_kana_RO + 0x30ef: 0x04dc, // XK_kana_WA + 0x30f2: 0x04a6, // XK_kana_WO + 0x30f3: 0x04dd, // XK_kana_N + 0x30fb: 0x04a5, // XK_kana_conjunctive + 0x30fc: 0x04b0, // XK_prolongedsound +}; + +export default { + lookup(u) { + // Latin-1 is one-to-one mapping + if (u >= 0x20 && u <= 0xff) { + return u; + } + + // Lookup table (fairly random) + const keysym = codepoints[u]; + if (keysym !== undefined) { + return keysym; + } + + // General mapping as final fallback + return 0x01000000 | u; + }, +}; diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/util.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/util.js new file mode 100644 index 000000000..2aa67bffe --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/util.js @@ -0,0 +1,215 @@ +import KeyTable from './keysym.js'; +import keysyms from './keysymdef.js'; +import vkeys from './vkeys.js'; +import fixedkeys from './fixedkeys.js'; +import DOMKeyTable from './domkeytable.js'; +import * as browser from '../util/browser.js'; + +// Get 'KeyboardEvent.code', handling legacy browsers +export function getKeycode(evt) { + // Are we getting proper key identifiers? + // (unfortunately Firefox and Chrome are crappy here and gives + // us an empty string on some platforms, rather than leaving it + // undefined) + if (evt.code) { + // Mozilla isn't fully in sync with the spec yet + switch (evt.code) { + case 'OSLeft': + return 'MetaLeft'; + case 'OSRight': + return 'MetaRight'; + } + + return evt.code; + } + + // The de-facto standard is to use Windows Virtual-Key codes + // in the 'keyCode' field for non-printable characters + if (evt.keyCode in vkeys) { + let code = vkeys[evt.keyCode]; + + // macOS has messed up this code for some reason + if (browser.isMac() && code === 'ContextMenu') { + code = 'MetaRight'; + } + + // The keyCode doesn't distinguish between left and right + // for the standard modifiers + if (evt.location === 2) { + switch (code) { + case 'ShiftLeft': + return 'ShiftRight'; + case 'ControlLeft': + return 'ControlRight'; + case 'AltLeft': + return 'AltRight'; + } + } + + // Nor a bunch of the numpad keys + if (evt.location === 3) { + switch (code) { + case 'Delete': + return 'NumpadDecimal'; + case 'Insert': + return 'Numpad0'; + case 'End': + return 'Numpad1'; + case 'ArrowDown': + return 'Numpad2'; + case 'PageDown': + return 'Numpad3'; + case 'ArrowLeft': + return 'Numpad4'; + case 'ArrowRight': + return 'Numpad6'; + case 'Home': + return 'Numpad7'; + case 'ArrowUp': + return 'Numpad8'; + case 'PageUp': + return 'Numpad9'; + case 'Enter': + return 'NumpadEnter'; + } + } + + return code; + } + + return 'Unidentified'; +} + +// Get 'KeyboardEvent.key', handling legacy browsers +export function getKey(evt) { + // Are we getting a proper key value? + if (evt.key !== undefined) { + // Mozilla isn't fully in sync with the spec yet + switch (evt.key) { + case 'OS': + return 'Meta'; + case 'LaunchMyComputer': + return 'LaunchApplication1'; + case 'LaunchCalculator': + return 'LaunchApplication2'; + } + + // iOS leaks some OS names + switch (evt.key) { + case 'UIKeyInputUpArrow': + return 'ArrowUp'; + case 'UIKeyInputDownArrow': + return 'ArrowDown'; + case 'UIKeyInputLeftArrow': + return 'ArrowLeft'; + case 'UIKeyInputRightArrow': + return 'ArrowRight'; + case 'UIKeyInputEscape': + return 'Escape'; + } + + // Broken behaviour in Chrome + if (evt.key === '\x00' && evt.code === 'NumpadDecimal') { + return 'Delete'; + } + + return evt.key; + } + + // Try to deduce it based on the physical key + const code = getKeycode(evt); + if (code in fixedkeys) { + return fixedkeys[code]; + } + + // If that failed, then see if we have a printable character + if (evt.charCode) { + return String.fromCharCode(evt.charCode); + } + + // At this point we have nothing left to go on + return 'Unidentified'; +} + +// Get the most reliable keysym value we can get from a key event +export function getKeysym(evt) { + const key = getKey(evt); + + if (key === 'Unidentified') { + return null; + } + + // First look up special keys + if (key in DOMKeyTable) { + let location = evt.location; + + // Safari screws up location for the right cmd key + if (key === 'Meta' && location === 0) { + location = 2; + } + + // And for Clear + if (key === 'Clear' && location === 3) { + let code = getKeycode(evt); + if (code === 'NumLock') { + location = 0; + } + } + + if (location === undefined || location > 3) { + location = 0; + } + + // The original Meta key now gets confused with the Windows key + // https://bugs.chromium.org/p/chromium/issues/detail?id=1020141 + // https://bugzilla.mozilla.org/show_bug.cgi?id=1232918 + if (key === 'Meta') { + let code = getKeycode(evt); + if (code === 'AltLeft') { + return KeyTable.XK_Meta_L; + } else if (code === 'AltRight') { + return KeyTable.XK_Meta_R; + } + } + + // macOS has Clear instead of NumLock, but the remote system is + // probably not macOS, so lying here is probably best... + if (key === 'Clear') { + let code = getKeycode(evt); + if (code === 'NumLock') { + return KeyTable.XK_Num_Lock; + } + } + + // Windows sends alternating symbols for some keys when using a + // Japanese layout. We have no way of synchronising with the IM + // running on the remote system, so we send some combined keysym + // instead and hope for the best. + if (browser.isWindows()) { + switch (key) { + case 'Zenkaku': + case 'Hankaku': + return KeyTable.XK_Zenkaku_Hankaku; + case 'Romaji': + case 'KanaMode': + return KeyTable.XK_Romaji; + } + } + + return DOMKeyTable[key][location]; + } + + // Now we need to look at the Unicode symbol instead + + // Special key? (FIXME: Should have been caught earlier) + if (key.length !== 1) { + return null; + } + + const codepoint = key.charCodeAt(); + if (codepoint) { + return keysyms.lookup(codepoint); + } + + return null; +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/vkeys.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/vkeys.js new file mode 100644 index 000000000..83795f9be --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/vkeys.js @@ -0,0 +1,116 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +/* + * Mapping between Microsoft® Windows® Virtual-Key codes and + * HTML key codes. + */ + +export default { + 0x08: 'Backspace', + 0x09: 'Tab', + 0x0a: 'NumpadClear', + 0x0d: 'Enter', + 0x10: 'ShiftLeft', + 0x11: 'ControlLeft', + 0x12: 'AltLeft', + 0x13: 'Pause', + 0x14: 'CapsLock', + 0x15: 'Lang1', + 0x19: 'Lang2', + 0x1b: 'Escape', + 0x1c: 'Convert', + 0x1d: 'NonConvert', + 0x20: 'Space', + 0x21: 'PageUp', + 0x22: 'PageDown', + 0x23: 'End', + 0x24: 'Home', + 0x25: 'ArrowLeft', + 0x26: 'ArrowUp', + 0x27: 'ArrowRight', + 0x28: 'ArrowDown', + 0x29: 'Select', + 0x2c: 'PrintScreen', + 0x2d: 'Insert', + 0x2e: 'Delete', + 0x2f: 'Help', + 0x30: 'Digit0', + 0x31: 'Digit1', + 0x32: 'Digit2', + 0x33: 'Digit3', + 0x34: 'Digit4', + 0x35: 'Digit5', + 0x36: 'Digit6', + 0x37: 'Digit7', + 0x38: 'Digit8', + 0x39: 'Digit9', + 0x5b: 'MetaLeft', + 0x5c: 'MetaRight', + 0x5d: 'ContextMenu', + 0x5f: 'Sleep', + 0x60: 'Numpad0', + 0x61: 'Numpad1', + 0x62: 'Numpad2', + 0x63: 'Numpad3', + 0x64: 'Numpad4', + 0x65: 'Numpad5', + 0x66: 'Numpad6', + 0x67: 'Numpad7', + 0x68: 'Numpad8', + 0x69: 'Numpad9', + 0x6a: 'NumpadMultiply', + 0x6b: 'NumpadAdd', + 0x6c: 'NumpadDecimal', + 0x6d: 'NumpadSubtract', + 0x6e: 'NumpadDecimal', // Duplicate, because buggy on Windows + 0x6f: 'NumpadDivide', + 0x70: 'F1', + 0x71: 'F2', + 0x72: 'F3', + 0x73: 'F4', + 0x74: 'F5', + 0x75: 'F6', + 0x76: 'F7', + 0x77: 'F8', + 0x78: 'F9', + 0x79: 'F10', + 0x7a: 'F11', + 0x7b: 'F12', + 0x7c: 'F13', + 0x7d: 'F14', + 0x7e: 'F15', + 0x7f: 'F16', + 0x80: 'F17', + 0x81: 'F18', + 0x82: 'F19', + 0x83: 'F20', + 0x84: 'F21', + 0x85: 'F22', + 0x86: 'F23', + 0x87: 'F24', + 0x90: 'NumLock', + 0x91: 'ScrollLock', + 0xa6: 'BrowserBack', + 0xa7: 'BrowserForward', + 0xa8: 'BrowserRefresh', + 0xa9: 'BrowserStop', + 0xaa: 'BrowserSearch', + 0xab: 'BrowserFavorites', + 0xac: 'BrowserHome', + 0xad: 'AudioVolumeMute', + 0xae: 'AudioVolumeDown', + 0xaf: 'AudioVolumeUp', + 0xb0: 'MediaTrackNext', + 0xb1: 'MediaTrackPrevious', + 0xb2: 'MediaStop', + 0xb3: 'MediaPlayPause', + 0xb4: 'LaunchMail', + 0xb5: 'MediaSelect', + 0xb6: 'LaunchApp1', + 0xb7: 'LaunchApp2', + 0xe1: 'AltRight', // Only when it is AltGraph +}; diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/xtscancodes.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/xtscancodes.js new file mode 100644 index 000000000..42fccb00e --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/input/xtscancodes.js @@ -0,0 +1,173 @@ +/* + * This file is auto-generated from keymaps.csv + * Database checksum sha256(76d68c10e97d37fe2ea459e210125ae41796253fb217e900bf2983ade13a7920) + * To re-generate, run: + * keymap-gen code-map --lang=js keymaps.csv html atset1 + */ +export default { + Again: 0xe005 /* html:Again (Again) -> linux:129 (KEY_AGAIN) -> atset1:57349 */, + AltLeft: 0x38 /* html:AltLeft (AltLeft) -> linux:56 (KEY_LEFTALT) -> atset1:56 */, + AltRight: 0xe038 /* html:AltRight (AltRight) -> linux:100 (KEY_RIGHTALT) -> atset1:57400 */, + ArrowDown: 0xe050 /* html:ArrowDown (ArrowDown) -> linux:108 (KEY_DOWN) -> atset1:57424 */, + ArrowLeft: 0xe04b /* html:ArrowLeft (ArrowLeft) -> linux:105 (KEY_LEFT) -> atset1:57419 */, + ArrowRight: 0xe04d /* html:ArrowRight (ArrowRight) -> linux:106 (KEY_RIGHT) -> atset1:57421 */, + ArrowUp: 0xe048 /* html:ArrowUp (ArrowUp) -> linux:103 (KEY_UP) -> atset1:57416 */, + AudioVolumeDown: 0xe02e /* html:AudioVolumeDown (AudioVolumeDown) -> linux:114 (KEY_VOLUMEDOWN) -> atset1:57390 */, + AudioVolumeMute: 0xe020 /* html:AudioVolumeMute (AudioVolumeMute) -> linux:113 (KEY_MUTE) -> atset1:57376 */, + AudioVolumeUp: 0xe030 /* html:AudioVolumeUp (AudioVolumeUp) -> linux:115 (KEY_VOLUMEUP) -> atset1:57392 */, + Backquote: 0x29 /* html:Backquote (Backquote) -> linux:41 (KEY_GRAVE) -> atset1:41 */, + Backslash: 0x2b /* html:Backslash (Backslash) -> linux:43 (KEY_BACKSLASH) -> atset1:43 */, + Backspace: 0xe /* html:Backspace (Backspace) -> linux:14 (KEY_BACKSPACE) -> atset1:14 */, + BracketLeft: 0x1a /* html:BracketLeft (BracketLeft) -> linux:26 (KEY_LEFTBRACE) -> atset1:26 */, + BracketRight: 0x1b /* html:BracketRight (BracketRight) -> linux:27 (KEY_RIGHTBRACE) -> atset1:27 */, + BrowserBack: 0xe06a /* html:BrowserBack (BrowserBack) -> linux:158 (KEY_BACK) -> atset1:57450 */, + BrowserFavorites: 0xe066 /* html:BrowserFavorites (BrowserFavorites) -> linux:156 (KEY_BOOKMARKS) -> atset1:57446 */, + BrowserForward: 0xe069 /* html:BrowserForward (BrowserForward) -> linux:159 (KEY_FORWARD) -> atset1:57449 */, + BrowserHome: 0xe032 /* html:BrowserHome (BrowserHome) -> linux:172 (KEY_HOMEPAGE) -> atset1:57394 */, + BrowserRefresh: 0xe067 /* html:BrowserRefresh (BrowserRefresh) -> linux:173 (KEY_REFRESH) -> atset1:57447 */, + BrowserSearch: 0xe065 /* html:BrowserSearch (BrowserSearch) -> linux:217 (KEY_SEARCH) -> atset1:57445 */, + BrowserStop: 0xe068 /* html:BrowserStop (BrowserStop) -> linux:128 (KEY_STOP) -> atset1:57448 */, + CapsLock: 0x3a /* html:CapsLock (CapsLock) -> linux:58 (KEY_CAPSLOCK) -> atset1:58 */, + Comma: 0x33 /* html:Comma (Comma) -> linux:51 (KEY_COMMA) -> atset1:51 */, + ContextMenu: 0xe05d /* html:ContextMenu (ContextMenu) -> linux:127 (KEY_COMPOSE) -> atset1:57437 */, + ControlLeft: 0x1d /* html:ControlLeft (ControlLeft) -> linux:29 (KEY_LEFTCTRL) -> atset1:29 */, + ControlRight: 0xe01d /* html:ControlRight (ControlRight) -> linux:97 (KEY_RIGHTCTRL) -> atset1:57373 */, + Convert: 0x79 /* html:Convert (Convert) -> linux:92 (KEY_HENKAN) -> atset1:121 */, + Copy: 0xe078 /* html:Copy (Copy) -> linux:133 (KEY_COPY) -> atset1:57464 */, + Cut: 0xe03c /* html:Cut (Cut) -> linux:137 (KEY_CUT) -> atset1:57404 */, + Delete: 0xe053 /* html:Delete (Delete) -> linux:111 (KEY_DELETE) -> atset1:57427 */, + Digit0: 0xb /* html:Digit0 (Digit0) -> linux:11 (KEY_0) -> atset1:11 */, + Digit1: 0x2 /* html:Digit1 (Digit1) -> linux:2 (KEY_1) -> atset1:2 */, + Digit2: 0x3 /* html:Digit2 (Digit2) -> linux:3 (KEY_2) -> atset1:3 */, + Digit3: 0x4 /* html:Digit3 (Digit3) -> linux:4 (KEY_3) -> atset1:4 */, + Digit4: 0x5 /* html:Digit4 (Digit4) -> linux:5 (KEY_4) -> atset1:5 */, + Digit5: 0x6 /* html:Digit5 (Digit5) -> linux:6 (KEY_5) -> atset1:6 */, + Digit6: 0x7 /* html:Digit6 (Digit6) -> linux:7 (KEY_6) -> atset1:7 */, + Digit7: 0x8 /* html:Digit7 (Digit7) -> linux:8 (KEY_7) -> atset1:8 */, + Digit8: 0x9 /* html:Digit8 (Digit8) -> linux:9 (KEY_8) -> atset1:9 */, + Digit9: 0xa /* html:Digit9 (Digit9) -> linux:10 (KEY_9) -> atset1:10 */, + Eject: 0xe07d /* html:Eject (Eject) -> linux:162 (KEY_EJECTCLOSECD) -> atset1:57469 */, + End: 0xe04f /* html:End (End) -> linux:107 (KEY_END) -> atset1:57423 */, + Enter: 0x1c /* html:Enter (Enter) -> linux:28 (KEY_ENTER) -> atset1:28 */, + Equal: 0xd /* html:Equal (Equal) -> linux:13 (KEY_EQUAL) -> atset1:13 */, + Escape: 0x1 /* html:Escape (Escape) -> linux:1 (KEY_ESC) -> atset1:1 */, + F1: 0x3b /* html:F1 (F1) -> linux:59 (KEY_F1) -> atset1:59 */, + F10: 0x44 /* html:F10 (F10) -> linux:68 (KEY_F10) -> atset1:68 */, + F11: 0x57 /* html:F11 (F11) -> linux:87 (KEY_F11) -> atset1:87 */, + F12: 0x58 /* html:F12 (F12) -> linux:88 (KEY_F12) -> atset1:88 */, + F13: 0x5d /* html:F13 (F13) -> linux:183 (KEY_F13) -> atset1:93 */, + F14: 0x5e /* html:F14 (F14) -> linux:184 (KEY_F14) -> atset1:94 */, + F15: 0x5f /* html:F15 (F15) -> linux:185 (KEY_F15) -> atset1:95 */, + F16: 0x55 /* html:F16 (F16) -> linux:186 (KEY_F16) -> atset1:85 */, + F17: 0xe003 /* html:F17 (F17) -> linux:187 (KEY_F17) -> atset1:57347 */, + F18: 0xe077 /* html:F18 (F18) -> linux:188 (KEY_F18) -> atset1:57463 */, + F19: 0xe004 /* html:F19 (F19) -> linux:189 (KEY_F19) -> atset1:57348 */, + F2: 0x3c /* html:F2 (F2) -> linux:60 (KEY_F2) -> atset1:60 */, + F20: 0x5a /* html:F20 (F20) -> linux:190 (KEY_F20) -> atset1:90 */, + F21: 0x74 /* html:F21 (F21) -> linux:191 (KEY_F21) -> atset1:116 */, + F22: 0xe079 /* html:F22 (F22) -> linux:192 (KEY_F22) -> atset1:57465 */, + F23: 0x6d /* html:F23 (F23) -> linux:193 (KEY_F23) -> atset1:109 */, + F24: 0x6f /* html:F24 (F24) -> linux:194 (KEY_F24) -> atset1:111 */, + F3: 0x3d /* html:F3 (F3) -> linux:61 (KEY_F3) -> atset1:61 */, + F4: 0x3e /* html:F4 (F4) -> linux:62 (KEY_F4) -> atset1:62 */, + F5: 0x3f /* html:F5 (F5) -> linux:63 (KEY_F5) -> atset1:63 */, + F6: 0x40 /* html:F6 (F6) -> linux:64 (KEY_F6) -> atset1:64 */, + F7: 0x41 /* html:F7 (F7) -> linux:65 (KEY_F7) -> atset1:65 */, + F8: 0x42 /* html:F8 (F8) -> linux:66 (KEY_F8) -> atset1:66 */, + F9: 0x43 /* html:F9 (F9) -> linux:67 (KEY_F9) -> atset1:67 */, + Find: 0xe041 /* html:Find (Find) -> linux:136 (KEY_FIND) -> atset1:57409 */, + Help: 0xe075 /* html:Help (Help) -> linux:138 (KEY_HELP) -> atset1:57461 */, + Hiragana: 0x77 /* html:Hiragana (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */, + Home: 0xe047 /* html:Home (Home) -> linux:102 (KEY_HOME) -> atset1:57415 */, + Insert: 0xe052 /* html:Insert (Insert) -> linux:110 (KEY_INSERT) -> atset1:57426 */, + IntlBackslash: 0x56 /* html:IntlBackslash (IntlBackslash) -> linux:86 (KEY_102ND) -> atset1:86 */, + IntlRo: 0x73 /* html:IntlRo (IntlRo) -> linux:89 (KEY_RO) -> atset1:115 */, + IntlYen: 0x7d /* html:IntlYen (IntlYen) -> linux:124 (KEY_YEN) -> atset1:125 */, + KanaMode: 0x70 /* html:KanaMode (KanaMode) -> linux:93 (KEY_KATAKANAHIRAGANA) -> atset1:112 */, + Katakana: 0x78 /* html:Katakana (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */, + KeyA: 0x1e /* html:KeyA (KeyA) -> linux:30 (KEY_A) -> atset1:30 */, + KeyB: 0x30 /* html:KeyB (KeyB) -> linux:48 (KEY_B) -> atset1:48 */, + KeyC: 0x2e /* html:KeyC (KeyC) -> linux:46 (KEY_C) -> atset1:46 */, + KeyD: 0x20 /* html:KeyD (KeyD) -> linux:32 (KEY_D) -> atset1:32 */, + KeyE: 0x12 /* html:KeyE (KeyE) -> linux:18 (KEY_E) -> atset1:18 */, + KeyF: 0x21 /* html:KeyF (KeyF) -> linux:33 (KEY_F) -> atset1:33 */, + KeyG: 0x22 /* html:KeyG (KeyG) -> linux:34 (KEY_G) -> atset1:34 */, + KeyH: 0x23 /* html:KeyH (KeyH) -> linux:35 (KEY_H) -> atset1:35 */, + KeyI: 0x17 /* html:KeyI (KeyI) -> linux:23 (KEY_I) -> atset1:23 */, + KeyJ: 0x24 /* html:KeyJ (KeyJ) -> linux:36 (KEY_J) -> atset1:36 */, + KeyK: 0x25 /* html:KeyK (KeyK) -> linux:37 (KEY_K) -> atset1:37 */, + KeyL: 0x26 /* html:KeyL (KeyL) -> linux:38 (KEY_L) -> atset1:38 */, + KeyM: 0x32 /* html:KeyM (KeyM) -> linux:50 (KEY_M) -> atset1:50 */, + KeyN: 0x31 /* html:KeyN (KeyN) -> linux:49 (KEY_N) -> atset1:49 */, + KeyO: 0x18 /* html:KeyO (KeyO) -> linux:24 (KEY_O) -> atset1:24 */, + KeyP: 0x19 /* html:KeyP (KeyP) -> linux:25 (KEY_P) -> atset1:25 */, + KeyQ: 0x10 /* html:KeyQ (KeyQ) -> linux:16 (KEY_Q) -> atset1:16 */, + KeyR: 0x13 /* html:KeyR (KeyR) -> linux:19 (KEY_R) -> atset1:19 */, + KeyS: 0x1f /* html:KeyS (KeyS) -> linux:31 (KEY_S) -> atset1:31 */, + KeyT: 0x14 /* html:KeyT (KeyT) -> linux:20 (KEY_T) -> atset1:20 */, + KeyU: 0x16 /* html:KeyU (KeyU) -> linux:22 (KEY_U) -> atset1:22 */, + KeyV: 0x2f /* html:KeyV (KeyV) -> linux:47 (KEY_V) -> atset1:47 */, + KeyW: 0x11 /* html:KeyW (KeyW) -> linux:17 (KEY_W) -> atset1:17 */, + KeyX: 0x2d /* html:KeyX (KeyX) -> linux:45 (KEY_X) -> atset1:45 */, + KeyY: 0x15 /* html:KeyY (KeyY) -> linux:21 (KEY_Y) -> atset1:21 */, + KeyZ: 0x2c /* html:KeyZ (KeyZ) -> linux:44 (KEY_Z) -> atset1:44 */, + Lang1: 0x72 /* html:Lang1 (Lang1) -> linux:122 (KEY_HANGEUL) -> atset1:114 */, + Lang2: 0x71 /* html:Lang2 (Lang2) -> linux:123 (KEY_HANJA) -> atset1:113 */, + Lang3: 0x78 /* html:Lang3 (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */, + Lang4: 0x77 /* html:Lang4 (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */, + Lang5: 0x76 /* html:Lang5 (Lang5) -> linux:85 (KEY_ZENKAKUHANKAKU) -> atset1:118 */, + LaunchApp1: 0xe06b /* html:LaunchApp1 (LaunchApp1) -> linux:157 (KEY_COMPUTER) -> atset1:57451 */, + LaunchApp2: 0xe021 /* html:LaunchApp2 (LaunchApp2) -> linux:140 (KEY_CALC) -> atset1:57377 */, + LaunchMail: 0xe06c /* html:LaunchMail (LaunchMail) -> linux:155 (KEY_MAIL) -> atset1:57452 */, + MediaPlayPause: 0xe022 /* html:MediaPlayPause (MediaPlayPause) -> linux:164 (KEY_PLAYPAUSE) -> atset1:57378 */, + MediaSelect: 0xe06d /* html:MediaSelect (MediaSelect) -> linux:226 (KEY_MEDIA) -> atset1:57453 */, + MediaStop: 0xe024 /* html:MediaStop (MediaStop) -> linux:166 (KEY_STOPCD) -> atset1:57380 */, + MediaTrackNext: 0xe019 /* html:MediaTrackNext (MediaTrackNext) -> linux:163 (KEY_NEXTSONG) -> atset1:57369 */, + MediaTrackPrevious: 0xe010 /* html:MediaTrackPrevious (MediaTrackPrevious) -> linux:165 (KEY_PREVIOUSSONG) -> atset1:57360 */, + MetaLeft: 0xe05b /* html:MetaLeft (MetaLeft) -> linux:125 (KEY_LEFTMETA) -> atset1:57435 */, + MetaRight: 0xe05c /* html:MetaRight (MetaRight) -> linux:126 (KEY_RIGHTMETA) -> atset1:57436 */, + Minus: 0xc /* html:Minus (Minus) -> linux:12 (KEY_MINUS) -> atset1:12 */, + NonConvert: 0x7b /* html:NonConvert (NonConvert) -> linux:94 (KEY_MUHENKAN) -> atset1:123 */, + NumLock: 0x45 /* html:NumLock (NumLock) -> linux:69 (KEY_NUMLOCK) -> atset1:69 */, + Numpad0: 0x52 /* html:Numpad0 (Numpad0) -> linux:82 (KEY_KP0) -> atset1:82 */, + Numpad1: 0x4f /* html:Numpad1 (Numpad1) -> linux:79 (KEY_KP1) -> atset1:79 */, + Numpad2: 0x50 /* html:Numpad2 (Numpad2) -> linux:80 (KEY_KP2) -> atset1:80 */, + Numpad3: 0x51 /* html:Numpad3 (Numpad3) -> linux:81 (KEY_KP3) -> atset1:81 */, + Numpad4: 0x4b /* html:Numpad4 (Numpad4) -> linux:75 (KEY_KP4) -> atset1:75 */, + Numpad5: 0x4c /* html:Numpad5 (Numpad5) -> linux:76 (KEY_KP5) -> atset1:76 */, + Numpad6: 0x4d /* html:Numpad6 (Numpad6) -> linux:77 (KEY_KP6) -> atset1:77 */, + Numpad7: 0x47 /* html:Numpad7 (Numpad7) -> linux:71 (KEY_KP7) -> atset1:71 */, + Numpad8: 0x48 /* html:Numpad8 (Numpad8) -> linux:72 (KEY_KP8) -> atset1:72 */, + Numpad9: 0x49 /* html:Numpad9 (Numpad9) -> linux:73 (KEY_KP9) -> atset1:73 */, + NumpadAdd: 0x4e /* html:NumpadAdd (NumpadAdd) -> linux:78 (KEY_KPPLUS) -> atset1:78 */, + NumpadComma: 0x7e /* html:NumpadComma (NumpadComma) -> linux:121 (KEY_KPCOMMA) -> atset1:126 */, + NumpadDecimal: 0x53 /* html:NumpadDecimal (NumpadDecimal) -> linux:83 (KEY_KPDOT) -> atset1:83 */, + NumpadDivide: 0xe035 /* html:NumpadDivide (NumpadDivide) -> linux:98 (KEY_KPSLASH) -> atset1:57397 */, + NumpadEnter: 0xe01c /* html:NumpadEnter (NumpadEnter) -> linux:96 (KEY_KPENTER) -> atset1:57372 */, + NumpadEqual: 0x59 /* html:NumpadEqual (NumpadEqual) -> linux:117 (KEY_KPEQUAL) -> atset1:89 */, + NumpadMultiply: 0x37 /* html:NumpadMultiply (NumpadMultiply) -> linux:55 (KEY_KPASTERISK) -> atset1:55 */, + NumpadParenLeft: 0xe076 /* html:NumpadParenLeft (NumpadParenLeft) -> linux:179 (KEY_KPLEFTPAREN) -> atset1:57462 */, + NumpadParenRight: 0xe07b /* html:NumpadParenRight (NumpadParenRight) -> linux:180 (KEY_KPRIGHTPAREN) -> atset1:57467 */, + NumpadSubtract: 0x4a /* html:NumpadSubtract (NumpadSubtract) -> linux:74 (KEY_KPMINUS) -> atset1:74 */, + Open: 0x64 /* html:Open (Open) -> linux:134 (KEY_OPEN) -> atset1:100 */, + PageDown: 0xe051 /* html:PageDown (PageDown) -> linux:109 (KEY_PAGEDOWN) -> atset1:57425 */, + PageUp: 0xe049 /* html:PageUp (PageUp) -> linux:104 (KEY_PAGEUP) -> atset1:57417 */, + Paste: 0x65 /* html:Paste (Paste) -> linux:135 (KEY_PASTE) -> atset1:101 */, + Pause: 0xe046 /* html:Pause (Pause) -> linux:119 (KEY_PAUSE) -> atset1:57414 */, + Period: 0x34 /* html:Period (Period) -> linux:52 (KEY_DOT) -> atset1:52 */, + Power: 0xe05e /* html:Power (Power) -> linux:116 (KEY_POWER) -> atset1:57438 */, + PrintScreen: 0x54 /* html:PrintScreen (PrintScreen) -> linux:99 (KEY_SYSRQ) -> atset1:84 */, + Props: 0xe006 /* html:Props (Props) -> linux:130 (KEY_PROPS) -> atset1:57350 */, + Quote: 0x28 /* html:Quote (Quote) -> linux:40 (KEY_APOSTROPHE) -> atset1:40 */, + ScrollLock: 0x46 /* html:ScrollLock (ScrollLock) -> linux:70 (KEY_SCROLLLOCK) -> atset1:70 */, + Semicolon: 0x27 /* html:Semicolon (Semicolon) -> linux:39 (KEY_SEMICOLON) -> atset1:39 */, + ShiftLeft: 0x2a /* html:ShiftLeft (ShiftLeft) -> linux:42 (KEY_LEFTSHIFT) -> atset1:42 */, + ShiftRight: 0x36 /* html:ShiftRight (ShiftRight) -> linux:54 (KEY_RIGHTSHIFT) -> atset1:54 */, + Slash: 0x35 /* html:Slash (Slash) -> linux:53 (KEY_SLASH) -> atset1:53 */, + Sleep: 0xe05f /* html:Sleep (Sleep) -> linux:142 (KEY_SLEEP) -> atset1:57439 */, + Space: 0x39 /* html:Space (Space) -> linux:57 (KEY_SPACE) -> atset1:57 */, + Suspend: 0xe025 /* html:Suspend (Suspend) -> linux:205 (KEY_SUSPEND) -> atset1:57381 */, + Tab: 0xf /* html:Tab (Tab) -> linux:15 (KEY_TAB) -> atset1:15 */, + Undo: 0xe007 /* html:Undo (Undo) -> linux:131 (KEY_UNDO) -> atset1:57351 */, + WakeUp: 0xe063 /* html:WakeUp (WakeUp) -> linux:143 (KEY_WAKEUP) -> atset1:57443 */, +}; diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/ra2.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/ra2.js new file mode 100644 index 000000000..ffcbf8b15 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/ra2.js @@ -0,0 +1,585 @@ +import Base64 from './base64.js'; +import { encodeUTF8 } from './util/strings.js'; +import EventTargetMixin from './util/eventtarget.js'; + +export class AESEAXCipher { + constructor() { + this._rawKey = null; + this._ctrKey = null; + this._cbcKey = null; + this._zeroBlock = new Uint8Array(16); + this._prefixBlock0 = this._zeroBlock; + this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + } + + async _encryptBlock(block) { + const encrypted = await window.crypto.subtle.encrypt( + { + name: 'AES-CBC', + iv: this._zeroBlock, + }, + this._cbcKey, + block + ); + return new Uint8Array(encrypted).slice(0, 16); + } + + async _initCMAC() { + const k1 = await this._encryptBlock(this._zeroBlock); + const k2 = new Uint8Array(16); + const v = k1[0] >>> 6; + for (let i = 0; i < 15; i++) { + k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2); + k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1); + } + const lut = [0x0, 0x87, 0x0e, 0x89]; + k2[14] ^= v >>> 1; + k2[15] = (k1[15] << 2) ^ lut[v]; + k1[15] = (k1[15] << 1) ^ lut[v >> 1]; + this._k1 = k1; + this._k2 = k2; + } + + async _encryptCTR(data, counter) { + const encrypted = await window.crypto.subtle.encrypt( + { + name: 'AES-CTR', + counter: counter, + length: 128, + }, + this._ctrKey, + data + ); + return new Uint8Array(encrypted); + } + + async _decryptCTR(data, counter) { + const decrypted = await window.crypto.subtle.decrypt( + { + name: 'AES-CTR', + counter: counter, + length: 128, + }, + this._ctrKey, + data + ); + return new Uint8Array(decrypted); + } + + async _computeCMAC(data, prefixBlock) { + if (prefixBlock.length !== 16) { + return null; + } + const n = Math.floor(data.length / 16); + const m = Math.ceil(data.length / 16); + const r = data.length - n * 16; + const cbcData = new Uint8Array((m + 1) * 16); + cbcData.set(prefixBlock); + cbcData.set(data, 16); + if (r === 0) { + for (let i = 0; i < 16; i++) { + cbcData[n * 16 + i] ^= this._k1[i]; + } + } else { + cbcData[(n + 1) * 16 + r] = 0x80; + for (let i = 0; i < 16; i++) { + cbcData[(n + 1) * 16 + i] ^= this._k2[i]; + } + } + let cbcEncrypted = await window.crypto.subtle.encrypt( + { + name: 'AES-CBC', + iv: this._zeroBlock, + }, + this._cbcKey, + cbcData + ); + + cbcEncrypted = new Uint8Array(cbcEncrypted); + const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16); + return mac; + } + + async setKey(key) { + this._rawKey = key; + this._ctrKey = await window.crypto.subtle.importKey('raw', key, { name: 'AES-CTR' }, false, ['encrypt', 'decrypt']); + this._cbcKey = await window.crypto.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['encrypt', 'decrypt']); + await this._initCMAC(); + } + + async encrypt(message, associatedData, nonce) { + const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); + const encrypted = await this._encryptCTR(message, nCMAC); + const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1); + const mac = await this._computeCMAC(encrypted, this._prefixBlock2); + for (let i = 0; i < 16; i++) { + mac[i] ^= nCMAC[i] ^ adCMAC[i]; + } + const res = new Uint8Array(16 + encrypted.length); + res.set(encrypted); + res.set(mac, encrypted.length); + return res; + } + + async decrypt(encrypted, associatedData, nonce, mac) { + const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); + const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1); + const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2); + for (let i = 0; i < 16; i++) { + computedMac[i] ^= nCMAC[i] ^ adCMAC[i]; + } + if (computedMac.length !== mac.length) { + return null; + } + for (let i = 0; i < mac.length; i++) { + if (computedMac[i] !== mac[i]) { + return null; + } + } + const res = await this._decryptCTR(encrypted, nCMAC); + return res; + } +} + +export class RA2Cipher { + constructor() { + this._cipher = new AESEAXCipher(); + this._counter = new Uint8Array(16); + } + + async setKey(key) { + await this._cipher.setKey(key); + } + + async makeMessage(message) { + const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]); + const encrypted = await this._cipher.encrypt(message, ad, this._counter); + for (let i = 0; i < 16 && this._counter[i]++ === 255; i++); + const res = new Uint8Array(message.length + 2 + 16); + res.set(ad); + res.set(encrypted, 2); + return res; + } + + async receiveMessage(length, encrypted, mac) { + const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]); + const res = await this._cipher.decrypt(encrypted, ad, this._counter, mac); + for (let i = 0; i < 16 && this._counter[i]++ === 255; i++); + return res; + } +} + +export class RSACipher { + constructor(keyLength) { + this._key = null; + this._keyLength = keyLength; + this._keyBytes = Math.ceil(keyLength / 8); + this._n = null; + this._e = null; + this._d = null; + this._nBigInt = null; + this._eBigInt = null; + this._dBigInt = null; + } + + _base64urlDecode(data) { + data = data.replace(/-/g, '+').replace(/_/g, '/'); + data = data.padEnd(Math.ceil(data.length / 4) * 4, '='); + return Base64.decode(data); + } + + _u8ArrayToBigInt(arr) { + let hex = '0x'; + for (let i = 0; i < arr.length; i++) { + hex += arr[i].toString(16).padStart(2, '0'); + } + return BigInt(hex); + } + + _padArray(arr, length) { + const res = new Uint8Array(length); + res.set(arr, length - arr.length); + return res; + } + + _bigIntToU8Array(bigint, padLength = 0) { + let hex = bigint.toString(16); + if (padLength === 0) { + padLength = Math.ceil(hex.length / 2) * 2; + } + hex = hex.padStart(padLength * 2, '0'); + const length = hex.length / 2; + const arr = new Uint8Array(length); + for (let i = 0; i < length; i++) { + arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + return arr; + } + + _modPow(b, e, m) { + if (m === 1n) { + return 0; + } + let r = 1n; + b = b % m; + while (e > 0) { + if (e % 2n === 1n) { + r = (r * b) % m; + } + e = e / 2n; + b = (b * b) % m; + } + return r; + } + + async generateKey() { + this._key = await window.crypto.subtle.generateKey( + { + name: 'RSA-OAEP', + modulusLength: this._keyLength, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: { name: 'SHA-256' }, + }, + true, + ['encrypt', 'decrypt'] + ); + const privateKey = await window.crypto.subtle.exportKey('jwk', this._key.privateKey); + this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes); + this._nBigInt = this._u8ArrayToBigInt(this._n); + this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes); + this._eBigInt = this._u8ArrayToBigInt(this._e); + this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes); + this._dBigInt = this._u8ArrayToBigInt(this._d); + } + + setPublicKey(n, e) { + if (n.length !== this._keyBytes || e.length !== this._keyBytes) { + return; + } + this._n = new Uint8Array(this._keyBytes); + this._e = new Uint8Array(this._keyBytes); + this._n.set(n); + this._e.set(e); + this._nBigInt = this._u8ArrayToBigInt(this._n); + this._eBigInt = this._u8ArrayToBigInt(this._e); + } + + encrypt(message) { + if (message.length > this._keyBytes - 11) { + return null; + } + const ps = new Uint8Array(this._keyBytes - message.length - 3); + window.crypto.getRandomValues(ps); + for (let i = 0; i < ps.length; i++) { + ps[i] = Math.floor((ps[i] * 254) / 255 + 1); + } + const em = new Uint8Array(this._keyBytes); + em[1] = 0x02; + em.set(ps, 2); + em.set(message, ps.length + 3); + const emBigInt = this._u8ArrayToBigInt(em); + const c = this._modPow(emBigInt, this._eBigInt, this._nBigInt); + return this._bigIntToU8Array(c, this._keyBytes); + } + + decrypt(message) { + if (message.length !== this._keyBytes) { + return null; + } + const msgBigInt = this._u8ArrayToBigInt(message); + const emBigInt = this._modPow(msgBigInt, this._dBigInt, this._nBigInt); + const em = this._bigIntToU8Array(emBigInt, this._keyBytes); + if (em[0] !== 0x00 || em[1] !== 0x02) { + return null; + } + let i = 2; + for (; i < em.length; i++) { + if (em[i] === 0x00) { + break; + } + } + if (i === em.length) { + return null; + } + return em.slice(i + 1, em.length); + } + + get keyLength() { + return this._keyLength; + } + + get n() { + return this._n; + } + + get e() { + return this._e; + } + + get d() { + return this._d; + } +} + +export default class RSAAESAuthenticationState extends EventTargetMixin { + constructor(sock, getCredentials) { + super(); + this._hasStarted = false; + this._checkSock = null; + this._checkCredentials = null; + this._approveServerResolve = null; + this._sockReject = null; + this._credentialsReject = null; + this._approveServerReject = null; + this._sock = sock; + this._getCredentials = getCredentials; + } + + _waitSockAsync(len) { + return new Promise((resolve, reject) => { + const hasData = () => !this._sock.rQwait('RA2', len); + if (hasData()) { + resolve(); + } else { + this._checkSock = () => { + if (hasData()) { + resolve(); + this._checkSock = null; + this._sockReject = null; + } + }; + this._sockReject = reject; + } + }); + } + + _waitApproveKeyAsync() { + return new Promise((resolve, reject) => { + this._approveServerResolve = resolve; + this._approveServerReject = reject; + }); + } + + _waitCredentialsAsync(subtype) { + const hasCredentials = () => { + if ( + subtype === 1 && + this._getCredentials().username !== undefined && + this._getCredentials().password !== undefined + ) { + return true; + } else if (subtype === 2 && this._getCredentials().password !== undefined) { + return true; + } + return false; + }; + return new Promise((resolve, reject) => { + if (hasCredentials()) { + resolve(); + } else { + this._checkCredentials = () => { + if (hasCredentials()) { + resolve(); + this._checkCredentials = null; + this._credentialsReject = null; + } + }; + this._credentialsReject = reject; + } + }); + } + + checkInternalEvents() { + if (this._checkSock !== null) { + this._checkSock(); + } + if (this._checkCredentials !== null) { + this._checkCredentials(); + } + } + + approveServer() { + if (this._approveServerResolve !== null) { + this._approveServerResolve(); + this._approveServerResolve = null; + } + } + + disconnect() { + if (this._sockReject !== null) { + this._sockReject(new Error('disconnect normally')); + this._sockReject = null; + } + if (this._credentialsReject !== null) { + this._credentialsReject(new Error('disconnect normally')); + this._credentialsReject = null; + } + if (this._approveServerReject !== null) { + this._approveServerReject(new Error('disconnect normally')); + this._approveServerReject = null; + } + } + + async negotiateRA2neAuthAsync() { + this._hasStarted = true; + // 1: Receive server public key + await this._waitSockAsync(4); + const serverKeyLengthBuffer = this._sock.rQslice(0, 4); + const serverKeyLength = this._sock.rQshift32(); + if (serverKeyLength < 1024) { + throw new Error('RA2: server public key is too short: ' + serverKeyLength); + } else if (serverKeyLength > 8192) { + throw new Error('RA2: server public key is too long: ' + serverKeyLength); + } + const serverKeyBytes = Math.ceil(serverKeyLength / 8); + await this._waitSockAsync(serverKeyBytes * 2); + const serverN = this._sock.rQshiftBytes(serverKeyBytes); + const serverE = this._sock.rQshiftBytes(serverKeyBytes); + const serverRSACipher = new RSACipher(serverKeyLength); + serverRSACipher.setPublicKey(serverN, serverE); + const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2); + serverPublickey.set(serverKeyLengthBuffer); + serverPublickey.set(serverN, 4); + serverPublickey.set(serverE, 4 + serverKeyBytes); + + // verify server public key + this.dispatchEvent( + new CustomEvent('serververification', { + detail: { type: 'RSA', publickey: serverPublickey }, + }) + ); + await this._waitApproveKeyAsync(); + + // 2: Send client public key + const clientKeyLength = 2048; + const clientKeyBytes = Math.ceil(clientKeyLength / 8); + const clientRSACipher = new RSACipher(clientKeyLength); + await clientRSACipher.generateKey(); + const clientN = clientRSACipher.n; + const clientE = clientRSACipher.e; + const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2); + clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24; + clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16; + clientPublicKey[2] = (clientKeyLength & 0xff00) >>> 8; + clientPublicKey[3] = clientKeyLength & 0xff; + clientPublicKey.set(clientN, 4); + clientPublicKey.set(clientE, 4 + clientKeyBytes); + this._sock.send(clientPublicKey); + + // 3: Send client random + const clientRandom = new Uint8Array(16); + window.crypto.getRandomValues(clientRandom); + const clientEncryptedRandom = serverRSACipher.encrypt(clientRandom); + const clientRandomMessage = new Uint8Array(2 + serverKeyBytes); + clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8; + clientRandomMessage[1] = serverKeyBytes & 0xff; + clientRandomMessage.set(clientEncryptedRandom, 2); + this._sock.send(clientRandomMessage); + + // 4: Receive server random + await this._waitSockAsync(2); + if (this._sock.rQshift16() !== clientKeyBytes) { + throw new Error('RA2: wrong encrypted message length'); + } + const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes); + const serverRandom = clientRSACipher.decrypt(serverEncryptedRandom); + if (serverRandom === null || serverRandom.length !== 16) { + throw new Error('RA2: corrupted server encrypted random'); + } + + // 5: Compute session keys and set ciphers + let clientSessionKey = new Uint8Array(32); + let serverSessionKey = new Uint8Array(32); + clientSessionKey.set(serverRandom); + clientSessionKey.set(clientRandom, 16); + serverSessionKey.set(clientRandom); + serverSessionKey.set(serverRandom, 16); + clientSessionKey = await window.crypto.subtle.digest('SHA-1', clientSessionKey); + clientSessionKey = new Uint8Array(clientSessionKey).slice(0, 16); + serverSessionKey = await window.crypto.subtle.digest('SHA-1', serverSessionKey); + serverSessionKey = new Uint8Array(serverSessionKey).slice(0, 16); + const clientCipher = new RA2Cipher(); + await clientCipher.setKey(clientSessionKey); + const serverCipher = new RA2Cipher(); + await serverCipher.setKey(serverSessionKey); + + // 6: Compute and exchange hashes + let serverHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2); + let clientHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2); + serverHash.set(serverPublickey); + serverHash.set(clientPublicKey, 4 + serverKeyBytes * 2); + clientHash.set(clientPublicKey); + clientHash.set(serverPublickey, 4 + clientKeyBytes * 2); + serverHash = await window.crypto.subtle.digest('SHA-1', serverHash); + clientHash = await window.crypto.subtle.digest('SHA-1', clientHash); + serverHash = new Uint8Array(serverHash); + clientHash = new Uint8Array(clientHash); + this._sock.send(await clientCipher.makeMessage(clientHash)); + await this._waitSockAsync(2 + 20 + 16); + if (this._sock.rQshift16() !== 20) { + throw new Error('RA2: wrong server hash'); + } + const serverHashReceived = await serverCipher.receiveMessage( + 20, + this._sock.rQshiftBytes(20), + this._sock.rQshiftBytes(16) + ); + if (serverHashReceived === null) { + throw new Error('RA2: failed to authenticate the message'); + } + for (let i = 0; i < 20; i++) { + if (serverHashReceived[i] !== serverHash[i]) { + throw new Error('RA2: wrong server hash'); + } + } + + // 7: Receive subtype + await this._waitSockAsync(2 + 1 + 16); + if (this._sock.rQshift16() !== 1) { + throw new Error('RA2: wrong subtype'); + } + let subtype = await serverCipher.receiveMessage(1, this._sock.rQshiftBytes(1), this._sock.rQshiftBytes(16)); + if (subtype === null) { + throw new Error('RA2: failed to authenticate the message'); + } + subtype = subtype[0]; + if (subtype === 1) { + if (this._getCredentials().username === undefined || this._getCredentials().password === undefined) { + this.dispatchEvent(new CustomEvent('credentialsrequired', { detail: { types: ['username', 'password'] } })); + } + } else if (subtype === 2) { + if (this._getCredentials().password === undefined) { + this.dispatchEvent(new CustomEvent('credentialsrequired', { detail: { types: ['password'] } })); + } + } else { + throw new Error('RA2: wrong subtype'); + } + await this._waitCredentialsAsync(subtype); + let username; + if (subtype === 1) { + username = encodeUTF8(this._getCredentials().username).slice(0, 255); + } else { + username = ''; + } + const password = encodeUTF8(this._getCredentials().password).slice(0, 255); + const credentials = new Uint8Array(username.length + password.length + 2); + credentials[0] = username.length; + credentials[username.length + 1] = password.length; + for (let i = 0; i < username.length; i++) { + credentials[i + 1] = username.charCodeAt(i); + } + for (let i = 0; i < password.length; i++) { + credentials[username.length + 2 + i] = password.charCodeAt(i); + } + this._sock.send(await clientCipher.makeMessage(credentials)); + } + + get hasStarted() { + return this._hasStarted; + } + + set hasStarted(s) { + this._hasStarted = s; + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/rfb.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/rfb.js new file mode 100644 index 000000000..f2056b994 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/rfb.js @@ -0,0 +1,3431 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2020 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +import { toUnsigned32bit, toSigned32bit } from './util/int.js'; +import * as Log from './util/logging.js'; +import { encodeUTF8, decodeUTF8 } from './util/strings.js'; +import { dragThreshold } from './util/browser.js'; +import { clientToElement } from './util/element.js'; +import { setCapture } from './util/events.js'; +import EventTargetMixin from './util/eventtarget.js'; +import Display from './display.js'; +import Inflator from './inflator.js'; +import Deflator from './deflator.js'; +import Keyboard from './input/keyboard.js'; +import GestureHandler from './input/gesturehandler.js'; +import Cursor from './util/cursor.js'; +import Websock from './websock.js'; +import DES from './des.js'; +import KeyTable from './input/keysym.js'; +import XtScancode from './input/xtscancodes.js'; +import { encodings } from './encodings.js'; +import RSAAESAuthenticationState from './ra2.js'; +import { MD5 } from './util/md5.js'; + +import RawDecoder from './decoders/raw.js'; +import CopyRectDecoder from './decoders/copyrect.js'; +import RREDecoder from './decoders/rre.js'; +import HextileDecoder from './decoders/hextile.js'; +import TightDecoder from './decoders/tight.js'; +import TightPNGDecoder from './decoders/tightpng.js'; +import ZRLEDecoder from './decoders/zrle.js'; +import JPEGDecoder from './decoders/jpeg.js'; + +// How many seconds to wait for a disconnect to finish +const DISCONNECT_TIMEOUT = 3; +const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)'; + +// Minimum wait (ms) between two mouse moves +const MOUSE_MOVE_DELAY = 17; + +// Wheel thresholds +const WHEEL_STEP = 50; // Pixels needed for one step +const WHEEL_LINE_HEIGHT = 19; // Assumed pixels for one line step + +// Gesture thresholds +const GESTURE_ZOOMSENS = 75; +const GESTURE_SCRLSENS = 50; +const DOUBLE_TAP_TIMEOUT = 1000; +const DOUBLE_TAP_THRESHOLD = 50; + +// Security types +const securityTypeNone = 1; +const securityTypeVNCAuth = 2; +const securityTypeRA2ne = 6; +const securityTypeTight = 16; +const securityTypeVeNCrypt = 19; +const securityTypeXVP = 22; +const securityTypeARD = 30; +const securityTypeMSLogonII = 113; + +// Special Tight security types +const securityTypeUnixLogon = 129; + +// VeNCrypt security types +const securityTypePlain = 256; + +// Extended clipboard pseudo-encoding formats +const extendedClipboardFormatText = 1; +/*eslint-disable no-unused-vars */ +const extendedClipboardFormatRtf = 1 << 1; +const extendedClipboardFormatHtml = 1 << 2; +const extendedClipboardFormatDib = 1 << 3; +const extendedClipboardFormatFiles = 1 << 4; +/*eslint-enable */ + +// Extended clipboard pseudo-encoding actions +const extendedClipboardActionCaps = 1 << 24; +const extendedClipboardActionRequest = 1 << 25; +const extendedClipboardActionPeek = 1 << 26; +const extendedClipboardActionNotify = 1 << 27; +const extendedClipboardActionProvide = 1 << 28; + +export default class RFB extends EventTargetMixin { + constructor(target, urlOrChannel, options) { + if (!target) { + throw new Error('Must specify target'); + } + if (!urlOrChannel) { + throw new Error('Must specify URL, WebSocket or RTCDataChannel'); + } + + // We rely on modern APIs which might not be available in an + // insecure context + if (!window.isSecureContext) { + Log.Error('noVNC requires a secure context (TLS). Expect crashes!'); + } + + super(); + + this._target = target; + + if (typeof urlOrChannel === 'string') { + this._url = urlOrChannel; + } else { + this._url = null; + this._rawChannel = urlOrChannel; + } + + // Connection details + options = options || {}; + this._rfbCredentials = options.credentials || {}; + this._shared = 'shared' in options ? !!options.shared : true; + this._repeaterID = options.repeaterID || ''; + this._wsProtocols = options.wsProtocols || []; + + // Internal state + this._rfbConnectionState = ''; + this._rfbInitState = ''; + this._rfbAuthScheme = -1; + this._rfbCleanDisconnect = true; + this._rfbRSAAESAuthenticationState = null; + + // Server capabilities + this._rfbVersion = 0; + this._rfbMaxVersion = 3.8; + this._rfbTightVNC = false; + this._rfbVeNCryptState = 0; + this._rfbXvpVer = 0; + + this._fbWidth = 0; + this._fbHeight = 0; + + this._fbName = ''; + + this._capabilities = { power: false }; + + this._supportsFence = false; + + this._supportsContinuousUpdates = false; + this._enabledContinuousUpdates = false; + + this._supportsSetDesktopSize = false; + this._screenID = 0; + this._screenFlags = 0; + + this._qemuExtKeyEventSupported = false; + + this._clipboardText = null; + this._clipboardServerCapabilitiesActions = {}; + this._clipboardServerCapabilitiesFormats = {}; + + // Internal objects + this._sock = null; // Websock object + this._display = null; // Display object + this._flushing = false; // Display flushing state + this._keyboard = null; // Keyboard input handler object + this._gestures = null; // Gesture input handler object + this._resizeObserver = null; // Resize observer object + + // Timers + this._disconnTimer = null; // disconnection timer + this._resizeTimeout = null; // resize rate limiting + this._mouseMoveTimer = null; + + // Decoder states + this._decoders = {}; + + this._FBU = { + rects: 0, + x: 0, + y: 0, + width: 0, + height: 0, + encoding: null, + }; + + // Mouse state + this._mousePos = {}; + this._mouseButtonMask = 0; + this._mouseLastMoveTime = 0; + this._viewportDragging = false; + this._viewportDragPos = {}; + this._viewportHasMoved = false; + this._accumulatedWheelDeltaX = 0; + this._accumulatedWheelDeltaY = 0; + + // Gesture state + this._gestureLastTapTime = null; + this._gestureFirstDoubleTapEv = null; + this._gestureLastMagnitudeX = 0; + this._gestureLastMagnitudeY = 0; + + // Bound event handlers + this._eventHandlers = { + focusCanvas: this._focusCanvas.bind(this), + handleResize: this._handleResize.bind(this), + handleMouse: this._handleMouse.bind(this), + handleWheel: this._handleWheel.bind(this), + handleGesture: this._handleGesture.bind(this), + handleRSAAESCredentialsRequired: this._handleRSAAESCredentialsRequired.bind(this), + handleRSAAESServerVerification: this._handleRSAAESServerVerification.bind(this), + }; + + // main setup + Log.Debug('>> RFB.constructor'); + + // Create DOM elements + this._screen = document.createElement('div'); + this._screen.style.display = 'flex'; + this._screen.style.width = '100%'; + this._screen.style.height = '100%'; + this._screen.style.overflow = 'auto'; + this._screen.style.background = DEFAULT_BACKGROUND; + this._canvas = document.createElement('canvas'); + this._canvas.style.margin = 'auto'; + // Some browsers add an outline on focus + this._canvas.style.outline = 'none'; + this._canvas.width = 0; + this._canvas.height = 0; + this._canvas.tabIndex = -1; + this._screen.appendChild(this._canvas); + + // Cursor + this._cursor = new Cursor(); + + // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes + // it. Result: no cursor at all until a window border or an edit field + // is hit blindly. But there are also VNC servers that draw the cursor + // in the framebuffer and don't send the empty local cursor. There is + // no way to satisfy both sides. + // + // The spec is unclear on this "initial cursor" issue. Many other + // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the + // initial cursor instead. + this._cursorImage = RFB.cursors.none; + + // populate decoder array with objects + this._decoders[encodings.encodingRaw] = new RawDecoder(); + this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder(); + this._decoders[encodings.encodingRRE] = new RREDecoder(); + this._decoders[encodings.encodingHextile] = new HextileDecoder(); + this._decoders[encodings.encodingTight] = new TightDecoder(); + this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder(); + this._decoders[encodings.encodingZRLE] = new ZRLEDecoder(); + this._decoders[encodings.encodingJPEG] = new JPEGDecoder(); + + // NB: nothing that needs explicit teardown should be done + // before this point, since this can throw an exception + try { + this._display = new Display(this._canvas); + } catch (exc) { + Log.Error('Display exception: ' + exc); + throw exc; + } + this._display.onflush = this._onFlush.bind(this); + + this._keyboard = new Keyboard(this._canvas); + this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); + + this._gestures = new GestureHandler(); + + this._sock = new Websock(); + this._sock.on('open', this._socketOpen.bind(this)); + this._sock.on('close', this._socketClose.bind(this)); + this._sock.on('message', this._handleMessage.bind(this)); + this._sock.on('error', this._socketError.bind(this)); + + this._expectedClientWidth = null; + this._expectedClientHeight = null; + this._resizeObserver = new ResizeObserver(this._eventHandlers.handleResize); + + // All prepared, kick off the connection + this._updateConnectionState('connecting'); + + Log.Debug('<< RFB.constructor'); + + // ===== PROPERTIES ===== + + this.dragViewport = false; + this.focusOnClick = true; + + this._viewOnly = false; + this._clipViewport = false; + this._clippingViewport = false; + this._scaleViewport = false; + this._resizeSession = false; + + this._showDotCursor = false; + if (options.showDotCursor !== undefined) { + Log.Warn('Specifying showDotCursor as a RFB constructor argument is deprecated'); + this._showDotCursor = options.showDotCursor; + } + + this._qualityLevel = 6; + this._compressionLevel = 2; + } + + // ===== PROPERTIES ===== + + get viewOnly() { + return this._viewOnly; + } + set viewOnly(viewOnly) { + this._viewOnly = viewOnly; + + if (this._rfbConnectionState === 'connecting' || this._rfbConnectionState === 'connected') { + if (viewOnly) { + this._keyboard.ungrab(); + } else { + this._keyboard.grab(); + } + } + } + + get capabilities() { + return this._capabilities; + } + + get clippingViewport() { + return this._clippingViewport; + } + _setClippingViewport(on) { + if (on === this._clippingViewport) { + return; + } + this._clippingViewport = on; + this.dispatchEvent(new CustomEvent('clippingviewport', { detail: this._clippingViewport })); + } + + get touchButton() { + return 0; + } + set touchButton(button) { + Log.Warn('Using old API!'); + } + + get clipViewport() { + return this._clipViewport; + } + set clipViewport(viewport) { + this._clipViewport = viewport; + this._updateClip(); + } + + get scaleViewport() { + return this._scaleViewport; + } + set scaleViewport(scale) { + this._scaleViewport = scale; + // Scaling trumps clipping, so we may need to adjust + // clipping when enabling or disabling scaling + if (scale && this._clipViewport) { + this._updateClip(); + } + this._updateScale(); + if (!scale && this._clipViewport) { + this._updateClip(); + } + } + + get resizeSession() { + return this._resizeSession; + } + set resizeSession(resize) { + this._resizeSession = resize; + if (resize) { + this._requestRemoteResize(); + } + } + + get showDotCursor() { + return this._showDotCursor; + } + set showDotCursor(show) { + this._showDotCursor = show; + this._refreshCursor(); + } + + get background() { + return this._screen.style.background; + } + set background(cssValue) { + this._screen.style.background = cssValue; + } + + get qualityLevel() { + return this._qualityLevel; + } + set qualityLevel(qualityLevel) { + if (!Number.isInteger(qualityLevel) || qualityLevel < 0 || qualityLevel > 9) { + Log.Error('qualityLevel must be an integer between 0 and 9'); + return; + } + + if (this._qualityLevel === qualityLevel) { + return; + } + + this._qualityLevel = qualityLevel; + + if (this._rfbConnectionState === 'connected') { + this._sendEncodings(); + } + } + + get compressionLevel() { + return this._compressionLevel; + } + set compressionLevel(compressionLevel) { + if (!Number.isInteger(compressionLevel) || compressionLevel < 0 || compressionLevel > 9) { + Log.Error('compressionLevel must be an integer between 0 and 9'); + return; + } + + if (this._compressionLevel === compressionLevel) { + return; + } + + this._compressionLevel = compressionLevel; + + if (this._rfbConnectionState === 'connected') { + this._sendEncodings(); + } + } + + // ===== PUBLIC METHODS ===== + + disconnect() { + this._updateConnectionState('disconnecting'); + this._sock.off('error'); + this._sock.off('message'); + this._sock.off('open'); + if (this._rfbRSAAESAuthenticationState !== null) { + this._rfbRSAAESAuthenticationState.disconnect(); + } + } + + approveServer() { + if (this._rfbRSAAESAuthenticationState !== null) { + this._rfbRSAAESAuthenticationState.approveServer(); + } + } + + sendCredentials(creds) { + this._rfbCredentials = creds; + this._resumeAuthentication(); + } + + sendCtrlAltDel() { + if (this._rfbConnectionState !== 'connected' || this._viewOnly) { + return; + } + Log.Info('Sending Ctrl-Alt-Del'); + + this.sendKey(KeyTable.XK_Control_L, 'ControlLeft', true); + this.sendKey(KeyTable.XK_Alt_L, 'AltLeft', true); + this.sendKey(KeyTable.XK_Delete, 'Delete', true); + this.sendKey(KeyTable.XK_Delete, 'Delete', false); + this.sendKey(KeyTable.XK_Alt_L, 'AltLeft', false); + this.sendKey(KeyTable.XK_Control_L, 'ControlLeft', false); + } + + machineShutdown() { + this._xvpOp(1, 2); + } + + machineReboot() { + this._xvpOp(1, 3); + } + + machineReset() { + this._xvpOp(1, 4); + } + + // Send a key press. If 'down' is not specified then send a down key + // followed by an up key. + sendKey(keysym, code, down) { + if (this._rfbConnectionState !== 'connected' || this._viewOnly) { + return; + } + + if (down === undefined) { + this.sendKey(keysym, code, true); + this.sendKey(keysym, code, false); + return; + } + + const scancode = XtScancode[code]; + + if (this._qemuExtKeyEventSupported && scancode) { + // 0 is NoSymbol + keysym = keysym || 0; + + Log.Info('Sending key (' + (down ? 'down' : 'up') + '): keysym ' + keysym + ', scancode ' + scancode); + + RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode); + } else { + if (!keysym) { + return; + } + Log.Info('Sending keysym (' + (down ? 'down' : 'up') + '): ' + keysym); + RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0); + } + } + + focus(options) { + this._canvas.focus(options); + } + + blur() { + this._canvas.blur(); + } + + clipboardPasteFrom(text) { + if (this._rfbConnectionState !== 'connected' || this._viewOnly) { + return; + } + + if ( + this._clipboardServerCapabilitiesFormats[extendedClipboardFormatText] && + this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify] + ) { + this._clipboardText = text; + RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]); + } else { + let length, i; + let data; + + length = 0; + // eslint-disable-next-line no-unused-vars + for (let codePoint of text) { + length++; + } + + data = new Uint8Array(length); + + i = 0; + for (let codePoint of text) { + let code = codePoint.codePointAt(0); + + /* Only ISO 8859-1 is supported */ + if (code > 0xff) { + code = 0x3f; // '?' + } + + data[i++] = code; + } + + RFB.messages.clientCutText(this._sock, data); + } + } + + getImageData() { + return this._display.getImageData(); + } + + toDataURL(type, encoderOptions) { + return this._display.toDataURL(type, encoderOptions); + } + + toBlob(callback, type, quality) { + return this._display.toBlob(callback, type, quality); + } + + // ===== PRIVATE METHODS ===== + + _connect() { + Log.Debug('>> RFB.connect'); + + if (this._url) { + Log.Info(`connecting to ${this._url}`); + this._sock.open(this._url, this._wsProtocols); + } else { + Log.Info(`attaching ${this._rawChannel} to Websock`); + this._sock.attach(this._rawChannel); + + if (this._sock.readyState === 'closed') { + throw Error('Cannot use already closed WebSocket/RTCDataChannel'); + } + + if (this._sock.readyState === 'open') { + // FIXME: _socketOpen() can in theory call _fail(), which + // isn't allowed this early, but I'm not sure that can + // happen without a bug messing up our state variables + this._socketOpen(); + } + } + + // Make our elements part of the page + this._target.appendChild(this._screen); + + this._gestures.attach(this._canvas); + + this._cursor.attach(this._canvas); + this._refreshCursor(); + + // Monitor size changes of the screen element + this._resizeObserver.observe(this._screen); + + // Always grab focus on some kind of click event + this._canvas.addEventListener('mousedown', this._eventHandlers.focusCanvas); + this._canvas.addEventListener('touchstart', this._eventHandlers.focusCanvas); + + // Mouse events + this._canvas.addEventListener('mousedown', this._eventHandlers.handleMouse); + this._canvas.addEventListener('mouseup', this._eventHandlers.handleMouse); + this._canvas.addEventListener('mousemove', this._eventHandlers.handleMouse); + // Prevent middle-click pasting (see handler for why we bind to document) + this._canvas.addEventListener('click', this._eventHandlers.handleMouse); + // preventDefault() on mousedown doesn't stop this event for some + // reason so we have to explicitly block it + this._canvas.addEventListener('contextmenu', this._eventHandlers.handleMouse); + + // Wheel events + this._canvas.addEventListener('wheel', this._eventHandlers.handleWheel); + + // Gesture events + this._canvas.addEventListener('gesturestart', this._eventHandlers.handleGesture); + this._canvas.addEventListener('gesturemove', this._eventHandlers.handleGesture); + this._canvas.addEventListener('gestureend', this._eventHandlers.handleGesture); + + Log.Debug('<< RFB.connect'); + } + + _disconnect() { + Log.Debug('>> RFB.disconnect'); + this._cursor.detach(); + this._canvas.removeEventListener('gesturestart', this._eventHandlers.handleGesture); + this._canvas.removeEventListener('gesturemove', this._eventHandlers.handleGesture); + this._canvas.removeEventListener('gestureend', this._eventHandlers.handleGesture); + this._canvas.removeEventListener('wheel', this._eventHandlers.handleWheel); + this._canvas.removeEventListener('mousedown', this._eventHandlers.handleMouse); + this._canvas.removeEventListener('mouseup', this._eventHandlers.handleMouse); + this._canvas.removeEventListener('mousemove', this._eventHandlers.handleMouse); + this._canvas.removeEventListener('click', this._eventHandlers.handleMouse); + this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse); + this._canvas.removeEventListener('mousedown', this._eventHandlers.focusCanvas); + this._canvas.removeEventListener('touchstart', this._eventHandlers.focusCanvas); + this._resizeObserver.disconnect(); + this._keyboard.ungrab(); + this._gestures.detach(); + this._sock.close(); + try { + this._target.removeChild(this._screen); + } catch (e) { + if (e.name === 'NotFoundError') { + // Some cases where the initial connection fails + // can disconnect before the _screen is created + } else { + throw e; + } + } + clearTimeout(this._resizeTimeout); + clearTimeout(this._mouseMoveTimer); + Log.Debug('<< RFB.disconnect'); + } + + _socketOpen() { + if (this._rfbConnectionState === 'connecting' && this._rfbInitState === '') { + this._rfbInitState = 'ProtocolVersion'; + Log.Debug('Starting VNC handshake'); + } else { + this._fail('Unexpected server connection while ' + this._rfbConnectionState); + } + } + + _socketClose(e) { + Log.Debug('WebSocket on-close event'); + let msg = ''; + if (e.code) { + msg = '(code: ' + e.code; + if (e.reason) { + msg += ', reason: ' + e.reason; + } + msg += ')'; + } + switch (this._rfbConnectionState) { + case 'connecting': + this._fail('Connection closed ' + msg); + break; + case 'connected': + // Handle disconnects that were initiated server-side + this._updateConnectionState('disconnecting'); + this._updateConnectionState('disconnected'); + break; + case 'disconnecting': + // Normal disconnection path + this._updateConnectionState('disconnected'); + break; + case 'disconnected': + this._fail('Unexpected server disconnect ' + 'when already disconnected ' + msg); + break; + default: + this._fail('Unexpected server disconnect before connecting ' + msg); + break; + } + this._sock.off('close'); + // Delete reference to raw channel to allow cleanup. + this._rawChannel = null; + } + + _socketError(e) { + Log.Warn('WebSocket on-error event'); + } + + _focusCanvas(event) { + if (!this.focusOnClick) { + return; + } + + this.focus({ preventScroll: true }); + } + + _setDesktopName(name) { + this._fbName = name; + this.dispatchEvent(new CustomEvent('desktopname', { detail: { name: this._fbName } })); + } + + _saveExpectedClientSize() { + this._expectedClientWidth = this._screen.clientWidth; + this._expectedClientHeight = this._screen.clientHeight; + } + + _currentClientSize() { + return [this._screen.clientWidth, this._screen.clientHeight]; + } + + _clientHasExpectedSize() { + const [currentWidth, currentHeight] = this._currentClientSize(); + return currentWidth == this._expectedClientWidth && currentHeight == this._expectedClientHeight; + } + + _handleResize() { + // Don't change anything if the client size is already as expected + if (this._clientHasExpectedSize()) { + return; + } + // If the window resized then our screen element might have + // as well. Update the viewport dimensions. + window.requestAnimationFrame(() => { + this._updateClip(); + this._updateScale(); + }); + + if (this._resizeSession) { + // Request changing the resolution of the remote display to + // the size of the local browser viewport. + + // In order to not send multiple requests before the browser-resize + // is finished we wait 0.5 seconds before sending the request. + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500); + } + } + + // Update state of clipping in Display object, and make sure the + // configured viewport matches the current screen size + _updateClip() { + const curClip = this._display.clipViewport; + let newClip = this._clipViewport; + + if (this._scaleViewport) { + // Disable viewport clipping if we are scaling + newClip = false; + } + + if (curClip !== newClip) { + this._display.clipViewport = newClip; + } + + if (newClip) { + // When clipping is enabled, the screen is limited to + // the size of the container. + const size = this._screenSize(); + this._display.viewportChangeSize(size.w, size.h); + this._fixScrollbars(); + this._setClippingViewport(size.w < this._display.width || size.h < this._display.height); + } else { + this._setClippingViewport(false); + } + + // When changing clipping we might show or hide scrollbars. + // This causes the expected client dimensions to change. + if (curClip !== newClip) { + this._saveExpectedClientSize(); + } + } + + _updateScale() { + if (!this._scaleViewport) { + this._display.scale = 1.0; + } else { + const size = this._screenSize(); + this._display.autoscale(size.w, size.h); + } + this._fixScrollbars(); + } + + // Requests a change of remote desktop size. This message is an extension + // and may only be sent if we have received an ExtendedDesktopSize message + _requestRemoteResize() { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = null; + + if (!this._resizeSession || this._viewOnly || !this._supportsSetDesktopSize) { + return; + } + + const size = this._screenSize(); + + RFB.messages.setDesktopSize(this._sock, Math.floor(size.w), Math.floor(size.h), this._screenID, this._screenFlags); + + Log.Debug('Requested new desktop size: ' + size.w + 'x' + size.h); + } + + // Gets the the size of the available screen + _screenSize() { + let r = this._screen.getBoundingClientRect(); + return { w: r.width, h: r.height }; + } + + _fixScrollbars() { + // This is a hack because Safari on macOS screws up the calculation + // for when scrollbars are needed. We get scrollbars when making the + // browser smaller, despite remote resize being enabled. So to fix it + // we temporarily toggle them off and on. + const orig = this._screen.style.overflow; + this._screen.style.overflow = 'hidden'; + // Force Safari to recalculate the layout by asking for + // an element's dimensions + this._screen.getBoundingClientRect(); + this._screen.style.overflow = orig; + } + + /* + * Connection states: + * connecting + * connected + * disconnecting + * disconnected - permanent state + */ + _updateConnectionState(state) { + const oldstate = this._rfbConnectionState; + + if (state === oldstate) { + Log.Debug("Already in state '" + state + "', ignoring"); + return; + } + + // The 'disconnected' state is permanent for each RFB object + if (oldstate === 'disconnected') { + Log.Error('Tried changing state of a disconnected RFB object'); + return; + } + + // Ensure proper transitions before doing anything + switch (state) { + case 'connected': + if (oldstate !== 'connecting') { + Log.Error('Bad transition to connected state, ' + 'previous connection state: ' + oldstate); + return; + } + break; + + case 'disconnected': + if (oldstate !== 'disconnecting') { + Log.Error('Bad transition to disconnected state, ' + 'previous connection state: ' + oldstate); + return; + } + break; + + case 'connecting': + if (oldstate !== '') { + Log.Error('Bad transition to connecting state, ' + 'previous connection state: ' + oldstate); + return; + } + break; + + case 'disconnecting': + if (oldstate !== 'connected' && oldstate !== 'connecting') { + Log.Error('Bad transition to disconnecting state, ' + 'previous connection state: ' + oldstate); + return; + } + break; + + default: + Log.Error('Unknown connection state: ' + state); + return; + } + + // State change actions + + this._rfbConnectionState = state; + + Log.Debug("New state '" + state + "', was '" + oldstate + "'."); + + if (this._disconnTimer && state !== 'disconnecting') { + Log.Debug('Clearing disconnect timer'); + clearTimeout(this._disconnTimer); + this._disconnTimer = null; + + // make sure we don't get a double event + this._sock.off('close'); + } + + switch (state) { + case 'connecting': + this._connect(); + break; + + case 'connected': + this.dispatchEvent(new CustomEvent('connect', { detail: {} })); + break; + + case 'disconnecting': + this._disconnect(); + + this._disconnTimer = setTimeout(() => { + Log.Error('Disconnection timed out.'); + this._updateConnectionState('disconnected'); + }, DISCONNECT_TIMEOUT * 1000); + break; + + case 'disconnected': + this.dispatchEvent(new CustomEvent('disconnect', { detail: { clean: this._rfbCleanDisconnect } })); + break; + } + } + + /* Print errors and disconnect + * + * The parameter 'details' is used for information that + * should be logged but not sent to the user interface. + */ + _fail(details) { + switch (this._rfbConnectionState) { + case 'disconnecting': + Log.Error('Failed when disconnecting: ' + details); + break; + case 'connected': + Log.Error('Failed while connected: ' + details); + break; + case 'connecting': + Log.Error('Failed when connecting: ' + details); + break; + default: + Log.Error('RFB failure: ' + details); + break; + } + this._rfbCleanDisconnect = false; //This is sent to the UI + + // Transition to disconnected without waiting for socket to close + this._updateConnectionState('disconnecting'); + this._updateConnectionState('disconnected'); + + return false; + } + + _setCapability(cap, val) { + this._capabilities[cap] = val; + this.dispatchEvent(new CustomEvent('capabilities', { detail: { capabilities: this._capabilities } })); + } + + _handleMessage() { + if (this._sock.rQlen === 0) { + Log.Warn('handleMessage called on an empty receive queue'); + return; + } + + switch (this._rfbConnectionState) { + case 'disconnected': + Log.Error('Got data while disconnected'); + break; + case 'connected': + while (true) { + if (this._flushing) { + break; + } + if (!this._normalMsg()) { + break; + } + if (this._sock.rQlen === 0) { + break; + } + } + break; + case 'connecting': + while (this._rfbConnectionState === 'connecting') { + if (!this._initMsg()) { + break; + } + } + break; + default: + Log.Error('Got data while in an invalid state'); + break; + } + } + + _handleKeyEvent(keysym, code, down) { + this.sendKey(keysym, code, down); + } + + _handleMouse(ev) { + /* + * We don't check connection status or viewOnly here as the + * mouse events might be used to control the viewport + */ + + if (ev.type === 'click') { + /* + * Note: This is only needed for the 'click' event as it fails + * to fire properly for the target element so we have + * to listen on the document element instead. + */ + if (ev.target !== this._canvas) { + return; + } + } + + // FIXME: if we're in view-only and not dragging, + // should we stop events? + ev.stopPropagation(); + ev.preventDefault(); + + if (ev.type === 'click' || ev.type === 'contextmenu') { + return; + } + + let pos = clientToElement(ev.clientX, ev.clientY, this._canvas); + + switch (ev.type) { + case 'mousedown': + setCapture(this._canvas); + this._handleMouseButton(pos.x, pos.y, true, 1 << ev.button); + break; + case 'mouseup': + this._handleMouseButton(pos.x, pos.y, false, 1 << ev.button); + break; + case 'mousemove': + this._handleMouseMove(pos.x, pos.y); + break; + } + } + + _handleMouseButton(x, y, down, bmask) { + if (this.dragViewport) { + if (down && !this._viewportDragging) { + this._viewportDragging = true; + this._viewportDragPos = { x: x, y: y }; + this._viewportHasMoved = false; + + // Skip sending mouse events + return; + } else { + this._viewportDragging = false; + + // If we actually performed a drag then we are done + // here and should not send any mouse events + if (this._viewportHasMoved) { + return; + } + + // Otherwise we treat this as a mouse click event. + // Send the button down event here, as the button up + // event is sent at the end of this function. + this._sendMouse(x, y, bmask); + } + } + + // Flush waiting move event first + if (this._mouseMoveTimer !== null) { + clearTimeout(this._mouseMoveTimer); + this._mouseMoveTimer = null; + this._sendMouse(x, y, this._mouseButtonMask); + } + + if (down) { + this._mouseButtonMask |= bmask; + } else { + this._mouseButtonMask &= ~bmask; + } + + this._sendMouse(x, y, this._mouseButtonMask); + } + + _handleMouseMove(x, y) { + if (this._viewportDragging) { + const deltaX = this._viewportDragPos.x - x; + const deltaY = this._viewportDragPos.y - y; + + if (this._viewportHasMoved || Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold) { + this._viewportHasMoved = true; + + this._viewportDragPos = { x: x, y: y }; + this._display.viewportChangePos(deltaX, deltaY); + } + + // Skip sending mouse events + return; + } + + this._mousePos = { x: x, y: y }; + + // Limit many mouse move events to one every MOUSE_MOVE_DELAY ms + if (this._mouseMoveTimer == null) { + const timeSinceLastMove = Date.now() - this._mouseLastMoveTime; + if (timeSinceLastMove > MOUSE_MOVE_DELAY) { + this._sendMouse(x, y, this._mouseButtonMask); + this._mouseLastMoveTime = Date.now(); + } else { + // Too soon since the latest move, wait the remaining time + this._mouseMoveTimer = setTimeout(() => { + this._handleDelayedMouseMove(); + }, MOUSE_MOVE_DELAY - timeSinceLastMove); + } + } + } + + _handleDelayedMouseMove() { + this._mouseMoveTimer = null; + this._sendMouse(this._mousePos.x, this._mousePos.y, this._mouseButtonMask); + this._mouseLastMoveTime = Date.now(); + } + + _sendMouse(x, y, mask) { + if (this._rfbConnectionState !== 'connected') { + return; + } + if (this._viewOnly) { + return; + } // View only, skip mouse events + + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), mask); + } + + _handleWheel(ev) { + if (this._rfbConnectionState !== 'connected') { + return; + } + if (this._viewOnly) { + return; + } // View only, skip mouse events + + ev.stopPropagation(); + ev.preventDefault(); + + let pos = clientToElement(ev.clientX, ev.clientY, this._canvas); + + let dX = ev.deltaX; + let dY = ev.deltaY; + + // Pixel units unless it's non-zero. + // Note that if deltamode is line or page won't matter since we aren't + // sending the mouse wheel delta to the server anyway. + // The difference between pixel and line can be important however since + // we have a threshold that can be smaller than the line height. + if (ev.deltaMode !== 0) { + dX *= WHEEL_LINE_HEIGHT; + dY *= WHEEL_LINE_HEIGHT; + } + + // Mouse wheel events are sent in steps over VNC. This means that the VNC + // protocol can't handle a wheel event with specific distance or speed. + // Therefor, if we get a lot of small mouse wheel events we combine them. + this._accumulatedWheelDeltaX += dX; + this._accumulatedWheelDeltaY += dY; + + // Generate a mouse wheel step event when the accumulated delta + // for one of the axes is large enough. + if (Math.abs(this._accumulatedWheelDeltaX) >= WHEEL_STEP) { + if (this._accumulatedWheelDeltaX < 0) { + this._handleMouseButton(pos.x, pos.y, true, 1 << 5); + this._handleMouseButton(pos.x, pos.y, false, 1 << 5); + } else if (this._accumulatedWheelDeltaX > 0) { + this._handleMouseButton(pos.x, pos.y, true, 1 << 6); + this._handleMouseButton(pos.x, pos.y, false, 1 << 6); + } + + this._accumulatedWheelDeltaX = 0; + } + if (Math.abs(this._accumulatedWheelDeltaY) >= WHEEL_STEP) { + if (this._accumulatedWheelDeltaY < 0) { + this._handleMouseButton(pos.x, pos.y, true, 1 << 3); + this._handleMouseButton(pos.x, pos.y, false, 1 << 3); + } else if (this._accumulatedWheelDeltaY > 0) { + this._handleMouseButton(pos.x, pos.y, true, 1 << 4); + this._handleMouseButton(pos.x, pos.y, false, 1 << 4); + } + + this._accumulatedWheelDeltaY = 0; + } + } + + _fakeMouseMove(ev, elementX, elementY) { + this._handleMouseMove(elementX, elementY); + this._cursor.move(ev.detail.clientX, ev.detail.clientY); + } + + _handleTapEvent(ev, bmask) { + let pos = clientToElement(ev.detail.clientX, ev.detail.clientY, this._canvas); + + // If the user quickly taps multiple times we assume they meant to + // hit the same spot, so slightly adjust coordinates + + if ( + this._gestureLastTapTime !== null && + Date.now() - this._gestureLastTapTime < DOUBLE_TAP_TIMEOUT && + this._gestureFirstDoubleTapEv.detail.type === ev.detail.type + ) { + let dx = this._gestureFirstDoubleTapEv.detail.clientX - ev.detail.clientX; + let dy = this._gestureFirstDoubleTapEv.detail.clientY - ev.detail.clientY; + let distance = Math.hypot(dx, dy); + + if (distance < DOUBLE_TAP_THRESHOLD) { + pos = clientToElement( + this._gestureFirstDoubleTapEv.detail.clientX, + this._gestureFirstDoubleTapEv.detail.clientY, + this._canvas + ); + } else { + this._gestureFirstDoubleTapEv = ev; + } + } else { + this._gestureFirstDoubleTapEv = ev; + } + this._gestureLastTapTime = Date.now(); + + this._fakeMouseMove(this._gestureFirstDoubleTapEv, pos.x, pos.y); + this._handleMouseButton(pos.x, pos.y, true, bmask); + this._handleMouseButton(pos.x, pos.y, false, bmask); + } + + _handleGesture(ev) { + let magnitude; + + let pos = clientToElement(ev.detail.clientX, ev.detail.clientY, this._canvas); + switch (ev.type) { + case 'gesturestart': + switch (ev.detail.type) { + case 'onetap': + this._handleTapEvent(ev, 0x1); + break; + case 'twotap': + this._handleTapEvent(ev, 0x4); + break; + case 'threetap': + this._handleTapEvent(ev, 0x2); + break; + case 'drag': + this._fakeMouseMove(ev, pos.x, pos.y); + this._handleMouseButton(pos.x, pos.y, true, 0x1); + break; + case 'longpress': + this._fakeMouseMove(ev, pos.x, pos.y); + this._handleMouseButton(pos.x, pos.y, true, 0x4); + break; + + case 'twodrag': + this._gestureLastMagnitudeX = ev.detail.magnitudeX; + this._gestureLastMagnitudeY = ev.detail.magnitudeY; + this._fakeMouseMove(ev, pos.x, pos.y); + break; + case 'pinch': + this._gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY); + this._fakeMouseMove(ev, pos.x, pos.y); + break; + } + break; + + case 'gesturemove': + switch (ev.detail.type) { + case 'onetap': + case 'twotap': + case 'threetap': + break; + case 'drag': + case 'longpress': + this._fakeMouseMove(ev, pos.x, pos.y); + break; + case 'twodrag': + // Always scroll in the same position. + // We don't know if the mouse was moved so we need to move it + // every update. + this._fakeMouseMove(ev, pos.x, pos.y); + while (ev.detail.magnitudeY - this._gestureLastMagnitudeY > GESTURE_SCRLSENS) { + this._handleMouseButton(pos.x, pos.y, true, 0x8); + this._handleMouseButton(pos.x, pos.y, false, 0x8); + this._gestureLastMagnitudeY += GESTURE_SCRLSENS; + } + while (ev.detail.magnitudeY - this._gestureLastMagnitudeY < -GESTURE_SCRLSENS) { + this._handleMouseButton(pos.x, pos.y, true, 0x10); + this._handleMouseButton(pos.x, pos.y, false, 0x10); + this._gestureLastMagnitudeY -= GESTURE_SCRLSENS; + } + while (ev.detail.magnitudeX - this._gestureLastMagnitudeX > GESTURE_SCRLSENS) { + this._handleMouseButton(pos.x, pos.y, true, 0x20); + this._handleMouseButton(pos.x, pos.y, false, 0x20); + this._gestureLastMagnitudeX += GESTURE_SCRLSENS; + } + while (ev.detail.magnitudeX - this._gestureLastMagnitudeX < -GESTURE_SCRLSENS) { + this._handleMouseButton(pos.x, pos.y, true, 0x40); + this._handleMouseButton(pos.x, pos.y, false, 0x40); + this._gestureLastMagnitudeX -= GESTURE_SCRLSENS; + } + break; + case 'pinch': + // Always scroll in the same position. + // We don't know if the mouse was moved so we need to move it + // every update. + this._fakeMouseMove(ev, pos.x, pos.y); + magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY); + if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) { + this._handleKeyEvent(KeyTable.XK_Control_L, 'ControlLeft', true); + while (magnitude - this._gestureLastMagnitudeX > GESTURE_ZOOMSENS) { + this._handleMouseButton(pos.x, pos.y, true, 0x8); + this._handleMouseButton(pos.x, pos.y, false, 0x8); + this._gestureLastMagnitudeX += GESTURE_ZOOMSENS; + } + while (magnitude - this._gestureLastMagnitudeX < -GESTURE_ZOOMSENS) { + this._handleMouseButton(pos.x, pos.y, true, 0x10); + this._handleMouseButton(pos.x, pos.y, false, 0x10); + this._gestureLastMagnitudeX -= GESTURE_ZOOMSENS; + } + } + this._handleKeyEvent(KeyTable.XK_Control_L, 'ControlLeft', false); + break; + } + break; + + case 'gestureend': + switch (ev.detail.type) { + case 'onetap': + case 'twotap': + case 'threetap': + case 'pinch': + case 'twodrag': + break; + case 'drag': + this._fakeMouseMove(ev, pos.x, pos.y); + this._handleMouseButton(pos.x, pos.y, false, 0x1); + break; + case 'longpress': + this._fakeMouseMove(ev, pos.x, pos.y); + this._handleMouseButton(pos.x, pos.y, false, 0x4); + break; + } + break; + } + } + + // Message Handlers + + _negotiateProtocolVersion() { + if (this._sock.rQwait('version', 12)) { + return false; + } + + const sversion = this._sock.rQshiftStr(12).substr(4, 7); + Log.Info('Server ProtocolVersion: ' + sversion); + let isRepeater = 0; + switch (sversion) { + case '000.000': // UltraVNC repeater + isRepeater = 1; + break; + case '003.003': + case '003.006': // UltraVNC + this._rfbVersion = 3.3; + break; + case '003.007': + this._rfbVersion = 3.7; + break; + case '003.008': + case '003.889': // Apple Remote Desktop + case '004.000': // Intel AMT KVM + case '004.001': // RealVNC 4.6 + case '005.000': // RealVNC 5.3 + this._rfbVersion = 3.8; + break; + default: + return this._fail('Invalid server version ' + sversion); + } + + if (isRepeater) { + let repeaterID = 'ID:' + this._repeaterID; + while (repeaterID.length < 250) { + repeaterID += '\0'; + } + this._sock.sendString(repeaterID); + return true; + } + + if (this._rfbVersion > this._rfbMaxVersion) { + this._rfbVersion = this._rfbMaxVersion; + } + + const cversion = '00' + parseInt(this._rfbVersion, 10) + '.00' + ((this._rfbVersion * 10) % 10); + this._sock.sendString('RFB ' + cversion + '\n'); + Log.Debug('Sent ProtocolVersion: ' + cversion); + + this._rfbInitState = 'Security'; + } + + _isSupportedSecurityType(type) { + const clientTypes = [ + securityTypeNone, + securityTypeVNCAuth, + securityTypeRA2ne, + securityTypeTight, + securityTypeVeNCrypt, + securityTypeXVP, + securityTypeARD, + securityTypeMSLogonII, + securityTypePlain, + ]; + + return clientTypes.includes(type); + } + + _negotiateSecurity() { + if (this._rfbVersion >= 3.7) { + // Server sends supported list, client decides + const numTypes = this._sock.rQshift8(); + if (this._sock.rQwait('security type', numTypes, 1)) { + return false; + } + + if (numTypes === 0) { + this._rfbInitState = 'SecurityReason'; + this._securityContext = 'no security types'; + this._securityStatus = 1; + return true; + } + + const types = this._sock.rQshiftBytes(numTypes); + Log.Debug('Server security types: ' + types); + + // Look for a matching security type in the order that the + // server prefers + this._rfbAuthScheme = -1; + for (let type of types) { + if (this._isSupportedSecurityType(type)) { + this._rfbAuthScheme = type; + break; + } + } + + if (this._rfbAuthScheme === -1) { + return this._fail('Unsupported security types (types: ' + types + ')'); + } + + this._sock.send([this._rfbAuthScheme]); + } else { + // Server decides + if (this._sock.rQwait('security scheme', 4)) { + return false; + } + this._rfbAuthScheme = this._sock.rQshift32(); + + if (this._rfbAuthScheme == 0) { + this._rfbInitState = 'SecurityReason'; + this._securityContext = 'authentication scheme'; + this._securityStatus = 1; + return true; + } + } + + this._rfbInitState = 'Authentication'; + Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme); + + return true; + } + + _handleSecurityReason() { + if (this._sock.rQwait('reason length', 4)) { + return false; + } + const strlen = this._sock.rQshift32(); + let reason = ''; + + if (strlen > 0) { + if (this._sock.rQwait('reason', strlen, 4)) { + return false; + } + reason = this._sock.rQshiftStr(strlen); + } + + if (reason !== '') { + this.dispatchEvent( + new CustomEvent('securityfailure', { detail: { status: this._securityStatus, reason: reason } }) + ); + + return this._fail('Security negotiation failed on ' + this._securityContext + ' (reason: ' + reason + ')'); + } else { + this.dispatchEvent(new CustomEvent('securityfailure', { detail: { status: this._securityStatus } })); + + return this._fail('Security negotiation failed on ' + this._securityContext); + } + } + + // authentication + _negotiateXvpAuth() { + if ( + this._rfbCredentials.username === undefined || + this._rfbCredentials.password === undefined || + this._rfbCredentials.target === undefined + ) { + this.dispatchEvent( + new CustomEvent('credentialsrequired', { detail: { types: ['username', 'password', 'target'] } }) + ); + return false; + } + + const xvpAuthStr = + String.fromCharCode(this._rfbCredentials.username.length) + + String.fromCharCode(this._rfbCredentials.target.length) + + this._rfbCredentials.username + + this._rfbCredentials.target; + this._sock.sendString(xvpAuthStr); + this._rfbAuthScheme = securityTypeVNCAuth; + return this._negotiateAuthentication(); + } + + // VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype + _negotiateVeNCryptAuth() { + // waiting for VeNCrypt version + if (this._rfbVeNCryptState == 0) { + if (this._sock.rQwait('vencrypt version', 2)) { + return false; + } + + const major = this._sock.rQshift8(); + const minor = this._sock.rQshift8(); + + if (!(major == 0 && minor == 2)) { + return this._fail('Unsupported VeNCrypt version ' + major + '.' + minor); + } + + this._sock.send([0, 2]); + this._rfbVeNCryptState = 1; + } + + // waiting for ACK + if (this._rfbVeNCryptState == 1) { + if (this._sock.rQwait('vencrypt ack', 1)) { + return false; + } + + const res = this._sock.rQshift8(); + + if (res != 0) { + return this._fail('VeNCrypt failure ' + res); + } + + this._rfbVeNCryptState = 2; + } + // must fall through here (i.e. no "else if"), beacause we may have already received + // the subtypes length and won't be called again + + if (this._rfbVeNCryptState == 2) { + // waiting for subtypes length + if (this._sock.rQwait('vencrypt subtypes length', 1)) { + return false; + } + + const subtypesLength = this._sock.rQshift8(); + if (subtypesLength < 1) { + return this._fail('VeNCrypt subtypes empty'); + } + + this._rfbVeNCryptSubtypesLength = subtypesLength; + this._rfbVeNCryptState = 3; + } + + // waiting for subtypes list + if (this._rfbVeNCryptState == 3) { + if (this._sock.rQwait('vencrypt subtypes', 4 * this._rfbVeNCryptSubtypesLength)) { + return false; + } + + const subtypes = []; + for (let i = 0; i < this._rfbVeNCryptSubtypesLength; i++) { + subtypes.push(this._sock.rQshift32()); + } + + // Look for a matching security type in the order that the + // server prefers + this._rfbAuthScheme = -1; + for (let type of subtypes) { + // Avoid getting in to a loop + if (type === securityTypeVeNCrypt) { + continue; + } + + if (this._isSupportedSecurityType(type)) { + this._rfbAuthScheme = type; + break; + } + } + + if (this._rfbAuthScheme === -1) { + return this._fail('Unsupported security types (types: ' + subtypes + ')'); + } + + this._sock.send([ + this._rfbAuthScheme >> 24, + this._rfbAuthScheme >> 16, + this._rfbAuthScheme >> 8, + this._rfbAuthScheme, + ]); + + this._rfbVeNCryptState == 4; + return true; + } + } + + _negotiatePlainAuth() { + if (this._rfbCredentials.username === undefined || this._rfbCredentials.password === undefined) { + this.dispatchEvent(new CustomEvent('credentialsrequired', { detail: { types: ['username', 'password'] } })); + return false; + } + + const user = encodeUTF8(this._rfbCredentials.username); + const pass = encodeUTF8(this._rfbCredentials.password); + + this._sock.send([ + (user.length >> 24) & 0xff, + (user.length >> 16) & 0xff, + (user.length >> 8) & 0xff, + user.length & 0xff, + ]); + this._sock.send([ + (pass.length >> 24) & 0xff, + (pass.length >> 16) & 0xff, + (pass.length >> 8) & 0xff, + pass.length & 0xff, + ]); + this._sock.sendString(user); + this._sock.sendString(pass); + + this._rfbInitState = 'SecurityResult'; + return true; + } + + _negotiateStdVNCAuth() { + if (this._sock.rQwait('auth challenge', 16)) { + return false; + } + + if (this._rfbCredentials.password === undefined) { + this.dispatchEvent(new CustomEvent('credentialsrequired', { detail: { types: ['password'] } })); + return false; + } + + // TODO(directxman12): make genDES not require an Array + const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); + const response = RFB.genDES(this._rfbCredentials.password, challenge); + this._sock.send(response); + this._rfbInitState = 'SecurityResult'; + return true; + } + + _negotiateARDAuth() { + if (this._rfbCredentials.username === undefined || this._rfbCredentials.password === undefined) { + this.dispatchEvent(new CustomEvent('credentialsrequired', { detail: { types: ['username', 'password'] } })); + return false; + } + + if (this._rfbCredentials.ardPublicKey != undefined && this._rfbCredentials.ardCredentials != undefined) { + // if the async web crypto is done return the results + this._sock.send(this._rfbCredentials.ardCredentials); + this._sock.send(this._rfbCredentials.ardPublicKey); + this._rfbCredentials.ardCredentials = null; + this._rfbCredentials.ardPublicKey = null; + this._rfbInitState = 'SecurityResult'; + return true; + } + + if (this._sock.rQwait('read ard', 4)) { + return false; + } + + let generator = this._sock.rQshiftBytes(2); // DH base generator value + + let keyLength = this._sock.rQshift16(); + + if (this._sock.rQwait('read ard keylength', keyLength * 2, 4)) { + return false; + } + + // read the server values + let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus + let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key + + let clientPrivateKey = window.crypto.getRandomValues(new Uint8Array(keyLength)); + let padding = Array.from(window.crypto.getRandomValues(new Uint8Array(64)), (byte) => + String.fromCharCode(65 + (byte % 26)) + ).join(''); + + this._negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding); + + return false; + } + + _modPow(base, exponent, modulus) { + let baseHex = '0x' + Array.from(base, (byte) => ('0' + (byte & 0xff).toString(16)).slice(-2)).join(''); + let exponentHex = '0x' + Array.from(exponent, (byte) => ('0' + (byte & 0xff).toString(16)).slice(-2)).join(''); + let modulusHex = '0x' + Array.from(modulus, (byte) => ('0' + (byte & 0xff).toString(16)).slice(-2)).join(''); + + let b = BigInt(baseHex); + let e = BigInt(exponentHex); + let m = BigInt(modulusHex); + let r = 1n; + b = b % m; + while (e > 0) { + if (e % 2n === 1n) { + r = (r * b) % m; + } + e = e / 2n; + b = (b * b) % m; + } + let hexResult = r.toString(16); + + while (hexResult.length / 2 < exponent.length || hexResult.length % 2 != 0) { + hexResult = '0' + hexResult; + } + + let bytesResult = []; + for (let c = 0; c < hexResult.length; c += 2) { + bytesResult.push(parseInt(hexResult.substr(c, 2), 16)); + } + return bytesResult; + } + + async _aesEcbEncrypt(string, key) { + // perform AES-ECB blocks + let keyString = Array.from(key, (byte) => String.fromCharCode(byte)).join(''); + let aesKey = await window.crypto.subtle.importKey('raw', MD5(keyString), { name: 'AES-CBC' }, false, ['encrypt']); + let data = new Uint8Array(string.length); + for (let i = 0; i < string.length; ++i) { + data[i] = string.charCodeAt(i); + } + let encrypted = new Uint8Array(data.length); + for (let i = 0; i < data.length; i += 16) { + let block = data.slice(i, i + 16); + let encryptedBlock = await window.crypto.subtle.encrypt( + { name: 'AES-CBC', iv: block }, + aesKey, + new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + ); + encrypted.set(new Uint8Array(encryptedBlock).slice(0, 16), i); + } + return encrypted; + } + + async _negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding) { + // calculate the DH keys + let clientPublicKey = this._modPow(generator, clientPrivateKey, prime); + let sharedKey = this._modPow(serverPublicKey, clientPrivateKey, prime); + + let username = encodeUTF8(this._rfbCredentials.username).substring(0, 63); + let password = encodeUTF8(this._rfbCredentials.password).substring(0, 63); + + let paddedUsername = username + '\0' + padding.substring(0, 63); + let paddedPassword = password + '\0' + padding.substring(0, 63); + let credentials = paddedUsername.substring(0, 64) + paddedPassword.substring(0, 64); + + let encrypted = await this._aesEcbEncrypt(credentials, sharedKey); + + this._rfbCredentials.ardCredentials = encrypted; + this._rfbCredentials.ardPublicKey = clientPublicKey; + + this._resumeAuthentication(); + } + + _negotiateTightUnixAuth() { + if (this._rfbCredentials.username === undefined || this._rfbCredentials.password === undefined) { + this.dispatchEvent(new CustomEvent('credentialsrequired', { detail: { types: ['username', 'password'] } })); + return false; + } + + this._sock.send([0, 0, 0, this._rfbCredentials.username.length]); + this._sock.send([0, 0, 0, this._rfbCredentials.password.length]); + this._sock.sendString(this._rfbCredentials.username); + this._sock.sendString(this._rfbCredentials.password); + this._rfbInitState = 'SecurityResult'; + return true; + } + + _negotiateTightTunnels(numTunnels) { + const clientSupportedTunnelTypes = { + 0: { vendor: 'TGHT', signature: 'NOTUNNEL' }, + }; + const serverSupportedTunnelTypes = {}; + // receive tunnel capabilities + for (let i = 0; i < numTunnels; i++) { + const capCode = this._sock.rQshift32(); + const capVendor = this._sock.rQshiftStr(4); + const capSignature = this._sock.rQshiftStr(8); + serverSupportedTunnelTypes[capCode] = { vendor: capVendor, signature: capSignature }; + } + + Log.Debug('Server Tight tunnel types: ' + serverSupportedTunnelTypes); + + // Siemens touch panels have a VNC server that supports NOTUNNEL, + // but forgets to advertise it. Try to detect such servers by + // looking for their custom tunnel type. + if ( + serverSupportedTunnelTypes[1] && + serverSupportedTunnelTypes[1].vendor === 'SICR' && + serverSupportedTunnelTypes[1].signature === 'SCHANNEL' + ) { + Log.Debug('Detected Siemens server. Assuming NOTUNNEL support.'); + serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' }; + } + + // choose the notunnel type + if (serverSupportedTunnelTypes[0]) { + if ( + serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor || + serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature + ) { + return this._fail("Client's tunnel type had the incorrect " + 'vendor or signature'); + } + Log.Debug('Selected tunnel type: ' + clientSupportedTunnelTypes[0]); + this._sock.send([0, 0, 0, 0]); // use NOTUNNEL + return false; // wait until we receive the sub auth count to continue + } else { + return this._fail("Server wanted tunnels, but doesn't support " + 'the notunnel type'); + } + } + + _negotiateTightAuth() { + if (!this._rfbTightVNC) { + // first pass, do the tunnel negotiation + if (this._sock.rQwait('num tunnels', 4)) { + return false; + } + const numTunnels = this._sock.rQshift32(); + if (numTunnels > 0 && this._sock.rQwait('tunnel capabilities', 16 * numTunnels, 4)) { + return false; + } + + this._rfbTightVNC = true; + + if (numTunnels > 0) { + this._negotiateTightTunnels(numTunnels); + return false; // wait until we receive the sub auth to continue + } + } + + // second pass, do the sub-auth negotiation + if (this._sock.rQwait('sub auth count', 4)) { + return false; + } + const subAuthCount = this._sock.rQshift32(); + if (subAuthCount === 0) { + // empty sub-auth list received means 'no auth' subtype selected + this._rfbInitState = 'SecurityResult'; + return true; + } + + if (this._sock.rQwait('sub auth capabilities', 16 * subAuthCount, 4)) { + return false; + } + + const clientSupportedTypes = { + STDVNOAUTH__: 1, + STDVVNCAUTH_: 2, + TGHTULGNAUTH: 129, + }; + + const serverSupportedTypes = []; + + for (let i = 0; i < subAuthCount; i++) { + this._sock.rQshift32(); // capNum + const capabilities = this._sock.rQshiftStr(12); + serverSupportedTypes.push(capabilities); + } + + Log.Debug('Server Tight authentication types: ' + serverSupportedTypes); + + for (let authType in clientSupportedTypes) { + if (serverSupportedTypes.indexOf(authType) != -1) { + this._sock.send([0, 0, 0, clientSupportedTypes[authType]]); + Log.Debug('Selected authentication type: ' + authType); + + switch (authType) { + case 'STDVNOAUTH__': // no auth + this._rfbInitState = 'SecurityResult'; + return true; + case 'STDVVNCAUTH_': + this._rfbAuthScheme = securityTypeVNCAuth; + return true; + case 'TGHTULGNAUTH': + this._rfbAuthScheme = securityTypeUnixLogon; + return true; + default: + return this._fail('Unsupported tiny auth scheme ' + '(scheme: ' + authType + ')'); + } + } + } + + return this._fail('No supported sub-auth types!'); + } + + _handleRSAAESCredentialsRequired(event) { + this.dispatchEvent(event); + } + + _handleRSAAESServerVerification(event) { + this.dispatchEvent(event); + } + + _negotiateRA2neAuth() { + if (this._rfbRSAAESAuthenticationState === null) { + this._rfbRSAAESAuthenticationState = new RSAAESAuthenticationState(this._sock, () => this._rfbCredentials); + this._rfbRSAAESAuthenticationState.addEventListener( + 'serververification', + this._eventHandlers.handleRSAAESServerVerification + ); + this._rfbRSAAESAuthenticationState.addEventListener( + 'credentialsrequired', + this._eventHandlers.handleRSAAESCredentialsRequired + ); + } + this._rfbRSAAESAuthenticationState.checkInternalEvents(); + if (!this._rfbRSAAESAuthenticationState.hasStarted) { + this._rfbRSAAESAuthenticationState + .negotiateRA2neAuthAsync() + .catch((e) => { + if (e.message !== 'disconnect normally') { + this._fail(e.message); + } + }) + .then(() => { + this.dispatchEvent(new CustomEvent('securityresult')); + this._rfbInitState = 'SecurityResult'; + return true; + }) + .finally(() => { + this._rfbRSAAESAuthenticationState.removeEventListener( + 'serververification', + this._eventHandlers.handleRSAAESServerVerification + ); + this._rfbRSAAESAuthenticationState.removeEventListener( + 'credentialsrequired', + this._eventHandlers.handleRSAAESCredentialsRequired + ); + this._rfbRSAAESAuthenticationState = null; + }); + } + return false; + } + + _negotiateMSLogonIIAuth() { + if (this._sock.rQwait('mslogonii dh param', 24)) { + return false; + } + + if (this._rfbCredentials.username === undefined || this._rfbCredentials.password === undefined) { + this.dispatchEvent(new CustomEvent('credentialsrequired', { detail: { types: ['username', 'password'] } })); + return false; + } + + const g = this._sock.rQshiftBytes(8); + const p = this._sock.rQshiftBytes(8); + const A = this._sock.rQshiftBytes(8); + const b = window.crypto.getRandomValues(new Uint8Array(8)); + const B = new Uint8Array(this._modPow(g, b, p)); + const secret = new Uint8Array(this._modPow(A, b, p)); + + const des = new DES(secret); + const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255); + const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63); + const usernameBytes = new Uint8Array(256); + const passwordBytes = new Uint8Array(64); + window.crypto.getRandomValues(usernameBytes); + window.crypto.getRandomValues(passwordBytes); + for (let i = 0; i < username.length; i++) { + usernameBytes[i] = username.charCodeAt(i); + } + usernameBytes[username.length] = 0; + for (let i = 0; i < password.length; i++) { + passwordBytes[i] = password.charCodeAt(i); + } + passwordBytes[password.length] = 0; + let x = new Uint8Array(secret); + for (let i = 0; i < 32; i++) { + for (let j = 0; j < 8; j++) { + x[j] ^= usernameBytes[i * 8 + j]; + } + x = des.enc8(x); + usernameBytes.set(x, i * 8); + } + x = new Uint8Array(secret); + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + x[j] ^= passwordBytes[i * 8 + j]; + } + x = des.enc8(x); + passwordBytes.set(x, i * 8); + } + this._sock.send(B); + this._sock.send(usernameBytes); + this._sock.send(passwordBytes); + this._rfbInitState = 'SecurityResult'; + return true; + } + + _negotiateAuthentication() { + switch (this._rfbAuthScheme) { + case securityTypeNone: + this._rfbInitState = 'SecurityResult'; + return true; + + case securityTypeXVP: + return this._negotiateXvpAuth(); + + case securityTypeARD: + return this._negotiateARDAuth(); + + case securityTypeVNCAuth: + return this._negotiateStdVNCAuth(); + + case securityTypeTight: + return this._negotiateTightAuth(); + + case securityTypeVeNCrypt: + return this._negotiateVeNCryptAuth(); + + case securityTypePlain: + return this._negotiatePlainAuth(); + + case securityTypeUnixLogon: + return this._negotiateTightUnixAuth(); + + case securityTypeRA2ne: + return this._negotiateRA2neAuth(); + + case securityTypeMSLogonII: + return this._negotiateMSLogonIIAuth(); + + default: + return this._fail('Unsupported auth scheme (scheme: ' + this._rfbAuthScheme + ')'); + } + } + + _handleSecurityResult() { + // There is no security choice, and hence no security result + // until RFB 3.7 + if (this._rfbVersion < 3.7) { + this._rfbInitState = 'ClientInitialisation'; + return true; + } + + if (this._sock.rQwait('VNC auth response ', 4)) { + return false; + } + + const status = this._sock.rQshift32(); + + if (status === 0) { + // OK + this._rfbInitState = 'ClientInitialisation'; + Log.Debug('Authentication OK'); + return true; + } else { + if (this._rfbVersion >= 3.8) { + this._rfbInitState = 'SecurityReason'; + this._securityContext = 'security result'; + this._securityStatus = status; + return true; + } else { + this.dispatchEvent(new CustomEvent('securityfailure', { detail: { status: status } })); + + return this._fail('Security handshake failed'); + } + } + } + + _negotiateServerInit() { + if (this._sock.rQwait('server initialization', 24)) { + return false; + } + + /* Screen size */ + const width = this._sock.rQshift16(); + const height = this._sock.rQshift16(); + + /* PIXEL_FORMAT */ + const bpp = this._sock.rQshift8(); + const depth = this._sock.rQshift8(); + const bigEndian = this._sock.rQshift8(); + const trueColor = this._sock.rQshift8(); + + const redMax = this._sock.rQshift16(); + const greenMax = this._sock.rQshift16(); + const blueMax = this._sock.rQshift16(); + const redShift = this._sock.rQshift8(); + const greenShift = this._sock.rQshift8(); + const blueShift = this._sock.rQshift8(); + this._sock.rQskipBytes(3); // padding + + // NB(directxman12): we don't want to call any callbacks or print messages until + // *after* we're past the point where we could backtrack + + /* Connection name/title */ + const nameLength = this._sock.rQshift32(); + if (this._sock.rQwait('server init name', nameLength, 24)) { + return false; + } + let name = this._sock.rQshiftStr(nameLength); + name = decodeUTF8(name, true); + + if (this._rfbTightVNC) { + if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { + return false; + } + // In TightVNC mode, ServerInit message is extended + const numServerMessages = this._sock.rQshift16(); + const numClientMessages = this._sock.rQshift16(); + const numEncodings = this._sock.rQshift16(); + this._sock.rQskipBytes(2); // padding + + const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16; + if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { + return false; + } + + // we don't actually do anything with the capability information that TIGHT sends, + // so we just skip the all of this. + + // TIGHT server message capabilities + this._sock.rQskipBytes(16 * numServerMessages); + + // TIGHT client message capabilities + this._sock.rQskipBytes(16 * numClientMessages); + + // TIGHT encoding capabilities + this._sock.rQskipBytes(16 * numEncodings); + } + + // NB(directxman12): these are down here so that we don't run them multiple times + // if we backtrack + Log.Info( + 'Screen: ' + + width + + 'x' + + height + + ', bpp: ' + + bpp + + ', depth: ' + + depth + + ', bigEndian: ' + + bigEndian + + ', trueColor: ' + + trueColor + + ', redMax: ' + + redMax + + ', greenMax: ' + + greenMax + + ', blueMax: ' + + blueMax + + ', redShift: ' + + redShift + + ', greenShift: ' + + greenShift + + ', blueShift: ' + + blueShift + ); + + // we're past the point where we could backtrack, so it's safe to call this + this._setDesktopName(name); + this._resize(width, height); + + if (!this._viewOnly) { + this._keyboard.grab(); + } + + this._fbDepth = 24; + + if (this._fbName === 'Intel(r) AMT KVM') { + Log.Warn('Intel AMT KVM only supports 8/16 bit depths. Using low color mode.'); + this._fbDepth = 8; + } + + RFB.messages.pixelFormat(this._sock, this._fbDepth, true); + this._sendEncodings(); + RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight); + + this._updateConnectionState('connected'); + return true; + } + + _sendEncodings() { + const encs = []; + + // In preference order + encs.push(encodings.encodingCopyRect); + // Only supported with full depth support + if (this._fbDepth == 24) { + encs.push(encodings.encodingTight); + encs.push(encodings.encodingTightPNG); + encs.push(encodings.encodingZRLE); + encs.push(encodings.encodingJPEG); + encs.push(encodings.encodingHextile); + encs.push(encodings.encodingRRE); + } + encs.push(encodings.encodingRaw); + + // Psuedo-encoding settings + encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel); + encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel); + + encs.push(encodings.pseudoEncodingDesktopSize); + encs.push(encodings.pseudoEncodingLastRect); + encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent); + encs.push(encodings.pseudoEncodingExtendedDesktopSize); + encs.push(encodings.pseudoEncodingXvp); + encs.push(encodings.pseudoEncodingFence); + encs.push(encodings.pseudoEncodingContinuousUpdates); + encs.push(encodings.pseudoEncodingDesktopName); + encs.push(encodings.pseudoEncodingExtendedClipboard); + + if (this._fbDepth == 24) { + encs.push(encodings.pseudoEncodingVMwareCursor); + encs.push(encodings.pseudoEncodingCursor); + } + + RFB.messages.clientEncodings(this._sock, encs); + } + + /* RFB protocol initialization states: + * ProtocolVersion + * Security + * Authentication + * SecurityResult + * ClientInitialization - not triggered by server message + * ServerInitialization + */ + _initMsg() { + switch (this._rfbInitState) { + case 'ProtocolVersion': + return this._negotiateProtocolVersion(); + + case 'Security': + return this._negotiateSecurity(); + + case 'Authentication': + return this._negotiateAuthentication(); + + case 'SecurityResult': + return this._handleSecurityResult(); + + case 'SecurityReason': + return this._handleSecurityReason(); + + case 'ClientInitialisation': + this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation + this._rfbInitState = 'ServerInitialisation'; + return true; + + case 'ServerInitialisation': + return this._negotiateServerInit(); + + default: + return this._fail('Unknown init state (state: ' + this._rfbInitState + ')'); + } + } + + // Resume authentication handshake after it was paused for some + // reason, e.g. waiting for a password from the user + _resumeAuthentication() { + // We use setTimeout() so it's run in its own context, just like + // it originally did via the WebSocket's event handler + setTimeout(this._initMsg.bind(this), 0); + } + + _handleSetColourMapMsg() { + Log.Debug('SetColorMapEntries'); + + return this._fail('Unexpected SetColorMapEntries message'); + } + + _handleServerCutText() { + Log.Debug('ServerCutText'); + + if (this._sock.rQwait('ServerCutText header', 7, 1)) { + return false; + } + + this._sock.rQskipBytes(3); // Padding + + let length = this._sock.rQshift32(); + length = toSigned32bit(length); + + if (this._sock.rQwait('ServerCutText content', Math.abs(length), 8)) { + return false; + } + + if (length >= 0) { + //Standard msg + const text = this._sock.rQshiftStr(length); + if (this._viewOnly) { + return true; + } + + this.dispatchEvent(new CustomEvent('clipboard', { detail: { text: text } })); + } else { + //Extended msg. + length = Math.abs(length); + const flags = this._sock.rQshift32(); + let formats = flags & 0x0000ffff; + let actions = flags & 0xff000000; + + let isCaps = !!(actions & extendedClipboardActionCaps); + if (isCaps) { + this._clipboardServerCapabilitiesFormats = {}; + this._clipboardServerCapabilitiesActions = {}; + + // Update our server capabilities for Formats + for (let i = 0; i <= 15; i++) { + let index = 1 << i; + + // Check if format flag is set. + if (formats & index) { + this._clipboardServerCapabilitiesFormats[index] = true; + // We don't send unsolicited clipboard, so we + // ignore the size + this._sock.rQshift32(); + } + } + + // Update our server capabilities for Actions + for (let i = 24; i <= 31; i++) { + let index = 1 << i; + this._clipboardServerCapabilitiesActions[index] = !!(actions & index); + } + + /* Caps handling done, send caps with the clients + capabilities set as a response */ + let clientActions = [ + extendedClipboardActionCaps, + extendedClipboardActionRequest, + extendedClipboardActionPeek, + extendedClipboardActionNotify, + extendedClipboardActionProvide, + ]; + RFB.messages.extendedClipboardCaps(this._sock, clientActions, { extendedClipboardFormatText: 0 }); + } else if (actions === extendedClipboardActionRequest) { + if (this._viewOnly) { + return true; + } + + // Check if server has told us it can handle Provide and there is clipboard data to send. + if (this._clipboardText != null && this._clipboardServerCapabilitiesActions[extendedClipboardActionProvide]) { + if (formats & extendedClipboardFormatText) { + RFB.messages.extendedClipboardProvide(this._sock, [extendedClipboardFormatText], [this._clipboardText]); + } + } + } else if (actions === extendedClipboardActionPeek) { + if (this._viewOnly) { + return true; + } + + if (this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) { + if (this._clipboardText != null) { + RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]); + } else { + RFB.messages.extendedClipboardNotify(this._sock, []); + } + } + } else if (actions === extendedClipboardActionNotify) { + if (this._viewOnly) { + return true; + } + + if (this._clipboardServerCapabilitiesActions[extendedClipboardActionRequest]) { + if (formats & extendedClipboardFormatText) { + RFB.messages.extendedClipboardRequest(this._sock, [extendedClipboardFormatText]); + } + } + } else if (actions === extendedClipboardActionProvide) { + if (this._viewOnly) { + return true; + } + + if (!(formats & extendedClipboardFormatText)) { + return true; + } + // Ignore what we had in our clipboard client side. + this._clipboardText = null; + + // FIXME: Should probably verify that this data was actually requested + let zlibStream = this._sock.rQshiftBytes(length - 4); + let streamInflator = new Inflator(); + let textData = null; + + streamInflator.setInput(zlibStream); + for (let i = 0; i <= 15; i++) { + let format = 1 << i; + + if (formats & format) { + let size = 0x00; + let sizeArray = streamInflator.inflate(4); + + size |= sizeArray[0] << 24; + size |= sizeArray[1] << 16; + size |= sizeArray[2] << 8; + size |= sizeArray[3]; + let chunk = streamInflator.inflate(size); + + if (format === extendedClipboardFormatText) { + textData = chunk; + } + } + } + streamInflator.setInput(null); + + if (textData !== null) { + let tmpText = ''; + for (let i = 0; i < textData.length; i++) { + tmpText += String.fromCharCode(textData[i]); + } + textData = tmpText; + + textData = decodeUTF8(textData); + if (textData.length > 0 && '\0' === textData.charAt(textData.length - 1)) { + textData = textData.slice(0, -1); + } + + textData = textData.replace('\r\n', '\n'); + + this.dispatchEvent(new CustomEvent('clipboard', { detail: { text: textData } })); + } + } else { + return this._fail('Unexpected action in extended clipboard message: ' + actions); + } + } + return true; + } + + _handleServerFenceMsg() { + if (this._sock.rQwait('ServerFence header', 8, 1)) { + return false; + } + this._sock.rQskipBytes(3); // Padding + let flags = this._sock.rQshift32(); + let length = this._sock.rQshift8(); + + if (this._sock.rQwait('ServerFence payload', length, 9)) { + return false; + } + + if (length > 64) { + Log.Warn('Bad payload length (' + length + ') in fence response'); + length = 64; + } + + const payload = this._sock.rQshiftStr(length); + + this._supportsFence = true; + + /* + * Fence flags + * + * (1<<0) - BlockBefore + * (1<<1) - BlockAfter + * (1<<2) - SyncNext + * (1<<31) - Request + */ + + if (!(flags & (1 << 31))) { + return this._fail('Unexpected fence response'); + } + + // Filter out unsupported flags + // FIXME: support syncNext + flags &= (1 << 0) | (1 << 1); + + // BlockBefore and BlockAfter are automatically handled by + // the fact that we process each incoming message + // synchronuosly. + RFB.messages.clientFence(this._sock, flags, payload); + + return true; + } + + _handleXvpMsg() { + if (this._sock.rQwait('XVP version and message', 3, 1)) { + return false; + } + this._sock.rQskipBytes(1); // Padding + const xvpVer = this._sock.rQshift8(); + const xvpMsg = this._sock.rQshift8(); + + switch (xvpMsg) { + case 0: // XVP_FAIL + Log.Error('XVP Operation Failed'); + break; + case 1: // XVP_INIT + this._rfbXvpVer = xvpVer; + Log.Info('XVP extensions enabled (version ' + this._rfbXvpVer + ')'); + this._setCapability('power', true); + break; + default: + this._fail('Illegal server XVP message (msg: ' + xvpMsg + ')'); + break; + } + + return true; + } + + _normalMsg() { + let msgType; + if (this._FBU.rects > 0) { + msgType = 0; + } else { + msgType = this._sock.rQshift8(); + } + + let first, ret; + switch (msgType) { + case 0: // FramebufferUpdate + ret = this._framebufferUpdate(); + if (ret && !this._enabledContinuousUpdates) { + RFB.messages.fbUpdateRequest(this._sock, true, 0, 0, this._fbWidth, this._fbHeight); + } + return ret; + + case 1: // SetColorMapEntries + return this._handleSetColourMapMsg(); + + case 2: // Bell + Log.Debug('Bell'); + this.dispatchEvent(new CustomEvent('bell', { detail: {} })); + return true; + + case 3: // ServerCutText + return this._handleServerCutText(); + + case 150: // EndOfContinuousUpdates + first = !this._supportsContinuousUpdates; + this._supportsContinuousUpdates = true; + this._enabledContinuousUpdates = false; + if (first) { + this._enabledContinuousUpdates = true; + this._updateContinuousUpdates(); + Log.Info('Enabling continuous updates.'); + } else { + // FIXME: We need to send a framebufferupdaterequest here + // if we add support for turning off continuous updates + } + return true; + + case 248: // ServerFence + return this._handleServerFenceMsg(); + + case 250: // XVP + return this._handleXvpMsg(); + + default: + this._fail('Unexpected server message (type ' + msgType + ')'); + Log.Debug('sock.rQslice(0, 30): ' + this._sock.rQslice(0, 30)); + return true; + } + } + + _onFlush() { + this._flushing = false; + // Resume processing + if (this._sock.rQlen > 0) { + this._handleMessage(); + } + } + + _framebufferUpdate() { + if (this._FBU.rects === 0) { + if (this._sock.rQwait('FBU header', 3, 1)) { + return false; + } + this._sock.rQskipBytes(1); // Padding + this._FBU.rects = this._sock.rQshift16(); + + // Make sure the previous frame is fully rendered first + // to avoid building up an excessive queue + if (this._display.pending()) { + this._flushing = true; + this._display.flush(); + return false; + } + } + + while (this._FBU.rects > 0) { + if (this._FBU.encoding === null) { + if (this._sock.rQwait('rect header', 12)) { + return false; + } + /* New FramebufferUpdate */ + + const hdr = this._sock.rQshiftBytes(12); + this._FBU.x = (hdr[0] << 8) + hdr[1]; + this._FBU.y = (hdr[2] << 8) + hdr[3]; + this._FBU.width = (hdr[4] << 8) + hdr[5]; + this._FBU.height = (hdr[6] << 8) + hdr[7]; + this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + (hdr[10] << 8) + hdr[11], 10); + } + + if (!this._handleRect()) { + return false; + } + + this._FBU.rects--; + this._FBU.encoding = null; + } + + this._display.flip(); + + return true; // We finished this FBU + } + + _handleRect() { + switch (this._FBU.encoding) { + case encodings.pseudoEncodingLastRect: + this._FBU.rects = 1; // Will be decreased when we return + return true; + + case encodings.pseudoEncodingVMwareCursor: + return this._handleVMwareCursor(); + + case encodings.pseudoEncodingCursor: + return this._handleCursor(); + + case encodings.pseudoEncodingQEMUExtendedKeyEvent: + this._qemuExtKeyEventSupported = true; + return true; + + case encodings.pseudoEncodingDesktopName: + return this._handleDesktopName(); + + case encodings.pseudoEncodingDesktopSize: + this._resize(this._FBU.width, this._FBU.height); + return true; + + case encodings.pseudoEncodingExtendedDesktopSize: + return this._handleExtendedDesktopSize(); + + default: + return this._handleDataRect(); + } + } + + _handleVMwareCursor() { + const hotx = this._FBU.x; // hotspot-x + const hoty = this._FBU.y; // hotspot-y + const w = this._FBU.width; + const h = this._FBU.height; + if (this._sock.rQwait('VMware cursor encoding', 1)) { + return false; + } + + const cursorType = this._sock.rQshift8(); + + this._sock.rQshift8(); //Padding + + let rgba; + const bytesPerPixel = 4; + + //Classic cursor + if (cursorType == 0) { + //Used to filter away unimportant bits. + //OR is used for correct conversion in js. + const PIXEL_MASK = 0xffffff00 | 0; + rgba = new Array(w * h * bytesPerPixel); + + if (this._sock.rQwait('VMware cursor classic encoding', w * h * bytesPerPixel * 2, 2)) { + return false; + } + + let andMask = new Array(w * h); + for (let pixel = 0; pixel < w * h; pixel++) { + andMask[pixel] = this._sock.rQshift32(); + } + + let xorMask = new Array(w * h); + for (let pixel = 0; pixel < w * h; pixel++) { + xorMask[pixel] = this._sock.rQshift32(); + } + + for (let pixel = 0; pixel < w * h; pixel++) { + if (andMask[pixel] == 0) { + //Fully opaque pixel + let bgr = xorMask[pixel]; + let r = (bgr >> 8) & 0xff; + let g = (bgr >> 16) & 0xff; + let b = (bgr >> 24) & 0xff; + + rgba[pixel * bytesPerPixel] = r; //r + rgba[pixel * bytesPerPixel + 1] = g; //g + rgba[pixel * bytesPerPixel + 2] = b; //b + rgba[pixel * bytesPerPixel + 3] = 0xff; //a + } else if ((andMask[pixel] & PIXEL_MASK) == PIXEL_MASK) { + //Only screen value matters, no mouse colouring + if (xorMask[pixel] == 0) { + //Transparent pixel + rgba[pixel * bytesPerPixel] = 0x00; + rgba[pixel * bytesPerPixel + 1] = 0x00; + rgba[pixel * bytesPerPixel + 2] = 0x00; + rgba[pixel * bytesPerPixel + 3] = 0x00; + } else if ((xorMask[pixel] & PIXEL_MASK) == PIXEL_MASK) { + //Inverted pixel, not supported in browsers. + //Fully opaque instead. + rgba[pixel * bytesPerPixel] = 0x00; + rgba[pixel * bytesPerPixel + 1] = 0x00; + rgba[pixel * bytesPerPixel + 2] = 0x00; + rgba[pixel * bytesPerPixel + 3] = 0xff; + } else { + //Unhandled xorMask + rgba[pixel * bytesPerPixel] = 0x00; + rgba[pixel * bytesPerPixel + 1] = 0x00; + rgba[pixel * bytesPerPixel + 2] = 0x00; + rgba[pixel * bytesPerPixel + 3] = 0xff; + } + } else { + //Unhandled andMask + rgba[pixel * bytesPerPixel] = 0x00; + rgba[pixel * bytesPerPixel + 1] = 0x00; + rgba[pixel * bytesPerPixel + 2] = 0x00; + rgba[pixel * bytesPerPixel + 3] = 0xff; + } + } + + //Alpha cursor. + } else if (cursorType == 1) { + if (this._sock.rQwait('VMware cursor alpha encoding', w * h * 4, 2)) { + return false; + } + + rgba = new Array(w * h * bytesPerPixel); + + for (let pixel = 0; pixel < w * h; pixel++) { + let data = this._sock.rQshift32(); + + rgba[pixel * 4] = (data >> 24) & 0xff; //r + rgba[pixel * 4 + 1] = (data >> 16) & 0xff; //g + rgba[pixel * 4 + 2] = (data >> 8) & 0xff; //b + rgba[pixel * 4 + 3] = data & 0xff; //a + } + } else { + Log.Warn('The given cursor type is not supported: ' + cursorType + ' given.'); + return false; + } + + this._updateCursor(rgba, hotx, hoty, w, h); + + return true; + } + + _handleCursor() { + const hotx = this._FBU.x; // hotspot-x + const hoty = this._FBU.y; // hotspot-y + const w = this._FBU.width; + const h = this._FBU.height; + + const pixelslength = w * h * 4; + const masklength = Math.ceil(w / 8) * h; + + let bytes = pixelslength + masklength; + if (this._sock.rQwait('cursor encoding', bytes)) { + return false; + } + + // Decode from BGRX pixels + bit mask to RGBA + const pixels = this._sock.rQshiftBytes(pixelslength); + const mask = this._sock.rQshiftBytes(masklength); + let rgba = new Uint8Array(w * h * 4); + + let pixIdx = 0; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + let maskIdx = y * Math.ceil(w / 8) + Math.floor(x / 8); + let alpha = (mask[maskIdx] << x % 8) & 0x80 ? 255 : 0; + rgba[pixIdx] = pixels[pixIdx + 2]; + rgba[pixIdx + 1] = pixels[pixIdx + 1]; + rgba[pixIdx + 2] = pixels[pixIdx]; + rgba[pixIdx + 3] = alpha; + pixIdx += 4; + } + } + + this._updateCursor(rgba, hotx, hoty, w, h); + + return true; + } + + _handleDesktopName() { + if (this._sock.rQwait('DesktopName', 4)) { + return false; + } + + let length = this._sock.rQshift32(); + + if (this._sock.rQwait('DesktopName', length, 4)) { + return false; + } + + let name = this._sock.rQshiftStr(length); + name = decodeUTF8(name, true); + + this._setDesktopName(name); + + return true; + } + + _handleExtendedDesktopSize() { + if (this._sock.rQwait('ExtendedDesktopSize', 4)) { + return false; + } + + const numberOfScreens = this._sock.rQpeek8(); + + let bytes = 4 + numberOfScreens * 16; + if (this._sock.rQwait('ExtendedDesktopSize', bytes)) { + return false; + } + + const firstUpdate = !this._supportsSetDesktopSize; + this._supportsSetDesktopSize = true; + + // Normally we only apply the current resize mode after a + // window resize event. However there is no such trigger on the + // initial connect. And we don't know if the server supports + // resizing until we've gotten here. + if (firstUpdate) { + this._requestRemoteResize(); + } + + this._sock.rQskipBytes(1); // number-of-screens + this._sock.rQskipBytes(3); // padding + + for (let i = 0; i < numberOfScreens; i += 1) { + // Save the id and flags of the first screen + if (i === 0) { + this._screenID = this._sock.rQshiftBytes(4); // id + this._sock.rQskipBytes(2); // x-position + this._sock.rQskipBytes(2); // y-position + this._sock.rQskipBytes(2); // width + this._sock.rQskipBytes(2); // height + this._screenFlags = this._sock.rQshiftBytes(4); // flags + } else { + this._sock.rQskipBytes(16); + } + } + + /* + * The x-position indicates the reason for the change: + * + * 0 - server resized on its own + * 1 - this client requested the resize + * 2 - another client requested the resize + */ + + // We need to handle errors when we requested the resize. + if (this._FBU.x === 1 && this._FBU.y !== 0) { + let msg = ''; + // The y-position indicates the status code from the server + switch (this._FBU.y) { + case 1: + msg = 'Resize is administratively prohibited'; + break; + case 2: + msg = 'Out of resources'; + break; + case 3: + msg = 'Invalid screen layout'; + break; + default: + msg = 'Unknown reason'; + break; + } + Log.Warn('Server did not accept the resize request: ' + msg); + } else { + this._resize(this._FBU.width, this._FBU.height); + } + + return true; + } + + _handleDataRect() { + let decoder = this._decoders[this._FBU.encoding]; + if (!decoder) { + this._fail('Unsupported encoding (encoding: ' + this._FBU.encoding + ')'); + return false; + } + + try { + return decoder.decodeRect( + this._FBU.x, + this._FBU.y, + this._FBU.width, + this._FBU.height, + this._sock, + this._display, + this._fbDepth + ); + } catch (err) { + this._fail('Error decoding rect: ' + err); + return false; + } + } + + _updateContinuousUpdates() { + if (!this._enabledContinuousUpdates) { + return; + } + + RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0, this._fbWidth, this._fbHeight); + } + + _resize(width, height) { + this._fbWidth = width; + this._fbHeight = height; + + this._display.resize(this._fbWidth, this._fbHeight); + + // Adjust the visible viewport based on the new dimensions + this._updateClip(); + this._updateScale(); + + this._updateContinuousUpdates(); + + // Keep this size until browser client size changes + this._saveExpectedClientSize(); + } + + _xvpOp(ver, op) { + if (this._rfbXvpVer < ver) { + return; + } + Log.Info('Sending XVP operation ' + op + ' (version ' + ver + ')'); + RFB.messages.xvpOp(this._sock, ver, op); + } + + _updateCursor(rgba, hotx, hoty, w, h) { + this._cursorImage = { + rgbaPixels: rgba, + hotx: hotx, + hoty: hoty, + w: w, + h: h, + }; + this._refreshCursor(); + } + + _shouldShowDotCursor() { + // Called when this._cursorImage is updated + if (!this._showDotCursor) { + // User does not want to see the dot, so... + return false; + } + + // The dot should not be shown if the cursor is already visible, + // i.e. contains at least one not-fully-transparent pixel. + // So iterate through all alpha bytes in rgba and stop at the + // first non-zero. + for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) { + if (this._cursorImage.rgbaPixels[i]) { + return false; + } + } + + // At this point, we know that the cursor is fully transparent, and + // the user wants to see the dot instead of this. + return true; + } + + _refreshCursor() { + if (this._rfbConnectionState !== 'connecting' && this._rfbConnectionState !== 'connected') { + return; + } + const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage; + this._cursor.change(image.rgbaPixels, image.hotx, image.hoty, image.w, image.h); + } + + static genDES(password, challenge) { + const passwordChars = password.split('').map((c) => c.charCodeAt(0)); + return new DES(passwordChars).encrypt(challenge); + } +} + +// Class Methods +RFB.messages = { + keyEvent(sock, keysym, down) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 4; // msg-type + buff[offset + 1] = down; + + buff[offset + 2] = 0; + buff[offset + 3] = 0; + + buff[offset + 4] = keysym >> 24; + buff[offset + 5] = keysym >> 16; + buff[offset + 6] = keysym >> 8; + buff[offset + 7] = keysym; + + sock._sQlen += 8; + sock.flush(); + }, + + QEMUExtendedKeyEvent(sock, keysym, down, keycode) { + function getRFBkeycode(xtScanCode) { + const upperByte = keycode >> 8; + const lowerByte = keycode & 0x00ff; + if (upperByte === 0xe0 && lowerByte < 0x7f) { + return lowerByte | 0x80; + } + return xtScanCode; + } + + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 255; // msg-type + buff[offset + 1] = 0; // sub msg-type + + buff[offset + 2] = down >> 8; + buff[offset + 3] = down; + + buff[offset + 4] = keysym >> 24; + buff[offset + 5] = keysym >> 16; + buff[offset + 6] = keysym >> 8; + buff[offset + 7] = keysym; + + const RFBkeycode = getRFBkeycode(keycode); + + buff[offset + 8] = RFBkeycode >> 24; + buff[offset + 9] = RFBkeycode >> 16; + buff[offset + 10] = RFBkeycode >> 8; + buff[offset + 11] = RFBkeycode; + + sock._sQlen += 12; + sock.flush(); + }, + + pointerEvent(sock, x, y, mask) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 5; // msg-type + + buff[offset + 1] = mask; + + buff[offset + 2] = x >> 8; + buff[offset + 3] = x; + + buff[offset + 4] = y >> 8; + buff[offset + 5] = y; + + sock._sQlen += 6; + sock.flush(); + }, + + // Used to build Notify and Request data. + _buildExtendedClipboardFlags(actions, formats) { + let data = new Uint8Array(4); + let formatFlag = 0x00000000; + let actionFlag = 0x00000000; + + for (let i = 0; i < actions.length; i++) { + actionFlag |= actions[i]; + } + + for (let i = 0; i < formats.length; i++) { + formatFlag |= formats[i]; + } + + data[0] = actionFlag >> 24; // Actions + data[1] = 0x00; // Reserved + data[2] = 0x00; // Reserved + data[3] = formatFlag; // Formats + + return data; + }, + + extendedClipboardProvide(sock, formats, inData) { + // Deflate incomming data and their sizes + let deflator = new Deflator(); + let dataToDeflate = []; + + for (let i = 0; i < formats.length; i++) { + // We only support the format Text at this time + if (formats[i] != extendedClipboardFormatText) { + throw new Error('Unsupported extended clipboard format for Provide message.'); + } + + // Change lone \r or \n into \r\n as defined in rfbproto + inData[i] = inData[i].replace(/\r\n|\r|\n/gm, '\r\n'); + + // Check if it already has \0 + let text = encodeUTF8(inData[i] + '\0'); + + dataToDeflate.push( + (text.length >> 24) & 0xff, + (text.length >> 16) & 0xff, + (text.length >> 8) & 0xff, + text.length & 0xff + ); + + for (let j = 0; j < text.length; j++) { + dataToDeflate.push(text.charCodeAt(j)); + } + } + + let deflatedData = deflator.deflate(new Uint8Array(dataToDeflate)); + + // Build data to send + let data = new Uint8Array(4 + deflatedData.length); + data.set(RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionProvide], formats)); + data.set(deflatedData, 4); + + RFB.messages.clientCutText(sock, data, true); + }, + + extendedClipboardNotify(sock, formats) { + let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionNotify], formats); + RFB.messages.clientCutText(sock, flags, true); + }, + + extendedClipboardRequest(sock, formats) { + let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionRequest], formats); + RFB.messages.clientCutText(sock, flags, true); + }, + + extendedClipboardCaps(sock, actions, formats) { + let formatKeys = Object.keys(formats); + let data = new Uint8Array(4 + 4 * formatKeys.length); + + formatKeys.map((x) => parseInt(x)); + formatKeys.sort((a, b) => a - b); + + data.set(RFB.messages._buildExtendedClipboardFlags(actions, [])); + + let loopOffset = 4; + for (let i = 0; i < formatKeys.length; i++) { + data[loopOffset] = formats[formatKeys[i]] >> 24; + data[loopOffset + 1] = formats[formatKeys[i]] >> 16; + data[loopOffset + 2] = formats[formatKeys[i]] >> 8; + data[loopOffset + 3] = formats[formatKeys[i]] >> 0; + + loopOffset += 4; + data[3] |= 1 << formatKeys[i]; // Update our format flags + } + + RFB.messages.clientCutText(sock, data, true); + }, + + clientCutText(sock, data, extended = false) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 6; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + let length; + if (extended) { + length = toUnsigned32bit(-data.length); + } else { + length = data.length; + } + + buff[offset + 4] = length >> 24; + buff[offset + 5] = length >> 16; + buff[offset + 6] = length >> 8; + buff[offset + 7] = length; + + sock._sQlen += 8; + + // We have to keep track of from where in the data we begin creating the + // buffer for the flush in the next iteration. + let dataOffset = 0; + + let remaining = data.length; + while (remaining > 0) { + let flushSize = Math.min(remaining, sock._sQbufferSize - sock._sQlen); + for (let i = 0; i < flushSize; i++) { + buff[sock._sQlen + i] = data[dataOffset + i]; + } + + sock._sQlen += flushSize; + sock.flush(); + + remaining -= flushSize; + dataOffset += flushSize; + } + }, + + setDesktopSize(sock, width, height, id, flags) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 251; // msg-type + buff[offset + 1] = 0; // padding + buff[offset + 2] = width >> 8; // width + buff[offset + 3] = width; + buff[offset + 4] = height >> 8; // height + buff[offset + 5] = height; + + buff[offset + 6] = 1; // number-of-screens + buff[offset + 7] = 0; // padding + + // screen array + buff[offset + 8] = id >> 24; // id + buff[offset + 9] = id >> 16; + buff[offset + 10] = id >> 8; + buff[offset + 11] = id; + buff[offset + 12] = 0; // x-position + buff[offset + 13] = 0; + buff[offset + 14] = 0; // y-position + buff[offset + 15] = 0; + buff[offset + 16] = width >> 8; // width + buff[offset + 17] = width; + buff[offset + 18] = height >> 8; // height + buff[offset + 19] = height; + buff[offset + 20] = flags >> 24; // flags + buff[offset + 21] = flags >> 16; + buff[offset + 22] = flags >> 8; + buff[offset + 23] = flags; + + sock._sQlen += 24; + sock.flush(); + }, + + clientFence(sock, flags, payload) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 248; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + buff[offset + 4] = flags >> 24; // flags + buff[offset + 5] = flags >> 16; + buff[offset + 6] = flags >> 8; + buff[offset + 7] = flags; + + const n = payload.length; + + buff[offset + 8] = n; // length + + for (let i = 0; i < n; i++) { + buff[offset + 9 + i] = payload.charCodeAt(i); + } + + sock._sQlen += 9 + n; + sock.flush(); + }, + + enableContinuousUpdates(sock, enable, x, y, width, height) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 150; // msg-type + buff[offset + 1] = enable; // enable-flag + + buff[offset + 2] = x >> 8; // x + buff[offset + 3] = x; + buff[offset + 4] = y >> 8; // y + buff[offset + 5] = y; + buff[offset + 6] = width >> 8; // width + buff[offset + 7] = width; + buff[offset + 8] = height >> 8; // height + buff[offset + 9] = height; + + sock._sQlen += 10; + sock.flush(); + }, + + pixelFormat(sock, depth, trueColor) { + const buff = sock._sQ; + const offset = sock._sQlen; + + let bpp; + + if (depth > 16) { + bpp = 32; + } else if (depth > 8) { + bpp = 16; + } else { + bpp = 8; + } + + const bits = Math.floor(depth / 3); + + buff[offset] = 0; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + buff[offset + 4] = bpp; // bits-per-pixel + buff[offset + 5] = depth; // depth + buff[offset + 6] = 0; // little-endian + buff[offset + 7] = trueColor ? 1 : 0; // true-color + + buff[offset + 8] = 0; // red-max + buff[offset + 9] = (1 << bits) - 1; // red-max + + buff[offset + 10] = 0; // green-max + buff[offset + 11] = (1 << bits) - 1; // green-max + + buff[offset + 12] = 0; // blue-max + buff[offset + 13] = (1 << bits) - 1; // blue-max + + buff[offset + 14] = bits * 0; // red-shift + buff[offset + 15] = bits * 1; // green-shift + buff[offset + 16] = bits * 2; // blue-shift + + buff[offset + 17] = 0; // padding + buff[offset + 18] = 0; // padding + buff[offset + 19] = 0; // padding + + sock._sQlen += 20; + sock.flush(); + }, + + clientEncodings(sock, encodings) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 2; // msg-type + buff[offset + 1] = 0; // padding + + buff[offset + 2] = encodings.length >> 8; + buff[offset + 3] = encodings.length; + + let j = offset + 4; + for (let i = 0; i < encodings.length; i++) { + const enc = encodings[i]; + buff[j] = enc >> 24; + buff[j + 1] = enc >> 16; + buff[j + 2] = enc >> 8; + buff[j + 3] = enc; + + j += 4; + } + + sock._sQlen += j - offset; + sock.flush(); + }, + + fbUpdateRequest(sock, incremental, x, y, w, h) { + const buff = sock._sQ; + const offset = sock._sQlen; + + if (typeof x === 'undefined') { + x = 0; + } + if (typeof y === 'undefined') { + y = 0; + } + + buff[offset] = 3; // msg-type + buff[offset + 1] = incremental ? 1 : 0; + + buff[offset + 2] = (x >> 8) & 0xff; + buff[offset + 3] = x & 0xff; + + buff[offset + 4] = (y >> 8) & 0xff; + buff[offset + 5] = y & 0xff; + + buff[offset + 6] = (w >> 8) & 0xff; + buff[offset + 7] = w & 0xff; + + buff[offset + 8] = (h >> 8) & 0xff; + buff[offset + 9] = h & 0xff; + + sock._sQlen += 10; + sock.flush(); + }, + + xvpOp(sock, ver, op) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 250; // msg-type + buff[offset + 1] = 0; // padding + + buff[offset + 2] = ver; + buff[offset + 3] = op; + + sock._sQlen += 4; + sock.flush(); + }, +}; + +RFB.cursors = { + none: { + rgbaPixels: new Uint8Array(), + w: 0, + h: 0, + hotx: 0, + hoty: 0, + }, + + dot: { + /* eslint-disable indent */ + rgbaPixels: new Uint8Array([ + 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, + 0, 0, 0, 255, 255, 255, 255, 255, + ]), + /* eslint-enable indent */ + w: 3, + h: 3, + hotx: 1, + hoty: 1, + }, +}; diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/browser.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/browser.js new file mode 100644 index 000000000..d54b1591e --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/browser.js @@ -0,0 +1,159 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + * Browser feature support detection + */ + +import * as Log from './logging.js'; + +// Touch detection +export let isTouchDevice = + 'ontouchstart' in document.documentElement || + // requried for Chrome debugger + document.ontouchstart !== undefined || + // required for MS Surface + navigator.maxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0; +window.addEventListener( + 'touchstart', + function onFirstTouch() { + isTouchDevice = true; + window.removeEventListener('touchstart', onFirstTouch, false); + }, + false +); + +// The goal is to find a certain physical width, the devicePixelRatio +// brings us a bit closer but is not optimal. +export let dragThreshold = 10 * (window.devicePixelRatio || 1); + +let _supportsCursorURIs = false; + +try { + const target = document.createElement('canvas'); + target.style.cursor = + 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default'; + + if (target.style.cursor.indexOf('url') === 0) { + Log.Info('Data URI scheme cursor supported'); + _supportsCursorURIs = true; + } else { + Log.Warn('Data URI scheme cursor not supported'); + } +} catch (exc) { + Log.Error('Data URI scheme cursor test exception: ' + exc); +} + +export const supportsCursorURIs = _supportsCursorURIs; + +let _hasScrollbarGutter = true; +try { + // Create invisible container + const container = document.createElement('div'); + container.style.visibility = 'hidden'; + container.style.overflow = 'scroll'; // forcing scrollbars + document.body.appendChild(container); + + // Create a div and place it in the container + const child = document.createElement('div'); + container.appendChild(child); + + // Calculate the difference between the container's full width + // and the child's width - the difference is the scrollbars + const scrollbarWidth = container.offsetWidth - child.offsetWidth; + + // Clean up + container.parentNode.removeChild(container); + + _hasScrollbarGutter = scrollbarWidth != 0; +} catch (exc) { + Log.Error('Scrollbar test exception: ' + exc); +} +export const hasScrollbarGutter = _hasScrollbarGutter; + +/* + * The functions for detection of platforms and browsers below are exported + * but the use of these should be minimized as much as possible. + * + * It's better to use feature detection than platform detection. + */ + +/* OS */ + +export function isMac() { + return !!/mac/i.exec(navigator.platform); +} + +export function isWindows() { + return !!/win/i.exec(navigator.platform); +} + +export function isIOS() { + return ( + !!/ipad/i.exec(navigator.platform) || !!/iphone/i.exec(navigator.platform) || !!/ipod/i.exec(navigator.platform) + ); +} + +export function isAndroid() { + /* Android sets navigator.platform to Linux :/ */ + return !!navigator.userAgent.match('Android '); +} + +export function isChromeOS() { + /* ChromeOS sets navigator.platform to Linux :/ */ + return !!navigator.userAgent.match(' CrOS '); +} + +/* Browser */ + +export function isSafari() { + return ( + !!navigator.userAgent.match('Safari/...') && + !navigator.userAgent.match('Chrome/...') && + !navigator.userAgent.match('Chromium/...') && + !navigator.userAgent.match('Epiphany/...') + ); +} + +export function isFirefox() { + return !!navigator.userAgent.match('Firefox/...') && !navigator.userAgent.match('Seamonkey/...'); +} + +export function isChrome() { + return ( + !!navigator.userAgent.match('Chrome/...') && + !navigator.userAgent.match('Chromium/...') && + !navigator.userAgent.match('Edg/...') && + !navigator.userAgent.match('OPR/...') + ); +} + +export function isChromium() { + return !!navigator.userAgent.match('Chromium/...'); +} + +export function isOpera() { + return !!navigator.userAgent.match('OPR/...'); +} + +export function isEdge() { + return !!navigator.userAgent.match('Edg/...'); +} + +/* Engine */ + +export function isGecko() { + return !!navigator.userAgent.match('Gecko/...'); +} + +export function isWebKit() { + return !!navigator.userAgent.match('AppleWebKit/...') && !navigator.userAgent.match('Chrome/...'); +} + +export function isBlink() { + return !!navigator.userAgent.match('Chrome/...'); +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/cursor.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/cursor.js new file mode 100644 index 000000000..68c655df4 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/cursor.js @@ -0,0 +1,245 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +import { supportsCursorURIs, isTouchDevice } from './browser.js'; + +const useFallback = !supportsCursorURIs || isTouchDevice; + +export default class Cursor { + constructor() { + this._target = null; + + this._canvas = document.createElement('canvas'); + + if (useFallback) { + this._canvas.style.position = 'fixed'; + this._canvas.style.zIndex = '65535'; + this._canvas.style.pointerEvents = 'none'; + // Safari on iOS can select the cursor image + // https://bugs.webkit.org/show_bug.cgi?id=249223 + this._canvas.style.userSelect = 'none'; + this._canvas.style.WebkitUserSelect = 'none'; + // Can't use "display" because of Firefox bug #1445997 + this._canvas.style.visibility = 'hidden'; + } + + this._position = { x: 0, y: 0 }; + this._hotSpot = { x: 0, y: 0 }; + + this._eventHandlers = { + mouseover: this._handleMouseOver.bind(this), + mouseleave: this._handleMouseLeave.bind(this), + mousemove: this._handleMouseMove.bind(this), + mouseup: this._handleMouseUp.bind(this), + }; + } + + attach(target) { + if (this._target) { + this.detach(); + } + + this._target = target; + + if (useFallback) { + document.body.appendChild(this._canvas); + + const options = { capture: true, passive: true }; + this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options); + this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options); + this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options); + this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options); + } + + this.clear(); + } + + detach() { + if (!this._target) { + return; + } + + if (useFallback) { + const options = { capture: true, passive: true }; + this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options); + this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options); + this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options); + this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options); + + document.body.removeChild(this._canvas); + } + + this._target = null; + } + + change(rgba, hotx, hoty, w, h) { + if (w === 0 || h === 0) { + this.clear(); + return; + } + + this._position.x = this._position.x + this._hotSpot.x - hotx; + this._position.y = this._position.y + this._hotSpot.y - hoty; + this._hotSpot.x = hotx; + this._hotSpot.y = hoty; + + let ctx = this._canvas.getContext('2d'); + + this._canvas.width = w; + this._canvas.height = h; + + let img = new ImageData(new Uint8ClampedArray(rgba), w, h); + ctx.clearRect(0, 0, w, h); + ctx.putImageData(img, 0, 0); + + if (useFallback) { + this._updatePosition(); + } else { + let url = this._canvas.toDataURL(); + this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; + } + } + + clear() { + this._target.style.cursor = 'none'; + this._canvas.width = 0; + this._canvas.height = 0; + this._position.x = this._position.x + this._hotSpot.x; + this._position.y = this._position.y + this._hotSpot.y; + this._hotSpot.x = 0; + this._hotSpot.y = 0; + } + + // Mouse events might be emulated, this allows + // moving the cursor in such cases + move(clientX, clientY) { + if (!useFallback) { + return; + } + // clientX/clientY are relative the _visual viewport_, + // but our position is relative the _layout viewport_, + // so try to compensate when we can + if (window.visualViewport) { + this._position.x = clientX + window.visualViewport.offsetLeft; + this._position.y = clientY + window.visualViewport.offsetTop; + } else { + this._position.x = clientX; + this._position.y = clientY; + } + this._updatePosition(); + let target = document.elementFromPoint(clientX, clientY); + this._updateVisibility(target); + } + + _handleMouseOver(event) { + // This event could be because we're entering the target, or + // moving around amongst its sub elements. Let the move handler + // sort things out. + this._handleMouseMove(event); + } + + _handleMouseLeave(event) { + // Check if we should show the cursor on the element we are leaving to + this._updateVisibility(event.relatedTarget); + } + + _handleMouseMove(event) { + this._updateVisibility(event.target); + + this._position.x = event.clientX - this._hotSpot.x; + this._position.y = event.clientY - this._hotSpot.y; + + this._updatePosition(); + } + + _handleMouseUp(event) { + // We might get this event because of a drag operation that + // moved outside of the target. Check what's under the cursor + // now and adjust visibility based on that. + let target = document.elementFromPoint(event.clientX, event.clientY); + this._updateVisibility(target); + + // Captures end with a mouseup but we can't know the event order of + // mouseup vs releaseCapture. + // + // In the cases when releaseCapture comes first, the code above is + // enough. + // + // In the cases when the mouseup comes first, we need wait for the + // browser to flush all events and then check again if the cursor + // should be visible. + if (this._captureIsActive()) { + window.setTimeout(() => { + // We might have detached at this point + if (!this._target) { + return; + } + // Refresh the target from elementFromPoint since queued events + // might have altered the DOM + target = document.elementFromPoint(event.clientX, event.clientY); + this._updateVisibility(target); + }, 0); + } + } + + _showCursor() { + if (this._canvas.style.visibility === 'hidden') { + this._canvas.style.visibility = ''; + } + } + + _hideCursor() { + if (this._canvas.style.visibility !== 'hidden') { + this._canvas.style.visibility = 'hidden'; + } + } + + // Should we currently display the cursor? + // (i.e. are we over the target, or a child of the target without a + // different cursor set) + _shouldShowCursor(target) { + if (!target) { + return false; + } + // Easy case + if (target === this._target) { + return true; + } + // Other part of the DOM? + if (!this._target.contains(target)) { + return false; + } + // Has the child its own cursor? + // FIXME: How can we tell that a sub element has an + // explicit "cursor: none;"? + if (window.getComputedStyle(target).cursor !== 'none') { + return false; + } + return true; + } + + _updateVisibility(target) { + // When the cursor target has capture we want to show the cursor. + // So, if a capture is active - look at the captured element instead. + if (this._captureIsActive()) { + target = document.captureElement; + } + if (this._shouldShowCursor(target)) { + this._showCursor(); + } else { + this._hideCursor(); + } + } + + _updatePosition() { + this._canvas.style.left = this._position.x + 'px'; + this._canvas.style.top = this._position.y + 'px'; + } + + _captureIsActive() { + return document.captureElement && document.documentElement.contains(document.captureElement); + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/element.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/element.js new file mode 100644 index 000000000..d86316831 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/element.js @@ -0,0 +1,32 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2020 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/* + * HTML element utility functions + */ + +export function clientToElement(x, y, elem) { + const bounds = elem.getBoundingClientRect(); + let pos = { x: 0, y: 0 }; + // Clip to target bounds + if (x < bounds.left) { + pos.x = 0; + } else if (x >= bounds.right) { + pos.x = bounds.width - 1; + } else { + pos.x = x - bounds.left; + } + if (y < bounds.top) { + pos.y = 0; + } else if (y >= bounds.bottom) { + pos.y = bounds.height - 1; + } else { + pos.y = y - bounds.top; + } + return pos; +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/events.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/events.js new file mode 100644 index 000000000..6dce3bef6 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/events.js @@ -0,0 +1,135 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/* + * Cross-browser event and position routines + */ + +export function getPointerEvent(e) { + return e.changedTouches ? e.changedTouches[0] : e.touches ? e.touches[0] : e; +} + +export function stopEvent(e) { + e.stopPropagation(); + e.preventDefault(); +} + +// Emulate Element.setCapture() when not supported +let _captureRecursion = false; +let _elementForUnflushedEvents = null; +document.captureElement = null; +function _captureProxy(e) { + // Recursion protection as we'll see our own event + if (_captureRecursion) return; + + // Clone the event as we cannot dispatch an already dispatched event + const newEv = new e.constructor(e.type, e); + + _captureRecursion = true; + if (document.captureElement) { + document.captureElement.dispatchEvent(newEv); + } else { + _elementForUnflushedEvents.dispatchEvent(newEv); + } + _captureRecursion = false; + + // Avoid double events + e.stopPropagation(); + + // Respect the wishes of the redirected event handlers + if (newEv.defaultPrevented) { + e.preventDefault(); + } + + // Implicitly release the capture on button release + if (e.type === 'mouseup') { + releaseCapture(); + } +} + +// Follow cursor style of target element +function _capturedElemChanged() { + const proxyElem = document.getElementById('noVNC_mouse_capture_elem'); + proxyElem.style.cursor = window.getComputedStyle(document.captureElement).cursor; +} + +const _captureObserver = new MutationObserver(_capturedElemChanged); + +export function setCapture(target) { + if (target.setCapture) { + target.setCapture(); + document.captureElement = target; + } else { + // Release any existing capture in case this method is + // called multiple times without coordination + releaseCapture(); + + let proxyElem = document.getElementById('noVNC_mouse_capture_elem'); + + if (proxyElem === null) { + proxyElem = document.createElement('div'); + proxyElem.id = 'noVNC_mouse_capture_elem'; + proxyElem.style.position = 'fixed'; + proxyElem.style.top = '0px'; + proxyElem.style.left = '0px'; + proxyElem.style.width = '100%'; + proxyElem.style.height = '100%'; + proxyElem.style.zIndex = 10000; + proxyElem.style.display = 'none'; + document.body.appendChild(proxyElem); + + // This is to make sure callers don't get confused by having + // our blocking element as the target + proxyElem.addEventListener('contextmenu', _captureProxy); + + proxyElem.addEventListener('mousemove', _captureProxy); + proxyElem.addEventListener('mouseup', _captureProxy); + } + + document.captureElement = target; + + // Track cursor and get initial cursor + _captureObserver.observe(target, { attributes: true }); + _capturedElemChanged(); + + proxyElem.style.display = ''; + + // We listen to events on window in order to keep tracking if it + // happens to leave the viewport + window.addEventListener('mousemove', _captureProxy); + window.addEventListener('mouseup', _captureProxy); + } +} + +export function releaseCapture() { + if (document.releaseCapture) { + document.releaseCapture(); + document.captureElement = null; + } else { + if (!document.captureElement) { + return; + } + + // There might be events already queued. The event proxy needs + // access to the captured element for these queued events. + // E.g. contextmenu (right-click) in Microsoft Edge + // + // Before removing the capturedElem pointer we save it to a + // temporary variable that the unflushed events can use. + _elementForUnflushedEvents = document.captureElement; + document.captureElement = null; + + _captureObserver.disconnect(); + + const proxyElem = document.getElementById('noVNC_mouse_capture_elem'); + proxyElem.style.display = 'none'; + + window.removeEventListener('mousemove', _captureProxy); + window.removeEventListener('mouseup', _captureProxy); + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/eventtarget.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/eventtarget.js new file mode 100644 index 000000000..bf1de4331 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/eventtarget.js @@ -0,0 +1,34 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +export default class EventTargetMixin { + constructor() { + this._listeners = new Map(); + } + + addEventListener(type, callback) { + if (!this._listeners.has(type)) { + this._listeners.set(type, new Set()); + } + this._listeners.get(type).add(callback); + } + + removeEventListener(type, callback) { + if (this._listeners.has(type)) { + this._listeners.get(type).delete(callback); + } + } + + dispatchEvent(event) { + if (!this._listeners.has(event.type)) { + return true; + } + this._listeners.get(event.type).forEach((callback) => callback.call(this, event)); + return !event.defaultPrevented; + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/int.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/int.js new file mode 100644 index 000000000..bd3832e62 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/int.js @@ -0,0 +1,15 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2020 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +export function toUnsigned32bit(toConvert) { + return toConvert >>> 0; +} + +export function toSigned32bit(toConvert) { + return toConvert | 0; +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/logging.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/logging.js new file mode 100644 index 000000000..88db13aa8 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/logging.js @@ -0,0 +1,56 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/* + * Logging/debug routines + */ + +let _logLevel = 'warn'; + +let Debug = () => {}; +let Info = () => {}; +let Warn = () => {}; +let Error = () => {}; + +export function initLogging(level) { + if (typeof level === 'undefined') { + level = _logLevel; + } else { + _logLevel = level; + } + + Debug = Info = Warn = Error = () => {}; + + if (typeof window.console !== 'undefined') { + /* eslint-disable no-console, no-fallthrough */ + switch (level) { + case 'debug': + Debug = console.debug.bind(window.console); + case 'info': + Info = console.info.bind(window.console); + case 'warn': + Warn = console.warn.bind(window.console); + case 'error': + Error = console.error.bind(window.console); + case 'none': + break; + default: + throw new window.Error("invalid logging type '" + level + "'"); + } + /* eslint-enable no-console, no-fallthrough */ + } +} + +export function getLogging() { + return _logLevel; +} + +export { Debug, Error, Info, Warn }; + +// Initialize logging level +initLogging(); diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/md5.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/md5.js new file mode 100644 index 000000000..1debfe6ea --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/md5.js @@ -0,0 +1,382 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2021 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/* + * Performs MD5 hashing on a string of binary characters, returns an array of bytes + */ + +export function MD5(d) { + let r = M(V(Y(X(d), 8 * d.length))); + return r; +} + +function M(d) { + let f = new Uint8Array(d.length); + for (let i = 0; i < d.length; i++) { + f[i] = d.charCodeAt(i); + } + return f; +} + +function X(d) { + let r = Array(d.length >> 2); + for (let m = 0; m < r.length; m++) r[m] = 0; + for (let m = 0; m < 8 * d.length; m += 8) r[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32; + return r; +} + +function V(d) { + let r = ''; + for (let m = 0; m < 32 * d.length; m += 8) r += String.fromCharCode((d[m >> 5] >>> m % 32) & 255); + return r; +} + +function Y(d, g) { + (d[g >> 5] |= 128 << g % 32), (d[14 + (((g + 64) >>> 9) << 4)] = g); + let m = 1732584193, + f = -271733879, + r = -1732584194, + i = 271733878; + for (let n = 0; n < d.length; n += 16) { + let h = m, + t = f, + g = r, + e = i; + (f = ii( + (f = ii( + (f = ii( + (f = ii( + (f = hh( + (f = hh( + (f = hh( + (f = hh( + (f = gg( + (f = gg( + (f = gg( + (f = gg( + (f = ff( + (f = ff( + (f = ff( + (f = ff( + f, + (r = ff( + r, + (i = ff( + i, + (m = ff(m, f, r, i, d[n + 0], 7, -680876936)), + f, + r, + d[n + 1], + 12, + -389564586 + )), + m, + f, + d[n + 2], + 17, + 606105819 + )), + i, + m, + d[n + 3], + 22, + -1044525330 + )), + (r = ff( + r, + (i = ff( + i, + (m = ff(m, f, r, i, d[n + 4], 7, -176418897)), + f, + r, + d[n + 5], + 12, + 1200080426 + )), + m, + f, + d[n + 6], + 17, + -1473231341 + )), + i, + m, + d[n + 7], + 22, + -45705983 + )), + (r = ff( + r, + (i = ff( + i, + (m = ff(m, f, r, i, d[n + 8], 7, 1770035416)), + f, + r, + d[n + 9], + 12, + -1958414417 + )), + m, + f, + d[n + 10], + 17, + -42063 + )), + i, + m, + d[n + 11], + 22, + -1990404162 + )), + (r = ff( + r, + (i = ff( + i, + (m = ff(m, f, r, i, d[n + 12], 7, 1804603682)), + f, + r, + d[n + 13], + 12, + -40341101 + )), + m, + f, + d[n + 14], + 17, + -1502002290 + )), + i, + m, + d[n + 15], + 22, + 1236535329 + )), + (r = gg( + r, + (i = gg( + i, + (m = gg(m, f, r, i, d[n + 1], 5, -165796510)), + f, + r, + d[n + 6], + 9, + -1069501632 + )), + m, + f, + d[n + 11], + 14, + 643717713 + )), + i, + m, + d[n + 0], + 20, + -373897302 + )), + (r = gg( + r, + (i = gg(i, (m = gg(m, f, r, i, d[n + 5], 5, -701558691)), f, r, d[n + 10], 9, 38016083)), + m, + f, + d[n + 15], + 14, + -660478335 + )), + i, + m, + d[n + 4], + 20, + -405537848 + )), + (r = gg( + r, + (i = gg(i, (m = gg(m, f, r, i, d[n + 9], 5, 568446438)), f, r, d[n + 14], 9, -1019803690)), + m, + f, + d[n + 3], + 14, + -187363961 + )), + i, + m, + d[n + 8], + 20, + 1163531501 + )), + (r = gg( + r, + (i = gg(i, (m = gg(m, f, r, i, d[n + 13], 5, -1444681467)), f, r, d[n + 2], 9, -51403784)), + m, + f, + d[n + 7], + 14, + 1735328473 + )), + i, + m, + d[n + 12], + 20, + -1926607734 + )), + (r = hh( + r, + (i = hh(i, (m = hh(m, f, r, i, d[n + 5], 4, -378558)), f, r, d[n + 8], 11, -2022574463)), + m, + f, + d[n + 11], + 16, + 1839030562 + )), + i, + m, + d[n + 14], + 23, + -35309556 + )), + (r = hh( + r, + (i = hh(i, (m = hh(m, f, r, i, d[n + 1], 4, -1530992060)), f, r, d[n + 4], 11, 1272893353)), + m, + f, + d[n + 7], + 16, + -155497632 + )), + i, + m, + d[n + 10], + 23, + -1094730640 + )), + (r = hh( + r, + (i = hh(i, (m = hh(m, f, r, i, d[n + 13], 4, 681279174)), f, r, d[n + 0], 11, -358537222)), + m, + f, + d[n + 3], + 16, + -722521979 + )), + i, + m, + d[n + 6], + 23, + 76029189 + )), + (r = hh( + r, + (i = hh(i, (m = hh(m, f, r, i, d[n + 9], 4, -640364487)), f, r, d[n + 12], 11, -421815835)), + m, + f, + d[n + 15], + 16, + 530742520 + )), + i, + m, + d[n + 2], + 23, + -995338651 + )), + (r = ii( + r, + (i = ii(i, (m = ii(m, f, r, i, d[n + 0], 6, -198630844)), f, r, d[n + 7], 10, 1126891415)), + m, + f, + d[n + 14], + 15, + -1416354905 + )), + i, + m, + d[n + 5], + 21, + -57434055 + )), + (r = ii( + r, + (i = ii(i, (m = ii(m, f, r, i, d[n + 12], 6, 1700485571)), f, r, d[n + 3], 10, -1894986606)), + m, + f, + d[n + 10], + 15, + -1051523 + )), + i, + m, + d[n + 1], + 21, + -2054922799 + )), + (r = ii( + r, + (i = ii(i, (m = ii(m, f, r, i, d[n + 8], 6, 1873313359)), f, r, d[n + 15], 10, -30611744)), + m, + f, + d[n + 6], + 15, + -1560198380 + )), + i, + m, + d[n + 13], + 21, + 1309151649 + )), + (r = ii( + r, + (i = ii(i, (m = ii(m, f, r, i, d[n + 4], 6, -145523070)), f, r, d[n + 11], 10, -1120210379)), + m, + f, + d[n + 2], + 15, + 718787259 + )), + i, + m, + d[n + 9], + 21, + -343485551 + )), + (m = add(m, h)), + (f = add(f, t)), + (r = add(r, g)), + (i = add(i, e)); + } + return Array(m, f, r, i); +} + +function cmn(d, g, m, f, r, i) { + return add(rol(add(add(g, d), add(f, i)), r), m); +} + +function ff(d, g, m, f, r, i, n) { + return cmn((g & m) | (~g & f), d, g, r, i, n); +} + +function gg(d, g, m, f, r, i, n) { + return cmn((g & f) | (m & ~f), d, g, r, i, n); +} + +function hh(d, g, m, f, r, i, n) { + return cmn(g ^ m ^ f, d, g, r, i, n); +} + +function ii(d, g, m, f, r, i, n) { + return cmn(m ^ (g | ~f), d, g, r, i, n); +} + +function add(d, g) { + let m = (65535 & d) + (65535 & g); + return (((d >> 16) + (g >> 16) + (m >> 16)) << 16) | (65535 & m); +} + +function rol(d, g) { + return (d << g) | (d >>> (32 - g)); +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/strings.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/strings.js new file mode 100644 index 000000000..575c51f94 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/util/strings.js @@ -0,0 +1,28 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +// Decode from UTF-8 +export function decodeUTF8(utf8string, allowLatin1 = false) { + try { + return decodeURIComponent(escape(utf8string)); + } catch (e) { + if (e instanceof URIError) { + if (allowLatin1) { + // If we allow Latin1 we can ignore any decoding fails + // and in these cases return the original string + return utf8string; + } + } + throw e; + } +} + +// Encode to UTF-8 +export function encodeUTF8(DOMString) { + return unescape(encodeURIComponent(DOMString)); +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/core/websock.js b/gns3server/static/web-ui/assets/vnc-console/novnc/core/websock.js new file mode 100644 index 000000000..5253de5e2 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/core/websock.js @@ -0,0 +1,351 @@ +/* + * Websock: high-performance buffering wrapper + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * Websock is similar to the standard WebSocket / RTCDataChannel object + * but with extra buffer handling. + * + * Websock has built-in receive queue buffering; the message event + * does not contain actual data but is simply a notification that + * there is new data available. Several rQ* methods are available to + * read binary data off of the receive queue. + */ + +import * as Log from './util/logging.js'; + +// this has performance issues in some versions Chromium, and +// doesn't gain a tremendous amount of performance increase in Firefox +// at the moment. It may be valuable to turn it on in the future. +const MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB + +// Constants pulled from RTCDataChannelState enum +// https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/readyState#RTCDataChannelState_enum +const DataChannel = { + CONNECTING: 'connecting', + OPEN: 'open', + CLOSING: 'closing', + CLOSED: 'closed', +}; + +const ReadyStates = { + CONNECTING: [WebSocket.CONNECTING, DataChannel.CONNECTING], + OPEN: [WebSocket.OPEN, DataChannel.OPEN], + CLOSING: [WebSocket.CLOSING, DataChannel.CLOSING], + CLOSED: [WebSocket.CLOSED, DataChannel.CLOSED], +}; + +// Properties a raw channel must have, WebSocket and RTCDataChannel are two examples +const rawChannelProps = ['send', 'close', 'binaryType', 'onerror', 'onmessage', 'onopen', 'protocol', 'readyState']; + +export default class Websock { + constructor() { + this._websocket = null; // WebSocket or RTCDataChannel object + + this._rQi = 0; // Receive queue index + this._rQlen = 0; // Next write position in the receive queue + this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB) + // called in init: this._rQ = new Uint8Array(this._rQbufferSize); + this._rQ = null; // Receive queue + + this._sQbufferSize = 1024 * 10; // 10 KiB + // called in init: this._sQ = new Uint8Array(this._sQbufferSize); + this._sQlen = 0; + this._sQ = null; // Send queue + + this._eventHandlers = { + message: () => {}, + open: () => {}, + close: () => {}, + error: () => {}, + }; + } + + // Getters and Setters + + get readyState() { + let subState; + + if (this._websocket === null) { + return 'unused'; + } + + subState = this._websocket.readyState; + + if (ReadyStates.CONNECTING.includes(subState)) { + return 'connecting'; + } else if (ReadyStates.OPEN.includes(subState)) { + return 'open'; + } else if (ReadyStates.CLOSING.includes(subState)) { + return 'closing'; + } else if (ReadyStates.CLOSED.includes(subState)) { + return 'closed'; + } + + return 'unknown'; + } + + get sQ() { + return this._sQ; + } + + get rQ() { + return this._rQ; + } + + get rQi() { + return this._rQi; + } + + set rQi(val) { + this._rQi = val; + } + + // Receive Queue + get rQlen() { + return this._rQlen - this._rQi; + } + + rQpeek8() { + return this._rQ[this._rQi]; + } + + rQskipBytes(bytes) { + this._rQi += bytes; + } + + rQshift8() { + return this._rQshift(1); + } + + rQshift16() { + return this._rQshift(2); + } + + rQshift32() { + return this._rQshift(4); + } + + // TODO(directxman12): test performance with these vs a DataView + _rQshift(bytes) { + let res = 0; + for (let byte = bytes - 1; byte >= 0; byte--) { + res += this._rQ[this._rQi++] << (byte * 8); + } + return res; + } + + rQshiftStr(len) { + if (typeof len === 'undefined') { + len = this.rQlen; + } + let str = ''; + // Handle large arrays in steps to avoid long strings on the stack + for (let i = 0; i < len; i += 4096) { + let part = this.rQshiftBytes(Math.min(4096, len - i)); + str += String.fromCharCode.apply(null, part); + } + return str; + } + + rQshiftBytes(len) { + if (typeof len === 'undefined') { + len = this.rQlen; + } + this._rQi += len; + return new Uint8Array(this._rQ.buffer, this._rQi - len, len); + } + + rQshiftTo(target, len) { + if (len === undefined) { + len = this.rQlen; + } + // TODO: make this just use set with views when using a ArrayBuffer to store the rQ + target.set(new Uint8Array(this._rQ.buffer, this._rQi, len)); + this._rQi += len; + } + + rQslice(start, end = this.rQlen) { + return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); + } + + // Check to see if we must wait for 'num' bytes (default to FBU.bytes) + // to be available in the receive queue. Return true if we need to + // wait (and possibly print a debug message), otherwise false. + rQwait(msg, num, goback) { + if (this.rQlen < num) { + if (goback) { + if (this._rQi < goback) { + throw new Error('rQwait cannot backup ' + goback + ' bytes'); + } + this._rQi -= goback; + } + return true; // true means need more data + } + return false; + } + + // Send Queue + + flush() { + if (this._sQlen > 0 && this.readyState === 'open') { + this._websocket.send(this._encodeMessage()); + this._sQlen = 0; + } + } + + send(arr) { + this._sQ.set(arr, this._sQlen); + this._sQlen += arr.length; + this.flush(); + } + + sendString(str) { + this.send(str.split('').map((chr) => chr.charCodeAt(0))); + } + + // Event Handlers + off(evt) { + this._eventHandlers[evt] = () => {}; + } + + on(evt, handler) { + this._eventHandlers[evt] = handler; + } + + _allocateBuffers() { + this._rQ = new Uint8Array(this._rQbufferSize); + this._sQ = new Uint8Array(this._sQbufferSize); + } + + init() { + this._allocateBuffers(); + this._rQi = 0; + this._websocket = null; + } + + open(uri, protocols) { + this.attach(new WebSocket(uri, protocols)); + } + + attach(rawChannel) { + this.init(); + + // Must get object and class methods to be compatible with the tests. + const channelProps = [...Object.keys(rawChannel), ...Object.getOwnPropertyNames(Object.getPrototypeOf(rawChannel))]; + for (let i = 0; i < rawChannelProps.length; i++) { + const prop = rawChannelProps[i]; + if (channelProps.indexOf(prop) < 0) { + throw new Error('Raw channel missing property: ' + prop); + } + } + + this._websocket = rawChannel; + this._websocket.binaryType = 'arraybuffer'; + this._websocket.onmessage = this._recvMessage.bind(this); + + this._websocket.onopen = () => { + Log.Debug('>> WebSock.onopen'); + if (this._websocket.protocol) { + Log.Info('Server choose sub-protocol: ' + this._websocket.protocol); + } + + this._eventHandlers.open(); + Log.Debug('<< WebSock.onopen'); + }; + + this._websocket.onclose = (e) => { + Log.Debug('>> WebSock.onclose'); + this._eventHandlers.close(e); + Log.Debug('<< WebSock.onclose'); + }; + + this._websocket.onerror = (e) => { + Log.Debug('>> WebSock.onerror: ' + e); + this._eventHandlers.error(e); + Log.Debug('<< WebSock.onerror: ' + e); + }; + } + + close() { + if (this._websocket) { + if (this.readyState === 'connecting' || this.readyState === 'open') { + Log.Info('Closing WebSocket connection'); + this._websocket.close(); + } + + this._websocket.onmessage = () => {}; + } + } + + // private methods + _encodeMessage() { + // Put in a binary arraybuffer + // according to the spec, you can send ArrayBufferViews with the send method + return new Uint8Array(this._sQ.buffer, 0, this._sQlen); + } + + // We want to move all the unread data to the start of the queue, + // e.g. compacting. + // The function also expands the receive que if needed, and for + // performance reasons we combine these two actions to avoid + // unneccessary copying. + _expandCompactRQ(minFit) { + // if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place + // instead of resizing + const requiredBufferSize = (this._rQlen - this._rQi + minFit) * 8; + const resizeNeeded = this._rQbufferSize < requiredBufferSize; + + if (resizeNeeded) { + // Make sure we always *at least* double the buffer size, and have at least space for 8x + // the current amount of data + this._rQbufferSize = Math.max(this._rQbufferSize * 2, requiredBufferSize); + } + + // we don't want to grow unboundedly + if (this._rQbufferSize > MAX_RQ_GROW_SIZE) { + this._rQbufferSize = MAX_RQ_GROW_SIZE; + if (this._rQbufferSize - this.rQlen < minFit) { + throw new Error( + 'Receive Queue buffer exceeded ' + MAX_RQ_GROW_SIZE + ' bytes, and the new message could not fit' + ); + } + } + + if (resizeNeeded) { + const oldRQbuffer = this._rQ.buffer; + this._rQ = new Uint8Array(this._rQbufferSize); + this._rQ.set(new Uint8Array(oldRQbuffer, this._rQi, this._rQlen - this._rQi)); + } else { + this._rQ.copyWithin(0, this._rQi, this._rQlen); + } + + this._rQlen = this._rQlen - this._rQi; + this._rQi = 0; + } + + // push arraybuffer values onto the end of the receive que + _DecodeMessage(data) { + const u8 = new Uint8Array(data); + if (u8.length > this._rQbufferSize - this._rQlen) { + this._expandCompactRQ(u8.length); + } + this._rQ.set(u8, this._rQlen); + this._rQlen += u8.length; + } + + _recvMessage(e) { + this._DecodeMessage(e.data); + if (this.rQlen > 0) { + this._eventHandlers.message(); + if (this._rQlen == this._rQi) { + // All data has now been processed, this means we + // can reset the receive queue. + this._rQlen = 0; + this._rQi = 0; + } + } else { + Log.Debug('Ignoring empty message'); + } + } +} diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/vendor/pako/LICENSE b/gns3server/static/web-ui/assets/vnc-console/novnc/vendor/pako/LICENSE new file mode 100644 index 000000000..d082ae325 --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/vendor/pako/LICENSE @@ -0,0 +1,21 @@ +(The MIT License) + +Copyright (C) 2014-2016 by Vitaly Puzrin + +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: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. diff --git a/gns3server/static/web-ui/assets/vnc-console/novnc/vendor/pako/README.md b/gns3server/static/web-ui/assets/vnc-console/novnc/vendor/pako/README.md new file mode 100644 index 000000000..755df643d --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/novnc/vendor/pako/README.md @@ -0,0 +1,6 @@ +This is an ES6-modules-compatible version of +https://github.com/nodeca/pako, based on pako version 1.0.3. + +It's more-or-less a direct translation of the original, with unused parts +removed, and the dynamic support for non-typed arrays removed (since ES6 +modules don't work well with dynamic exports). diff --git a/gns3server/static/web-ui/assets/vnc-console/vnc-controller.js b/gns3server/static/web-ui/assets/vnc-console/vnc-controller.js new file mode 100644 index 000000000..e976fa55d --- /dev/null +++ b/gns3server/static/web-ui/assets/vnc-console/vnc-controller.js @@ -0,0 +1,1767 @@ +import RFB from './novnc/core/rfb.js'; + +(function () { + 'use strict'; + + // Parse URL parameters + const params = new URLSearchParams(window.location.search); + const wsUrl = params.get('ws_url'); + const nodeName = params.get('node_name') || 'VNC'; + const autoconnect = params.get('autoconnect') !== '0'; + + // DOM elements + const container = document.getElementById('vnc-container'); + const statusBar = document.getElementById('status-bar'); + const errorDialog = document.getElementById('error-dialog'); + const errorTitle = document.getElementById('error-title'); + const errorMessage = document.getElementById('error-message'); + const loadingIndicator = document.getElementById('loading'); + const toolbar = document.getElementById('vnc-toolbar'); + const toolbarToggle = document.getElementById('toolbar-toggle'); + const connectionStatus = document.getElementById('connection-status'); + const clipboardModal = document.getElementById('clipboard-modal'); + const modalOverlay = document.getElementById('modal-overlay'); + const clipboardText = document.getElementById('clipboard-text'); + + // State + let rfb = null; + let connectionTimeout = null; + let scale = 1.0; + let isConnected = false; + let toolbarVisible = false; + let mediaRecorder = null; + let recordedChunks = []; + let isRecording = false; + let isPaused = false; + let recordingStartTime = null; + let pausedStartTime = null; + let totalPausedTime = 0; + let recordingTimer = null; + let recordingAnimationFrame = null; + let canvasOverlay = null; + let overlayContext = null; + + // Click effect for recording + let clickEffects = []; // Array of {x, y, startTime} + let currentMousePos = null; // Current mouse position for cursor rendering + let drawAnimationFrame = null; // Animation frame ID for continuous drawing + let audioStream = null; // Audio stream for microphone + let cameraStream = null; // Camera stream for video recording + let cameraVideo = null; // Video element for camera preview + let recordingMode = 'vnc'; // Current recording mode: 'vnc', 'vnc-camera', 'camera' + let isMuted = false; // Microphone muted state + + // Debug mode - set to false in production + const DEBUG = false; + + // Logging utility + function log(message, level = 'info') { + if (!DEBUG && level !== 'error') return; // Only log errors in production + + const prefix = '[VNC Console]'; + const timestamp = new Date().toISOString(); + const logMessage = `${timestamp} ${prefix} ${level.toUpperCase()}: ${message}`; + + if (level === 'error') { + console.error(logMessage); + } else if (level === 'warn') { + console.warn(logMessage); + } else { + console.log(logMessage); + } + } + + // Update status display + function updateStatus(message, type = 'info') { + log(message, type); + statusBar.textContent = message; + statusBar.className = type; + statusBar.style.opacity = '1'; + + // Auto-hide success messages after 3 seconds + if (type === 'success') { + setTimeout(() => { + statusBar.style.opacity = '0'; + }, 3000); + } + } + + // Show error dialog + function showError(title, message) { + log(`${title}: ${message}`, 'error'); + + errorTitle.textContent = title; + errorMessage.textContent = message; + errorDialog.style.display = 'block'; + statusBar.style.display = 'none'; + + if (loadingIndicator) { + loadingIndicator.style.display = 'none'; + } + } + + // Hide loading indicator + function hideLoading() { + if (loadingIndicator) { + loadingIndicator.style.display = 'none'; + } + } + + // Validate required parameters + if (!wsUrl) { + showError( + 'Missing Parameter', + 'WebSocket URL (ws_url) is required. Please close this window and try opening the console again.' + ); + return; + } + + // Set page title + document.title = `VNC Console - ${nodeName}`; + log(`Opening VNC console for node: ${nodeName}`); + + // ===== Toolbar Functions ===== + + // Attach RFB event listeners + function attachRfbEventListeners(rfbInstance) { + // Event: Connecting + rfbInstance.addEventListener('connecting', () => { + log('Connecting to VNC server...'); + updateStatus('Connecting...'); + updateConnectionStatus('Connecting...', '#ffa500'); + }); + + // Event: Connected + rfbInstance.addEventListener('connect', () => { + clearTimeout(connectionTimeout); + hideLoading(); + isConnected = true; + updateStatus('Connected', 'success'); + updateConnectionStatus('Connected', '#4caf50'); + log('Successfully connected to VNC server'); + + // Force initial scale + setTimeout(() => { + if (rfbInstance) { + rfbInstance.scaleViewport = true; + if (typeof rfbInstance.sendScaleConfig === 'function') { + rfbInstance.sendScaleConfig(); + log('VNC display scaled to fit viewport'); + } + } + }, 500); + }); + + // Event: Disconnect + rfbInstance.addEventListener('disconnect', (e) => { + clearTimeout(connectionTimeout); + hideLoading(); + isConnected = false; + + // Note: Do NOT stop recording on disconnect + // Recording continues locally, even if VNC connection is lost + // This allows users to get a complete video with possible black screen periods + + const clean = e.detail && e.detail.clean; + + if (clean) { + // Normal disconnect + log('Disconnected from VNC server (clean)'); + updateStatus('Disconnected', 'warning'); + updateConnectionStatus('Disconnected', '#ff9800'); + } else { + // Abnormal disconnect + const reason = e.detail ? e.detail.reason : 'Unknown error'; + log(`Connection lost: ${reason}`, 'error'); + updateConnectionStatus('Disconnected', '#f44336'); + showError( + 'Connection Lost', + `The VNC connection was closed unexpectedly.\n\nReason: ${reason}\n\n` + + 'Please check if the node is still running and try again.' + ); + } + }); + + // Event: Credentials required + rfbInstance.addEventListener('credentialsrequired', () => { + log('VNC server requires authentication'); + const password = prompt('Enter VNC password:'); + + if (password) { + log('Sending VNC credentials'); + rfbInstance.sendCredentials({ password: password }); + } else { + showError( + 'Authentication Required', + 'This VNC server requires a password. Please close this window and try again with the correct credentials.' + ); + rfbInstance.disconnect(); + } + }); + + // Event: Security failure + rfbInstance.addEventListener('securityfailure', () => { + clearTimeout(connectionTimeout); + log('Security negotiation failed', 'error'); + showError( + 'Security Error', + 'Security negotiation with the VNC server failed. This could be due to:\n\n' + + '• Unsupported security type\n' + + '• TLS/SSL configuration mismatch\n' + + '• Protocol version incompatibility\n\n' + + 'Please check the server configuration and try again.' + ); + }); + + // Event: Clipboard + rfbInstance.addEventListener('clipboard', (e) => { + if (e.detail && e.detail.text) { + log('Clipboard data received from server'); + // Clipboard handling could be added here if needed + } + }); + + log('RFB event listeners attached'); + } + + // Toggle toolbar visibility + function toggleToolbar() { + toolbarVisible = !toolbarVisible; + if (toolbarVisible) { + toolbar.classList.remove('hidden'); + toolbarToggle.classList.add('active'); + container.classList.add('with-toolbar'); + } else { + toolbar.classList.add('hidden'); + toolbarToggle.classList.remove('active'); + container.classList.remove('with-toolbar'); + } + updateContainerPadding(); + log(`Toolbar ${toolbarVisible ? 'shown' : 'hidden'}`); + } + + // Update container padding based on toolbar and recording state + function updateContainerPadding() { + // Remove all padding classes first + container.classList.remove('with-toolbar', 'with-recording'); + + // Add toolbar padding if visible + if (toolbarVisible) { + container.classList.add('with-toolbar'); + } + + // Add recording padding if recording + if (isRecording) { + container.classList.add('with-recording'); + } + } + + // Create recording overlay canvas + function createRecordingOverlay() { + // Get VNC canvas + let vncCanvas = null; + if (typeof rfb.get_canvas === 'function') { + vncCanvas = rfb.get_canvas(); + } else if (rfb.canvas) { + vncCanvas = rfb.canvas; + } else { + vncCanvas = container.querySelector('canvas'); + } + + if (!vncCanvas) { + log('Cannot find VNC canvas for overlay', 'error'); + return false; + } + + // Create overlay canvas + canvasOverlay = document.createElement('canvas'); + canvasOverlay.style.position = 'absolute'; + canvasOverlay.style.top = '0'; + canvasOverlay.style.left = '0'; + canvasOverlay.style.pointerEvents = 'none'; // Let clicks pass through + canvasOverlay.style.zIndex = '100'; + canvasOverlay.width = vncCanvas.width; + canvasOverlay.height = vncCanvas.height; + + // Add to container (before the loading element) + container.insertBefore(canvasOverlay, container.firstChild); + + overlayContext = canvasOverlay.getContext('2d'); + log('Recording overlay created'); + + return true; + } + + // Destroy recording overlay canvas + function destroyRecordingOverlay() { + if (canvasOverlay && canvasOverlay.parentNode) { + canvasOverlay.parentNode.removeChild(canvasOverlay); + canvasOverlay = null; + overlayContext = null; + log('Recording overlay destroyed'); + } + } + + // Draw recording timestamp on overlay + function drawRecordingTimestamp() { + if (!overlayContext || !canvasOverlay || !recordingStartTime) { + return; + } + + // Clear overlay + overlayContext.clearRect(0, 0, canvasOverlay.width, canvasOverlay.height); + + // Calculate elapsed time + let elapsed = Date.now() - recordingStartTime - totalPausedTime; + if (isPaused && pausedStartTime) { + elapsed -= Date.now() - pausedStartTime; + } + + const seconds = Math.floor(elapsed / 1000); + const minutes = Math.floor(seconds / 60) + .toString() + .padStart(2, '0'); + const secs = (seconds % 60).toString().padStart(2, '0'); + const timestamp = `${minutes}:${secs}`; + + // Draw timestamp text at top center (no background) + overlayContext.font = 'bold 32px monospace'; + overlayContext.fillStyle = isPaused ? '#ff9800' : '#f44336'; + overlayContext.textAlign = 'center'; + overlayContext.textBaseline = 'top'; + overlayContext.textShadow = '2px 2px 4px rgba(0, 0, 0, 0.8)'; + overlayContext.fillText(`⏺ ${timestamp}`, canvasOverlay.width / 2, 20); + + // Request next frame + if (isRecording) { + recordingAnimationFrame = requestAnimationFrame(drawRecordingTimestamp); + } + } + + // Start recording overlay animation + function startRecordingOverlay() { + if (createRecordingOverlay()) { + drawRecordingTimestamp(); + } + } + + // Stop recording overlay animation + function stopRecordingOverlay() { + if (recordingAnimationFrame) { + cancelAnimationFrame(recordingAnimationFrame); + recordingAnimationFrame = null; + } + destroyRecordingOverlay(); + } + + // Toggle fullscreen + function toggleFullscreen() { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen().catch((err) => { + log(`Failed to enter fullscreen: ${err.message}`, 'error'); + }); + } else { + document.exitFullscreen(); + } + } + + // Send Ctrl+Alt+Del + function sendCtrlAltDel() { + if (rfb && isConnected) { + log('Sending Ctrl+Alt+Del'); + rfb.sendCtrlAltDel(); + } + } + + // Send Ctrl+Alt+Backspace + function sendCtrlAltBackspace() { + if (rfb && isConnected) { + log('Sending Ctrl+Alt+Backspace'); + // Send key sequence: Ctrl down, Alt down, Backspace down/up, Alt up, Ctrl up + rfb.sendKey(0xffe3, 'ControlLeft', true); // Ctrl down + rfb.sendKey(0xffe9, 'AltLeft', true); // Alt down + rfb.sendKey(0xff08, 'BackSpace', true); // Backspace down + rfb.sendKey(0xff08, 'BackSpace', false); // Backspace up + rfb.sendKey(0xffe9, 'AltLeft', false); // Alt up + rfb.sendKey(0xffe3, 'ControlLeft', false); // Ctrl up + } + } + + // Send Tab + function sendTab() { + if (rfb && isConnected) { + log('Sending Tab'); + rfb.sendKey(0xff09, 'Tab'); + } + } + + // Send Escape + function sendEsc() { + if (rfb && isConnected) { + log('Sending Esc'); + rfb.sendKey(0xff1b, 'Escape'); + } + } + + // Get selected modifiers + function getSelectedModifiers() { + const modifiers = []; + const ctrlAltCheckbox = document.getElementById('mod-ctrl-alt'); + + // Special case: Ctrl+Alt checkbox overrides individual ones + if (ctrlAltCheckbox && ctrlAltCheckbox.checked) { + return ['ctrl', 'alt']; + } + + if (document.getElementById('mod-ctrl').checked) { + modifiers.push('ctrl'); + } + if (document.getElementById('mod-alt').checked) { + modifiers.push('alt'); + } + if (document.getElementById('mod-shift').checked) { + modifiers.push('shift'); + } + + return modifiers; + } + + // Send key combination with modifiers + function sendKeyCombination(fNum) { + if (rfb && isConnected) { + const modifiers = getSelectedModifiers(); + const keysym = 0xffbe + fNum - 1; // F1-F12 keysyms + + // Build description + const modDesc = modifiers.length > 0 ? modifiers.join('+').toUpperCase() + '+' : ''; + log(`Sending ${modDesc}F${fNum}`); + + // Send modifiers (key down) + const modifierKeys = { + ctrl: { code: 0xffe3, name: 'ControlLeft' }, + alt: { code: 0xffe9, name: 'AltLeft' }, + shift: { code: 0xffe1, name: 'ShiftLeft' }, + }; + + modifiers.forEach((mod) => { + rfb.sendKey(modifierKeys[mod].code, modifierKeys[mod].name, true); + }); + + // Send function key + rfb.sendKey(keysym, `F${fNum}`, true); + rfb.sendKey(keysym, `F${fNum}`, false); + + // Release modifiers (key up) + modifiers.reverse().forEach((mod) => { + rfb.sendKey(modifierKeys[mod].code, modifierKeys[mod].name, false); + }); + } + } + + // Send Print Screen + function sendPrintScreen() { + if (rfb && isConnected) { + log('Sending Print Screen'); + rfb.sendKey(0xff15, 'Print'); + } + } + + // Toggle microphone mute + function toggleMute() { + isMuted = !isMuted; + + const muteBtn = document.getElementById('btn-mute'); + if (muteBtn) { + muteBtn.textContent = isMuted ? '🔇 Muted' : '🔊 Mic'; + muteBtn.classList.toggle('recording', isMuted); + } + + // If recording is active, toggle audio track + if (audioStream) { + audioStream.getAudioTracks().forEach((track) => { + track.enabled = !isMuted; + }); + log(isMuted ? 'Microphone muted' : 'Microphone unmuted'); + } else { + log(isMuted ? 'Will mute microphone when recording starts' : 'Microphone will be enabled when recording'); + } + } + + // Take screenshot and download + function takeScreenshot() { + if (!rfb || !isConnected) { + log('Cannot take screenshot: not connected', 'error'); + return; + } + + try { + // Get the VNC canvas - try different methods + let canvas = null; + + // Method 1: Try rfb.get_canvas() + if (typeof rfb.get_canvas === 'function') { + canvas = rfb.get_canvas(); + } + + // Method 2: Try rfb.canvas + if (!canvas && rfb.canvas) { + canvas = rfb.canvas; + } + + // Method 3: Query canvas from container + if (!canvas) { + canvas = container.querySelector('canvas'); + } + + if (!canvas) { + log('Cannot get VNC canvas - tried all methods', 'error'); + return; + } + + log(`Got canvas: ${canvas.width}x${canvas.height}`); + + // Convert canvas to data URL first (more compatible) + const dataUrl = canvas.toDataURL('image/png'); + + // Create download link + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const filename = `vnc-screenshot-${timestamp}.png`; + + const a = document.createElement('a'); + a.href = dataUrl; + a.download = filename; + a.style.display = 'none'; + document.body.appendChild(a); + + // Trigger download + a.click(); + + // Cleanup + setTimeout(() => { + document.body.removeChild(a); + log(`Screenshot saved: ${filename}`); + updateStatus('Screenshot saved', 'success'); + }, 100); + } catch (error) { + log(`Screenshot error: ${error.message}`, 'error'); + log(error.stack, 'error'); + } + } + + // Toggle recording (start/pause/resume) + function toggleRecording() { + if (!rfb || !isConnected) { + log('Cannot record: not connected', 'error'); + return; + } + + if (!isRecording) { + // Start new recording + startRecording(); + } else if (!isPaused) { + // Pause recording + pauseRecording(); + } else { + // Resume recording + resumeRecording(); + } + } + + // Start recording + async function startRecording() { + try { + // Get recording mode from select + const modeSelect = document.getElementById('record-mode'); + recordingMode = modeSelect ? modeSelect.value : 'vnc'; + log(`Starting recording in mode: ${recordingMode}`); + + let vncCanvas = null; + let recordingCanvas, recordingCtx; + + // Create camera video element for camera modes + if (recordingMode === 'vnc-camera' || recordingMode === 'camera') { + try { + cameraStream = await navigator.mediaDevices.getUserMedia({ + video: { width: 320, height: 240 }, + audio: false, + }); + + // Use existing preview video element from DOM + const previewVideo = document.getElementById('camera-preview'); + if (previewVideo) { + previewVideo.srcObject = cameraStream; + previewVideo.style.display = 'inline-block'; + cameraVideo = previewVideo; + } else { + // Create video element for camera if preview not found + cameraVideo = document.createElement('video'); + cameraVideo.srcObject = cameraStream; + cameraVideo.muted = true; + cameraVideo.autoplay = true; + } + await cameraVideo.play(); + log('Camera initialized for recording'); + } catch (err) { + log(`Camera not available: ${err.message}`, 'warn'); + // Fall back to VNC only mode + recordingMode = 'vnc'; + cameraStream = null; + cameraVideo = null; + } + } + + if (recordingMode === 'vnc' || recordingMode === 'vnc-camera') { + // Get the VNC canvas + if (typeof rfb.get_canvas === 'function') { + vncCanvas = rfb.get_canvas(); + } else if (rfb.canvas) { + vncCanvas = rfb.canvas; + } else { + vncCanvas = container.querySelector('canvas'); + } + + if (!vncCanvas) { + log('Cannot find VNC canvas for recording', 'error'); + updateStatus('Recording failed: Cannot find VNC canvas', 'error'); + return; + } + + log(`Got canvas: ${vncCanvas.width}x${vncCanvas.height}`); + + // Create a recording canvas + recordingCanvas = document.createElement('canvas'); + recordingCanvas.width = vncCanvas.width; + recordingCanvas.height = vncCanvas.height; + recordingCtx = recordingCanvas.getContext('2d', { willReadFrequently: true }); + } else { + // Pure camera mode - create canvas based on camera resolution + const camWidth = cameraVideo.videoWidth || 640; + const camHeight = cameraVideo.videoHeight || 480; + + recordingCanvas = document.createElement('canvas'); + recordingCanvas.width = camWidth; + recordingCanvas.height = camHeight; + recordingCtx = recordingCanvas.getContext('2d', { willReadFrequently: true }); + } + + // Capture stream from the recording canvas + const refreshRate = window.screen.refreshRate || 30; + log(`Display refresh rate: ${refreshRate}Hz, using ${Math.min(refreshRate, 60)}Hz for recording`); + + let videoStream; + if (recordingCanvas) { + videoStream = recordingCanvas.captureStream(Math.min(refreshRate, 60)); + } else { + // Pure camera mode - use camera stream directly + videoStream = cameraStream; + } + + // Get microphone audio + let combinedStream = videoStream; + try { + audioStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); + // Apply mute state + audioStream.getAudioTracks().forEach((track) => { + track.enabled = !isMuted; + combinedStream.addTrack(track); + }); + log(isMuted ? 'Microphone added but muted' : 'Microphone audio added to recording'); + } catch (err) { + log(`Microphone not available: ${err.message}`, 'warn'); + audioStream = null; + } + + // Create MediaRecorder + const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9') ? 'video/webm;codecs=vp9' : 'video/webm'; + + mediaRecorder = new MediaRecorder(combinedStream, { mimeType }); + recordedChunks = []; + + // Continuous draw function to ensure video has actual duration + // Handle camera mode switching during recording + async function handleCameraModeSwitch(newMode) { + // Only process if mode changed and recording is active + if (newMode === recordingMode || !isRecording) return; + + const oldMode = recordingMode; + recordingMode = newMode; + log(`Switching recording mode from ${oldMode} to ${newMode}`); + + // Show/hide camera preview based on mode + const previewVideo = document.getElementById('camera-preview'); + + // Need camera for vnc-camera or camera mode + if (newMode === 'vnc-camera' || newMode === 'camera') { + if (!cameraStream) { + try { + cameraStream = await navigator.mediaDevices.getUserMedia({ + video: { width: 320, height: 240 }, + audio: false, + }); + + // Use existing preview video element from DOM + if (previewVideo) { + previewVideo.srcObject = cameraStream; + previewVideo.style.display = 'inline-block'; + cameraVideo = previewVideo; + } else { + cameraVideo = document.createElement('video'); + cameraVideo.srcObject = cameraStream; + cameraVideo.muted = true; + cameraVideo.autoplay = true; + } + await cameraVideo.play(); + log('Camera initialized after mode switch'); + } catch (err) { + log(`Camera not available after mode switch: ${err.message}`, 'warn'); + recordingMode = 'vnc'; // Fall back to VNC only + // Show warning to user + const modeSelect = document.getElementById('record-mode'); + if (modeSelect) modeSelect.value = 'vnc'; + } + } else { + // Camera stream exists, just show preview + if (previewVideo) { + previewVideo.style.display = 'inline-block'; + cameraVideo = previewVideo; + } + } + } + + // If switching to VNC only, hide camera preview but keep stream + if (newMode === 'vnc' && cameraStream) { + if (previewVideo) { + previewVideo.style.display = 'none'; + } + log('Switched to VNC only mode, camera hidden but available'); + } + } + + function continuousDraw() { + // Always keep drawing, but only record when not paused + // This ensures the animation loop continues for when we resume + + // Dynamically get recording mode from select (allows switching during recording) + const modeSelect = document.getElementById('record-mode'); + const currentMode = modeSelect ? modeSelect.value : 'vnc'; + + // Handle camera mode switching during recording + if (isRecording) { + handleCameraModeSwitch(currentMode); + } + + if (currentMode === 'vnc-camera' && vncCanvas && recordingCtx) { + // Draw VNC canvas + recordingCtx.drawImage(vncCanvas, 0, 0); + + // Draw camera in corner (picture-in-picture) + if (cameraVideo) { + const pipWidth = 200; + const pipHeight = 150; + const pipX = recordingCanvas.width - pipWidth - 10; + const pipY = recordingCanvas.height - pipHeight - 10; + + // Draw semi-transparent background + recordingCtx.fillStyle = 'rgba(0, 0, 0, 0.3)'; + recordingCtx.fillRect(pipX - 2, pipY - 2, pipWidth + 4, pipHeight + 4); + + // Draw camera feed + recordingCtx.drawImage(cameraVideo, pipX, pipY, pipWidth, pipHeight); + } + } else if (currentMode === 'vnc' && vncCanvas && recordingCtx) { + // VNC only mode + recordingCtx.drawImage(vncCanvas, 0, 0); + } else if (currentMode === 'camera' && cameraVideo) { + // Camera only mode + recordingCtx.drawImage(cameraVideo, 0, 0, recordingCanvas.width, recordingCanvas.height); + } + + // Draw recording timestamp on the recording canvas + if (recordingStartTime) { + // Calculate elapsed time + let elapsed = Date.now() - recordingStartTime - totalPausedTime; + + const seconds = Math.floor(elapsed / 1000); + const minutes = Math.floor(seconds / 60) + .toString() + .padStart(2, '0'); + const secs = (seconds % 60).toString().padStart(2, '0'); + const timestamp = `${minutes}:${secs}`; + + // Draw timestamp at top center (no background) + recordingCtx.font = 'bold 32px monospace'; + recordingCtx.fillStyle = '#f44336'; + recordingCtx.textAlign = 'center'; + recordingCtx.textBaseline = 'top'; + recordingCtx.textShadow = '2px 2px 4px rgba(0, 0, 0, 0.8)'; + recordingCtx.fillText(`⏺ ${timestamp}`, recordingCanvas.width / 2, 20); + } + + // Draw GNS3 watermark at bottom right (artistic style) + recordingCtx.save(); + recordingCtx.font = 'bold italic 40px serif'; + recordingCtx.textAlign = 'right'; + recordingCtx.textBaseline = 'bottom'; + + // Add shadow for depth effect + recordingCtx.shadowColor = 'rgba(0, 0, 0, 0.5)'; + recordingCtx.shadowBlur = 4; + recordingCtx.shadowOffsetX = 2; + recordingCtx.shadowOffsetY = 2; + + // Draw with gradient-like effect using two overlapping texts + recordingCtx.fillStyle = 'rgba(255, 255, 255, 0.12)'; + recordingCtx.fillText('GNS3', recordingCanvas.width - 22, recordingCanvas.height - 22); + recordingCtx.fillStyle = 'rgba(255, 255, 255, 0.2)'; + recordingCtx.fillText('GNS3', recordingCanvas.width - 20, recordingCanvas.height - 20); + recordingCtx.restore(); + + // Steganography watermark - embed GNS3 in pixel data (for verification) + // Embed with multiple positions and redundancy for robustness + const frameCount = Math.floor(((Date.now() - recordingStartTime) / 1000) * 60); + if (frameCount % 30 === 0) { + // Every ~0.5 seconds + const watermarkText = 'GNS3'; + + // Multiple embedding positions for redundancy + const positions = [ + { x: recordingCanvas.width - 100, y: recordingCanvas.height - 40 }, + { x: recordingCanvas.width - 180, y: recordingCanvas.height - 30 }, + { x: recordingCanvas.width - 60, y: recordingCanvas.height - 60 }, + ]; + + positions.forEach((pos, posIdx) => { + // Get image data from watermark area + const imageData = recordingCtx.getImageData(pos.x, pos.y, 40, 30); + const data = imageData.data; + + // Embed watermark 3 times with offset for redundancy + for (let copy = 0; copy < 3; copy++) { + const offset = copy * 10; + + for (let i = 0; i < watermarkText.length; i++) { + const charCode = watermarkText.charCodeAt(i); + for (let bit = 0; bit < 8; bit++) { + const pixelIdx = ((i + offset) * 8 + bit) * 4; + if (pixelIdx < data.length) { + const bitValue = (charCode >> bit) & 1; + // Modify blue channel's 2nd bit (more robust than LSB) + data[pixelIdx + 2] = (data[pixelIdx + 2] & 0xfd) | (bitValue << 1); + } + } + } + } + + // Put modified data back + recordingCtx.putImageData(imageData, pos.x, pos.y); + }); + } + + // Draw mouse cursor + if (currentMousePos) { + recordingCtx.save(); + // Draw arrow cursor + recordingCtx.beginPath(); + recordingCtx.moveTo(currentMousePos.x, currentMousePos.y); + recordingCtx.lineTo(currentMousePos.x, currentMousePos.y + 18); + recordingCtx.lineTo(currentMousePos.x + 5, currentMousePos.y + 14); + recordingCtx.lineTo(currentMousePos.x + 10, currentMousePos.y + 14); + recordingCtx.closePath(); + recordingCtx.fillStyle = '#fff'; + recordingCtx.fill(); + recordingCtx.strokeStyle = '#000'; + recordingCtx.lineWidth = 1; + recordingCtx.stroke(); + recordingCtx.restore(); + } + + // Draw click effects (ripples) + const currentTime = Date.now(); + + clickEffects = clickEffects.filter((effect) => { + const age = currentTime - effect.startTime; + if (age > 600) { + // Remove effects older than 600ms + return false; + } + + // Calculate ripple animation + const progress = age / 600; // 0 to 1 + const maxRadius = 40; + const currentRadius = progress * maxRadius; + const alpha = 1 - progress; // Fade out + + // Save context state + recordingCtx.save(); + + // Draw expanding ripple + recordingCtx.beginPath(); + recordingCtx.arc(effect.x, effect.y, currentRadius, 0, Math.PI * 2); + recordingCtx.strokeStyle = `rgba(244, 67, 54, ${alpha})`; // Red + recordingCtx.lineWidth = 3; + recordingCtx.stroke(); + + // Draw outer ripple (slightly delayed) + if (progress > 0.2) { + const outerProgress = (progress - 0.2) / 0.8; + const outerRadius = outerProgress * maxRadius * 1.5; + const outerAlpha = (1 - outerProgress) * 0.5; + recordingCtx.beginPath(); + recordingCtx.arc(effect.x, effect.y, outerRadius, 0, Math.PI * 2); + recordingCtx.strokeStyle = `rgba(244, 67, 54, ${outerAlpha})`; + recordingCtx.lineWidth = 2; + recordingCtx.stroke(); + } + + // Draw center dot + recordingCtx.beginPath(); + recordingCtx.arc(effect.x, effect.y, 6, 0, Math.PI * 2); + recordingCtx.fillStyle = `rgba(244, 67, 54, ${alpha})`; + recordingCtx.fill(); + + // Restore context state + recordingCtx.restore(); + + return true; + }); + + // Request next frame to ensure continuous recording + drawAnimationFrame = requestAnimationFrame(continuousDraw); + } + + // Handle data available + mediaRecorder.ondataavailable = (event) => { + if (event.data && event.data.size > 0) { + recordedChunks.push(event.data); + } + }; + + // Handle recording stop + mediaRecorder.onstop = () => { + // Stop continuous drawing + if (drawAnimationFrame) { + cancelAnimationFrame(drawAnimationFrame); + drawAnimationFrame = null; + } + + // Clear click effects + clickEffects = []; + currentMousePos = null; + + // Stop and clear audio stream + if (audioStream) { + audioStream.getTracks().forEach((track) => track.stop()); + audioStream = null; + } + + // Stop and clear camera stream + if (cameraStream) { + cameraStream.getTracks().forEach((track) => track.stop()); + cameraStream = null; + } + cameraVideo = null; + + // Hide camera preview + const previewVideo = document.getElementById('camera-preview'); + if (previewVideo) { + previewVideo.style.display = 'none'; + previewVideo.srcObject = null; + } + + const blob = new Blob(recordedChunks, { type: 'video/webm' }); + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const filename = `vnc-recording-${timestamp}.webm`; + const url = URL.createObjectURL(blob); + + // Auto-download + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + + setTimeout(() => { + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, 100); + + log(`Recording saved: ${filename}`); + updateStatus('Recording saved', 'success'); + + // Update container padding after stop + updateContainerPadding(); + }; + + // Start recording + mediaRecorder.start(); + isRecording = true; + isPaused = false; + recordingStartTime = Date.now(); + totalPausedTime = 0; + + // Start continuous drawing + continuousDraw(); + + // Update container padding + updateContainerPadding(); + + // Update UI + updateRecordingUI(); + + // Reset time display + const timeDisplay = document.getElementById('recording-time'); + if (timeDisplay) { + timeDisplay.textContent = '00:00'; + log('Time display initialized'); + } else { + log('WARNING: recording-time element not found!', 'error'); + } + + // Start timer + updateRecordingTimer(); + recordingTimer = setInterval(updateRecordingTimer, 1000); + + log('Recording started (canvas capture mode)'); + } catch (error) { + log(`Recording error: ${error.message}`, 'error'); + log(error.stack, 'error'); + updateStatus('Recording failed: ' + error.message, 'error'); + } + } + + // Pause recording + function pauseRecording() { + if (mediaRecorder && isRecording && !isPaused) { + mediaRecorder.pause(); + isPaused = true; + pausedStartTime = Date.now(); + + // Note: We don't stop continuousDrawing anymore - it keeps running + // so it will be ready when we resume + + // Stop updating overlay (freeze the timestamp display) + if (recordingAnimationFrame) { + cancelAnimationFrame(recordingAnimationFrame); + recordingAnimationFrame = null; + } + + // Update UI + updateRecordingUI(); + + log('Recording paused'); + } + } + + // Resume recording + function resumeRecording() { + if (mediaRecorder && isRecording && isPaused) { + mediaRecorder.resume(); + isPaused = false; + + // Add paused time to total + if (pausedStartTime) { + totalPausedTime += Date.now() - pausedStartTime; + pausedStartTime = null; + } + + // Note: continuousDrawing is already running, no need to restart + + // Resume overlay animation + drawRecordingTimestamp(); + + // Update UI + updateRecordingUI(); + + log('Recording resumed'); + } + } + + // Stop recording + function stopRecording() { + if (mediaRecorder && isRecording) { + log('Stopping recording...'); + + // If paused, resume first to properly finalize + if (isPaused) { + log('Resuming paused recording before stopping...'); + mediaRecorder.resume(); + isPaused = false; + if (pausedStartTime) { + totalPausedTime += Date.now() - pausedStartTime; + pausedStartTime = null; + } + } + + // Stop the recorder + mediaRecorder.stop(); + isRecording = false; + isPaused = false; + + // Update UI to reset state + const recordBtn = document.getElementById('btn-record-pause'); + const stopBtn = document.getElementById('btn-record-stop'); + const indicator = document.getElementById('recording-indicator'); + + if (recordBtn) { + recordBtn.textContent = '⏺ Record'; + recordBtn.classList.remove('recording'); + recordBtn.style.display = 'inline-flex'; + } + + if (stopBtn) { + stopBtn.style.display = 'none'; + } + + if (indicator) { + indicator.classList.remove('show'); + indicator.style.background = 'rgba(244, 67, 54, 0.95)'; + } + + // Reset timer display + const timeDisplay = document.getElementById('recording-time'); + if (timeDisplay) { + timeDisplay.textContent = '00:00'; + } + + // Stop timer + if (recordingTimer) { + clearInterval(recordingTimer); + recordingTimer = null; + } + + // Reset state variables + recordingStartTime = null; + pausedStartTime = null; + totalPausedTime = 0; + + // Reset mute state + isMuted = false; + const muteBtn = document.getElementById('btn-mute'); + if (muteBtn) { + muteBtn.textContent = '🔊 Mic'; + muteBtn.classList.remove('recording'); + } + + // Stop recording overlay + stopRecordingOverlay(); + + // Update container padding (remove recording padding) + updateContainerPadding(); + + log('Recording stopped and UI reset'); + } + } + + // Update recording UI based on state + function updateRecordingUI() { + const recordBtn = document.getElementById('btn-record-pause'); + const stopBtn = document.getElementById('btn-record-stop'); + const indicator = document.getElementById('recording-indicator'); + + if (!recordBtn || !stopBtn || !indicator) { + log('Missing recording UI elements', 'warn'); + log(`recordBtn: ${!!recordBtn}, stopBtn: ${!!stopBtn}, indicator: ${!!indicator}`, 'warn'); + return; + } + + if (!isRecording) { + // Not recording + recordBtn.textContent = '⏺ Record'; + recordBtn.classList.remove('recording'); + recordBtn.style.display = 'inline-flex'; + stopBtn.style.display = 'none'; + indicator.classList.remove('show'); + indicator.style.background = 'rgba(244, 67, 54, 0.95)'; + log('UI: Not recording state'); + } else if (isPaused) { + // Paused + recordBtn.textContent = '▶ Resume'; + recordBtn.classList.add('recording'); + recordBtn.style.display = 'inline-flex'; + stopBtn.style.display = 'inline-flex'; + indicator.classList.add('show'); + indicator.style.background = 'rgba(255, 152, 0, 0.95)'; // Orange + log('UI: Paused state'); + } else { + // Recording + recordBtn.textContent = '⏸ Pause'; + recordBtn.classList.add('recording'); + recordBtn.style.display = 'inline-flex'; + stopBtn.style.display = 'inline-flex'; + indicator.classList.add('show'); + indicator.style.background = 'rgba(244, 67, 54, 0.95)'; // Red + log('UI: Recording state'); + } + + // Force browser reflow + if (indicator.classList.contains('show')) { + indicator.offsetHeight; // Trigger reflow + } + } + + // Update recording timer display + function updateRecordingTimer() { + if (!recordingStartTime) return; + + let elapsed = Date.now() - recordingStartTime - totalPausedTime; + + // If currently paused, don't count the paused time + if (isPaused && pausedStartTime) { + elapsed -= Date.now() - pausedStartTime; + } + + const seconds = Math.floor(elapsed / 1000); + const minutes = Math.floor(seconds / 60) + .toString() + .padStart(2, '0'); + const secs = (seconds % 60).toString().padStart(2, '0'); + + const timeDisplay = document.getElementById('recording-time'); + if (timeDisplay) { + timeDisplay.textContent = `${minutes}:${secs}`; + } + } + + // Toggle dropdown menu + function toggleDropdown(dropdownId, menuId) { + const dropdown = document.getElementById(dropdownId); + const menu = document.getElementById(menuId); + + if (dropdown && menu) { + const isOpen = menu.classList.contains('show'); + + // Close all other dropdowns first + document.querySelectorAll('.dropdown-menu').forEach((m) => { + m.classList.remove('show'); + }); + document.querySelectorAll('.dropdown').forEach((d) => { + d.classList.remove('active'); + }); + + // Toggle current dropdown + if (!isOpen) { + menu.classList.add('show'); + dropdown.classList.add('active'); + log(`Dropdown ${dropdownId} opened`); + } else { + log(`Dropdown ${dropdownId} closed`); + } + } + } + + // Close dropdown when clicking outside + function handleClickOutside(event) { + const dropdowns = document.querySelectorAll('.dropdown'); + dropdowns.forEach((dropdown) => { + const menu = dropdown.querySelector('.dropdown-menu'); + if (menu && menu.classList.contains('show')) { + if (!dropdown.contains(event.target)) { + menu.classList.remove('show'); + dropdown.classList.remove('active'); + log('Dropdown closed (clicked outside)'); + } + } + }); + } + + // Scale up + function scaleUp() { + if (rfb && isConnected) { + scale = Math.min(scale + 0.1, 3.0); + rfb.scaleViewport = false; + rfb.scale = scale; + if (typeof rfb.sendScaleConfig === 'function') { + rfb.sendScaleConfig(); + } + log(`Scale up to ${scale.toFixed(1)}x`); + } + } + + // Scale down + function scaleDown() { + if (rfb && isConnected) { + scale = Math.max(scale - 0.1, 0.3); + rfb.scaleViewport = false; + rfb.scale = scale; + if (typeof rfb.sendScaleConfig === 'function') { + rfb.sendScaleConfig(); + } + log(`Scale down to ${scale.toFixed(1)}x`); + } + } + + // Scale to fit + function scaleToFit() { + if (rfb && isConnected) { + scale = 1.0; + rfb.scaleViewport = true; + if (typeof rfb.sendScaleConfig === 'function') { + rfb.sendScaleConfig(); + } + log('Scale to fit viewport'); + } + } + + // Connect/Reconnect + function connect() { + if (rfb) { + // Check if already connected + if (rfb._rfb_connection_state === 'connected') { + log('Already connected'); + return; + } + + // If disconnected, we need to recreate the RFB object + log('Reconnecting...'); + + // Clean up old RFB object + try { + if (rfb._rfb_connection_state !== 'disconnected') { + rfb.disconnect(); + } + } catch (e) { + log(`Error during disconnect: ${e.message}`, 'warn'); + } + + // Remove old RAF (resize animation frame) if any + container.innerHTML = ''; + + // Create new RFB instance + rfb = new RFB(container, wsUrl, { + resizeSession: false, + scaleViewport: true, + }); + + // Re-attach all event listeners + attachRfbEventListeners(rfb); + + // Connect + rfb.connect(); + + log('RFB reconnection initiated'); + } else { + log('RFB object not initialized, cannot connect', 'error'); + } + } + + // Disconnect + function disconnect() { + if (rfb && isConnected) { + log('Disconnecting...'); + rfb.disconnect(); + isConnected = false; + updateConnectionStatus('Disconnected', '#ff9800'); + } + } + + // Update connection status display + function updateConnectionStatus(status, color = '#fff') { + connectionStatus.textContent = `● ${status}`; + connectionStatus.style.color = color; + } + + // Open clipboard modal + function openClipboard() { + clipboardModal.style.display = 'block'; + modalOverlay.style.display = 'block'; + clipboardText.value = ''; + clipboardText.focus(); + log('Clipboard modal opened'); + } + + // Close clipboard modal + function closeClipboard() { + clipboardModal.style.display = 'none'; + modalOverlay.style.display = 'none'; + log('Clipboard modal closed'); + } + + // Send clipboard text to remote + function sendClipboard() { + const text = clipboardText.value; + if (rfb && isConnected && text) { + rfb.clipboardPasteFrom(text); + log(`Sent clipboard text (${text.length} chars)`); + closeClipboard(); + updateStatus('Clipboard sent', 'success'); + } + } + + // Setup modifier checkbox interactions + function setupModifierCheckboxes() { + const ctrlAltCheckbox = document.getElementById('mod-ctrl-alt'); + const ctrlCheckbox = document.getElementById('mod-ctrl'); + const altCheckbox = document.getElementById('mod-alt'); + const shiftCheckbox = document.getElementById('mod-shift'); + + // Ctrl+Alt checkbox logic + if (ctrlAltCheckbox) { + ctrlAltCheckbox.addEventListener('change', () => { + if (ctrlAltCheckbox.checked) { + // Uncheck individual Ctrl and Alt + ctrlCheckbox.checked = false; + altCheckbox.checked = false; + updateCheckboxVisual(ctrlCheckbox); + updateCheckboxVisual(altCheckbox); + } + updateCheckboxVisual(ctrlAltCheckbox); + }); + } + + // Individual Ctrl checkbox + if (ctrlCheckbox) { + ctrlCheckbox.addEventListener('change', () => { + if (ctrlCheckbox.checked && ctrlAltCheckbox.checked) { + ctrlAltCheckbox.checked = false; + updateCheckboxVisual(ctrlAltCheckbox); + } + updateCheckboxVisual(ctrlCheckbox); + }); + } + + // Individual Alt checkbox + if (altCheckbox) { + altCheckbox.addEventListener('change', () => { + if (altCheckbox.checked && ctrlAltCheckbox.checked) { + ctrlAltCheckbox.checked = false; + updateCheckboxVisual(ctrlAltCheckbox); + } + updateCheckboxVisual(altCheckbox); + }); + } + + // Shift checkbox + if (shiftCheckbox) { + shiftCheckbox.addEventListener('change', () => { + updateCheckboxVisual(shiftCheckbox); + }); + } + + log('Modifier checkbox interactions setup'); + } + + // Update checkbox visual state + function updateCheckboxVisual(checkbox) { + const parent = checkbox.closest('.modifier-checkbox'); + if (checkbox.checked) { + parent.classList.add('active'); + } else { + parent.classList.remove('active'); + } + } + + // Setup toolbar event listeners + function setupToolbarListeners() { + // Toolbar toggle + toolbarToggle.addEventListener('click', toggleToolbar); + + // Connection controls + document.getElementById('btn-fullscreen').addEventListener('click', toggleFullscreen); + document.getElementById('btn-connect').addEventListener('click', connect); + document.getElementById('btn-disconnect').addEventListener('click', disconnect); + + // Dropdown toggle + document.getElementById('send-keys-toggle').addEventListener('click', (e) => { + e.stopPropagation(); + toggleDropdown('send-keys-dropdown', 'send-keys-menu'); + }); + + // Prevent dropdown from closing when clicking inside + document.getElementById('send-keys-menu').addEventListener('click', (e) => { + e.stopPropagation(); + }); + + // Setup modifier checkbox interactions + setupModifierCheckboxes(); + + // Function Keys (F1-F12) + for (let i = 1; i <= 12; i++) { + const btn = document.getElementById(`btn-f${i}`); + if (btn) { + btn.addEventListener('click', () => sendKeyCombination(i)); + } + } + + // Quick Actions + document.getElementById('btn-ctrlaltdel').addEventListener('click', sendCtrlAltDel); + document.getElementById('btn-ctrlaltbs').addEventListener('click', sendCtrlAltBackspace); + document.getElementById('btn-tab').addEventListener('click', sendTab); + document.getElementById('btn-esc').addEventListener('click', sendEsc); + document.getElementById('btn-print').addEventListener('click', sendPrintScreen); + + // Scale controls + document.getElementById('btn-scale-up').addEventListener('click', scaleUp); + document.getElementById('btn-scale-down').addEventListener('click', scaleDown); + document.getElementById('btn-scale-fit').addEventListener('click', scaleToFit); + + // Screen capture + document.getElementById('btn-screenshot').addEventListener('click', takeScreenshot); + document.getElementById('btn-mute').addEventListener('click', toggleMute); + document.getElementById('btn-record-pause').addEventListener('click', toggleRecording); + document.getElementById('btn-record-stop').addEventListener('click', stopRecording); + + // Clipboard + document.getElementById('btn-clipboard').addEventListener('click', openClipboard); + document.getElementById('btn-clipboard-send').addEventListener('click', sendClipboard); + document.getElementById('btn-clipboard-cancel').addEventListener('click', closeClipboard); + modalOverlay.addEventListener('click', closeClipboard); + + // Close dropdowns when clicking outside + document.addEventListener('click', handleClickOutside); + + log('Toolbar event listeners setup'); + } + + // Initialize toolbar + setupToolbarListeners(); + + // Connection timeout handler + if (autoconnect) { + connectionTimeout = setTimeout(() => { + if (rfb && rfb._rfb_connection_state !== 'connected') { + showError( + 'Connection Timeout', + 'Could not connect to the VNC server. Please verify:\n\n' + + '• The node is started\n' + + '• The console type is set to VNC\n' + + '• Your network connection is stable\n' + + '• No firewall is blocking the connection' + ); + if (rfb) { + rfb.disconnect(); + } + } + }, 15000); // 15 second timeout + } + + // Initialize noVNC + try { + log('Initializing noVNC RFB connection...'); + log(`WebSocket URL: ${wsUrl.replace(/ticket=[^&]+/, 'ticket=***')}`); // Hide token in logs + + // Create RFB instance + rfb = new RFB(container, wsUrl, { + resizeSession: false, // Don't resize the remote session + scaleViewport: true, // Scale the display to fit the container + // Additional noVNC options can be added here + }); + + // Attach event listeners using the extracted function + attachRfbEventListeners(rfb); + + // Focus management + container.addEventListener('click', () => { + if (rfb) { + rfb.focus(); + log('VNC canvas focused'); + } + }); + + // Mouse click tracking for recording effects + // Use document-level listener to capture all clicks, even if noVNC intercepts them + document.addEventListener( + 'mousedown', + (e) => { + log(`Document MouseDown detected: recording=${isRecording}, paused=${isPaused}`); + + if (!isRecording || isPaused) { + return; + } + + // Check if click is within VNC container + const containerRect = container.getBoundingClientRect(); + if ( + e.clientX < containerRect.left || + e.clientX > containerRect.right || + e.clientY < containerRect.top || + e.clientY > containerRect.bottom + ) { + log('Click outside VNC container, ignoring'); + return; + } + + log('Click inside VNC container, processing...'); + + // Get VNC canvas to calculate coordinates + let vncCanvas = null; + if (typeof rfb.get_canvas === 'function') { + vncCanvas = rfb.get_canvas(); + } else if (rfb.canvas) { + vncCanvas = rfb.canvas; + } else { + vncCanvas = container.querySelector('canvas'); + } + + if (!vncCanvas) { + log('Cannot find VNC canvas for click tracking'); + return; + } + + log(`VNC Canvas: ${vncCanvas.width}x${vncCanvas.height}`); + + // Get canvas position and dimensions + const rect = vncCanvas.getBoundingClientRect(); + log(`Canvas rect: left=${rect.left}, top=${rect.top}, width=${rect.width}, height=${rect.height}`); + log(`Mouse client: x=${e.clientX}, y=${e.clientY}`); + + // Calculate click position relative to canvas + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + // Scale coordinates to match canvas internal resolution + const scaleX = vncCanvas.width / rect.width; + const scaleY = vncCanvas.height / rect.height; + const canvasX = x * scaleX; + const canvasY = y * scaleY; + + log( + `Click position: relative=(${x.toFixed(0)}, ${y.toFixed(0)}), scaled=(${canvasX.toFixed( + 0 + )}, ${canvasY.toFixed(0)})` + ); + + // Add click effect + const effect = { + x: canvasX, + y: canvasY, + startTime: Date.now(), + }; + clickEffects.push(effect); + + log(`✓ Click effect added! Total effects: ${clickEffects.length}`); + }, + true + ); // Use capture phase to catch events before noVNC + + // Track mouse position for cursor rendering in recording + document.addEventListener( + 'mousemove', + (e) => { + // Only track when recording + if (!isRecording) { + currentMousePos = null; + return; + } + + // Get VNC canvas + let vncCanvas = null; + if (typeof rfb.get_canvas === 'function') { + vncCanvas = rfb.get_canvas(); + } else if (rfb.canvas) { + vncCanvas = rfb.canvas; + } else { + vncCanvas = container.querySelector('canvas'); + } + + if (!vncCanvas) { + return; + } + + // Get canvas position and dimensions + const rect = vncCanvas.getBoundingClientRect(); + + // Check if mouse is within canvas + if (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom) { + // Calculate position relative to canvas + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + // Scale to canvas resolution + const scaleX = vncCanvas.width / rect.width; + const scaleY = vncCanvas.height / rect.height; + currentMousePos = { + x: x * scaleX, + y: y * scaleY, + }; + } else { + currentMousePos = null; + } + }, + true + ); + + // Keyboard shortcuts (as alternative to toolbar buttons) + document.addEventListener('keydown', (e) => { + // Ctrl+Alt+Del + if (e.ctrlKey && e.altKey && e.key === 'Delete') { + e.preventDefault(); + sendCtrlAltDel(); + } + + // Ctrl+Alt+Backspace + if (e.ctrlKey && e.altKey && e.key === 'Backspace') { + e.preventDefault(); + sendCtrlAltBackspace(); + } + + // Tab handling for browser shortcuts + if (e.ctrlKey && e.shiftKey && e.key === 'Tab') { + log('Ctrl+Shift+Tab pressed (allowing browser default)'); + } + + // F5 and F11 - allow browser defaults for refresh and fullscreen + if (e.key === 'F5' || e.key === 'F11') { + log(`Function key ${e.key} pressed (allowing browser default)`); + return; + } + + // Prevent some browser shortcuts that interfere with VNC + if (e.ctrlKey && e.key === 's') { + e.preventDefault(); // Prevent save + log('Prevented Ctrl+S (save)'); + } + + if (e.ctrlKey && e.key === 'p') { + e.preventDefault(); // Prevent print + log('Prevented Ctrl+P (print)'); + } + + // F11 is handled by browser for fullscreen, no need to prevent + }); + + // Window resize handling + window.addEventListener('resize', () => { + if (rfb && rfb._rfb_connection_state === 'connected') { + // Re-apply scaling after resize + rfb.scaleViewport = true; + if (typeof rfb.sendScaleConfig === 'function') { + rfb.sendScaleConfig(); + } + log('Window resized, VNC display rescaled'); + } + }); + + // Cleanup on page unload + window.addEventListener('beforeunload', (e) => { + if (rfb) { + log('Page unloading, disconnecting VNC...'); + rfb.disconnect(); + } + }); + + // Handle visibility change (pause/resume) + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + log('Page hidden (VNC connection paused)'); + } else { + log('Page visible (VNC connection active)'); + if (rfb) { + rfb.focus(); + } + } + }); + + log('noVNC RFB initialized successfully'); + } catch (error) { + clearTimeout(connectionTimeout); + hideLoading(); + showError( + 'Initialization Error', + `Failed to initialize VNC console:\n\n${error.message}\n\n` + + 'Please ensure you are using a modern browser with WebSocket support.' + ); + log(`Initialization error: ${error.message}`, 'error'); + log(error.stack, 'error'); + } +})(); diff --git a/gns3server/static/web-ui/assets/xpra-html5/LICENSE b/gns3server/static/web-ui/assets/xpra-html5/LICENSE new file mode 100644 index 000000000..0d2166859 --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/LICENSE @@ -0,0 +1,372 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, + having made, import, or transfer of either its Contributions or + its Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under, + and making use of, this License. For legal entities, "You" + includes any entity that controls, is controlled by, or is under + common control with You. For purposes of this definition, + "control" means (a) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of + the outstanding shares or beneficial ownership of such entity. + +2. License Grants and Conditions +--------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 3.3) or under the terms of a Secondary License (if +allowed under Section 3.3), and You may not additional grants that +convert any of the rights granted to You under this License to a +subsequent license with different terms. + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +The Sections below define the conditions under which Your license to +use Covered Software will be terminated. These Sections also define +how the license is reinstated if it is terminated, and how the +Contributor may limit Your ability to make further Modifications to +the Covered Software. Except as expressly stated in this License, no +other conditions or restrictions are placed on Your rights to use +Covered Software. + +2.8. Distribution + +You may distribute Covered Software, provided that You: + +(a) comply with all conditions in Section 3; and + +(b) make all Your Contributions available under this License; and + +(c) include a copy of this License (including the copyright notice + and license grant) with every copy of the Covered Software You + distribute; and + +(d) ensure that all Your Modifications to Covered Software are + available under this License; and + +(e) ensure that all Your Contributions to Covered Software are + available under this License; and + +(f) ensure that all Your Modifications to the Covered Software in + Executable Form are made available under this License, either: + + (i) by providing the Source Code of the Executable Form, or + + (ii) by making the Source Code of the Executable Form available + under the terms of this License, at a location no more remote + than the location from which You made the Executable Form + available, and for at least twelve (12) months. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Code + +You may distribute Covered Software in Source Code form, provided that +You: + +(a) include a copy of this License (including the copyright notice + and license grant) with every copy of the Covered Software You + distribute; and + +(b) ensure that all Your Contributions are available under this + License; and + +(c) include a NOTICE file with all Your Contributions, and + ensure that such NOTICE includes details of how You can be + contacted. + +3.2. Distribution of Executable Form + +You may distribute Covered Software in Executable form, provided that +You: + +(a) ensure that all Your Contributions are available under this + License; and + +(b) include a copy of this License (including the copyright notice + and license grant) with every copy of the Covered Software You + distribute; and + +(c) ensure that all Your Modifications to the Covered Software in + Executable Form are made available under this License, either: + + (i) by providing the Source Code of the Executable Form, or + + (ii) by making the Source Code of the Executable Form available + under the terms of this License, at a location no more remote + than the location from which You made the Executable Form + available, and for at least twelve (12) months; and + +(d) ensure that all Your Contributions to the Covered Software in + Executable Form are made available under this License; and + +(e) ensure that all Your Modifications to the Covered Software in + Executable Form are made available under this License; and + +(f) if the Executable Form is made available, include a NOTICE file + with all Your Contributions, and ensure that such NOTICE includes + details of how You can be contacted. + +3.3. Distribution of a Larger Work + +You may distribute a Larger Work, provided that You: + +(a) comply with all conditions in Section 3 with respect to the + Covered Software contained in the Larger Work; and + +(b) ensure that all Your Contributions to the Larger Work are + available under this License. + +3.4. Notices + +You must retain all copyright, patent, trademark, and attribution +notices in Covered Software, except: + +(a) to the extent necessary to comply with the notice requirements + of this License; or + +(b) if the original notice is altered, then You must add a notice + indicating that You have altered the copyright, patent, trademark, + and attribution notices. + +3.5. Application of Additional Terms + +You may choose to offer, and charge a fee for, acceptance of support, +warranty, indemnity, or other liability obligations and/or rights +consistent with this License. However, in accepting such obligations, +You may act only on Your own behalf and on Your sole responsibility, +not on behalf of any other Contributor, and only if You agree to +indemnify, defend, and hold each Contributor harmless for any +liability incurred by, or claims asserted against, such Contributor +by reason of your accepting any such warranty or other liability. + +4. Inability to Comply +---------------------- + +If it is impossible for You to comply with any of the terms of this +License due to statute or judicial regulation, then You cannot: + +(a) distribute the Covered Software; or + +(b) grant any rights to it; or + +(c) remove any of the above conditions from this License. + +5. Termination +-------------- + +5.1. Termination + +You may terminate this License at any time by notifying all +Contributors. This License and the rights granted hereunder will +terminate automatically if You fail to comply with terms herein and +fail to cure such breach within 30 days of becoming aware of the +breach. Provisions which, by their nature, must remain in effect beyond +the termination of this License shall survive. + +5.2. Reinstatement + +If You initiate litigation against any entity by asserting patent +infringement claims (including declaratory judgment, counterclaim, +or cross-claim) against that entity regarding any Contributions of +that entity, then any patent licenses granted to You under this +License for that Contribution shall terminate. However, if You +settle such litigation and pay all litigation costs, then any +patent licenses granted to You under this License for that +Contribution shall be reinstated. + +5.3. Limitation on Liability + +Except as required by applicable law or agreed to in writing, a +Contributor is not liable to You for damages, including any direct, +indirect, special, incidental, or consequential damages of any +character arising as a result of this License or out of the use or +inability to use the Covered Software (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or +malfunction, or any and all other commercial damages or losses), +even if such Contributor has been advised of the possibility of such +damages. + +6. Miscellaneous +---------------- + +6.1. US Government Users + +The Covered Software is a "commercial item" as defined in 48 C.F.R. +2.101 (Oct. 1995), consisting of "commercial computer software" and +"commercial computer software documentation," as such terms are used +in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 +and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all +U.S. Government End Users acquire Covered Software with only those +rights set forth herein. + +6.2. Attribution + +This License may not be modified without the express written +permission of the parties to this License, except as allowed in +Section 3.3. + +7. Versions of the License +--------------------------- + +7.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in +Section 7.3, no one other than the license steward has the right +to modify or publish new versions of this License. + +7.2. Effective Date + +This License Version 2.0 was first published on January 2016. + +7.3. Other Versions + +You may distribute under the terms of this License, or under the +terms of a subsequent version of this License (see Section 3.3), or +under the terms of a Secondary License (if permitted under Section +3.3). + +Exhibit A - Source Code Form License Notice +------------------------------------------- + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + +This Source Code Form is "Incompatible With Secondary Licenses", as +defined by the Mozilla Public License, v. 2.0. diff --git a/gns3server/static/web-ui/assets/xpra-html5/README.md b/gns3server/static/web-ui/assets/xpra-html5/README.md new file mode 100644 index 000000000..5d1c45fac --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/README.md @@ -0,0 +1,69 @@ +# xpra-html5 + +HTML5 client for Xpra remote desktop server. + +## Version + +This is based on **xpra-html5 version 19** from the official Xpra project. + +## License + +This project is licensed under the Mozilla Public License Version 2.0 (MPL-2.0). + +See the [LICENSE](LICENSE) file for the full license text. + +## Copyright + +Copyright (c) 2013 Antoine Martin +Copyright (c) 2014 Joshua Higgins + +## Source + +This is a modified version of the xpra-html5 client from the official Xpra project: +- Official repository: https://github.com/Xpra-org/xpra-html5 +- Project website: https://xpra.org/ +- Version: 19 + +## Modifications from v19 + +This version has been modified for integration with GNS3 Web UI: + +### Client.js +No modifications - using original v19 code. + +### Protocol.js +- **Line 191**: Added documentation comment clarifying that the 'binary' subprotocol is required by xpra protocol +- Code remains unchanged from v19 + +### index.html +- **Lines 927-933**: Simplified connection close callback + - **Before**: Redirected to connect.html with extensive parameter passing + - **After**: Only logs the close reason, no redirection + - **Impact**: Simplified error handling for embedded GNS3 Web UI usage +- Removed 82 lines of redirection logic + +### TypeScript Service (xpra-console.service.ts) +- **buildXpraConsolePageUrl()**: Improved WebSocket URL parsing + - **Before**: Manual parsing and concatenation of path and token + - **After**: Using URL API to correctly extract pathname and search + - **Impact**: More reliable handling of query parameters and tokens + +## Summary of Changes + +- **Total files modified**: 3 +- **Lines added**: 11 +- **Lines removed**: 95 +- **Net change**: -84 lines (simplified) + +All modifications maintain compliance with MPL-2.0 license requirements. + +## License Compliance + +This project complies with the MPL-2.0 license requirements: +- All source code modifications are made available under MPL-2.0 +- Original copyright notices are preserved +- A copy of the MPL-2.0 license is included +- Modifications are clearly documented + +For more information about MPL-2.0, see: +https://www.mozilla.org/en-US/MPL/2.0/FAQ/ diff --git a/gns3server/static/web-ui/assets/xpra-html5/background.svg b/gns3server/static/web-ui/assets/xpra-html5/background.svg new file mode 100644 index 000000000..5a1498ee8 --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/background.svg @@ -0,0 +1,88 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/gns3server/static/web-ui/assets/xpra-html5/clipboard.html b/gns3server/static/web-ui/assets/xpra-html5/clipboard.html new file mode 100644 index 000000000..3e56b2d93 --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/clipboard.html @@ -0,0 +1,192 @@ + + + + + + + Xpra HTML5 Clipboard Test Page + + + + + + +
+
+ +
+ + +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+

+        
+ +
+ +
+
+

+        
+ +
+ + +
+ + + +
+ +
+ + +
+
+ + + + + diff --git a/gns3server/static/web-ui/assets/xpra-html5/connect.html b/gns3server/static/web-ui/assets/xpra-html5/connect.html new file mode 100644 index 000000000..c195f5879 --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/connect.html @@ -0,0 +1,1620 @@ + + + + + + + + + + Xpra HTML5 Client + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + diff --git a/gns3server/static/web-ui/assets/xpra-html5/crypto.html b/gns3server/static/web-ui/assets/xpra-html5/crypto.html new file mode 100644 index 000000000..78882ddba --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/crypto.html @@ -0,0 +1,174 @@ + + + + + + + Xpra HTML5 AES Test Page + + + + + + + +
+
+

+      
+ +
+ + + + + diff --git a/gns3server/static/web-ui/assets/xpra-html5/css/client.css b/gns3server/static/web-ui/assets/xpra-html5/css/client.css new file mode 100644 index 000000000..57a3523dd --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/css/client.css @@ -0,0 +1,612 @@ +html, +body { + height: 100%; + width: 100%; + padding: 0; + margin: 0; + overflow: hidden; + background-color: #e8e8e8; + background-image: url(../background.svg), linear-gradient(135deg, #e8e8e8 0%, #f5f5f5 50%, #e8e8e8 100%); + background-position: center center; + background-repeat: no-repeat; + background-size: 700px 700px, cover; +} + +div.window canvas { + touch-action: none; + border-radius: 0 0 3px 3px; +} + +div.window canvas.gpu-trigger { + transform: transale3d(0, 0, 0); + /* Refer to client.js */ +} + +body.desktop { + background-color: #555; +} + +div#top_bar { + background-color: #444; + width: 100%; + height: 48px; +} + +div#top_bar img { + width: 48px; + height: 48px; +} + +div#connect_form { + display: none; +} + +div#disconnect_form { + display: none; +} + +.overlay { + background-color: black; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.5; + z-index: 10; + text-align: center; + padding-top: 3em; +} + +#progress-label { + font-size: 1.4em; + color: white; + line-height: 2em; + height: 2em; +} + +#progress-details { + font-size: 1.2em; + color: white; + line-height: 1.5em; + height: 1.5em; +} + +#progress-bar { + height: 2em; +} + +canvas { + border: none; + padding: 0; + margin: 0; +} + +#pasteboard { + position: fixed; + top: -40px; + width: 100px; + height: 30px; + left: 1px; +} + +.undecorated { + position: absolute; + padding: 0; + overflow: hidden; +} + +.window { + border: 1px; + position: absolute; + padding: 0; + overflow: hidden; + /* required for radius clip */ +} + +.window.override-redirect {} + +.window.tray { + position: relative; + float: left; +} + +.window.desktop { + box-shadow: none; +} + +.window-NORMAL {} + +.window.border { + border-color: grey; + border-width: 1px; + border-style: solid; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); + border-radius: 6px 6px 3px 3px; +} + +.windowicon { + display: inline-block; + position: absolute; + width: 30px; + height: 30px; + left: 0; + top: 0; + overflow: hidden; + cursor: default; + background-color: transparent; +} + +.windowtitle { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + display: inline-block; + float: left; + position: absolute; + top: 0; + left: 0; + overflow: hidden; + text-overflow: ellipsis; + padding: 5; + padding-right: 48; + white-space: nowrap; + cursor: default; +} + +.windowbuttons { + display: inline-block; + position: absolute; + z-index: 1; + top: 0; + right: 0; + padding-right: 5px; + cursor: pointer; + padding-top: 7px; + background-color: transparent; +} + +.above { + z-index: 10000; +} + +.below { + z-index: 0; +} + +div.windowinfocus { + z-index: 5000; +} + +.windowhead { + height: 30px; + color: #000000; + background-color: #a0a0a0; + border-bottom: 1px solid #c0c0c0; +} + +div.windowinfocus div.windowhead { + background-color: #ffffff; +} + +.modal { + z-index: 20000; +} + +.spinneroverlay { + width: 100%; + height: 100%; + display: none; + background-color: black; + opacity: 0.5; + position: absolute; +} + +.spinnermiddle { + display: table-cell; + vertical-align: middle; +} + +.ui-resizable-helper { + border: 2px dotted #00f; + z-index: 30000 !important; +} + +.ui-resizable-handle { + position: absolute; + font-size: 0.1px; + display: block; +} + +.ui-resizable-disabled .ui-resizable-handle, +.ui-resizable-autohide .ui-resizable-handle { + display: none; +} + +.ui-resizable-n { + cursor: n-resize; + height: 7px; + width: 100%; + top: -5px; + left: 0; +} + +.ui-resizable-s { + cursor: s-resize; + height: 7px; + width: 100%; + bottom: -5px; + left: 0; +} + +.ui-resizable-e { + cursor: e-resize; + width: 7px; + right: -5px; + top: 0; + height: 100%; +} + +.ui-resizable-w { + cursor: w-resize; + width: 7px; + left: -5px; + top: 0; + height: 100%; +} + +.ui-resizable-se { + cursor: se-resize; + width: 12px; + height: 12px; + right: 1px; + bottom: 1px; +} + +.ui-resizable-sw { + cursor: sw-resize; + width: 9px; + height: 9px; + left: -5px; + bottom: -5px; +} + +.ui-resizable-nw { + cursor: nw-resize; + width: 9px; + height: 9px; + left: -5px; + top: -5px; +} + +.ui-resizable-ne { + cursor: ne-resize; + width: 9px; + height: 9px; + right: -5px; + top: -5px; +} + +.notifications { + position: absolute; + left: auto; + right: 0; + top: 96px; + bottom: auto; + width: 600px; + height: auto; + z-index: 10001; + transition: height 0.5s, opacity 0.5s, top 0.5s, box-shadow 0.5s, bottom 0.5s; + overflow: hidden; + padding: 40px; + visibility: hidden; +} + +.notifications .alert { + visibility: visible; +} + +.notification_icon { + max-width: 128px; + max-height: 128px; + float: left; + padding: 0.4rem; + padding-left: 1rem; +} + +.notification_buttons { + clear: both; + margin: 10px 0 0 auto; + font-weight: 500; + font-size: 1.2rem; + height: 2em; +} + +.notification_button { + cursor: pointer; + float: right; + margin: 5px; + padding: 5px; + border: 1px solid black; + border-radius: 5px; + background-color: #f8f8f8; + color: #000; +} + +.notification_button:hover { + background-color: #000; + color: #ffffff; +} + +@media screen and (max-width: 600px) { + .notifications { + bottom: 0; + box-shadow: none; + padding-top: 5rem; + } +} + +@media screen { + .notifications .alert { + box-shadow: 0 1rem 2rem rgba(21, 36, 48, 0.75); + background-color: white; + overflow: visible; + border-radius: 8px; + text-align: center; + padding: 0.5rem; + opacity: 40; + margin-top: -10rem; + position: relative; + transform: translateY(10rem); + transition: left 0.5s, margin 0.5s, opacity 0.5s, transform 0.5s; + } +} + +@media screen and (max-width: 600px) { + .notifications .alert { + opacity: 0; + margin-top: 0; + transform: translateY(0); + } +} + +.notifications .alert span { + display: block; + padding: 0; + margin: 0 3rem; + font-weight: 300; + font-size: 0.9rem; + text-overflow: ellipsis; + line-height: 1rem; + max-height: 5rem; +} + +.notifications .alert span.title { + margin: 1rem; + font-weight: 600; + font-size: 1.3rem; + line-height: 1.3rem; +} + +.notifications .alert span.message { + text-align: left; +} + +.notifications .alert span a { + text-decoration: none; +} + +.notifications .alert .dismiss { + width: 2rem; + position: absolute; + right: 1rem; + top: 1em; + transform: scale(1, 1) translateY(-50%); + font-size: 2rem; + transition: margin 0.5s, opacity 0.5s, transform 0.5s; + text-align: center; + cursor: pointer; + color: inherit; + font-weight: 300; + transform-origin: center center; +} + +#about, +#sessioninfo, +#bugreport { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + background-color: #eee; + border: 2px solid #ddd; + display: none; + z-index: 1000000; + margin: auto; + text-align: center; + padding: 20px; +} + +#about, +#sessioninfo, +#bugreport { + position: fixed; + margin: auto; + width: 640px; + height: fit-content; + top: 50%; + left: 50%; + margin-left: -320px; + margin-top: -140px; +} + +#bugreport { + text-align: left; +} + +#shadow_pointer { + width: 32px; + height: 32px; + position: absolute; + top: 0; + left: 0; + border: 1px; + z-index: 100000; + display: none; + pointer-events: none; +} + +table#sessiondata { + width: 90%; + margin: auto; +} + +table#sessiondata th { + text-align: left; +} + +table#sessiondata td { + text-align: right; +} + +#upload { + display: none; +} + +.action-icons { + position: relative; + float: left; +} + +.action-events { + position: relative; + float: left; +} + +#action-link { + display: none; + color: #99f; +} + +.clock_block { + position: relative; + float: left; + padding-left: 100px; +} + +#clock_text { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + color: #fff; +} + +.audio { + float: right; + padding-right: 10px; +} + +.simple-keyboard { + z-index: 100000; +} + +#login-overlay { + position: fixed; + display: none; + width: 100%; + height: 100%; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + z-index: 20; + cursor: pointer; +} + +#login-box { + height: fit-content; + width: 480px; + margin: auto; + margin-top: 20px; + position: relative; + z-index: 3; + background: #d8d8d8; + border-radius: 20px; + padding: 20px; +} + +#login-innerbox { + margin: auto; + height: 100%; + z-index: 3; + background: #e8e8e8; + border-radius: 20px; + text-align: center; +} + +#login-header { + font-size: 2em; + clear: both; + display: inline-block; + margin: 0.5em; +} + +#login-innerbox input { + font-size: 1.5em; + clear: both; + display: inline-block; + margin: 0.5em; + padding: 2px; + border: 2px solid #101010; + border-radius: 4px; +} + +#password-box { + background: url("../icons/authentication.png") no-repeat; + margin-left: 2em; + margin-right: 2em; +} + +#login-buttons { + margin: 2em; + margin-bottom: 0; +} + +.login-button { + margin: 1em; + padding: 10px 32px; + font-size: 18px; + line-height: 1.3333333; + color: #fff; + background-color: #5cb85c; + display: inline-block; + text-align: center; + white-space: nowrap; + vertical-align: middle; + touch-action: manipulation; + cursor: pointer; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} + +.login-button:focus, +.login-button:hover { + background-color: #449d44; +} + +@media screen { + .notifications .alert.visible { + margin-top: 0; + opacity: 1; + transform: translateY(0); + } + + .notifications .alert.hidden { + overflow: hidden; + margin-top: -2.125rem; + z-index: -1; + } +} + +@media screen and (max-width: 600px) { + .notifications .alert.hidden { + transform: translateY(0) translateX(-10rem); + } +} diff --git a/gns3server/static/web-ui/assets/xpra-html5/css/connect.css b/gns3server/static/web-ui/assets/xpra-html5/css/connect.css new file mode 100644 index 000000000..540d2ec2c --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/css/connect.css @@ -0,0 +1,306 @@ +/* Screenreader-only */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: none; +} + +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + + color: #333; +} + +.form-signin-heading { + font-weight: 500; + font-size: 30px; +} + +/* Version number */ +h5 { + margin-top: 10px; + margin-bottom: 10px; + + font-weight: 500; +} + +/* Forms for ports, passwords, etc... */ +.form-control::placeholder { + color: #999; + opacity: 1; +} + +.form-control { + display: inline-block; + vertical-align: middle; + + height: 34px; + padding: 6px 12px; + + color: #555; + + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + + border: 1px solid #ccc; + border-radius: 4px; + + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; +} + +/* Checkbox labels */ +#ssl-span, +#webtransport-span, +#encryption-span { + font-size: 14px; +} + +/* Radio button labels */ +label { + margin-bottom: 5px; + font-weight: bold; + + font-size: 14px; +} + +/* Start session text boxes */ +#action_connect_group, +#action_start_group, +#action_start_desktop_group, +#action_shadow_group { + max-height: 34px; +} + +#action_connect_group>.form-control, +#action_start_group>.form-control, +#action_start_desktop_group>.form-control, +#action_shadow_group>.form-control { + position: relative; + top: 0; +} + +.list-style-none.action { + margin: 0; +} + +.panel-group .list-style-none.action { + margin-bottom: 40px; +} + +/* Connect buttons */ +.form-group { + margin-bottom: 30px; +} + +.btn { + border: 1px solid transparent; + + cursor: pointer; +} + +.btn-lg { + padding: 10px 16px; + + border-radius: 6px; + + font-size: 18px; +} + +.btn-success { + color: white; + background-color: #5cb85c; + border-color: #4cac4c; +} + +.btn-success:hover { + color: white; + background-color: #449d44; + border-color: #398439; +} + +/* Advanced options */ +.panel.panel-default { + font-family: Helvetica Neue, Helvetica, Arial, sans-serif; +} + +.panel-group .panel { + margin-bottom: 0; + + border-radius: 4px; + border: 1px solid #ddd; +} + +#expandopts { + color: #333; + background-color: #f5f5f5; + + padding: 10px 15px; + + border-color: #ddd; + border-radius: 3px 3px 0 0; +} + +.panel-title { + margin-top: 0; + margin-bottom: 0; + + font-size: 16px; + font-weight: 500; + + color: #333; +} + +.list-group { + position: relative; + top: -16px; + padding: 0; +} + +.list-group-item { + display: block; + list-style-type: none; + + line-height: 1.4; + padding: 0; + margin-bottom: -1px; + + background-color: white; + + border: 1px solid #ddd; + border-left: none; + border-right: none; + + font-size: 14px; +} + +.list-group-item>select { + padding: 4px 1px 4px 5px; +} + +.list-group-item>.form-control { + display: block; +} + +/* Background */ +div.bg-image { + position: fixed; + height: 100%; + width: 100%; + padding: 0; + margin: 0; + top: 0; + left: 0; + background-image: url(../background.jpg); + background-position: center center; + background-repeat: no-repeat; + background-size: cover; + opacity: 0.3; + z-index: -1000; +} + +div.container { + margin-top: 40px; + margin-bottom: 40px; +} + +.form-signin { + max-width: 530px; + padding: 15px; + margin: 0 auto; +} + +.form-or-heading { + text-align: center; +} + +.form-signin h5 { + padding-left: 8em; +} + +.form-signin .form-signin-heading, +.form-signin .checkbox { + margin-bottom: 10px; +} + +.form-signin .checkbox, +.form-signin .radiobox { + font-weight: normal; +} + +.form-signin .form-control { + position: relative; + box-sizing: border-box; + font-size: 16px; + max-width: 16em; +} + +.form-signin .form-control.command { + margin-left: 1.6em; +} + +.form-signin .form-control:focus { + z-index: 2; +} + +.form-signin .panel-group { + padding-top: 8px; +} + +.form-signin .security-box { + padding-left: 5em; +} + +.form-signin .security-box #key { + font-size: 12px; + padding: 0 10px; + height: 20px; +} + +.form-signin .list-group-item { + padding: 6px 10px; +} + +.form-signin .list-style-none { + list-style-type: none; + margin-bottom: 5px; +} + +.form-signin .action input { + font-size: 12px; + padding: 2px; +} + +.form-signin img.command-icon { + object-fit: scale-down; + max-width: 32px; + max-height: 32px; +} + +.form-signin span.warning { + color: red; + font-size: smaller; + padding-left: 10px; +} + +.form-signin span.disabled { + color: grey; +} + +.form-signin img.field-icon { + margin-left: -32px; + position: relative; + z-index: 2; + font-family: "bootstrap-icons"; + width: 1.2em; + height: 1.2em; +} + +.form-signin span#server-info { + padding-left: 20px; + font-size: 11px; + font-style: italic; +} diff --git a/gns3server/static/web-ui/assets/xpra-html5/css/icon.css b/gns3server/static/web-ui/assets/xpra-html5/css/icon.css new file mode 100644 index 000000000..ed0d164f4 --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/css/icon.css @@ -0,0 +1,36 @@ +@font-face { + font-family: "materialicons"; + src: url(../icons/materialicons-regular.woff) format("woff"); +} + +.Icon, +*[data-icon]:before { + display: inline-block; + position: relative; + width: 40px; + height: 40px; + line-height: 40px; + + font-family: "materialicons"; + font-size: 24px; + text-align: center; + + color: black; + + content: attr(data-icon); + + /* This is important for IE, which has it off by default */ + font-feature-settings: "liga" 1; +} + +.Icon.-small { + font-size: 18px; +} + +.icon-toggled:before { + color: #777777; +} + +.icon-paused:before { + color: gold; +} diff --git a/gns3server/static/web-ui/assets/xpra-html5/css/menu-skin.css b/gns3server/static/web-ui/assets/xpra-html5/css/menu-skin.css new file mode 100644 index 000000000..165620b79 --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/css/menu-skin.css @@ -0,0 +1,197 @@ +/* +MIT License + +Copyright (c) 2019 Mark Harkin, 2016 Dylan Hicks (aka. dylanh333) + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +*/ + +#float_menu_button { + background: white; + height: 50px; + width: 12px; + border-radius: 0 10px 10px 0; + + position: relative; + left: 30px; + top: -125px; +} + +#float_menu_arrow { + color: black; + border-color: black; + + position: relative; + right: 14px; + top: 5px; +} + +#float_menu { + display: none; + position: fixed; + height: 30px; + padding: 0 20px 0 0; + z-index: 100000; + background: white; + + border-color: grey; + border-width: 1px; + border-style: solid; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); +} + +.menu-divleft { + float: left; + clear: left; +} + +.menu-divright { + float: right; + clear: right; +} + +.menu-content-left { + white-space: nowrap; + float: left; + width: 0; + height: 24px; + padding: 3px 2px; + vertical-align: middle; + text-decoration: none; + display: inline; +} + +.menu-content-right { + white-space: nowrap; + float: right; + width: 0; + height: 16px; + padding: 7px 2px; + vertical-align: middle; + text-decoration: none; + display: inline; +} + +.windowlist-li { + min-width: 400px; +} + +.Menu { + color: black; +} + +.Menu.-vertical { + border-radius: 0 3px 3px 0; +} + +/* Foreground */ +.Menu, +.Menu li.-hasSubmenu>a:after { + color: black; + border-color: black; + /* chevron colour */ +} + +/* Background */ +.Menu, +.Menu li ul { + background: white; +} + +/* Border */ +.Menu, +.Menu li ul { + box-shadow: none; +} + +.Menu li ul, +.Menu.-floating { + border-radius: 3px; +} + +.Menu.-horizontal>li>ul { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.Menu:not(.-alignRight) li>ul, +.Menu.-floating:not(.-alignRight) { + border-top-left-radius: 0; +} + +.Menu.-alignRight li>ul, +.Menu.-floating.-alignRight { + border-top-right-radius: 0; +} + +/* Drop shadow on Menus and header */ +.Menu li ul, +.Menu.-floating { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2), + 0 3px 1px 0 rgba(0, 0, 0, 0.2), 0 6px 12px 0 rgba(0, 0, 0, 0.15); +} + +#window_preview { + display: none; + width: 65%; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 80000; + background-color: rgba(0, 0, 0, 0.7); + box-shadow: 0 0 25px rgba(0, 0, 0, 0.5); + border-radius: 2px; + padding: 20px; +} + +.slick-track { + display: flex !important; +} + +.slick-slide { + margin: 7px; +} + +.slick-current { + box-shadow: 0 0 20px rgba(255, 255, 255, 65%); + border-radius: 2px; + border: 3px solid rgba(255, 255, 255, 50%); +} + +.window-preview-item-container { + display: inline-block; + height: 100%; +} + +.window-preview-item-img { + display: inline-block; + width: 100%; + height: 100%; +} + +.window-preview-item-text { + color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-family: "auto"; + padding: 4px; +} diff --git a/gns3server/static/web-ui/assets/xpra-html5/css/menu.css b/gns3server/static/web-ui/assets/xpra-html5/css/menu.css new file mode 100644 index 000000000..7f1a05138 --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/css/menu.css @@ -0,0 +1,309 @@ +/* +MIT License + +Copyright (c) 2019 Mark Harkin, 2016 Dylan Hicks (aka. dylanh333) + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +*/ + +.Menu { + display: none; + position: relative; + z-index: 100000; +} + +/* Three basic menu layouts */ +/* This also enforces that menu is a ul, and that + * layout modifier is specified + */ +ul.Menu.-horizontal, +ul.Menu.-vertical { + display: inline-block; +} + +ul.Menu.-floating { + display: block; + position: absolute; +} + +/* Menu and menu-item layout */ +.Menu, +.Menu li, +.Menu li>ul { + list-style: none; + padding: 0; + margin: 0; +} + +.Menu li { + display: block; + position: relative; + white-space: nowrap; + word-break: keep-all; +} + +.Menu.-horizontal>li { + display: inline-block; + float: left; +} + +.Menu li>* { + display: block; + position: relative; +} + +.Menu li>ul { + position: absolute; + min-width: 100%; + top: 0; + left: 100%; +} + +.Menu.-horizontal.-alignRight li>ul { + left: auto; + right: 100%; +} + +.Menu.-horizontal.-alignRight>li>ul { + right: 0; +} + +.Menu.-horizontal>li>ul { + top: auto; + left: auto; +} + +/* Menu behaviour */ +.Menu li>ul, +.Menu.-floating { + display: none; +} + +.Menu li>ul.-visible, +ul.Menu.-floating.-visible { + display: block; +} + +/* Menu animation */ +.Menu li>ul, +.Menu.-horizontal.-alignRight li>ul, +.Menu.-floating { + opacity: 1; + transform: scale(1) translateY(0); + transform-origin: left top; +} + +.Menu.-alignRight li>ul, +.Menu.-floating.-alignRight { + transform-origin: right top; +} + +.Menu li>ul.-animating, +.Menu.-floating.-animating { + opacity: 0 !important; + transform: scale(0.96) translateX(-16px); +} + +.Menu li>ul.-animating { + z-index: -1 !important; +} + +.Menu.-horizontal>li>ul.-animating { + transform: scale(0.96) translateY(-16px); +} + +.Menu.-alignRight li>ul.-animating, +.Menu.-floating.-alignRight.-animating { + transform: scale(0.96) translateX(16px); +} + +.Menu.-horizontal.-alignRight>li>ul.-animating { + transform: scale(0.96) translateY(-16px); +} + +/* Menu item icons */ +.Menu *[data-icon]:before { + position: absolute; + left: 0; + top: 0; + bottom: 0; + margin: auto 0; +} + +.Menu .Icon, +.Menu *[data-icon]:before { + line-height: inherit; +} + +.Menu .Icon { + padding: 0; +} + +.Menu *:empty[data-icon] { + padding-left: 0 !important; + padding-right: 0 !important; +} + +/* Submenu chevrons */ +.Menu li.-hasSubmenu>a:after { + display: block; + position: absolute; + + width: 8px; + height: 8px; + right: 8px; + bottom: 0; + top: 0; + margin: auto 0; + + transform: rotate(45deg); + border-width: 1px; + border-color: black; + border-style: solid solid none none; + + content: ""; +} + +.Menu.-horizontal>li.-hasSubmenu>a:after { + width: 4px; + height: 4px; + bottom: 4px; + top: auto; + left: 0; + right: 0; + margin: 0 auto; + + border-style: none solid solid none; +} + +.Menu li.-hasSubmenu.-noChevron>a:after { + display: none; +} + +/* +** Configurable values +*/ +/* Height of navbar, and menu items */ +/* All of these must be of equal value */ +.Menu { + line-height: 30px; +} + +.Menu.-horizontal, +.Menu li, +.Menu li> :first-child { + height: 30px; +} + +.Menu *[data-icon]:before, +.Menu .Icon { + width: 30px; + height: 30px; +} + +.Menu *[data-icon] { + min-width: 30px; + min-height: 30px; + padding-left: 30px; +} + +/* Default font settings for menu */ +.Menu { + font-family: Arial, Helvetica, sans-serif; + font-size: 16px; +} + +/* Icon font sizes */ +.Menu *[data-icon]:before, +.Menu .Icon { + font-size: 24px; +} + +/* For submenus */ +.Menu>li ul .Icon, +.Menu.-floating .Icon, +.Menu>li ul *[data-icon]:before, +.Menu.-floating *[data-icon]:before { + font-size: 18px; +} + +/* Colouring of menus */ +/* Foreground */ +.Menu, +.Menu li.-hasSubmenu>a:after { + color: black; + border-color: black; + /* chevron colour */ +} + +/* Background */ +.Menu ul, +.Menu.-floating { + background: white; +} + +/* Padding for each menu item */ +.Menu li>* { + padding: 0 12px; +} + +/* Indent of chevron */ +.Menu li li.-hasSubmenu>a:after, +.Menu:not(.-horizontal)>li.-hasSubmenu>a:after { + right: 12px; +} + +.Menu li li.-hasSubmenu>a, +.Menu:not(.-horizontal)>li.-hasSubmenu>a { + padding-right: 28px; +} + +/* Minimum width of dropdown menus */ +.Menu.-horizontal li>ul, +.Menu.-floating { + min-width: 200px; +} + +/* Animation speed of dropdown menus */ +.Menu li>ul, +.Menu.-floating { + transition: width 0.1s, height 0.1s, transform 0.1s, opacity 0.1s; +} + +/* Styling of hyperlink text */ +.Menu li>a { + text-decoration: none; + color: inherit; +} + +/* Animation speed of :hover shading */ +.Menu li>a:first-child { + transition: background-color 0.2s; +} + +/* Colour of :hover shading */ +.Menu li:hover>a:first-child, +.Menu li.-active>a:first-child { + background-color: rgba(0, 0, 0, 0.1); +} + +/* +** End configurable values +*/ diff --git a/gns3server/static/web-ui/assets/xpra-html5/css/simple-keyboard.css b/gns3server/static/web-ui/assets/xpra-html5/css/simple-keyboard.css new file mode 100644 index 000000000..a89ad4c14 --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/css/simple-keyboard.css @@ -0,0 +1,122 @@ +/*! + * + * simple-keyboard v2.32.56 + * https://github.com/hodgef/simple-keyboard + * + * Copyright (c) Francisco Hodge (https://github.com/hodgef) + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +.hg-theme-default { + width: 100%; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + box-sizing: border-box; + overflow: hidden; + touch-action: manipulation; +} + +.hg-theme-default .hg-button span { + pointer-events: none; +} + +.hg-theme-default button.hg-button { + border-width: 0; + outline: 0; + font-size: inherit; +} + +.hg-theme-default { + font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", + Helvetica, Arial, "Lucida Grande", sans-serif; + background-color: #ececec; + padding: 5px; + border-radius: 5px; +} + +.hg-theme-default .hg-button { + display: inline-block; + flex-grow: 1; +} + +.hg-theme-default .hg-row { + display: flex; +} + +.hg-theme-default .hg-row:not(:last-child) { + margin-bottom: 5px; +} + +.hg-theme-default .hg-row .hg-button-container, +.hg-theme-default .hg-row .hg-button:not(:last-child) { + margin-right: 5px; +} + +.hg-theme-default .hg-row>div:last-child { + margin-right: 0; +} + +.hg-theme-default .hg-row .hg-button-container { + display: flex; +} + +.hg-theme-default .hg-button { + box-shadow: 0 0 3px -1px rgba(0, 0, 0, 0.3); + height: 40px; + border-radius: 5px; + box-sizing: border-box; + padding: 5px; + background: #fff; + border-bottom: 1px solid #b5b5b5; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +.hg-theme-default .hg-button.hg-activeButton { + background: #efefef; +} + +.hg-theme-default.hg-layout-numeric .hg-button { + width: 33.3%; + height: 60px; + align-items: center; + display: flex; + justify-content: center; +} + +.hg-theme-default .hg-button.hg-button-numpadadd, +.hg-theme-default .hg-button.hg-button-numpadenter { + height: 85px; +} + +.hg-theme-default .hg-button.hg-button-numpad0 { + width: 105px; +} + +.hg-theme-default .hg-button.hg-button-com { + max-width: 85px; +} + +.hg-theme-default .hg-button.hg-standardBtn.hg-button-at { + max-width: 45px; +} + +.hg-theme-default .hg-button.hg-selectedButton { + background: rgba(5, 25, 70, 0.53); + color: #fff; +} + +.hg-theme-default .hg-button.hg-standardBtn[data-skbtn=".com"] { + max-width: 82px; +} + +.hg-theme-default .hg-button.hg-standardBtn[data-skbtn="@"] { + max-width: 60px; +} diff --git a/gns3server/static/web-ui/assets/xpra-html5/css/simple-keyboard.css.map b/gns3server/static/web-ui/assets/xpra-html5/css/simple-keyboard.css.map new file mode 100644 index 000000000..89b583b73 --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/css/simple-keyboard.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["index.css","webpack://./src/lib/components/Keyboard.css"],"names":[],"mappings":"AAAA;;;;;;;;;;EAUE,CCNF,kBACE,UAAW,CACX,wBAAiB,CAAjB,qBAAiB,CAAjB,oBAAiB,CAAjB,gBAAiB,CACjB,qBAAsB,CACtB,eAAgB,CAChB,yBACF,CAEA,kCACE,mBACF,CAGA,mCACE,cAAe,CACf,SAAU,CACV,iBACF,CAEA,kBACE,oHAC+C,CAC/C,wBAAyB,CACzB,WAAY,CACZ,iBACF,CAEA,6BACE,oBAAqB,CACrB,WAEF,CAEA,0BACE,YACF,CAEA,2CACE,iBACF,CAMA,qGACE,gBACF,CAEA,yCACE,cACF,CAEA,+CACE,YACF,CAEA,6BACE,sCAA+C,CAC/C,WAAY,CACZ,iBAAkB,CAClB,qBAAsB,CACtB,WAAY,CACZ,eAAiB,CACjB,+BAAgC,CAChC,cAAe,CACf,YAAa,CACb,kBAAmB,CACnB,sBAAuB,CACvB,yCACF,CAEA,6CACE,kBACF,CAEA,+CACE,WAAY,CACZ,WAAY,CACZ,kBAAmB,CACnB,YAAa,CACb,sBACF,CAMA,oGACE,WACF,CAEA,+CACE,WACF,CAEA,2CACE,cACF,CAEA,yDACE,cACF,CAEA,+CACE,4BAAiC,CACjC,UACF,CAEA,+DACE,cACF,CAEA,4DACE,cACF","file":"index.css","sourcesContent":["/*!\n * \n * simple-keyboard v2.32.56\n * https://github.com/hodgef/simple-keyboard\n * \n * Copyright (c) Francisco Hodge (https://github.com/hodgef)\n * \n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n * \n */\n/**\n * simple-keyboard\n * Theme: hg-theme-default\n */\n.hg-theme-default {\n width: 100%;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n box-sizing: border-box;\n overflow: hidden;\n touch-action: manipulation;\n}\n\n.hg-theme-default .hg-button span {\n pointer-events: none;\n}\n\n/* When using option \"useButtonTag\" */\n.hg-theme-default button.hg-button {\n border-width: 0;\n outline: 0;\n font-size: inherit;\n}\n\n.hg-theme-default {\n font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\",\n Helvetica, Arial, \"Lucida Grande\", sans-serif;\n background-color: #ececec;\n padding: 5px;\n border-radius: 5px;\n}\n\n.hg-theme-default .hg-button {\n display: inline-block;\n flex-grow: 1;\n cursor: pointer;\n}\n\n.hg-theme-default .hg-row {\n display: flex;\n}\n\n.hg-theme-default .hg-row:not(:last-child) {\n margin-bottom: 5px;\n}\n\n.hg-theme-default .hg-row .hg-button:not(:last-child) {\n margin-right: 5px;\n}\n\n.hg-theme-default .hg-row .hg-button-container {\n margin-right: 5px;\n}\n\n.hg-theme-default .hg-row > div:last-child {\n margin-right: 0;\n}\n\n.hg-theme-default .hg-row .hg-button-container {\n display: flex;\n}\n\n.hg-theme-default .hg-button {\n box-shadow: 0px 0px 3px -1px rgba(0, 0, 0, 0.3);\n height: 40px;\n border-radius: 5px;\n box-sizing: border-box;\n padding: 5px;\n background: white;\n border-bottom: 1px solid #b5b5b5;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\n.hg-theme-default .hg-button.hg-activeButton {\n background: #efefef;\n}\n\n.hg-theme-default.hg-layout-numeric .hg-button {\n width: 33.3%;\n height: 60px;\n align-items: center;\n display: flex;\n justify-content: center;\n}\n\n.hg-theme-default .hg-button.hg-button-numpadadd {\n height: 85px;\n}\n\n.hg-theme-default .hg-button.hg-button-numpadenter {\n height: 85px;\n}\n\n.hg-theme-default .hg-button.hg-button-numpad0 {\n width: 105px;\n}\n\n.hg-theme-default .hg-button.hg-button-com {\n max-width: 85px;\n}\n\n.hg-theme-default .hg-button.hg-standardBtn.hg-button-at {\n max-width: 45px;\n}\n\n.hg-theme-default .hg-button.hg-selectedButton {\n background: rgba(5, 25, 70, 0.53);\n color: white;\n}\n\n.hg-theme-default .hg-button.hg-standardBtn[data-skbtn=\".com\"] {\n max-width: 82px;\n}\n\n.hg-theme-default .hg-button.hg-standardBtn[data-skbtn=\"@\"] {\n max-width: 60px;\n}\n\n","/**\n * simple-keyboard\n * Theme: hg-theme-default\n */\n.hg-theme-default {\n width: 100%;\n user-select: none;\n box-sizing: border-box;\n overflow: hidden;\n touch-action: manipulation;\n}\n\n.hg-theme-default .hg-button span {\n pointer-events: none;\n}\n\n/* When using option \"useButtonTag\" */\n.hg-theme-default button.hg-button {\n border-width: 0;\n outline: 0;\n font-size: inherit;\n}\n\n.hg-theme-default {\n font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\",\n Helvetica, Arial, \"Lucida Grande\", sans-serif;\n background-color: #ececec;\n padding: 5px;\n border-radius: 5px;\n}\n\n.hg-theme-default .hg-button {\n display: inline-block;\n flex-grow: 1;\n cursor: pointer;\n}\n\n.hg-theme-default .hg-row {\n display: flex;\n}\n\n.hg-theme-default .hg-row:not(:last-child) {\n margin-bottom: 5px;\n}\n\n.hg-theme-default .hg-row .hg-button:not(:last-child) {\n margin-right: 5px;\n}\n\n.hg-theme-default .hg-row .hg-button-container {\n margin-right: 5px;\n}\n\n.hg-theme-default .hg-row > div:last-child {\n margin-right: 0;\n}\n\n.hg-theme-default .hg-row .hg-button-container {\n display: flex;\n}\n\n.hg-theme-default .hg-button {\n box-shadow: 0px 0px 3px -1px rgba(0, 0, 0, 0.3);\n height: 40px;\n border-radius: 5px;\n box-sizing: border-box;\n padding: 5px;\n background: white;\n border-bottom: 1px solid #b5b5b5;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\n.hg-theme-default .hg-button.hg-activeButton {\n background: #efefef;\n}\n\n.hg-theme-default.hg-layout-numeric .hg-button {\n width: 33.3%;\n height: 60px;\n align-items: center;\n display: flex;\n justify-content: center;\n}\n\n.hg-theme-default .hg-button.hg-button-numpadadd {\n height: 85px;\n}\n\n.hg-theme-default .hg-button.hg-button-numpadenter {\n height: 85px;\n}\n\n.hg-theme-default .hg-button.hg-button-numpad0 {\n width: 105px;\n}\n\n.hg-theme-default .hg-button.hg-button-com {\n max-width: 85px;\n}\n\n.hg-theme-default .hg-button.hg-standardBtn.hg-button-at {\n max-width: 45px;\n}\n\n.hg-theme-default .hg-button.hg-selectedButton {\n background: rgba(5, 25, 70, 0.53);\n color: white;\n}\n\n.hg-theme-default .hg-button.hg-standardBtn[data-skbtn=\".com\"] {\n max-width: 82px;\n}\n\n.hg-theme-default .hg-button.hg-standardBtn[data-skbtn=\"@\"] {\n max-width: 60px;\n}\n"]} \ No newline at end of file diff --git a/gns3server/static/web-ui/assets/xpra-html5/css/slick.css b/gns3server/static/web-ui/assets/xpra-html5/css/slick.css new file mode 100644 index 000000000..851309019 --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/css/slick.css @@ -0,0 +1,109 @@ +/* Slider */ +.slick-slider { + position: relative; + + display: block; + box-sizing: border-box; + + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + -webkit-touch-callout: none; + -ms-touch-action: pan-y; + touch-action: pan-y; + -webkit-tap-highlight-color: transparent; +} + +.slick-list { + position: relative; + + display: block; + overflow: hidden; + + margin: 0; + padding: 0; +} + +.slick-list:focus { + outline: none; +} + +.slick-list.dragging { + cursor: hand; +} + +.slick-slider .slick-track, +.slick-slider .slick-list { + transform: translate3d(0, 0, 0); +} + +.slick-track { + position: relative; + top: 0; + left: 0; + + display: block; + margin-left: auto; + margin-right: auto; +} + +.slick-track:before, +.slick-track:after { + display: table; + + content: ""; +} + +.slick-track:after { + clear: both; +} + +.slick-loading .slick-track { + visibility: hidden; +} + +.slick-slide { + display: none; + float: left; + + height: 100%; + min-height: 1px; +} + +[dir="rtl"] .slick-slide { + float: right; +} + +.slick-slide img { + display: block; +} + +.slick-slide.slick-loading img { + display: none; +} + +.slick-slide.dragging img { + pointer-events: none; +} + +.slick-initialized .slick-slide { + display: block; +} + +.slick-loading .slick-slide { + visibility: hidden; +} + +.slick-vertical .slick-slide { + display: block; + + height: auto; + + border: 1px solid transparent; +} + +.slick-arrow.slick-hidden { + display: none; +} diff --git a/gns3server/static/web-ui/assets/xpra-html5/css/spinner.css b/gns3server/static/web-ui/assets/xpra-html5/css/spinner.css new file mode 100644 index 000000000..e26319970 --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/css/spinner.css @@ -0,0 +1,41 @@ +.spinner { + margin: 60px auto; + font-size: 10px; + position: relative; + text-indent: -9999em; + border-top: 1.1em solid rgba(255, 255, 255, 0.2); + border-right: 1.1em solid rgba(255, 255, 255, 0.2); + border-bottom: 1.1em solid rgba(255, 255, 255, 0.2); + border-left: 1.1em solid #ffffff; + -ms-transform: translateZ(0); + transform: translateZ(0); + -webkit-animation: load8 1.1s infinite linear; + animation: load8 1.1s infinite linear; +} + +.spinner, +.spinner:after { + border-radius: 50%; + width: 10em; + height: 10em; +} + +@-webkit-keyframes load8 { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +@keyframes load8 { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} diff --git a/gns3server/static/web-ui/assets/xpra-html5/default-settings.txt b/gns3server/static/web-ui/assets/xpra-html5/default-settings.txt new file mode 100644 index 000000000..fafe7542d --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/default-settings.txt @@ -0,0 +1,34 @@ +# Xpra HTML5 default settings +# +# ie: +# port = 10000 +# keyboard_layout = gb +# encoding = auto +# bandwidth_limit = 10000000 +# debug_keyboard = true + +blocked-hosts = xpra.org,www.xpra.org + +min-quality = 10 +min-speed = 50 + +# Configure xpra HTML5 client defaults - disable unused features +file_transfer=false +printing=false +sound=false +audio=false +sound_button=false +fullscreen_button=false +keyboard_button=false +cursor_lock_button=false +keyboard=false +mediasource=false +aurora=false +http-stream=false + +# Menu toolbar position: top-left, top, top-right, novnc +toolbar_position=top-right + +# hide some menus: +# xpramenu=false +# cursor_lock_button=false diff --git a/gns3server/static/web-ui/assets/xpra-html5/digest.html b/gns3server/static/web-ui/assets/xpra-html5/digest.html new file mode 100644 index 000000000..d67e234f2 --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/digest.html @@ -0,0 +1,82 @@ + + + + + + + Xpra HTML5 AES Test Page + + + + + + + +
+
+

+      
+ +
+ + + + + diff --git a/gns3server/static/web-ui/assets/xpra-html5/favicon.ico b/gns3server/static/web-ui/assets/xpra-html5/favicon.ico new file mode 100644 index 000000000..6b05128e6 Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/favicon.ico differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/favicon.png b/gns3server/static/web-ui/assets/xpra-html5/favicon.png new file mode 100644 index 000000000..28c5f38bb Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/favicon.png differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/authentication.png b/gns3server/static/web-ui/assets/xpra-html5/icons/authentication.png new file mode 100644 index 000000000..1c75c0513 Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/authentication.png differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/close.png b/gns3server/static/web-ui/assets/xpra-html5/icons/close.png new file mode 100644 index 000000000..3c29292d4 Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/close.png differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/default_cursor.png b/gns3server/static/web-ui/assets/xpra-html5/icons/default_cursor.png new file mode 100644 index 000000000..aa39aa4a6 Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/default_cursor.png differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/empty.png b/gns3server/static/web-ui/assets/xpra-html5/icons/empty.png new file mode 100644 index 000000000..eaf1b55b1 Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/empty.png differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/eye-slash.png b/gns3server/static/web-ui/assets/xpra-html5/icons/eye-slash.png new file mode 100644 index 000000000..5015fd569 Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/eye-slash.png differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/eye.png b/gns3server/static/web-ui/assets/xpra-html5/icons/eye.png new file mode 100644 index 000000000..76b9fc909 Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/eye.png differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/fullscreen.png b/gns3server/static/web-ui/assets/xpra-html5/icons/fullscreen.png new file mode 100644 index 000000000..4c4aebb99 Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/fullscreen.png differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/materialicons-regular.ttf b/gns3server/static/web-ui/assets/xpra-html5/icons/materialicons-regular.ttf new file mode 100644 index 000000000..9d09b0feb Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/materialicons-regular.ttf differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/materialicons-regular.woff b/gns3server/static/web-ui/assets/xpra-html5/icons/materialicons-regular.woff new file mode 100644 index 000000000..4c67ea26c Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/materialicons-regular.woff differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/materialicons-regular.woff2 b/gns3server/static/web-ui/assets/xpra-html5/icons/materialicons-regular.woff2 new file mode 100644 index 000000000..5492a6e75 Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/materialicons-regular.woff2 differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/maximize.png b/gns3server/static/web-ui/assets/xpra-html5/icons/maximize.png new file mode 100644 index 000000000..e487352ba Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/maximize.png differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/minimize.png b/gns3server/static/web-ui/assets/xpra-html5/icons/minimize.png new file mode 100644 index 000000000..608a633da Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/minimize.png differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/noicon.png b/gns3server/static/web-ui/assets/xpra-html5/icons/noicon.png new file mode 100644 index 000000000..5e6ce3e17 Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/noicon.png differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/unfullscreen.png b/gns3server/static/web-ui/assets/xpra-html5/icons/unfullscreen.png new file mode 100644 index 000000000..dfd67631f Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/unfullscreen.png differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/icons/xpra-logo.png b/gns3server/static/web-ui/assets/xpra-html5/icons/xpra-logo.png new file mode 100644 index 000000000..6e326d977 Binary files /dev/null and b/gns3server/static/web-ui/assets/xpra-html5/icons/xpra-logo.png differ diff --git a/gns3server/static/web-ui/assets/xpra-html5/index.html b/gns3server/static/web-ui/assets/xpra-html5/index.html new file mode 100644 index 000000000..cb4908c3a --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/index.html @@ -0,0 +1,1600 @@ + + + + + + + xpra websockets client + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+
+ +
+ +
+ +
+
+
+ + +
+
+
+
+
+ +
+ +
+ +
+ +
+ +
+ shadow pointer +
+ +
+ +
+ +
+ +
+ +
+

Xpra HTML5 Client

+

Version 19

+ + Copyright (c) 2013 Antoine Martin <antoine@xpra.org> +
+ Copyright (c) 2014 Joshua Higgins <josh@kxes.net> +
+
+
+ + This code is available under the + Mozilla Public License Version 2.0 +
+ For more information, see + xpra HTML5 project. +
+
+ +
+

Session Information

+

Connection Data

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Session Connection Data Information +
Server Endpoint
Server Display
Server Platform
Server Load
Session Connected
Server Latency
Client Latency
+
+ +
+

Xpra Bug Report

+
+
+ Summary: + +
+ Description: +
+ +
+ + +
+
+ +
+ +
+ +
+ + + + + diff --git a/gns3server/static/web-ui/assets/xpra-html5/js/Client.js b/gns3server/static/web-ui/assets/xpra-html5/js/Client.js new file mode 100644 index 000000000..630e42687 --- /dev/null +++ b/gns3server/static/web-ui/assets/xpra-html5/js/Client.js @@ -0,0 +1,4991 @@ +/* + * Copyright (c) 2013 Antoine Martin + * Copyright (c) 2016 David Brushinski + * Copyright (c) 2014 Joshua Higgins + * Copyright (c) 2015 Spikes, Inc. + * Licensed under MPL 2.0 + * + * xpra client + * + * requires: + * Protocol.js + * Window.js + * Keycodes.js + */ + +const WORKER = Boolean(window.Worker); +const CLIPBOARD_IMAGES = true; +const CLIPBOARD_EVENT_DELAY = 100; +const DECODE_WORKER = WORKER && !!window.createImageBitmap; +const SHOW_START_MENU = true; +const FILE_SIZE_LIMIT = 4 * 1024 * 1024 * 1024; //are we even allowed to allocate this much memory? +const FILE_CHUNKS_SIZE = 128 * 1024; +const MAX_CONCURRENT_FILES = 5; +const CHUNK_TIMEOUT = 10 * 1000; +const MODAL_FOCUS = true; + +const TEXT_PLAIN = "text/plain"; +const UTF8_STRING = "UTF8_STRING"; +const TEXT_HTML = "text/html"; + +const FLOAT_MENU_SELECTOR = "#float_menu"; +const PASTEBOARD_SELECTOR = "#pasteboard"; +const WINDOW_PREVIEW_SELECTOR = "#window_preview"; + +const BELL_SOUND = "data:audio/wav;base64,//uQRAAAAWMSLwUIYAAsYkXgoQwAEaYLWfkWgAI0wWs/ItAAAGDgYtAgAyN+QWaAAihwMWm4G8QQRDiMcCBcH3Cc+CDv/7xA4Tvh9Rz/y8QADBwMWgQAZG/ILNAARQ4GLTcDeIIIhxGOBAuD7hOfBB3/94gcJ3w+o5/5eIAIAAAVwWgQAVQ2ORaIQwEMAJiDg95G4nQL7mQVWI6GwRcfsZAcsKkJvxgxEjzFUgfHoSQ9Qq7KNwqHwuB13MA4a1q/DmBrHgPcmjiGoh//EwC5nGPEmS4RcfkVKOhJf+WOgoxJclFz3kgn//dBA+ya1GhurNn8zb//9NNutNuhz31f////9vt///z+IdAEAAAK4LQIAKobHItEIYCGAExBwe8jcToF9zIKrEdDYIuP2MgOWFSE34wYiR5iqQPj0JIeoVdlG4VD4XA67mAcNa1fhzA1jwHuTRxDUQ//iYBczjHiTJcIuPyKlHQkv/LHQUYkuSi57yQT//uggfZNajQ3Vmz+Zt//+mm3Wm3Q576v////+32///5/EOgAAADVghQAAAAA//uQZAUAB1WI0PZugAAAAAoQwAAAEk3nRd2qAAAAACiDgAAAAAAABCqEEQRLCgwpBGMlJkIz8jKhGvj4k6jzRnqasNKIeoh5gI7BJaC1A1AoNBjJgbyApVS4IDlZgDU5WUAxEKDNmmALHzZp0Fkz1FMTmGFl1FMEyodIavcCAUHDWrKAIA4aa2oCgILEBupZgHvAhEBcZ6joQBxS76AgccrFlczBvKLC0QI2cBoCFvfTDAo7eoOQInqDPBtvrDEZBNYN5xwNwxQRfw8ZQ5wQVLvO8OYU+mHvFLlDh05Mdg7BT6YrRPpCBznMB2r//xKJjyyOh+cImr2/4doscwD6neZjuZR4AgAABYAAAABy1xcdQtxYBYYZdifkUDgzzXaXn98Z0oi9ILU5mBjFANmRwlVJ3/6jYDAmxaiDG3/6xjQQCCKkRb/6kg/wW+kSJ5//rLobkLSiKmqP/0ikJuDaSaSf/6JiLYLEYnW/+kXg1WRVJL/9EmQ1YZIsv/6Qzwy5qk7/+tEU0nkls3/zIUMPKNX/6yZLf+kFgAfgGyLFAUwY//uQZAUABcd5UiNPVXAAAApAAAAAE0VZQKw9ISAAACgAAAAAVQIygIElVrFkBS+Jhi+EAuu+lKAkYUEIsmEAEoMeDmCETMvfSHTGkF5RWH7kz/ESHWPAq/kcCRhqBtMdokPdM7vil7RG98A2sc7zO6ZvTdM7pmOUAZTnJW+NXxqmd41dqJ6mLTXxrPpnV8avaIf5SvL7pndPvPpndJR9Kuu8fePvuiuhorgWjp7Mf/PRjxcFCPDkW31srioCExivv9lcwKEaHsf/7ow2Fl1T/9RkXgEhYElAoCLFtMArxwivDJJ+bR1HTKJdlEoTELCIqgEwVGSQ+hIm0NbK8WXcTEI0UPoa2NbG4y2K00JEWbZavJXkYaqo9CRHS55FcZTjKEk3NKoCYUnSQ0rWxrZbFKbKIhOKPZe1cJKzZSaQrIyULHDZmV5K4xySsDRKWOruanGtjLJXFEmwaIbDLX0hIPBUQPVFVkQkDoUNfSoDgQGKPekoxeGzA4DUvnn4bxzcZrtJyipKfPNy5w+9lnXwgqsiyHNeSVpemw4bWb9psYeq//uQZBoABQt4yMVxYAIAAAkQoAAAHvYpL5m6AAgAACXDAAAAD59jblTirQe9upFsmZbpMudy7Lz1X1DYsxOOSWpfPqNX2WqktK0DMvuGwlbNj44TleLPQ+Gsfb+GOWOKJoIrWb3cIMeeON6lz2umTqMXV8Mj30yWPpjoSa9ujK8SyeJP5y5mOW1D6hvLepeveEAEDo0mgCRClOEgANv3B9a6fikgUSu/DmAMATrGx7nng5p5iimPNZsfQLYB2sDLIkzRKZOHGAaUyDcpFBSLG9MCQALgAIgQs2YunOszLSAyQYPVC2YdGGeHD2dTdJk1pAHGAWDjnkcLKFymS3RQZTInzySoBwMG0QueC3gMsCEYxUqlrcxK6k1LQQcsmyYeQPdC2YfuGPASCBkcVMQQqpVJshui1tkXQJQV0OXGAZMXSOEEBRirXbVRQW7ugq7IM7rPWSZyDlM3IuNEkxzCOJ0ny2ThNkyRai1b6ev//3dzNGzNb//4uAvHT5sURcZCFcuKLhOFs8mLAAEAt4UWAAIABAAAAAB4qbHo0tIjVkUU//uQZAwABfSFz3ZqQAAAAAngwAAAE1HjMp2qAAAAACZDgAAAD5UkTE1UgZEUExqYynN1qZvqIOREEFmBcJQkwdxiFtw0qEOkGYfRDifBui9MQg4QAHAqWtAWHoCxu1Yf4VfWLPIM2mHDFsbQEVGwyqQoQcwnfHeIkNt9YnkiaS1oizycqJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU="; + +const SAFE_ENCODINGS = [ + "jpeg", + "png", + "png/P", + "png/L", + "rgb", + "rgb32", + "rgb24", + "scroll", + "void", +]; + +const METADATA_SUPPORTED = [ + "fullscreen", "maximized", + "iconic", "above", "below", + "title", "size-hints", + "class-instance", "transient-for", "window-type", + "has-alpha", "decorations", "override-redirect", + "tray", "modal", "opacity", +] + +// This option adds the CSS class .gpu-trigger to the windows. +// The article at https://www.urbaninsight.com/article/improving-html5-app-performance-gpu-accelerated-css-transitions +// states that adding 'transform: transale3d(0,0,0);' is the strongest CSS indication for the browser to enable hardware acceleration. +const TRY_GPU_TRIGGER = true; + +function truncate(input) { + if (!input) { + return input; + } + const s = input.toString(); + if (s.length > 5) { + return s.slice(0, 5) + "..."; // eslint-disable-line prefer-template + } + return s; +} + +class XpraClient { + constructor(container) { + // the container div is the "screen" on the HTML page where we + // are able to draw our windows in. + this.container = document.querySelector(`#${container}`); + if (!this.container) { + throw new Error("invalid container element"); + } + // assign callback for window resize event + if (window.jQuery) { + jQuery(window).resize( + jQuery.debounce(250, (e) => this._screen_resized(e)) + ); + } + + this.protocol = null; + + this.init_settings(); + this.init_state(); + } + + init_settings() { + //server: + this.host = null; + this.port = null; + this.ssl = null; + this.webtransport = false; + this.path = ""; + this.username = ""; + this.passwords = []; + this.insecure = false; + this.uri = ""; + this.packet_encoder = null; + //connection options: + this.sharing = false; + this.open_url = true; + this.steal = true; + this.remote_logging = true; + this.debug_categories = []; + this.start_new_session = null; + this.clipboard_enabled = false; + this.clipboard_poll = false; + this.clipboard_preferred_format = TEXT_PLAIN; + this.file_transfer = false; + this.remote_file_size_limit = 0; + this.remote_file_chunks = 0; + this.send_chunks_in_progress = new Map(); + this.receive_chunks_in_progress = new Map(); + this.keyboard_layout = null; + this.printing = false; + this.key_packets = []; + this.clipboard_delayed_event_time = 0; + + this.scale = 1; + this.vrefresh = -1; + this.bandwidth_limit = 0; + this.reconnect = true; + this.reconnect_count = 10; + this.reconnect_in_progress = false; + this.reconnect_delay = 2000; //wait 2 seconds before retrying + this.reconnect_attempt = 0; + this.swap_keys = Utilities.isMacOS(); + this.HELLO_TIMEOUT = 30_000; + this.OPEN_TIMEOUT = 10_000; + this.PING_TIMEOUT = 15_000; + this.PING_GRACE = 2000; + this.PING_FREQUENCY = 5000; + this.INFO_FREQUENCY = 1000; + this.uuid = Utilities.getHexUUID(); + this.offscreen_api = false; + this.try_gpu = TRY_GPU_TRIGGER; + + this.init_encodings(); + } + + init_encodings() { + this.encoding = "auto"; + //basic set of encodings: + //(more may be added after checking via the DecodeWorker) + this.supported_encodings = [ + "jpeg", + "png", + "png/P", + "png/L", + "rgb", + "rgb32", + "rgb24", + "scroll", + "void", + ]; + //extra encodings we enable if validated via the decode worker: + //(we also validate jpeg and png as a sanity check) + this.check_encodings = [ + "jpeg", + "png", + "png/P", + "png/L", + "rgb", + "rgb32", + "rgb24", + "scroll", + "webp", + "void", + "avif", + ]; + // this may be overriden after detecting the offscreen worker: + const video_max_size = [1024, 768]; + this.encoding_options = { + "": this.encoding, + "icons": { + "max_size": [30, 30], + "greedy": true, + }, + "transparency": true, + "rgb_lz4": (lz4 && lz4.decode != "undefined"), + "decoder-speed": { + "video": 0 + }, + "color-gamut": Utilities.getColorGamut(), + "video_scaling": true, + "video_max_size": video_max_size, + "full_csc_modes": { + "mpeg1": ["YUV420P"], + "h264": ["YUV420P"], + "mpeg4+mp4": ["YUV420P"], + "h264+mp4": ["YUV420P"], + "vp8+webm": ["YUV420P"], + "webp": ["BGRX", "BGRA"], + "jpeg": ["BGRX", "BGRA", "BGR", "RGBX", "RGBA", "RGB", "YUV420P", "YUV422P", "YUV444P"], + "vp8": ["YUV420P"], + }, + "h264": { + "score-delta": 80, + "YUV420P": { + "profile": "baseline", + "level": "2.1", + "cabac": false, + "deblocking-filter": false, + "fast-decode": true, + }, + }, + "h264+mp4": { + "score-delta": 50, + "YUV420P": { + "profile": "baseline", + "level": "3.0", + }, + }, + //prefer unmuxed VPX + "vp8": { + "score-delta": 70, + }, + "mpeg4+mp4": { + "score-delta": 40, + }, + "vp8+webm": { + "score-delta": 40, + }, + }; + } + + init_state() { + // state + this.connected = false; + this.session_name = undefined; + this.desktop_width = 0; + this.desktop_height = 0; + this.desktop_width = this.container.clientWidth; + this.desktop_height = this.container.clientHeight; + this.server_remote_logging = false; + this.server_start_time = -1; + this.client_start_time = new Date(); + // some client stuff + this.capabilities = {}; + this.RGB_FORMATS = ["RGBX", "RGBA", "RGB"]; + this.disconnect_reason = null; + this.password_prompt_fn = null; + this.keycloak_prompt_fn = null; + // audio + this.audio = null; + this.audio_enabled = false; + this.audio_mediasource_enabled = MediaSourceUtil.getMediaSourceClass() != undefined; + this.audio_aurora_enabled = typeof AV !== "undefined" && + AV != undefined && + AV.Decoder != undefined && + AV.Player.fromXpraSource != undefined; + this.audio_codecs = {}; + this.audio_framework = null; + this.audio_aurora_ctx = null; + this.audio_codec = null; + this.audio_context = new AudioContext(); + this.audio_state = ""; + this.aurora_codecs = {}; + this.mediasource_codecs = {}; + // encryption + this.encryption = false; + this.encryption_key = null; + this.cipher_in_caps = null; + this.cipher_out_caps = null; + // detect locale change: + this.browser_language = Utilities.getFirstBrowserLanguage(); + this.browser_language_change_embargo_time = 0; + this.key_layout = null; + this.last_keycode_pressed = 0; + this.last_key_packet = []; + // mouse + this.buttons_pressed = new Set(); + this.last_button_event = [-1, false, -1, -1]; + this.mousedown_event = null; + this.mouseup_event = null; + this.last_mouse_x = null; + this.last_mouse_y = null; + this.wheel_delta_x = 0; + this.wheel_delta_y = 0; + this.mouse_grabbed = false; + this.scroll_reverse_x = false; + this.scroll_reverse_y = "auto"; + // clipboard + this.clipboard_direction = default_settings["clipboard_direction"] || "both"; + this.clipboard_datatype = null; + this.clipboard_buffer = ""; + this.clipboard_server_buffers = {}; + this.clipboard_pending = false; + this.clipboard_targets = [TEXT_HTML, UTF8_STRING, "TEXT", "STRING", TEXT_PLAIN]; + // printing / file-transfer: + this.remote_printing = false; + this.remote_file_transfer = false; + this.remote_open_files = false; + // hello + this.hello_timer = null; + this.open_timer = null; + this.info_timer = null; + this.info_request_pending = false; + this.server_last_info = {}; + // ping + this.ping_timeout_timer = null; + this.ping_grace_timer = null; + this.ping_timer = null; + this.last_ping_server_time = 0; + this.last_ping_local_time = 0; + this.last_ping_echoed_time = 0; + this.server_ping_latency = 0; + this.client_ping_latency = 0; + this.server_load = null; + this.server_ok = false; + //packet handling + this.decode_worker = null; + this.decode_worker_timeout = 0; + // floating menu + this.toolbar_position = "top"; + + this.server_display = ""; + this.server_platform = ""; + this.server_resize_exact = false; + this.server_screen_sizes = []; + this.server_is_desktop = false; + this.server_is_shadow = false; + this.server_readonly = false; + + this.server_connection_data = false; + + this.xdg_menu = null; + // a list of our windows + this.id_to_window = {}; + this.ui_events = 0; + this.pending_redraw = []; + this.draw_pending = 0; + // basic window management + this.focused_wid = 0; + + const me = this; + const screen_element = jQuery("#screen"); + screen_element.mousedown((e) => this.on_mousedown(e)); + screen_element.mouseup((e) => this.on_mouseup(e)); + document.getElementById("screen").addEventListener("mousemove", (e) => this.on_mousemove(e)); + + const div = document.querySelector("#screen"); + + function on_mousescroll(e) { + me.on_mousescroll(e); + return e.preventDefault(); + } + if (Utilities.isEventSupported("wheel")) { + div.addEventListener("wheel", on_mousescroll, false); + } else { + this.warn("browser does not support scroll wheel events"); + } + } + + send() { + this.debug("network", "sending a", arguments[0], "packet"); + if (this.protocol) { + this.protocol.send.apply(this.protocol, arguments); + } + } + + send_log(level, arguments_) { + if (this.remote_logging && this.server_remote_logging && this.connected) { + try { + const sargs = []; + for (const argument of arguments_) { + sargs.push(unescape(encodeURIComponent(String(argument)))); + } + this.send([PACKET_TYPES.logging, level, sargs]); + } catch { + this.cerror("remote logging failed"); + for (const index in arguments_) { + const argument = arguments_[index]; + this.clog(" argument", index, typeof argument, ":", `'${argument}'`); + } + } + } + } + exc() { + //first argument is the exception: + const exception = arguments[0]; + let arguments_ = [...arguments]; + arguments_ = arguments_.splice(1); + if (arguments_.length > 0) { + this.cerror(arguments_); + } + if (exception.stack) { + try { + //logging.ERROR = 40 + this.send_log(40, [exception.stack]); + } catch { + //we tried our best + } + } + } + error() { + //logging.ERROR = 40 + this.send_log(40, arguments); + Reflect.apply(this.cerror, this, arguments); + } + cerror() { + Utilities.cerror.apply(Utilities, arguments); + } + warn() { + //logging.WARN = 30 + this.send_log(30, arguments); + Reflect.apply(this.cwarn, this, arguments); + } + cwarn() { + Utilities.cwarn.apply(Utilities, arguments); + } + log() { + //logging.INFO = 20 + this.send_log(20, arguments); + Reflect.apply(this.clog, this, arguments); + } + clog() { + Utilities.clog.apply(Utilities, arguments); + } + debug() { + const category = arguments[0]; + const arguments_ = [...arguments]; + if (this.debug_categories.includes(category)) { + if (category !== "network") { + //logging.DEBUG = 10 + this.send_log(10, arguments_); + } + Reflect.apply(this.cdebug, this, arguments); + } + } + cdebug() { + Utilities.cdebug.apply(Utilities, arguments); + } + + init(ignore_blacklist) { + this.on_connection_progress("Initializing", "", 20); + this.init_audio(ignore_blacklist); + this.init_packet_handlers(); + this.init_keyboard(); + if (this.scale !== 1) { + this.container.style.width = `${100 * this.scale}%`; + this.container.style.height = `${100 * this.scale}%`; + this.container.style.transform = `scale(${1 / this.scale})`; + this.container.style.transformOrigin = "top left"; + } + } + + init_packet_handlers() { + // the client holds a list of packet handlers + this.packet_handlers = { + [PACKET_TYPES.control]: this._process_control, + [PACKET_TYPES.ack_file_chunk]: this._process_ack_file_chunk, + [PACKET_TYPES.bell]: this._process_bell, + [PACKET_TYPES.challenge]: this._process_challenge, + [PACKET_TYPES.clipboard_request]: this._process_clipboard_request, + [PACKET_TYPES.clipboard_token]: this._process_clipboard_token, + [PACKET_TYPES.close]: this._process_close, + [PACKET_TYPES.configure_override_redirect]: this._process_configure_override_redirect, + [PACKET_TYPES.cursor]: this._process_cursor, + [PACKET_TYPES.desktop_size]: this._process_desktop_size, + [PACKET_TYPES.disconnect]: this._process_disconnect, + [PACKET_TYPES.draw]: this._process_draw, + [PACKET_TYPES.encodings]: this._process_encodings, + [PACKET_TYPES.eos]: this._process_eos, + [PACKET_TYPES.error]: this._process_error, + [PACKET_TYPES.hello]: this._process_hello, + [PACKET_TYPES.info_response]: this._process_info_response, + [PACKET_TYPES.initiate_moveresize]: this._process_initiate_moveresize, + [PACKET_TYPES.lost_window]: this._process_lost_window, + [PACKET_TYPES.new_override_redirect]: this._process_new_override_redirect, + [PACKET_TYPES.new_tray]: this._process_new_tray, + [PACKET_TYPES.new_window]: this._process_new_window, + [PACKET_TYPES.notify_close]: this._process_notify_close, + [PACKET_TYPES.notify_show]: this._process_notify_show, + [PACKET_TYPES.open]: this._process_open, + [PACKET_TYPES.open_url]: this._process_open_url, + [PACKET_TYPES.ping]: this._process_ping, + [PACKET_TYPES.ping_echo]: this._process_ping_echo, + [PACKET_TYPES.pointer_position]: this._process_pointer_position, + [PACKET_TYPES.raise_window]: this._process_raise_window, + [PACKET_TYPES.send_file]: this._process_send_file, + [PACKET_TYPES.send_file_chunk]: this._process_send_file_chunk, + [PACKET_TYPES.set_clipboard_enabled]: this._process_set_clipboard_enabled, + [PACKET_TYPES.setting_change]: this._process_setting_change, + [PACKET_TYPES.sound_data]: this._process_sound_data, + [PACKET_TYPES.startup_complete]: this._process_startup_complete, + [PACKET_TYPES.window_icon]: this._process_window_icon, + [PACKET_TYPES.window_metadata]: this._process_window_metadata, + [PACKET_TYPES.window_move_resize]: this._process_window_move_resize, + [PACKET_TYPES.window_resized]: this._process_window_resized, + }; + } + + on_connection_progress(state, details, progress) { + //can be overriden + this.clog(state, details); + } + + callback_close(reason) { + if (reason === undefined) { + reason = "unknown reason"; + } + this.clog(`connection closed: ${reason}`); + } + + connect() { + let details = `${this.host}:${this.port}${this.path}`; + if (this.ssl) { + details += " with ssl"; + } + this.schedule_open_timer(); + this.on_connection_progress("Connecting to server", details, 30); + // open the web socket, started it in a worker if available + // check we have enough information for encryption + if (this.encryption && !this.encryption_key) { + this.disconnect("no key specified for encryption"); + return; + } + this.initialize_workers(); + } + + initialize_workers() { + // detect webworker support and degrade gracefully + if (window.Worker) { + this.clog("we have webworker support"); + } + else { + this.clog("no webworker support at all."); + } + + if (this.webtransport) { + this.protocol = new XpraWebTransportProtocol(); + } else if (WORKER) { + this.protocol = new XpraProtocolWorkerHost(); + } else { + this.protocol = new XpraProtocol(); + } + this.open_protocol(); + + this.decode_worker_timeout = false; + if (!DECODE_WORKER) { + this.supported_encodings = SAFE_ENCODINGS; + this.offscreen_api = false; + this.decode_worker = null; + return; + } + + let decode_worker; + if (this.offscreen_api) { + // check that it is actually available: + this.offscreen_api = DECODE_WORKER && XpraOffscreenWorker.isAvailable(this.ssl); + } + + if (this.offscreen_api) { + this.clog("initializing offscreen decode worker"); + decode_worker = new Worker("js/OffscreenDecodeWorker.js"); + } else { + this.clog("initializing regular decode worker"); + decode_worker = new Worker("js/DecodeWorker.js"); + } + decode_worker.addEventListener( + "message", + (e) => { + const data = e.data; + if (data["draw"]) { + this.do_process_draw(data["draw"], data["start"]); + return; + } + if (data["error"]) { + const message = data["error"]; + const packet = data["packet"]; + const wid = packet[1]; + const width = packet[2]; + const height = packet[3]; + const coding = packet[6]; + const packet_sequence = packet[8]; + this.clog("decode error on ", coding, "packet sequence", packet_sequence, ":", message); + if (!this.offscreen_api) { + this.clog(" pixel data:", packet[7]); + } + this.do_send_damage_sequence(packet_sequence, wid, width, height, -1, message); + return; + } + switch (data["result"]) { + case true: { + const formats = [...data["formats"]]; + this.clog("we can decode using a worker:", decode_worker); + if (this.decode_worker_timeout) { + this.clog("but worker initialization took too long!"); + decode_worker.postMessage({cmd: "close"}); + this.decode_worker = null; + } + else { + if (this.offscreen_api) { + this.set_encoding_option('video_max_size', [4096, 4096]); + } + this.supported_encodings = formats; + this.clog("full list of supported encodings:", this.supported_encodings); + this.decode_worker = decode_worker; + } + break; + } + case false: + this.clog(`we can't decode using a worker: ${data["errors"]}`); + this.decode_worker = null; + break; + default: + this.clog("client got unknown message from the decode worker"); + this.decode_worker = null; + } + }, + false + ); + this.clog("decode worker will check:", this.check_encodings); + decode_worker.postMessage({cmd: "check", encodings: this.check_encodings}); + } + + open_protocol() { + // set protocol to deliver packets to our packet router + this.protocol.set_packet_handler((packet) => this._route_packet(packet)); + // make uri + let uri = ""; + if (this.webtransport) { + uri = "https"; + } else { + if (this.ssl) { + uri = "wss"; + } else { + uri = "ws"; + } + } + uri += "://"; + uri += this.host; + if (this.port) { + uri += `:${this.port}`; + } + uri += this.path; + // do open + this.uri = uri; + this.on_connection_progress("Opening WebSocket connection", uri, 50); + this.protocol.open(uri); + // console.log("open_protocol() done"); + } + + request_refresh(wid) { + this.send([PACKET_TYPES.buffer_refresh, wid, 0, 100, + { + "refresh-now": true, + batch: { + reset: true + }, + }, + {}, //no client_properties + ]); + } + + redraw_windows() { + for (const wid in this.id_to_window) { + const win = this.id_to_window[wid]; + this.request_redraw(win); + } + } + + remove_windows() { + for (const wid in this.id_to_window) { + const win = this.id_to_window[wid]; + window.removeWindowListItem(win.wid); + win.destroy(); + } + if (this.decode_worker) { + this.decode_worker.postMessage({cmd: "close"}); + this.decode_worker = null; + } + } + + send_close_window(win) { + this.send([PACKET_TYPES.close_window, win.wid]); + } + + close_protocol() { + this.connected = false; + if (this.protocol) { + this.protocol.close(); + this.protocol.terminate(); + this.protocol = null; + } + } + + clear_timers() { + this.stop_info_timer(); + this.cancel_hello_timer(); + if (this.ping_timer) { + clearTimeout(this.ping_timer); + this.ping_timer = null; + } + if (this.ping_timeout_timer) { + clearTimeout(this.ping_timeout_timer); + this.ping_timeout_timer = null; + } + if (this.ping_grace_timer) { + clearTimeout(this.ping_grace_timer); + this.ping_grace_timer = null; + } + } + + set_encoding(encoding) { + // add an encoding to our hello.encodings list + this.clog("encoding:", encoding); + this.encoding = encoding; + } + + set_encoding_option(option, value) { + this.clog("encoding: ", option, "=", value); + this.encoding_options[option] = value; + } + + _route_packet(packet) { + // ctx refers to `this` because we came through a callback + const packet_type = Utilities.s(packet[0]); + this.debug("network", "received a", packet_type, "packet"); + const function_ = this.packet_handlers[packet_type]; + if (function_ === undefined) { + this.cerror("no packet handler for ", packet_type); + this.clog(packet); + } else { + function_.call(this, packet); + } + } + + _screen_resized(event) { + // send the desktop_size packet so server knows we changed size + if (!this.connected) { + return; + } + if (this.container.clientWidth === this.desktop_width && this.container.clientHeight === this.desktop_height) { + // unchanged + return; + } + this.desktop_width = this.container.clientWidth; + this.desktop_height = this.container.clientHeight; + const dpi = this._get_DPI(); + const packet = [PACKET_TYPES.configure_display, { + "desktop-size": [this.desktop_width, this.desktop_height], + // "desktop-size-unscaled": [this.desktop_width, this.desktop_height], - we don't do desktop scaling + "monitors": this._get_monitors(), + "dpi": {"x": dpi, "y": dpi}, + "vrefresh": this.vrefresh, + }]; + this.send(packet); + // call the screen_resized function on all open windows + for (const index in this.id_to_window) { + const win = this.id_to_window[index]; + win.screen_resized(); + + // Force fullscreen on a a given window name from the provided settings + if ( + default_settings !== undefined && + default_settings.auto_fullscreen !== undefined && + default_settings.auto_fullscreen.length > 0 + ) { + const pattern = new RegExp(`.*${default_settings.auto_fullscreen}.*`); + if (win.fullscreen === false && pattern.test(win.metadata.title)) { + clog(`auto fullscreen window: ${win.metadata.title}`); + win.set_fullscreen(true); + win.screen_resized(); + } + } + + // Make a DESKTOP-type window fullscreen automatically. + // This resizes things like xfdesktop according to the window size. + if (this.fullscreen === false && this.client.is_window_desktop(win)) { + clog(`auto fullscreen desktop window: ${this.metadata.title}`); + this.set_fullscreen(true); + this.screen_resized(); + } + } + // Re-position floating toolbar menu + this.position_float_menu(); + } + + /** + * Keyboard + */ + init_keyboard() { + this.query_keyboard_map(); + } + do_init_keyboard() { + // modifier keys: + this.num_lock_modifier = null; + this.alt_modifier = null; + this.control_modifier = "control"; + this.meta_modifier = null; + this.altgr_modifier = null; + this.altgr_state = false; + + this.capture_keyboard = false; + // assign the key callbacks + document.addEventListener("keydown", (e) => { + const preview_element = $(WINDOW_PREVIEW_SELECTOR); + if (e.code === "Escape" && preview_element.is(":visible")) { + client.toggle_window_preview(); + return e.stopPropagation() || e.preventDefault(); + } + if (e.code === "Tab") { + if (preview_element.is(":visible")) { + // Select next for previous window. + const number_slides = $(".window-preview-item-container").length; + const current_slide = preview_element.slick("slickCurrentSlide"); + let next_index = current_slide; + next_index = e.shiftKey ? + (current_slide - 1) % number_slides : + (current_slide + 1) % number_slides; + preview_element.slick("goTo", next_index, true); + return e.stopPropagation() || e.preventDefault(); + } else if (e.altKey) { + // Alt+Tab shows window preview. and goes to the next window. + client.toggle_window_preview((e, slick) => { + const number_slides = slick.slideCount; + const current_slide = slick.currentSlide; + const next_index = (current_slide + 1) % number_slides; + setTimeout(() => { + slick.goTo(next_index, true); + }, 10); + }); + return e.stopPropagation() || e.preventDefault(); + } + } + const r = this._keyb_onkeydown(e); + if (!r) { + e.preventDefault(); + } + }); + document.addEventListener("keyup", (e) => { + if ( + (e.code === "Tab" || e.code.startsWith("Alt")) && + $(WINDOW_PREVIEW_SELECTOR).is(":visible") + ) { + if (e.code.startsWith("Alt")) { + client.toggle_window_preview(); + } + return e.stopPropagation() || e.preventDefault(); + } + const r = this._keyb_onkeyup(e); + if (!r) { + e.preventDefault(); + } + }); + } + + query_keyboard_map() { + const keyboard = navigator.keyboard; + this.keyboard_map = {}; + if (!navigator.keyboard) { + this.do_init_keyboard(); + return; + } + keyboard.getLayoutMap().then((keyboardLayoutMap) => { + clog("got a keyboard layout map:", keyboardLayoutMap); + clog("keys:", [...keyboardLayoutMap.keys()]); + for (const key of keyboardLayoutMap.keys()) { + const value = keyboardLayoutMap[key]; + cdebug("keyboard", key, "=", value); + this.keyboard_map[key] = value; + } + this.do_init_keyboard(); + }, + (error) => { + this.cwarn("failed to get keyboard layout:", error); + this.do_init_keyboard(); + } + ); + if (keyboard.addEventListener) { + keyboard.addEventListener("layoutchange", () => + this.clog("keyboard layout has changed!") + ); + } + } + + send_keymap() { + const keymap = this._get_keymap_caps(); + const props = {"keymap": keymap}; + this.send([PACKET_TYPES.keymap_changed, props, false]); + } + + _keyb_get_modifiers(event) { + /** + * Returns the modifiers set for the current event. + * We get the list of modifiers using "get_event_modifiers" + * then we translate them. + */ + //convert generic modifiers "meta" and "alt" into their x11 name: + const modifiers = get_event_modifiers(event); + return this.translate_modifiers(modifiers); + } + + translate_modifiers(modifiers) { + /** + * We translate "alt" and "meta" into their keymap name. + * (usually "mod1") + * And also swap keys for macos clients. + */ + //convert generic modifiers "meta" and "alt" into their x11 name: + const alt = this.alt_modifier; + let control = this.control_modifier; + let meta = this.meta_modifier; + const altgr = this.altgr_modifier; + if (this.swap_keys) { + meta = this.control_modifier; + control = this.meta_modifier; + } + + const new_modifiers = [...modifiers]; + let index = modifiers.indexOf("meta"); + if (index >= 0 && meta) new_modifiers[index] = meta; + index = modifiers.indexOf("control"); + if (index >= 0 && control) new_modifiers[index] = control; + index = modifiers.indexOf("alt"); + if (index >= 0 && alt) new_modifiers[index] = alt; + index = modifiers.indexOf("numlock"); + if (index >= 0) { + if (this.num_lock_modifier) { + new_modifiers[index] = this.num_lock_modifier; + } else { + new_modifiers.splice(index, 1); + } + } + index = modifiers.indexOf("capslock"); + if (index >= 0) { + new_modifiers[index] = "lock"; + } + + //add altgr? + if (this.altgr_state && altgr && !new_modifiers.includes(altgr)) { + new_modifiers.push(altgr); + //remove spurious modifiers: + index = new_modifiers.indexOf(alt); + if (index >= 0) new_modifiers.splice(index, 1); + index = new_modifiers.indexOf(control); + if (index >= 0) new_modifiers.splice(index, 1); + } + return new_modifiers; + } + + _check_browser_language(key_layout) { + /** + * This function may send the new detected keyboard layout. + * (ignoring the keyboard_layout preference) + */ + const now = performance.now(); + if (now < this.browser_language_change_embargo_time) { + return; + } + let new_layout; + if (key_layout) { + new_layout = key_layout; + } else { + //we may have used a different layout for a specific key, + //and now this new key doesn't need it anymore, + //so we may want to switch back to the original layout: + const l = Utilities.getFirstBrowserLanguage(); + if (l && this.browser_language !== l) { + //if the browser language has changed, + //this takes precedence over the configuration + this.clog("browser language changed from", this.browser_language, "to", l); + this.browser_language = l; + new_layout = Utilities.getKeyboardLayout(); + } else { + //this will honour the setting supplied by the user on the connect page + //or default to Utilities.getKeyboardLayout() + new_layout = this._get_keyboard_layout() || "us"; + } + } + if (new_layout && this.key_layout !== new_layout) { + this.key_layout = new_layout; + this.clog("keyboard layout changed from", this.key_layout, "to", key_layout); + this.send_keymap(); + //changing the language too quickly can cause problems server side, + //wait a bit before checking again: + this.browser_language_change_embargo_time = now + 1000; + } else { + //check again after 100ms minimum + this.browser_language_change_embargo_time = now + 100; + } + } + + _keyb_process(pressed, event) { + if (this.server_readonly) { + return true; + } + if (!this.capture_keyboard) { + return true; + } + /** + * Process a key event: key pressed or key released. + * Figure out the keycode, keyname, modifiers, etc + * And send the event to the server. + */ + + let keyname = event.code || ""; + const keycode = event.which || event.keyCode; + if (keycode === 229) { + //this usually fires when we have received the event via "oninput" already + return; + } + let keystring = event.key || String.fromCharCode(keycode); + let unpress_now = false; + this.debug("keyboard", "last keycode pressed=", this.last_keycode_pressed, ", keycode=", keycode, + ", pressed=", pressed, ", str=", keystring); + const dead = keystring.toLowerCase() === "dead"; + if (dead && ((this.last_keycode_pressed !== keycode && !pressed) || pressed)) { + //dead key unpress without first getting a key pressed event, + //or just a regular pressed dead key, in both cases send a pair: + pressed = true; + unpress_now = true; + } + + this.last_keycode_pressed = pressed ? keycode : 0; + + this.debug("keyboard", "processKeyEvent(", pressed, ", ", event, ") key=", keyname, "keycode=", keycode, "dead=", dead); + + //sync numlock + if (keycode === 144 && pressed) { + this.num_lock = !this.num_lock; + } + + let key_language = null; + //some special keys are better mapped by name: + const map_string = this.keyboard_map[keyname]; + if (dead && map_string && map_string in DEAD_KEYS) { + keyname = DEAD_KEYS[map_string]; + keystring = map_string; + this.debug("keyboard", "dead key:", keyname); + } else if (keyname in KEY_TO_NAME) { + keyname = KEY_TO_NAME[keyname]; + } else if (keyname === "" && keystring in KEY_TO_NAME) { + keyname = KEY_TO_NAME[keystring]; + } + //special case for numpad, + //try to distinguish arrowpad and numpad: + //(for arrowpad, keyname==str) + else if (keyname !== keystring && keystring in NUMPAD_TO_NAME) { + keyname = NUMPAD_TO_NAME[keystring]; + this.num_lock = "0123456789.".includes(keyname); + } + //next try mapping the actual character + else if (keystring in CHAR_TO_NAME) { + keyname = CHAR_TO_NAME[keystring]; + if (keyname.includes("_")) { + //ie: Thai_dochada + const lang = keyname.split("_")[0]; + key_language = KEYSYM_TO_LAYOUT[lang]; + } + } + //fallback to keycode map: + else { + if (keycode in CHARCODE_TO_NAME) { + keyname = CHARCODE_TO_NAME[keycode]; + } + //may override with shifted table: + if (event.getModifierState && event.getModifierState("Shift") && keycode in CHARCODE_TO_NAME_SHIFTED) { + keyname = CHARCODE_TO_NAME_SHIFTED[keycode]; + } + } + + this._check_browser_language(key_language); + + const DOM_KEY_LOCATION_RIGHT = 2; + if (keyname.match("_L$") && event.location === DOM_KEY_LOCATION_RIGHT) + keyname = keyname.replace("_L", "_R"); + + //AltGr: keep track of pressed state + if ( + keystring === "AltGraph" || + (keyname === "Alt_R" && (Utilities.isWindows() || Utilities.isMacOS())) || + (keyname === "Alt_L" && Utilities.isMacOS()) + ) { + this.altgr_state = pressed; + keyname = "ISO_Level3_Shift"; + keystring = "AltGraph"; + } + + const raw_modifiers = get_event_modifiers(event); + const modifiers = this._keyb_get_modifiers(event); + const keyval = keycode; + const group = 0; + + const shift = modifiers.includes("shift"); + const capslock = modifiers.includes("capslock"); + if ((capslock && shift) || (!capslock && !shift)) { + keystring = keystring.toLowerCase(); + } + + const ostr = keystring; + if (this.swap_keys) { + if (keyname === "Control_L") { + keyname = "Meta_L"; + keystring = "meta"; + } else if (keyname === "Meta_L") { + keyname = "Control_L"; + keystring = "control"; + } else if (keyname === "Control_R") { + keyname = "Meta_R"; + keystring = "meta"; + } else if (keyname === "Meta_R") { + keyname = "Control_R"; + keystring = "control"; + } + } + + //macos will swallow the key release event if the meta modifier is pressed, + //so simulate one immediately: + if (pressed && Utilities.isMacOS() && raw_modifiers.includes("meta") && ostr !== "meta") { + unpress_now = true; + } + + let allow_default = false; + if (this.clipboard_enabled && client.clipboard_direction !== "to-server") { + //allow some key events that need to be seen by the browser + //for handling the clipboard: + let clipboard_modifier_keys = ["Control_L", "Control_R", "Shift_L", "Shift_R"]; + let clipboard_modifier = "control"; + if (Utilities.isMacOS()) { + //Apple does things differently, as usual: + clipboard_modifier_keys = ["Meta_L", "Meta_R", "Shift_L", "Shift_R"]; + clipboard_modifier = "meta"; + } + //let the OS see Control (or Meta on macos) and Shift: + if (clipboard_modifier_keys.includes(keyname)) { + this.debug("keyboard", "passing clipboard modifier key event to browser:", keyname); + allow_default = true; + } + //let the OS see Shift + Insert: + if (shift && keyname === "Insert") { + this.debug("keyboard", "passing clipboard combination Shift+Insert to browser"); + allow_default = true; + } + const is_clipboard_modifier_set = raw_modifiers.includes(clipboard_modifier); + if (is_clipboard_modifier_set) { + const l = keyname.toLowerCase(); + if (l === "c" || l === "x" || l === "v") { + this.debug("keyboard", "passing clipboard combination to browser:", clipboard_modifier, "+", keyname); + allow_default = true; + if (l === "v") { + this.clipboard_delayed_event_time = performance.now() + CLIPBOARD_EVENT_DELAY; + } + } + } + } + + const wid = this.focused_wid; + this.debug("keyboard", "focused=", this.focused, "keyname=", keyname); + let packet = [PACKET_TYPES.key_action, wid, keyname, pressed, modifiers, keyval, keystring, keycode, group]; + this.key_packets.push(packet); + if (unpress_now) { + packet = [PACKET_TYPES.key_action, wid, keyname, false, modifiers, keyval, keystring, keycode, group]; + this.key_packets.push(packet); + } + + //if there is a chance that we're in the process of handling + //a clipboard event (a click or control-v) + //then we send with a slight delay: + let delay = 0; + const now = performance.now(); + if (this.clipboard_delayed_event_time > now) { + delay = this.clipboard_delayed_event_time - now; + } + const me = this; + setTimeout(() => { + while (this.key_packets.length > 0) { + const key_packet = me.key_packets.shift(); + this.last_key_packet = key_packet; + this.send(key_packet); + } + }, delay); + if (keyname === "F11") { + this.debug("keyboard", "allowing default handler for", keyname); + allow_default = true; + } + return allow_default; + } + + _keyb_onkeydown(event) { + return this._keyb_process(true, event); + } + + _keyb_onkeyup(event) { + return this._keyb_process(false, event); + } + + _get_keyboard_layout() { + this.debug("keyboard", "_get_keyboard_layout() keyboard_layout=", this.keyboard_layout); + if (this.keyboard_layout) return this.keyboard_layout; + return Utilities.getKeyboardLayout(); + } + + _get_keycodes() { + //keycodes.append((nn(keyval), nn(name), nn(keycode), nn(group), nn(level))) + const keycodes = []; + let kc; + for (const keycode in CHARCODE_TO_NAME) { + kc = Number.parseInt(keycode); + keycodes.push([kc, CHARCODE_TO_NAME[keycode], kc, 0, 0]); + } + return keycodes; + } + + _get_desktop_size() { + return [this.desktop_width, this.desktop_height]; + } + + _get_DPI() { + const dpi_div = document.querySelector("#dpi"); + if (dpi_div && dpi_div.offsetWidth > 0 && dpi_div.offsetHeight > 0) { + return Math.round((dpi_div.offsetWidth + dpi_div.offsetHeight) / 2); + } + //alternative: + if ("deviceXDPI" in screen) { + return (screen.systemXDPI + screen.systemYDPI) / 2; + } + //default: + return 96; + } + + _get_screen_sizes() { + const dpi = this._get_DPI(); + const screen_size = [this.container.clientWidth, this.container.clientHeight]; + const wmm = Math.round((screen_size[0] * 25.4) / dpi); + const hmm = Math.round((screen_size[1] * 25.4) / dpi); + const monitor = ["Canvas", 0, 0, screen_size[0], screen_size[1], wmm, hmm]; + let name = "HTML"; + if (navigator.userAgentData) { + const brands = navigator.userAgentData.brands; + + for (let index = 0; index < brands.length; index++) { + const brand_info = brands[index]; + const brand = brand_info.brand; + if (brand && !brand.startsWith("Not") && !brand.endsWith("Brand")) { + name = brand + " " + brand_info.version; + } + } + } + const screen = [ + name, + screen_size[0], + screen_size[1], + wmm, + hmm, + [monitor], + 0, + 0, + screen_size[0], + screen_size[1], + ]; + //just a single screen: + return [screen]; + } + + _update_capabilities(appendobj) { + for (const attribute in appendobj) { + this.capabilities[attribute] = appendobj[attribute]; + } + } + + /** + * Ping + */ + _check_server_echo(ping_sent_time) { + const last = this.server_ok; + this.server_ok = this.last_ping_echoed_time >= ping_sent_time; + if (last !== this.server_ok) { + if (!this.server_ok) { + this.clog("server connection is not responding, drawing spinners..."); + } else { + this.clog("server connection is OK"); + } + for (const index in this.id_to_window) { + const win = this.id_to_window[index]; + win.set_spinner(this.server_ok); + } + } + } + + _check_echo_timeout(ping_time) { + if (this.reconnect_in_progress) { + return; + } + if (this.last_ping_echoed_time > 0 && this.last_ping_echoed_time < ping_time) { + if (this.reconnect && this.reconnect_attempt < this.reconnect_count) { + this.warn("ping timeout - reconnecting"); + this.reconnect_attempt++; + this.do_reconnect(); + } else { + // no point in telling the server here... + this.disconnect(`server ping timeout, waited ${this.PING_TIMEOUT}ms without a response`); + } + } + } + + _emit_event(event_type) { + const event = document.createEvent("Event"); + event.initEvent(event_type, true, true); + document.dispatchEvent(event); + } + emit_connection_lost(event_type) { + this._emit_event("connection-lost"); + } + emit_connection_established(event_type) { + this._emit_event("connection-established"); + } + + /** + * Hello + */ + _send_hello(counter) { + counter = counter || 0; + this.decode_worker_timeout = counter >= 100; + if (!DECODE_WORKER || this.decode_worker || this.decode_worker_timeout) { + // we don't need to wait for the decode worker (ie: disabled), + // or we have successfully initialized it, + // or we have already waited too long... + this.do_send_hello(null, null); + return; + } + if (counter === 0) { + this.on_connection_progress("Waiting for decode worker", "", 70); + } + setTimeout(() => this._send_hello(counter + 1), 100); + } + + do_send_hello(challenge_response, client_salt) { + this.on_connection_progress("Sending handshake", "", 90); + // make the base hello + this._make_hello_base(); + // handle a challenge if we need to + if (this.passwords.length > 0 && !challenge_response) { + // tell the server we expect a challenge (this is a partial hello) + this.capabilities["challenge"] = true; + this.clog("sending partial hello"); + } else { + this.clog("sending hello"); + // finish the hello + this._make_hello(); + } + if (challenge_response) { + this._update_capabilities({ + "challenge_response": challenge_response + }); + if (client_salt) { + this._update_capabilities({ + "challenge_client_salt": client_salt + }); + } + } + this.clog("sending hello capabilities", this.capabilities); + // verify: + for (const key in this.capabilities) { + if (key === undefined || key === null) { + throw new Error("invalid null or undefined key in hello packet data"); + } + const value = this.capabilities[key]; + if (value === undefined || value === null) { + throw new Error(`invalid null or undefined value for key ${key} in hello packet data`); + } + } + // send the packet + this.send([PACKET_TYPES.hello, this.capabilities]); + this.schedule_hello_timer(); + } + + _make_hello_base() { + this.capabilities = {}; + this._update_capabilities({ + "version": Utilities.VERSION, + "client_type": "HTML5", + "display": this._get_display_caps(), + "build": this._get_build_caps(), + "platform": this._get_platform_caps(), + "session-type": Utilities.getSimpleUserAgentString(), + "session-type.full": navigator.userAgent, + "username": this.username, + "uuid": this.uuid, + "argv": [window.location.href], + "share": this.sharing, + "steal": this.steal, + "mouse.show": true, + "vrefresh": this.vrefresh, + "file-chunks": FILE_CHUNKS_SIZE, + "setting-change": true, // Required by v5 servers + "xdg-menu-update": true, + "xdg-menu": true, + "control_commands": [ + "log", "redraw", "stop-audio", "toggle-keyboard", + "toggle-float-menu", "toggle-window-preview", + ], + }); + this._update_capabilities(this._get_network_caps()); + if (this.encryption) { + this.cipher_in_caps = this._get_cipher_caps() + this._update_capabilities({ + "encryption": this.cipher_in_caps + }); + console.info("setting cipher in caps=", JSON.stringify(this.cipher_in_caps)); + this.protocol.set_cipher_in(this.cipher_in_caps, this.encryption_key); + } + if (this.start_new_session) { + this._update_capabilities({ + "start-new-session": this.start_new_session, + }); + } + } + + _make_hello() { + this.desktop_width = this.container.clientWidth; + this.desktop_height = this.container.clientHeight; + this.key_layout = this._get_keyboard_layout(); + + this._update_capabilities({ + auto_refresh_delay: 500, + "metadata.supported": METADATA_SUPPORTED, + "encodings": { + "": this.supported_encodings, + "core": this.supported_encodings, + "rgb_formats": this.RGB_FORMATS, + "window-icon": ["png"], + "cursor": ["png"], + "packet": true, + }, + "encoding": this._get_encoding_caps(), + "audio": this._get_audio_caps(), + "clipboard": this._get_clipboard_caps(), + "pointer": this._get_pointer_caps(), + "keymap": this._get_keymap_caps(), + "file": this._get_file_caps(), + "wants": ["audio", ], + // encoding stuff + windows: true, + "window.pre-map": true, + //partial support: + keyboard: true, + screen_sizes: this._get_screen_sizes(), + dpi: { + "x": this._get_DPI(), + "y": this._get_DPI(), + }, + notifications: { + "enabled": true, + }, + cursors: true, + bell: true, + system_tray: true, + //we cannot handle this (GTK only): + named_cursors: false, + }); + } + + _get_display_caps() { + return { + "refresh-rate": this.vrefresh, + "desktop_size": [this.desktop_width, this.desktop_height], + "desktop_mode_size": [this.desktop_width, this.desktop_height], + "screen_sizes": this._get_screen_sizes(), + "monitors": this._get_monitors(), + } + } + + _get_monitors() { + const monitors = new Map(); + monitors.set(0, this._get_monitor()); + return monitors; + } + + _get_monitor() { + const dpi = this._get_DPI(); + const wmm = Math.round((this.desktop_width * 25.4) / dpi); + const hmm = Math.round((this.desktop_height * 25.4) / dpi); + return { + "geometry": [0, 0, this.desktop_width, this.desktop_height], + "primary": true, + "refresh-rate": this.vrefresh, + // "scale-factor": 1, + "width-mm": wmm, + "height-mm": hmm, + "manufacturer": Utilities.getOS(), + "model": Utilities.getBrowserName(), + // "subpixel-layout": "BGR", - not available in browsers + "workarea": [0, 0, this.desktop_width, this.desktop_height], + "name": "Canvas", + } + } + + _get_file_caps() { + return { + "enabled": true, + "printing": this.printing, + "open-url": this.open_url, + "size-limit": 32 * 1024 * 1024, + } + } + + _get_network_caps() { + const digests = this._get_digests(); + return { + "digest": digests, + "salt-digest": digests, + "compression_level": 1, + "rencodeplus": true, + "brotli": (typeof BrotliDecode === "function"), + "lz4": Boolean(lz4 && lz4.decode), + "bandwidth-limit": this.bandwidth_limit, + "connection-data": Utilities.getConnectionInfo(), + "network": { + "pings": 5, + } + } + } + + _get_digests() { + const digests = ["xor", "keycloak", "hmac+sha256"]; + + if (typeof crypto.subtle !== "undefined") { + try { + this.debug("network", "crypto.subtle=", crypto.subtle); + for (const hash of ["sha1", "sha256", "sha384", "sha512"]) { + digests.push("hmac+" + hash); + } + this.debug("network", "digests:", digests); + } catch { + this.cerror("Error probing crypto.subtle digests"); + } + } else { + this.clog("cryptography library 'crypto.subtle' not found"); + } + return digests; + } + + _get_cipher_caps() { + const enc = this.encryption.split("-")[0]; + if (enc !== "AES") { + throw `invalid encryption specified: '${enc}'`; + } + const mode = this.encryption.split("-")[1] || "CBC"; + return { + "cipher": enc, + "mode": mode, + "iv": Utilities.getSecureRandomString(16), + "key_salt": Utilities.getSecureRandomBytes(64), + "key_size": 32, //256 bits + "key_hash": "SHA1", + "key_stretch_iterations": 1000, + "padding.options": ["PKCS#7"], + "always-pad": true, + "stream": false, + } + } + + + _get_build_caps() { + return { + "revision": Utilities.REVISION, + "local_modifications": Utilities.LOCAL_MODIFICATIONS, + "branch": Utilities.BRANCH, + } + } + + _get_platform_caps() { + return { + "": Utilities.getPlatformName(), + "name": Utilities.getPlatformName(), + "processor": Utilities.getPlatformProcessor(), + "platform": navigator.appVersion, + } + } + + _get_audio_caps() { + return { + "receive": true, + "send": true, + "decoders": Object.keys(this.audio_codecs), + } + } + + _get_keymap_caps() { + return { + "layout": this.key_layout, + "keycodes": this._get_keycodes(), + } + } + + _get_pointer_caps() { + return { + "double_click": {}, + } + } + + _get_clipboard_caps() { + let selections; + if (navigator.clipboard && navigator.clipboard.readText && navigator.clipboard.writeText) { + //we don't need the primary contents, + //we can use the async clipboard + selections = ["CLIPBOARD"]; + this.log("using new navigator.clipboard"); + } else { + selections = ["CLIPBOARD", "PRIMARY"]; + this.log("legacy clipboard"); + } + this.log("clipboard polling: ", this.clipboard_poll); + + this.clipboard_targets = [this.clipboard_preferred_format]; + for (const target of [TEXT_HTML, UTF8_STRING, "TEXT", "STRING", TEXT_PLAIN]) { + if (target !== this.clipboard_preferred_format) { + this.clipboard_targets.push(target); + } + } + if (CLIPBOARD_IMAGES && navigator.clipboard && Object.hasOwn(navigator.clipboard, "write")) { + this.clipboard_targets.push("image/png"); + } else { + this.log("no clipboard write support: no images, navigator.clipboard=", navigator.clipboard); + } + this.log("clipboard targets: ", this.clipboard_targets); + this.log("clipboard preferred format: ", this.clipboard_preferred_format); + + return { + "enabled": this.clipboard_enabled, + "want_targets": true, + "greedy": true, + "selections": selections, + "preferred-targets": this.clipboard_targets, + } + } + + _get_encoding_caps() { + return this.encoding_options; + } + + on_first_ui_event() { + //this hook can be overriden + } + + _new_ui_event() { + if (this.ui_events === 0) { + this.on_first_ui_event(); + } + this.ui_events++; + } + + /** + * Mouse handlers + */ + getMouse(e) { + const windowIsLocked = Boolean(document.pointerLockElement); + + // get mouse position take into account scroll + let mx = e.clientX + jQuery(document).scrollLeft(); + let my = e.clientY + jQuery(document).scrollTop(); + + if (windowIsLocked) { + mx = e.movementX; + my = e.movementY; + } + + if (this.scale !== 1) { + mx = Math.round(mx * this.scale); + my = Math.round(my * this.scale); + } + + // check last mouse position incase the event + // hasn't provided it - bug #854 + if (isNaN(mx) || isNaN(my)) { + if (!isNaN(this.last_mouse_x) && !isNaN(this.last_mouse_y)) { + mx = this.last_mouse_x; + my = this.last_mouse_y; + } else { + // should we avoid sending NaN to the server? + mx = 0; + my = 0; + } + } else { + if (windowIsLocked) { + this.last_mouse_x += mx; + this.last_mouse_y += my; + } + this.last_mouse_x = mx; + this.last_mouse_y = my; + } + + let mbutton = 0; + if ("which" in e) + // Gecko (Firefox), WebKit (Safari/Chrome) & Opera + mbutton = Math.max(0, e.which); + else if ("button" in e) + // IE, Opera (zero based) + mbutton = Math.max(0, e.button) + 1; + + mx = this.last_mouse_x; + my = this.last_mouse_y; + + // We return a simple javascript object (a hash) with x and y defined + return { + x: mx, + y: my, + button: mbutton + }; + } + + on_mousedown(e, win) { + this.mousedown_event = e; + this.mouseup_event = null; + this.do_window_mouse_click(e, win, true); + return !win; + } + + on_mouseup(e, win) { + this.mouseup_event = e; + this.do_window_mouse_click(e, win, false); + return !win; + } + + on_mousemove(e, win) { + if (this.mouse_grabbed) { + return true; + } + + // Ignore events when server is readonly, disconnected or if the event is not over the screen while in shadow mode + if (this.server_readonly || !this.connected || (!win && this.server_is_shadow)) { + return !win; + } + const mouse = this.getMouse(e); + const x = Math.round(mouse.x); + const y = Math.round(mouse.y); + const modifiers = this._keyb_get_modifiers(e); + const buttons = []; + const coords = [x, y]; + let wid = 0; + if (this.server_is_desktop) { + wid = 1; + } + if (win) { + wid = win.wid; + // add relative coordinates: + const pos = win.get_internal_geometry(); + coords.push(Math.round(mouse.x - pos.x)); + coords.push(Math.round(mouse.y - pos.y)); + e.preventDefault(); + } + this.send([PACKET_TYPES.pointer_position, wid, coords, modifiers, buttons]); + return !win; + } + + release_buttons(e, win) { + const mouse = this.getMouse(e); + const x = Math.round(mouse.x); + const y = Math.round(mouse.y); + const modifiers = this._keyb_get_modifiers(e); + const pressed = false; + const coords = [x, y]; + let wid = 0; + if (win) { + wid = win.wid; + // add relative coordinates: + const pos = win.get_internal_geometry(); + coords.push(Math.round(mouse.x - pos.x)); + coords.push(Math.round(mouse.y - pos.y)); + } + for (const button of this.buttons_pressed) { + this.send_button_action(wid, button, pressed, coords, modifiers); + } + } + + do_window_mouse_click(e, win, pressed) { + if (win) { + e.preventDefault(); + } + + // Ignore events when server is readonly, disconnected or if the event is not over the screen while in shadow mode + if (this.server_readonly || this.mouse_grabbed || !this.connected || (!win && this.server_is_shadow)) { + return; + } + // Skip processing if clicked on float menu + if ( + $(e.target).attr("id") === FLOAT_MENU_SELECTOR.slice(1) || + $(e.target).parents(FLOAT_MENU_SELECTOR).length > 0 + ) { + this.debug("clicked on float_menu, skipping event handler", e); + return; + } + let send_delay = 0; + const client = this; + if (client.clipboard_direction !== "to-server" && this._poll_clipboard(e)) { + send_delay = CLIPBOARD_EVENT_DELAY; + } + const mouse = this.getMouse(e, win); + const x = Math.round(mouse.x); + const y = Math.round(mouse.y); + const modifiers = this._keyb_get_modifiers(e); + const coords = [x, y]; + let wid = 0; + if (win) { + wid = win.wid; + // add relative coordinates: + const pos = win.get_internal_geometry(); + coords.push(Math.round(mouse.x - pos.x)); + coords.push(Math.round(mouse.y - pos.y)); + } + // dont call set focus unless the focus has actually changed + if (wid > 0 && this.focused_wid !== wid) { + this.set_focus(win); + } + + if (window.cursor_lock && win.canvas) { + $("#cursor-lock-button").removeClass("icon-paused"); + win.canvas.requestPointerLock(); + } + + let button = mouse.button; + const lbe = this.last_button_event; + if (lbe[0] === button && lbe[1] === pressed && lbe[2] === x && lbe[3] === y) { + //duplicate! + this.debug("mouse", "skipping duplicate click event"); + return; + } + this.last_button_event = [button, pressed, x, y]; + this.debug("mouse", "click:", button, pressed, x, y); + if (button === 4) { + button = 8; + } else if (button === 5) { + button = 9; + } + function send_button_action() { + client.clipboard_delayed_event_time = performance.now() + CLIPBOARD_EVENT_DELAY; + client.send_button_action(wid, button, pressed, coords, modifiers); + } + if (send_delay) { + setTimeout(send_button_action, send_delay) + } + else { + send_button_action(); + } + } + + send_button_action(wid, button, pressed, coords, modifiers) { + const buttons = []; + if (pressed) { + this.buttons_pressed.add(button); + } else { + this.buttons_pressed.delete(button); + } + this.send([PACKET_TYPES.button_action, wid, button, pressed, coords, modifiers, buttons]); + } + + // Source: https://deepmikoto.com/coding/1--javascript-detect-mouse-wheel-direction + detect_vertical_scroll_direction(e) { + if (!e) { + //IE? In any case, detection won't work: + return 0; + } + let delta = 0; + if (e.wheelDelta) { + // will work in most cases + delta = e.wheelDelta; + } else if (e.detail) { + // fallback for Firefox + delta = -e.detail; + } + if (!delta) { + return 0; + } + if (delta > 0) { + return -1; + } + if (delta < 0) { + return 1; + } + return 0; + } + + on_mousescroll(e, win) { + // Ignore events when server is readonly, disconnected or if the event is not over the screen while in shadow mode + if (this.server_readonly || this.mouse_grabbed || !this.connected || (!win && this.server_is_shadow)) { + return false; + } + const mouse = this.getMouse(e); + const x = Math.round(mouse.x); + const y = Math.round(mouse.y); + const modifiers = this._keyb_get_modifiers(e); + const buttons = []; + let wid = 0; + const coords = [x, y]; + if (win) { + wid = win.wid; + // add relative coordinates: + const pos = win.get_internal_geometry(); + coords.push(Math.round(mouse.x - pos.x)); + coords.push(Math.round(mouse.y - pos.y)); + } + const wheel = Utilities.normalizeWheel(e); + this.debug("mouse", "normalized wheel event:", wheel); + //clamp to prevent event floods: + let px = Math.min(1200, wheel.pixelX); + let py = Math.min(1200, wheel.pixelY); + if (this.scroll_reverse_x) { + px = -px; + } + if (this.scroll_reverse_y == true || (this.scroll_reverse_x == "auto" && this.detect_vertical_scroll_direction(e) < 0 && py > 0)) { + py = -py; + } + const apx = Math.abs(px); + const apy = Math.abs(py); + if (this.server_precise_wheel) { + if (apx > 0) { + const button_x = px >= 0 ? 7 : 6; + const xdist = Math.round((px * 1000) / 120); + this.send([PACKET_TYPES.wheel_motion, wid, button_x, -xdist, coords, modifiers, buttons]); + } + if (apy > 0) { + const button_y = py >= 0 ? 5 : 4; + const ydist = Math.round((py * 1000) / 120); + this.send([PACKET_TYPES.wheel_motion, wid, button_y, -ydist, coords, modifiers, buttons]); + } + return; + } + //generate a single event if we can, or add to accumulators: + if (apx >= 40 && apx <= 160) { + this.wheel_delta_x = px > 0 ? 120 : -120; + } else { + this.wheel_delta_x += px; + } + if (apy >= 40 && apy <= 160) { + this.wheel_delta_y = py > 0 ? 120 : -120; + } else { + this.wheel_delta_y += py; + } + //send synthetic click+release as many times as needed: + let wx = Math.abs(this.wheel_delta_x); + let wy = Math.abs(this.wheel_delta_y); + const button_x = this.wheel_delta_x >= 0 ? 7 : 6; + const button_y = this.wheel_delta_y >= 0 ? 5 : 4; + while (wx >= 120) { + wx -= 120; + this.send([PACKET_TYPES.button_action, wid, button_x, true, coords, modifiers, buttons]); + this.send([PACKET_TYPES.button_action, wid, button_x, false, coords, modifiers, buttons]); + } + while (wy >= 120) { + wy -= 120; + this.send([PACKET_TYPES.button_action, wid, button_y, true, coords, modifiers, buttons]); + this.send([PACKET_TYPES.button_action, wid, button_y, false, coords, modifiers, buttons]); + } + //store left overs: + this.wheel_delta_x = this.wheel_delta_x >= 0 ? wx : -wx; + this.wheel_delta_y = this.wheel_delta_y >= 0 ? wy : -wy; + e.preventDefault(); + return false; + } + + init_clipboard() { + this.clog("initializing clipboard: enabled=", this.clipboard_enabled, + ", poll=", this.clipboard_poll, ", preferred format=", this.clipboard_preferred_format); + if (!this.clipboard_enabled) { + return; + } + window.addEventListener("paste", (e) => { + let clipboardData = (e.originalEvent || e).clipboardData; + if (clipboardData && clipboardData.files && clipboardData.files.length > 0) { + const files = clipboardData.files; + this.clog("paste got", files.length, "files"); + for (let index = 0; index < files.length; index++) { + const file = files.item(index); + this.send_file(file); + } + e.preventDefault(); + return; + } + const fmt = this.clipboard_preferred_format; + if ((fmt === TEXT_PLAIN || fmt === UTF8_STRING) && navigator.clipboard && navigator.clipboard.readText) { + navigator.clipboard.readText().then( + (text) => { + this.cdebug("clipboard", "paste event, text=", text); + this.clipboard_buffer = text; + const data = Utilities.StringToUint8(text); + this.send_clipboard_token(data); + }, + (error) => this.cdebug("clipboard", "paste event failed:", error) + ); + return; + } + let clipboard_buffer = clipboardData.getData(fmt); + if (clipboard_buffer) { + this.cdebug("clipboard", "paste event, ", fmt, "=", clipboard_buffer); + this.clipboard_buffer = clipboard_buffer; + const data = Utilities.StringToUint8(clipboard_buffer); + this.send_clipboard_token(data, [fmt]); + } + }); + window.addEventListener("copy", (e) => { + const clipboard_buffer = this.get_clipboard_buffer(); + const pasteboard = $(PASTEBOARD_SELECTOR); + pasteboard.text(clipboard_buffer); + pasteboard.select(); + this.cdebug("clipboard", "copy event, clipboard buffer=", clipboard_buffer); + this.clipboard_pending = false; + }); + window.addEventListener("cut", (e) => { + const clipboard_buffer = this.get_clipboard_buffer(); + const pasteboard = $(PASTEBOARD_SELECTOR); + pasteboard.text(clipboard_buffer); + pasteboard.select(); + this.cdebug("clipboard", "cut event, clipboard buffer=", clipboard_buffer); + this.clipboard_pending = false; + }); + const screen = $("#screen"); + screen.on("click", (e) => this.may_set_clipboard()); + screen.keypress(() => this.may_set_clipboard()); + } + + may_set_clipboard(e) { + this.cdebug("clipboard", "pending=", this.clipboard_pending, "buffer=", truncate(this.clipboard_buffer)); + if (!this.clipboard_pending) { + return; + } + let clipboard_buffer = this.get_clipboard_buffer(); + const clipboard_datatype = (this.get_clipboard_datatype() || "").toLowerCase(); + const is_text = clipboard_datatype.includes("text") || clipboard_datatype.includes("string"); + if (!is_text) { + //maybe just abort here instead? + clipboard_buffer = ""; + } + const pasteboard = $(PASTEBOARD_SELECTOR); + pasteboard.text(clipboard_buffer); + pasteboard.select(); + this.cdebug("clipboard", "click event, with pending clipboard datatype=", clipboard_datatype, ", buffer=", + clipboard_buffer + ); + let success = false; + if ( + Object.hasOwn(window, "clipboardData") && + Object.hasOwn(window.clipboardData, "setData") && + typeof window.clipboardData.setData === "function" + ) { + try { + window.clipboardData.setData(clipboard_datatype, clipboard_buffer); + success = true; + } catch { + success = false; + } + } + if (!success && is_text) { + success = document.execCommand("copy"); + } else { + //probably no point in trying again? + } + if (success) { + //clipboard_buffer may have been cleared if not set to text: + this.clipboard_buffer = clipboard_buffer; + this.clipboard_pending = false; + } + } + + _poll_clipboard(e) { + if (!this.clipboard_poll) { + return; + } + //see if the clipboard contents have changed: + if (this.clipboard_pending) { + //we're still waiting to set the clipboard.. + return false; + } + this.read_clipboard(e); + } + + read_clipboard(e) { + if (this.clipboard_enabled === false) { + return; + } + if (navigator.clipboard && navigator.clipboard.clipboardData) { + this.debug("clipboard", "polling using", navigator.clipboard.clipboardData); + this.read_clipboard_data(); + return false; + } + if (navigator.clipboard && navigator.clipboard.readText) { + this.debug("clipboard", "polling using", navigator.clipboard.readText); + this.read_clipboard_text(); + return false; + } + //fallback code for legacy mode: + let datatype = TEXT_PLAIN; + let clipboardData = (e.originalEvent || e).clipboardData; + if (!clipboardData) { + this.debug("clipboard", "polling: no data available"); + return false; + } + const raw_clipboard_buffer = clipboardData.getData(datatype); + if (raw_clipboard_buffer === null) { + return false; + } + const clipboard_buffer = unescape(encodeURIComponent(raw_clipboard_buffer)); + this.debug("clipboard", "paste event, data=", clipboard_buffer); + if (clipboard_buffer === this.clipboard_buffer) { + return false; + } + this.debug("clipboard", "clipboard contents have changed"); + this.clipboard_buffer = clipboard_buffer; + this.send_clipboard_token(clipboard_buffer, [datatype]); + this.clipboard_delayed_event_time = performance.now() + CLIPBOARD_EVENT_DELAY; + return true; + } + + read_clipboard_data() { + if (this.clipboard_enabled === false) { + return; + } + navigator.clipboard.clipboardData.read(TEXT_HTML).then( + (text) => { + this.debug("clipboard", "paste event, text/html=", text); + if (!text) { + // try with plain text: + read_clipboard_text(); + return; + } + const clipboard_buffer = text; + if (clipboard_buffer !== this.clipboard_buffer) { + this.debug("clipboard", "clipboard contents have changed"); + this.clipboard_buffer = clipboard_buffer; + this.send_clipboard_token(clipboard_buffer, [TEXT_HTML]); + this.clipboard_delayed_event_time = performance.now() + CLIPBOARD_EVENT_DELAY; + } + this.clipboard_pending = false; + }, + (error) => { + this.debug("clipboard", "paste event failed:", error); + this.clipboard_pending = false; + } + ); + } + + read_clipboard_text() { + if (this.clipboard_enabled === false) { + return; + } + client.debug("clipboard", "read_clipboard_text()"); + //warning: this can take a while, + //so we may send the click before the clipboard contents... + navigator.clipboard.readText().then( + (text) => { + this.debug("clipboard", "paste event, text=", text); + const clipboard_buffer = unescape(encodeURIComponent(text)); + if (clipboard_buffer !== this.clipboard_buffer) { + this.debug("clipboard", "clipboard contents have changed"); + this.clipboard_buffer = clipboard_buffer; + this.send_clipboard_token(clipboard_buffer); + this.clipboard_delayed_event_time = performance.now() + CLIPBOARD_EVENT_DELAY; + } + this.clipboard_pending = false; + }, + (error) => { + this.debug("clipboard", "paste event failed:", error); + this.clipboard_pending = false; + } + ); + } + + /** + * Focus + */ + set_focus(win) { + if (!win || this.server_readonly || !this.connected) { + return; + } + // don't send focus packet for override_redirect windows! + if (win.override_redirect || win.tray) { + return; + } + if (win.minimized) { + //tell server to map it: + win.toggle_minimized(); + } + + // keep modal windows on top: + let modal = false; + if (MODAL_FOCUS) { + for (const index in this.id_to_window) { + modal = modal || this.id_to_window[index].metadata.modal; + } + } + if (modal && !win.metadata.modal) { + // don't take focus from a modal window + return; + } + + const wid = win.wid; + if (this.focused_wid === wid) { + return; + } + + // Keep DESKTOP-type windows per default settings lower than all other windows. + // Only allow focus if all other windows are minimized. + if ( + default_settings !== undefined && + default_settings.auto_fullscreen_desktop_class !== undefined && + default_settings.auto_fullscreen_desktop_class.length > 0 + ) { + const auto_fullscreen_desktop_class = default_settings.auto_fullscreen_desktop_class; + if ( + win.windowtype === "DESKTOP" && + win.metadata["class-instance"].includes(auto_fullscreen_desktop_class) + ) { + for (const index in this.id_to_window) { + const otherwin = this.id_to_window[index]; + if (otherwin.wid !== win.wid && !otherwin.minimized) { + return; + } + } + } + } + + const top_stacking_layer = Object.keys(this.id_to_window).length; + const old_stacking_layer = win.stacking_layer; + const had_focus = this.focused_wid; + this.focused_wid = wid; + this.send([PACKET_TYPES.focus, wid, []]); + //set the focused flag on the window specified, + //adjust stacking order: + let iwin = null; + for (const index in this.id_to_window) { + iwin = this.id_to_window[index]; + iwin.focused = iwin.wid === wid; + if (iwin.focused) { + iwin.stacking_layer = top_stacking_layer; + this.send_configure_window(iwin, { + focused: true + }, true); + } else { + //move it down to fill the gap: + if (iwin.stacking_layer > old_stacking_layer) { + iwin.stacking_layer--; + } + if (had_focus === index) { + this.send_configure_window(iwin, { + focused: false + }, true); + } + } + iwin.updateFocus(); + iwin.update_zindex(); + } + } + + /* + * detect DESKTOP-type window from settings + */ + is_window_desktop(win) { + if ( + default_settings !== undefined && + default_settings.auto_fullscreen_desktop_class !== undefined && + default_settings.auto_fullscreen_desktop_class.length > 0 + ) { + const auto_fullscreen_desktop_class = default_settings.auto_fullscreen_desktop_class; + if ( + win.windowtype === "DESKTOP" && + win.metadata["class-instance"].includes(auto_fullscreen_desktop_class) + ) { + return true; + } + } + return false; + } + + /* + * Show/Hide the window preview list + */ + toggle_window_preview(init_callback) { + const preview_element = $(WINDOW_PREVIEW_SELECTOR); + + preview_element.on("init", (e, slick) => { + if (init_callback) { + init_callback(e, slick); + } + }); + + preview_element.on("afterChange", (event, slick, currentSlide) => { + const wid = $(".slick-current .window-preview-item-container").data("wid"); + const win = this.id_to_window[wid]; + if (!win.minimized) { + win.focus(); + } + }); + + $(window).on("click", this._handle_window_list_blur); + $(window).on("contextmenu", this._handle_window_list_blur); + + if (preview_element.is(":visible")) { + // Restore the current selection if it's minimized. + const wid = $(".slick-current .window-preview-item-container").data("wid"); + this.clog(`current wid: ${wid}`); + const win = this.id_to_window[wid]; + if (win.minimized) { + win.focus(); + } + + // Clear the list of window elements. + preview_element.children().remove(); + + preview_element.slick("unslick"); + preview_element.children().remove(); + preview_element.hide(); + preview_element.off("afterChange"); + preview_element.off("init"); + $(window).off("click", this._handle_window_list_blur); + $(window).off("contextmenu", this._handle_window_list_blur); + return; + } + + // Clear the list of window elements. + preview_element.children().remove(); + + // Sort windows by stacking order.; + const windows_sorted = Object.values(client.id_to_window).filter((win) => { + // skip DESKTOP type windows. + return !client.is_window_desktop(win); + }); + + if (windows_sorted.length === 0) { + return; + } + + const container_width = 200 * Math.min(4, windows_sorted.length); + preview_element.css("width", `${container_width}px`); + + windows_sorted.sort((a, b) => { + if (a.stacking_layer < b.stacking_layer) { + return 1; + } + if (a.stacking_layer > b.stacking_layer) { + return -1; + } + return 0; + }); + + // Add all open windows to the list. + for (const index in windows_sorted) { + const win = windows_sorted[index]; + const item_container = $("
"); + item_container.data("wid", win.wid); + item_container.addClass("window-preview-item-container"); + + // Text + const item_text_element = $("
"); + item_text_element.addClass("window-preview-item-text"); + item_text_element.text(win.title); + + // Window image + const png_base64 = win.canvas.toDataURL("image/png"); + const img_element = $(""); + img_element.addClass("window-preview-item-img"); + img_element.attr("src", png_base64); + + item_container.append(item_text_element); + item_container.append(img_element); + + preview_element.append(item_container); + } + + preview_element.show(); + + preview_element.slick({ + centerMode: true, + focusOnSelect: true, + focusOnChange: true, + touchMove: false, + centerPadding: "0px", + slidesToShow: Math.max(1, Math.min(4, windows_sorted.length)), + slidesToScroll: 1, + infinite: true, + adaptiveHeight: false, + speed: 0, + prevArrow: null, + nextArrow: null, + easing: "null", + waitForAnimate: false, + }); + } + + /* + * Handle closing of window list if clickout outside of area. + */ + _handle_window_list_blur(e) { + if ($(WINDOW_PREVIEW_SELECTOR).is(":visible")) { + if (e.target.id === WINDOW_PREVIEW_SELECTOR.slice(1)) { + return; + } + if ($(e.target).parents(WINDOW_PREVIEW_SELECTOR).length > 0) { + return; + } + if ($(e.target).hasClass("window-list-button")) { + return; + } + if ( + $(e.target).parents(FLOAT_MENU_SELECTOR).length > 0 && + $(e.target).parent().has("#open_windows_list") + ) { + return; + } + // Clicked outside window list, close it. + client.toggle_window_preview(); + } + } + + /* + * packet processing functions start here + */ + + on_open() { + //this hook can be overriden + } + + _process_open() { + this.cancel_open_timer(); + // call the send_hello function + this.on_connection_progress("WebSocket connection established", "", 60); + // wait timeout seconds for a hello, then bomb + this._send_hello(); + this.on_open(); + } + + schedule_open_timer() { + this.cancel_open_timer(); + this.open_timer = setTimeout(() => { + let reconnect = this.reconnect || this.reconnect_attempt < this.reconnect_count; + if (reconnect) { + this.close_protocol(); + this.reconnect_attempt++; + this.do_reconnect(); + } else { + this.disconnect_reason = "failed to open connection"; + this.close(); + } + }, this.OPEN_TIMEOUT); + } + + cancel_open_timer() { + if (this.open_timer) { + clearTimeout(this.open_timer); + this.open_timer = null; + } + } + + schedule_hello_timer() { + this.cancel_hello_timer(); + this.hello_timer = setTimeout(() => { + this.disconnect_reason = "Did not receive hello before timeout reached, not an Xpra server?"; + this.close(); + }, this.HELLO_TIMEOUT); + } + + cancel_hello_timer() { + if (this.hello_timer) { + clearTimeout(this.hello_timer); + this.hello_timer = null; + } + } + + _process_error(packet) { + const code = Number.parseInt(packet[2]); + let reconnect = this.reconnect || this.reconnect_attempt < this.reconnect_count; + if (reconnect && [0, 1006, 1008, 1010, 1014, 1015].includes(code)) { + // don't re-connect unless we had actually managed to connect + // (because these specific websocket error codes are likely permanent) + reconnect = this.connected; + } + this.cerror("websocket error: ", packet[1], "code: ", code, "reason: ", this.disconnect_reason, + ", connected: ", this.connected, ", reconnect: ", reconnect); + if (this.reconnect_in_progress) { + return; + } + this.packet_disconnect_reason(packet); + this.close_audio(); + if (!reconnect) { + // call the client's close callback + this.close(); + } + } + + packet_disconnect_reason(packet) { + if (!this.disconnect_reason && packet[1]) { + const code = packet[2]; + if (!this.connected && [0, 1006, 1008, 1010, 1014, 1015].includes(code)) { + this.disconnect_reason = "connection failed, invalid address?"; + } else { + this.disconnect_reason = packet[1]; + let index = 2; + while (packet.length > index && packet[index]) { + this.disconnect_reason += `\n${packet[index]}`; + index++; + } + } + } + } + + do_reconnect() { + //try again: + this.reconnect_in_progress = true; + setTimeout(() => { + try { + this.remove_windows(); + this.close_audio(); + this.cancel_all_files(); + this.clear_timers(); + this.init_state(); + this.close_protocol(); + this.emit_connection_lost(); + this.connect(); + } finally { + this.reconnect_in_progress = false; + } + }, this.reconnect_delay); + } + + _process_close(packet) { + this.clog("websocket closed: ", packet[1], "reason: ", this.disconnect_reason, + ", reconnect: ", this.reconnect, ", reconnect attempt: ", this.reconnect_attempt); + if (this.reconnect_in_progress) { + return; + } + this.packet_disconnect_reason(packet); + if (this.reconnect && this.reconnect_attempt < this.reconnect_count) { + this.emit_connection_lost(); + this.close_protocol(); + this.reconnect_attempt++; + this.do_reconnect(); + } else { + this.close(); + } + } + + disconnect(reason) { + this.disconnect_reason = reason || "unknown"; + this.close(); + } + + close() { + if (this.reconnect_in_progress) { + return; + } + this.clog("client closed"); + this.cancel_open_timer(); + this.cancel_hello_timer(); + this.cancel_all_files(); + this.emit_connection_lost(); + this.remove_windows(); + this.close_audio(); + this.clear_timers(); + this.close_protocol(); + // call the client's close callback + this.callback_close(this.disconnect_reason); + } + + _process_disconnect(packet) { + this.debug("main", "disconnect reason:", packet[1]); + if (this.reconnect_in_progress) { + return; + } + // save the disconnect reason + this.packet_disconnect_reason(packet); + this.close(); + // call the client's close callback + this.callback_close(this.disconnect_reason); + } + + _process_startup_complete(packet) { + this.log("startup complete"); + this.emit_connection_established(); + } + + _connection_change(e) { + const ci = Utilities.getConnectionInfo(); + this.clog("connection status - change event=", e, ", connection info=", ci, "tell server:", this.server_connection_data); + if (ci && this.server_connection_data) { + this.send([PACKET_TYPES.connection_data, ci]); + } + } + + _process_hello(packet) { + this.cancel_open_timer(); + this.cancel_hello_timer(); + const hello = packet[1]; + this.clog("received hello capabilities", hello); + if (!hello["rencodeplus"]) { + throw "no common packet encoders, 'rencodeplus' is required by this client"; + } + + this.session_name = hello["session_name"]; + $("title").text(this.session_name); + + this.server_display = hello["display"] || ""; + this.server_platform = hello["platform"] || ""; + this.server_remote_logging = hello["remote-logging.multi-line"]; + if (this.server_remote_logging && this.remote_logging) { + //hook remote logging: + Utilities.log = () => this.log(arguments); + Utilities.warn = () => this.warn(arguments); + Utilities.error = () => this.error(arguments); + Utilities.exc = () => this.exc(arguments); + } + + // check for server encryption caps update + if (this.encryption) { + this.cipher_out_caps = hello["encryption"]; + console.info("cipher out caps=", JSON.stringify(this.cipher_out_caps)); + this.protocol.set_cipher_out(this.cipher_out_caps, this.encryption_key); + } + + const version = Utilities.s(hello["version"]); + try { + const vparts = version.split("."); + const vno = vparts.map((x) => Number.parseInt(x)); + if (vno[0] <= 0 && vno[1] < 10) { + this.disconnect(`unsupported version: ${version}`); + this.close(); + return; + } + } catch { + this.disconnect(`error parsing version number '${version}'`); + this.close(); + return; + } + this.log("got hello: server version", version, "accepted our connection"); + // stuff that must be done after hello + this._process_modifier_keycodes(hello["modifier_keycodes"] || {}); + this._process_audio_caps(hello["audio"] || {}); + if (SHOW_START_MENU) { + this.xdg_menu = hello["xdg-menu"]; + if (this.xdg_menu) { + this.process_xdg_menu(); + } + } + if (!hello["client-shutdown"]) { + $("#shutdown_menu_entry").hide(); + } + if ( + !this.file_transfer || + (!hello["file-transfer"] && (!hello["file"] || !hello["file"]["enabled"])) + ) { + $("#upload_menu_entry").hide(); + $("#download_menu_entry").hide(); + } + + this.server_is_desktop = Boolean(hello["desktop"]); + this.server_is_shadow = Boolean(hello["shadow"]); + this.server_readonly = Boolean(hello["readonly"]); + if (this.server_is_desktop || this.server_is_shadow) { + jQuery("body").addClass("desktop"); + } + this.server_resize_exact = hello["resize_exact"] || false; + this.server_screen_sizes = hello["screen-sizes"] || []; + this.clog("server screen sizes:", this.server_screen_sizes); + + this.server_precise_wheel = hello["wheel.precise"] || false; + + this.remote_open_files = Boolean(hello["open-files"]); + this.remote_file_transfer = Boolean(hello["file-transfer"]); + this.remote_printing = Boolean(hello["printing"]); + if (this.remote_printing && this.printing) { + // send our printer definition + const printers = { + "HTML5 client": { + "printer-info": "Print to PDF in client browser", + "printer-make-and-model": "HTML5 client version", + mimetypes: ["application/pdf"], + }, + }; + this.send([PACKET_TYPES.printers, printers]); + } + this.server_connection_data = hello["connection-data"]; + if (Object.hasOwn(navigator, "connection")) { + navigator.connection.addEventListener("change", this._connection_change); + this._connection_change(); + } + + // don't send clipboard packets to servers that don't want them: + this.clipboard_enabled = Boolean(hello["clipboard"] || false); + + // file transfer attributes: + this.remote_file_size_limit = hello["max-file-size"]; + this.remote_file_chunks = Math.max(0, Math.min(this.remote_file_size_limit, hello["file-chunks"] || 0)); + + // start sending our own pings + this._send_ping(); + this.ping_timer = setInterval(() => this._send_ping(), this.PING_FREQUENCY); + this.reconnect_attempt = 0; + // Drop start_new_session to avoid creating new displays + // on reconnect + this.start_new_session = null; + this.on_connection_progress("Session started", "", 100); + this.on_connect(); + this.connected = true; + + this.send_keymap(); + } + + _process_control(packet) { + const action = packet[1]; + console.info("control: ", action, packet); + if (action === "log") { + this.clog("log action:", packet); + } + else if (action === "redraw") { + this.redraw_windows() + } + else if (action === "stop-audio") { + this.close_audio(); + } + else if (action === "toggle-keyboard") { + toggle_keyboard(); + } + else if (action === "toggle-float-menu") { + toggle_float_menu(); + } + else if (action === "toggle-window-preview") { + toggle_window_preview(); + } + else { + this.cerror("unhandled control action:", action); + } + } + + _process_modifier_keycodes(modifier_keycodes) { + // find the modifier to use for Num_Lock + if (!modifier_keycodes) { + return; + } + for (const modifier in modifier_keycodes) { + if (Object.hasOwn(modifier_keycodes, modifier)) { + const mappings = modifier_keycodes[modifier]; + for (const keycode in mappings) { + const keys = mappings[keycode]; + for (const index in keys) { + const key = keys[index]; + if (key === "Num_Lock") { + this.num_lock_modifier = modifier; + } else if (key === "Alt_L") { + this.alt_modifier = modifier; + } else if (key === "Meta_L") { + this.meta_modifier = modifier; + } else if (key === "ISO_Level3_Shift" || key === "Mode_switch") { + this.altgr_modifier = modifier; + } else if (key === "Control_L") { + this.control_modifier = modifier; + } + } + } + } + } + } + + _process_audio_caps(audio_caps) { + if (!this.audio_enabled) { + this.on_audio_state_change("disabled", ""); + return; + } + if (!audio_caps["send"]) { + this.audio_enabled = false; + this.on_audio_state_change("disabled", "server does not support speaker forwarding"); + return; + } + this.server_audio_codecs = audio_caps["encoders"]; + if (!this.server_audio_codecs) { + this.audio_enabled = false; + this.on_audio_state_change("disabled", "audio codecs missing on the server"); + return; + } + this.log("audio codecs supported by the server:", this.server_audio_codecs); + if (!this.server_audio_codecs.includes(this.audio_codec)) { + this.warn(`audio codec ${this.audio_codec} is not supported by the server`); + this.audio_codec = null; + //find the best codec we can use: + for (let codec of MediaSourceConstants.PREFERRED_CODEC_ORDER) { + if (codec in this.audio_codecs && this.server_audio_codecs.includes(codec)) { + this.audio_framework = this.mediasource_codecs[codec] ? + "mediasource" : + "aurora"; + this.audio_codec = codec; + this.log("using", this.audio_framework, "audio codec", codec); + break; + } + } + if (!this.audio_codec) { + this.audio_enabled = false; + this.on_audio_state_change("disabled", "no matching audio codec"); + return; + } + } + //with Firefox, we have to wait for a user event.. + if (this.audio_enabled && !Utilities.isFirefox()) { + this._sound_start_receiving(); + } + } + + _process_encodings(packet) { + const caps = packet[1]; + this.log("update encodings:", Object.keys(caps)); + } + + process_xdg_menu() { + this.log("received xdg start menu data"); + let key; + //remove current menu: + $("#startmenu li").remove(); + const startmenu = document.querySelector("#startmenu"); + for (key in this.xdg_menu) { + const category = this.xdg_menu[key]; + const li = document.createElement("li"); + li.className = "-hasSubmenu"; + + const catDivLeft = document.createElement("div"); + catDivLeft.className = "menu-divleft"; + catDivLeft.append(this.xdg_image(category.IconData, category.IconType)); + + const a = document.createElement("a"); + a.append(catDivLeft); + a.append(document.createTextNode(this.xdg_menu[key].Name)); + a.href = "#"; + li.append(a); + + const ul = document.createElement("ul"); + + //TODO need to figure out how to do this properly + a.addEventListener("mouseenter", function() { + this.parentElement.childNodes[1].className = "-visible"; + }); + a.addEventListener("mouseleave", function() { + this.parentElement.childNodes[1].className = ""; + }); + + const xdg_menu_cats = category.Entries; + for (key in xdg_menu_cats) { + const entry = xdg_menu_cats[key]; + const li2 = document.createElement("li"); + const a2 = document.createElement("a"); + + let name = entry.Name; + name = Utilities.trimString(name, 15); + let command = ""; + if (entry.Exec) { + command = entry.Exec.replace(/%[FUfu]/g, ""); + } + + const divLeft = document.createElement("div"); + divLeft.className = "menu-divleft"; + divLeft.append(this.xdg_image(entry.IconData, entry.IconType)); + + const titleDiv = document.createElement("div"); + titleDiv.append(document.createTextNode(name)); + titleDiv.className = "menu-content-left"; + divLeft.append(titleDiv); + + a2.append(divLeft); + a2.title = command; + + const me = this; + a2.addEventListener("click", function() { + const ignore = "False"; + me.start_command(this.innerText, this.title, ignore); + document.querySelector("#menu_list").className = "-hide"; + }); + a2.addEventListener("mouseenter", function() { + this.parentElement.parentElement.className = "-visible"; + }); + a2.addEventListener("mouseleave", function() { + this.parentElement.parentElement.className = ""; + }); + + li2.append(a2); + ul.append(li2); + } + li.append(ul); + startmenu.append(li); + } + + if (this.xdg_menu.length === 0) { + startmenu.css("box-shadow", "none"); + } + } + + _process_setting_change(packet) { + const setting = packet[1]; + const value = packet[2]; + if ((setting === "xdg-menu" || setting === "menu") && SHOW_START_MENU) { + this.xdg_menu = value; + if (this.xdg_menu) { + this.process_xdg_menu(); + $("#startmenuentry").show(); + } + } + else if (setting === "session_name") { + this.session_name = value; + jQuery("title").text(value); + } + } + + xdg_image(icon_data, icon_type) { + const img = new Image(); + if (typeof icon_data !== "undefined") { + let image_type = "image/" + icon_type; + if (icon_type == "svg") { + image_type = "image/svg+xml"; + } + img.src = "data:" + image_type + ";base64," + Utilities.ArrayBufferToBase64(icon_data); + } + img.className = "menu-content-left"; + img.height = 24; + img.width = 24; + return img; + } + + on_connect() { + //this hook can be overriden + } + + _process_challenge(packet) { + this.cancel_open_timer(); + this.cancel_hello_timer(); + if (this.encryption) { + if (packet.length >= 3) { + this.cipher_out_caps = packet[2]; + this.protocol.set_cipher_out(this.cipher_out_caps, this.encryption_key); + } else { + this.disconnect("challenge does not contain encryption details to use for the response"); + return; + } + } + const digest = packet[3]; + const server_salt = Uint8ToString(packet[1]); + const salt_digest = packet[4] || "xor"; + const prompt = (packet[5] || "password").replace(/[^\d+,. /:a-z]/gi, ""); + this.clog("process challenge:", digest); + const client = this; + + function call_do_process_challenge(password) { + Utilities.clog("call_do_process_challenge(", password, ")"); + if (!client || !client.protocol) { + return; + } + if (!password) { + client.disconnect("password prompt cancelled"); + return; + } + client.do_process_challenge(digest, server_salt, salt_digest, password); + } + if (this.passwords.length > 0) { + if (!this.is_digest_safe(digest)) { + this.disconnect("refusing to send a password over an insecure connection"); + return; + } + const password = this.passwords.shift(); + call_do_process_challenge(password); + return; + } + if (digest.startsWith("keycloak") && this.keycloak_prompt_fn) { + this.keycloak_prompt_fn(server_salt, call_do_process_challenge); + return; + } + if (this.password_prompt_fn && this.is_digest_safe(digest)) { + if (!this.is_digest_safe(digest)) { + this.disconnect("refusing to prompt for a password over an insecure connection"); + return; + } + const address = `${client.host}:${client.port}`; + this.password_prompt_fn(`The server at ${address} requires a ${prompt}`, call_do_process_challenge); + return; + } + this.disconnect("No password specified for authentication challenge"); + } + + is_digest_safe(digest) { + return digest !== "xor" || this.ssl || this.encryption || this.insecure || Utilities.isSafeHost(this.host); + } + + do_process_challenge(digest, server_salt, salt_digest, password) { + let client_salt = null; + let l = server_salt.length; + //don't use xor over unencrypted connections unless explicitly allowed: + if (!this.is_digest_safe(digest)) { + this.disconnect(`server requested digest xor, cowardly refusing to use it without encryption with ${this.host}`); + return; + } + + if (salt_digest === "xor") { + if (l < 16 || l > 256) { + this.disconnect(`invalid server salt length for xor digest:${l}`); + return; + } + } else { + //other digest, 32 random bytes is enough: + l = 32; + } + const challenge_digest = digest.startsWith("keycloak") ? "xor" : digest; + client_salt = Utilities.getSecureRandomString(l); + this.clog("combining client salt:", Utilities.convertToHex(client_salt)); + this.clog("with server salt:", Utilities.convertToHex(server_salt)); + this.clog("using", salt_digest); + Utilities.gendigest(salt_digest, client_salt, server_salt) + .then(salt => { + const hex_salt = Utilities.convertToHex(salt); + this.clog(salt_digest, ":", hex_salt); + if (challenge_digest == "xor") { + // shortcut: no need for async API, do not convert the result to hex + const xored = Utilities.xor(password, hex_salt); + this.do_send_hello(xored, client_salt); + return; + } + + Utilities.gendigest(challenge_digest, password, hex_salt) + .then(challenge_response => { + const hex_challenge = Utilities.convertToHex(challenge_response); + this.do_send_hello(hex_challenge, client_salt) + }) + .catch(err => { + this.cerror("challenge digest error", err); + this.disconnect("failed to generate challenge response " + err); + }); + }) + .catch(err => { + this.cerror("salt digest error", err); + this.disconnect("failed to generate challenge response " + err); + }); + } + + _send_ping() { + if (this.reconnect_in_progress || !this.connected) { + return; + } + const now_ms = Math.ceil(performance.now()); + this.send([PACKET_TYPES.ping, now_ms]); + // add timeout to wait for ping timout + this.ping_timeout_timer = setTimeout( + () => this._check_echo_timeout(now_ms), + this.PING_TIMEOUT + ); + // add timeout to detect temporary ping miss for spinners + const wait = 2000; + this.ping_grace_timer = setTimeout( + () => this._check_server_echo(now_ms), + wait + ); + } + + _process_ping(packet) { + const echotime = packet[1]; + this.last_ping_server_time = echotime; + if (packet.length > 2) { + //prefer system time (packet[1] is monotonic) + this.last_ping_server_time = packet[2]; + } + let sid = ""; + if (packet.length >= 4) { + sid = packet[3]; + } + this.last_ping_local_time = Date.now(); + const l1 = 0; + const l2 = 0; + const l3 = 0; + this.send([PACKET_TYPES.ping_echo, echotime, l1, l2, l3, 0, sid]); + } + + _process_ping_echo(packet) { + this.last_ping_echoed_time = packet[1]; + const l1 = packet[2]; + const l2 = packet[3]; + const l3 = packet[4]; + this.client_ping_latency = packet[5]; + this.server_ping_latency = Math.ceil(performance.now()) - this.last_ping_echoed_time; + this.server_load = [l1 / 1000, l2 / 1000, l3 / 1000]; + // make sure server goes OK immediately instead of waiting for next timeout + this._check_server_echo(0); + } + + /** + * Info + */ + start_info_timer() { + if (this.info_timer) { + return; + } + this.info_timer = setInterval(() => { + if (this.info_timer) { + this.send_info_request(); + } + }, this.INFO_FREQUENCY); + } + send_info_request() { + if (!this.info_request_pending) { + this.send([PACKET_TYPES.info_request, [this.uuid], + [], + [] + ]); + this.info_request_pending = true; + } + } + _process_info_response(packet) { + this.info_request_pending = false; + this.server_last_info = packet[1]; + this.debug("network", "info-response:", this.server_last_info); + const event = document.createEvent("Event"); + event.initEvent("info-response", true, true); + event.data = this.server_last_info; + document.dispatchEvent(event); + } + stop_info_timer() { + if (this.info_timer) { + clearTimeout(this.info_timer); + this.info_timer = null; + this.info_request_pending = false; + } + } + + /** + * System Tray forwarding + */ + + position_float_menu() { + const float_menu_element = $(FLOAT_MENU_SELECTOR); + const toolbar_height = float_menu_element.height(); + const toolbar_width = float_menu_element.width(); + let left = float_menu_element.offset().left || 0; + let top = float_menu_element.offset().top || 0; + const screen = $("#screen"); + const screen_width = screen.width(); + const screen_height = screen.height(); + + if (this.toolbar_position === "custom") { + //no calculations needed + } else if (this.toolbar_position === "top-left") { + left = 0; + top = 0; + } else if (this.toolbar_position === "top") { + left = screen_width / 2 - toolbar_width / 2; + } else if (this.toolbar_position === "top-right") { + left = screen_width - toolbar_width; + } else if (this.toolbar_position === "novnc") { + left = 0; + top = screen_height / 2 - toolbar_height / 2 - 100; + } + float_menu_element.offset({ + top, + left + }); + } + + _process_new_tray(packet) { + const wid = packet[1]; + const metadata = packet[4]; + const mydiv = document.createElement("div"); + mydiv.id = String(wid); + const mycanvas = document.createElement("canvas"); + mydiv.append(mycanvas); + + const float_tray = document.querySelector("#float_tray"); + const float_menu = document.querySelector(FLOAT_MENU_SELECTOR); + const float_menu_element = $(FLOAT_MENU_SELECTOR); + float_menu_element.children().show(); + //increase size for tray icon + const new_width = float_menu_width + float_menu_item_size - float_menu_padding + 5; + float_menu.style.width = `${new_width}px`; + float_menu_width = float_menu_element.width() + 10; + mydiv.style.backgroundColor = "white"; + + float_tray.append(mydiv); + const x = 0; + const y = 0; + const w = float_menu_item_size; + const h = float_menu_item_size; + + mycanvas.width = w; + mycanvas.height = h; + this.id_to_window[wid] = new XpraWindow(this, wid, + x, y, w, h, + metadata, false, true, {}, + //TODO: send new tray geometry to the server using send_tray_configure + () => this.debug("tray", "tray geometry changed (ignored)"), + (event, window) => this.on_mousemove(event, window), + (event, window) => this.on_mousedown(event, window), + (event, window) => this.on_mouseup(event, window), + (event, window) => this.on_mousescroll(event, window), + () => this.debug("tray", "tray set focus (ignored)"), + () => this.debug("tray", "tray closed (ignored)"), + this.scale + ); + this.send_tray_configure(wid); + } + + send_tray_configure(wid) { + const div = jQuery(`#${wid}`); + const x = Math.round(div.offset().left); + const y = Math.round(div.offset().top); + const w = float_menu_item_size; + const h = float_menu_item_size; + this.clog("tray", wid, "position:", x, y); + this.send([PACKET_TYPES.configure_window, Number(wid), x, y, w, h, {}]); + } + + reconfigure_all_trays() { + const float_menu = document.querySelector(FLOAT_MENU_SELECTOR); + let float_menu_width = float_menu_item_size * float_menu_item_count + float_menu_padding; + for (const twid in this.id_to_window) { + const twin = this.id_to_window[twid]; + if (twin && twin.tray) { + float_menu_width = float_menu_width + float_menu_item_size; + this.send_tray_configure(twid); + } + } + + // only set if float_menu is visible + if ($(FLOAT_MENU_SELECTOR).width() > 0) { + float_menu.style.width = float_menu_width; + this.position_float_menu(); + } + } + + suspend() { + // this is not a power event, + // usually just triggered by the `visibilitychange` listener, + // we only want to tell the server to slow down the refresh rate, + const options = { + "batch": { + "reset": true, + "delay": 1000, + "locked": true, + "always": true, + }, + } + this.clog("suspend event, sending control refresh with options=", options); + this.send_control_refresh(1, options); + for (const index in this.id_to_window) { + const win = this.id_to_window[index]; + win.suspend(); + } + } + + resume() { + for (const index in this.id_to_window) { + const win = this.id_to_window[index]; + win.resume(); + } + const options = { + "batch": { + "reset": true, + }, + "refresh-now": true, + } + this.clog("resume event, sending control refresh with options=", options); + this.send_control_refresh(100, options); + this.redraw_windows(); + } + + send_control_refresh(quality, options) { + const wid = 0; + const client_options = {}; + const packet = [PACKET_TYPES.buffer_refresh, wid, 0, quality, options, client_options]; + this.send(packet); + } + + +/** + * Windows + */ + _new_window(wid, x, y, w, h, metadata, override_redirect, client_properties) { + // each window needs their own DIV that contains a canvas + const mydiv = document.createElement("div"); + mydiv.id = String(wid); + + const screen = document.querySelector("#screen"); + screen.append(mydiv); + // create the XpraWindow object to own the new div + const win = new XpraWindow(this, wid, + x, y, w, h, + metadata, + override_redirect, false, + client_properties, + (window) => this.send_configure_window(window, {}, false), + (event, window) => this.on_mousemove(event, window), + (event, window) => this.on_mousedown(event, window), + (event, window) => this.on_mouseup(event, window), + (event, window) => this.on_mousescroll(event, window), + (window) => this.set_focus(window), + (window) => this.send_close_window(window), + this.scale + ); + if (this.server_is_desktop || this.server_is_shadow) { + window.noWindowList(); + } else if (win && win.decorations) { + const trimmedTitle = Utilities.trimString(win.title, 30); + window.addWindowListItem(win, wid, trimmedTitle); + } + this.id_to_window[wid] = win; + if (!override_redirect) { + const geom = win.get_internal_geometry(); + this.send([PACKET_TYPES.map_window, wid, geom.x, geom.y, geom.w, geom.h, win.client_properties]); + this.set_focus(win); + } + } + + _new_window_common(packet, override_redirect) { + const wid = packet[1]; + let x = packet[2]; + let y = packet[3]; + let w = packet[4]; + let h = packet[5]; + const metadata = packet[6]; + if (wid in this.id_to_window) + throw new Error(`we already have a window ${wid}`); + if (w <= 0 || h <= 0) { + this.error("window dimensions are wrong:", w, h); + w = 1; + h = 1; + } + let client_properties = {}; + if (packet.length >= 8) client_properties = packet[7]; + if (x === 0 && y === 0 && !metadata["set-initial-position"] && !metadata["fullscreen"]) { + //find a good position for it + const l = Object.keys(this.id_to_window).length; + if (l === 0) { + //first window: center it + if (w <= this.desktop_width) { + x = Math.round((this.desktop_width - w) / 2); + } + if (h <= this.desktop_height) { + y = Math.round((this.desktop_height - h) / 2); + } + } else { + x = Math.min(l * 10, Math.max(0, this.desktop_width - 100)); + y = 96; + } + } + this._new_window(wid, x, y, w, h, metadata, override_redirect, client_properties); + this._new_ui_event(); + } + + send_configure_window(win, state, skip_geometry) { + const geom = win.get_internal_geometry(); + const wid = win.wid; + const packet = [PACKET_TYPES.configure_window, wid, geom.x, geom.y, geom.w, geom.h, + win.client_properties, 0, state, skip_geometry, + ]; + this.send(packet); + } + + _process_new_window(packet) { + this._new_window_common(packet, false); + } + + _process_new_override_redirect(packet) { + this._new_window_common(packet, true); + } + + _process_window_metadata(packet) { + const wid = packet[1]; + const metadata = packet[2]; + const win = this.id_to_window[wid]; + if (win) { + win.update_metadata(metadata); + } + } + + _process_initiate_moveresize(packet) { + const wid = packet[1]; + const win = this.id_to_window[wid]; + if (!win) { + this.log("cannot initiate moveresize, window", wid, "not found"); + return; + } + const x_root = packet[2]; + const y_root = packet[3]; + const direction = packet[4]; + const button = packet[5]; + const source_indication = packet[6]; + let event = this.mousedown_event; + if (this.mouseup_event && direction === MOVERESIZE_CANCEL) { + event = this.mouseup_event; + } + this.log("initiate moveresize on", win, "mouse event=", event); + win.initiate_moveresize(event, x_root, y_root, direction, button, source_indication); + } + + _process_pointer_position(packet) { + const wid = packet[1]; + let x = packet[2]; + let y = packet[3]; + const win = this.id_to_window[wid]; + //we can use window relative coordinates: + if (packet.length >= 6 && win) { + const pos = win.get_internal_geometry(); + x = pos.x + packet[4]; + y = pos.y + packet[5]; + } + const shadow_pointer = document.querySelector("#shadow_pointer"); + const style = shadow_pointer.style; + let cursor_url; + let w; + let h; + let xhot; + let yhot; + if (win.cursor_data) { + cursor_url = win.cursor_data[0]; + xhot = win.cursor_data[1]; + yhot = win.cursor_data[2]; + w = win.cursor_data[3]; + h = win.cursor_data[4]; + } else { + w = 32; + h = 32; + xhot = 8; + yhot = 3; + cursor_url = "icons/default_cursor.png"; + } + x -= xhot; + y -= yhot; + style.width = `${w}px`; + style.height = `${h}px`; + shadow_pointer.src = cursor_url; + style.left = `${x}px`; + style.top = `${y}px`; + style.display = "inline"; + } + + on_last_window() { + //this hook can be overriden + } + + _process_lost_window(packet) { + const wid = packet[1]; + const win = this.id_to_window[wid]; + if (win && win.decorations) { + window.removeWindowListItem(wid); + } + try { + delete this.id_to_window[wid]; + } catch {} + if (win) { + win.destroy(); + this.clog("lost window, was tray=", win.tray); + if (win.tray) { + //other trays may have moved: + this.reconfigure_all_trays(); + } + } + this.clog("lost window", wid, ", remaining: ", Object.keys(this.id_to_window)); + if (Object.keys(this.id_to_window).length === 0) { + this.on_last_window(); + this.auto_focus(); + } else if (win && win.focused) { + //it had focus, find the next highest: + this.auto_focus(); + } + if (this.decode_worker) { + this.decode_worker.postMessage({cmd: "remove", wid}); + } + } + + auto_focus() { + let highest_window = null; + let highest_stacking = -1; + let modal = false; + if (MODAL_FOCUS) { + for (const index in this.id_to_window) { + const win = this.id_to_window[index]; + modal = modal || win.metadata.modal; + } + } + for (const index in this.id_to_window) { + const win = this.id_to_window[index]; + if (win.minimized || win.tray) { + continue; + } + if (modal && !win.metadata.modal) { + continue; + } + if (win.stacking_layer > highest_stacking) { + highest_window = win; + highest_stacking = win.stacking_layer; + } + } + if (highest_window) { + this.set_focus(highest_window); + } else { + this.focused_wid = 0; + this.send([PACKET_TYPES.focus, 0, []]); + } + } + + _process_raise_window(packet) { + const wid = packet[1]; + const win = this.id_to_window[wid]; + this.set_focus(win); + } + + _process_window_resized(packet) { + const wid = packet[1]; + const width = packet[2]; + const height = packet[3]; + const win = this.id_to_window[wid]; + if (win) { + win.resize(width, height); + } + } + + _process_window_move_resize(packet) { + const [, wid, x, y, width, height] = packet; + this.id_to_window[wid]?.move_resize(x, y, width, height); + } + + _process_configure_override_redirect(packet) { + const [, wid, x, y, width, height] = packet; + this.id_to_window[wid]?.move_resize(x, y, width, height); + } + + _process_desktop_size(packet) { + //we don't use this yet, + //we could use this to clamp the windows to a certain area + } + + _process_bell(packet) { + const percent = packet[3]; + const pitch = packet[4]; + const duration = packet[5]; + if (this.audio_context) { + const oscillator = this.audio_context.createOscillator(); + const gainNode = this.audio_context.createGain(); + oscillator.connect(gainNode); + gainNode.connect(this.audio_context.destination); + gainNode.gain.setValueAtTime(percent, this.audio_context.currentTime); + oscillator.frequency.setValueAtTime(pitch, this.audio_context.currentTime); + oscillator.start(); + setTimeout(() => oscillator.stop(), duration); + } else { + const snd = new Audio(BELL_SOUND); + snd.play(); + } + } + + /** + * Notifications + */ + _process_notify_show(packet) { + //TODO: add UI switch to disable notifications + const nid = packet[2]; + const replaces_nid = packet[4]; + const summary = Utilities.s(packet[6]); + const body = Utilities.s(packet[7]); + const expire_timeout = packet[8]; + const icon = packet[9]; + const actions = packet[10]; + const hints = packet[11]; + if (window.closeNotification) { + if (replaces_nid > 0) { + window.closeNotification(replaces_nid); + } + window.closeNotification(nid); + } + + const context = this; + + function notify() { + let icon_url = ""; + if (icon && icon[0] === "png") { + icon_url = `data:image/png;base64,${Utilities.ToBase64(icon[3])}`; + context.clog("notification icon_url=", icon_url); + } + const notification = new Notification(summary, { + body, + icon: icon_url, + }); + const reason = 2; //closed by the user - best guess... + notification.addEventListener("close", () => + context.send([PACKET_TYPES.notification_close, nid, reason, ""]) + ); + notification.addEventListener("click", () => + context.log("user clicked on notification", nid) + ); + } + + if ("Notification" in window && actions.length === 0) { + //we have notification support in the browser + if (Notification.permission === "granted") { + notify(); + return; + } else if (Notification.permission !== "denied") { + Notification.requestPermission(function(permission) { + if (permission === "granted") { + notify(); + } + }); + return; + } + } + + if (window.doNotification) { + window.doNotification("info", nid, summary, body, expire_timeout, icon, actions, hints, + function(nid, action_id) { + context.send([PACKET_TYPES.notification_action, nid, action_id]); + }, + function(nid, reason, text) { + context.send([PACKET_TYPES.notification_close, nid, reason, text || ""]); + } + ); + } + context._new_ui_event(); + } + + _process_notify_close(packet) { + const nid = packet[1]; + if (window.closeNotification) { + window.closeNotification(nid); + } + } + + /** + * Cursors + */ + reset_cursor() { + for (const wid in this.id_to_window) { + const win = this.id_to_window[wid]; + win.reset_cursor(); + } + } + + _process_cursor(packet) { + if (packet.length < 9) { + this.reset_cursor(); + return; + } + //we require a png encoded cursor packet: + const encoding = packet[1]; + if (encoding !== "png") { + this.warn(`invalid cursor encoding: ${encoding}`); + return; + } + const w = packet[4]; + const h = packet[5]; + const xhot = packet[6]; + const yhot = packet[7]; + const img_data = packet[9]; + for (const wid in this.id_to_window) { + const win = this.id_to_window[wid]; + win.set_cursor(encoding, w, h, xhot, yhot, img_data); + } + } + + _process_window_icon(packet) { + const wid = packet[1]; + const w = packet[2]; + const h = packet[3]; + const encoding = packet[4]; + const img_data = packet[5]; + this.debug("main", "window-icon: ", encoding, " size ", w, "x", h); + const win = this.id_to_window[wid]; + if (win) { + const source = win.update_icon(w, h, encoding, img_data); + //update favicon too: + if (wid === this.focused_wid || this.server_is_desktop || this.server_is_shadow) { + jQuery("#favicon").attr("href", source); + } + } + } + + /** + * Window Painting + */ + _process_draw(packet) { + //ensure that the pixel data is in a byte array: + const coding = Utilities.s(packet[6]); + let img_data = packet[7]; + const raw_buffers = []; + const now = performance.now(); + if (coding !== "scroll") { + raw_buffers.push(img_data.buffer); + } + if (this.decode_worker) { + this.decode_worker.postMessage({cmd: "decode", packet, start: now}, raw_buffers); + //the worker draw event will call do_process_draw + } else { + this.do_process_draw(packet, now); + } + } + + _process_eos(packet) { + this.do_process_draw(packet, 0); + const wid = packet[1]; + if (this.decode_worker) { + this.decode_worker.postMessage({cmd: "eos", wid}); + } + } + + request_redraw(win) { + if (document.hidden) { + this.debug("draw", "not redrawing, document.hidden=", document.hidden); + return; + } + + if (this.offscreen_api && this.decode_worker) { + this.decode_worker.postMessage({cmd: "redraw", wid: win.wid}); + return; + } + // request that drawing to screen takes place at next available opportunity if possible + this.debug("draw", "request_redraw for", win); + win.swap_buffers(); + if (!window.requestAnimationFrame) { + // requestAnimationFrame is not available, draw immediately + win.draw(); + return; + } + if (!this.pending_redraw.includes(win)) { + this.pending_redraw.push(win); + } + if (this.draw_pending) { + // already scheduled + return; + } + // schedule a screen refresh if one is not already due: + this.draw_pending = performance.now(); + window.requestAnimationFrame(() => { + this.draw_pending_list(); + }); + } + + draw_pending_list() { + const elapsed = performance.now() - this.draw_pending; + this.debug("draw", "animation frame:", this.pending_redraw.length, "windows to paint, processing delay", elapsed, "ms"); + this.draw_pending = 0; + // draw all the windows in the list: + while (this.pending_redraw.length > 0) { + const w = this.pending_redraw.shift(); + w.draw(); + } + } + + do_send_damage_sequence(packet_sequence, wid, width, height, decode_time, message) { + const protocol = this.protocol; + if (!protocol) { + return; + } + const packet = [PACKET_TYPES.damage_sequence, packet_sequence, wid, width, height, decode_time, message]; + if (decode_time < 0) { + this.cwarn("decode error packet:", packet); + } + protocol.send(packet); + } + + do_process_draw(packet, start) { + if (!packet) { + //no valid draw packet, likely handle errors for that here + return; + } + const ptype = packet[0]; + const wid = packet[1]; + const win = this.id_to_window[wid]; + if (ptype === "eos") { + this.debug("draw", "eos for window", wid); + if (win) { + win.eos(); + } + return; + } + + const width = packet[4]; + const height = packet[5]; + const coding = Utilities.s(packet[6]); + const packet_sequence = packet[8]; + const options = packet[10] || {}; + const protocol = this.protocol; + if (!protocol) { + return; + } + const me = this; + + function send_damage_sequence(decode_time, message) { + me.do_send_damage_sequence(packet_sequence, wid, width, height, decode_time, message); + } + const client = this; + + function decode_result(error) { + const flush = options["flush"] || 0; + let decode_time = Math.round(1000 * performance.now() - 1000 * start); + if (flush === 0) { + client.request_redraw(win); + } + if (error || start === 0) { + client.request_redraw(win); + decode_time = -1; + } + client.debug("draw", "decode time for ", coding, " sequence ", packet_sequence, ": ", decode_time, ", flush=", flush); + send_damage_sequence(decode_time, error || ""); + } + if (!win) { + this.debug("draw", "cannot paint, window not found:", wid); + send_damage_sequence(-1, `window ${wid} not found`); + return; + } + if (coding === "offscreen-painted") { + const decode_time = options["decode_time"]; + send_damage_sequence(decode_time || 0, ""); + return; + } + try { + win.paint(packet, decode_result); + } catch (error) { + this.exc(error, "error painting", coding, "sequence no", packet_sequence); + send_damage_sequence(-1, String(error)); + //there may be other screen updates pending: + win.paint_pending = 0; + win.may_paint_now(); + this.request_redraw(win); + } + } + + /** + * Audio + */ + init_audio(ignore_audio_blacklist) { + this.debug("audio", "init_audio() enabled=", this.audio_enabled, ", mediasource enabled=", + this.audio_mediasource_enabled, ", aurora enabled=", this.audio_aurora_enabled); + if (this.audio_mediasource_enabled) { + this.mediasource_codecs = MediaSourceUtil.getMediaSourceAudioCodecs(ignore_audio_blacklist); + for (const codec_option in this.mediasource_codecs) { + this.audio_codecs[codec_option] = this.mediasource_codecs[codec_option]; + } + } + if (this.audio_aurora_enabled) { + this.aurora_codecs = MediaSourceUtil.getAuroraAudioCodecs(); + for (const codec_option in this.aurora_codecs) { + if (codec_option in this.audio_codecs) { + //we already have native MediaSource support! + continue; + } + this.audio_codecs[codec_option] = this.aurora_codecs[codec_option]; + } + } + this.debug("audio", "codecs:", this.audio_codecs); + if (!this.audio_codecs) { + this.audio_codec = null; + this.audio_enabled = false; + this.warn("no valid audio codecs found"); + return; + } + if (!(this.audio_codec in this.audio_codecs)) { + if (this.audio_codec) { + this.warn(`invalid audio codec: ${this.audio_codec}`); + this.warn(`codecs found: ${this.audio_codecs}`); + } + this.audio_codec = MediaSourceUtil.getDefaultAudioCodec(this.audio_codecs); + if (this.audio_codec) { + if (this.audio_mediasource_enabled && this.audio_codec in this.mediasource_codecs) { + this.audio_framework = "mediasource"; + } else if (this.audio_aurora_enabled && !Utilities.isIE()) { + this.audio_framework = "aurora"; + } + if (this.audio_framework) { + this.log(`using ${this.audio_framework} audio codec: ${this.audio_codec}`); + } else { + this.warn("no valid audio framework - cannot enable audio"); + this.audio_enabled = false; + } + } else { + this.warn("no valid audio codec found"); + this.audio_enabled = false; + } + } else { + this.log(`using ${this.audio_framework} audio codec: ${this.audio_codec}`); + } + this.log("audio codecs: ", Object.keys(this.audio_codecs)); + } + + _sound_start_receiving() { + if (!this.audio_framework || !this.audio_codec) { + //choose a codec + framework to use + const codecs_supported = MediaSourceUtil.get_supported_codecs(this.audio_mediasource_enabled, this.audio_aurora_enabled, false); + const audio_codec = MediaSourceUtil.get_best_codec(codecs_supported); + if (!audio_codec) { + this.log("no codec found"); + return; + } + const acparts = audio_codec.split(":"); + this.audio_framework = acparts[0]; + this.audio_codec = acparts[1]; + } + try { + this.audio_buffers = []; + this.audio_buffers_count = 0; + if (this.audio_framework === "mediasource") { + this._sound_start_mediasource(); + } else { + this._sound_start_aurora(); + } + } catch (error) { + this.exc(error, "error starting audio player"); + } + } + + _send_sound_start() { + this.log(`audio: requesting ${this.audio_codec} stream from the server`); + this.send([PACKET_TYPES.sound_control, "start", this.audio_codec]); + } + + _sound_start_aurora() { + this.audio_aurora_ctx = AV.Player.fromXpraSource(); + this._send_sound_start(); + } + + _sound_start_mediasource() { + const me = this; + + function audio_error(event) { + if (!me.media_source) { + //already closed + me.debug("audio", `media_source is closed, ignoring audio error: ${event}`); + return; + } + if (me.audio) { + me.error(`${event} error: ${me.audio.error}`); + if (me.audio.error) { + me.error(MediaSourceConstants.ERROR_CODE[me.audio.error.code]); + } + } else { + me.error(`${event} error`); + } + me.close_audio(); + } + + //Create a MediaSource: + this.media_source = MediaSourceUtil.getMediaSource(); + if (this.debug) { + MediaSourceUtil.addMediaSourceEventDebugListeners(this.media_source, "audio"); + } + this.media_source.addEventListener("error", (e) => + audio_error("audio source") + ); + + //Create an