1
0
Fork 0

Compare commits

...

43 commits

Author SHA1 Message Date
1be713f9b7
Updating source url in copyright.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:27:36 +02:00
91eab4dc7e
Switching from releases to tags in watch file.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:27:36 +02:00
4350bd83af
Releasing debian version 1.7.7-2.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:27:29 +02:00
edc37f2cc3
Moving writable startup-option to the front as apparently order matters.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:27:29 +02:00
9654d6d36d
Releasing debian version 1.7.7-1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:27:22 +02:00
bd2129e08a
Starting ttyd by default in read-write mode (ttyd 1.7.4 changed to read-only default which makes sense as a general default, in Debian ttyd is by default started on localhost only and requireing login).
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:27:22 +02:00
351a44ed4f
Updating to standards-version 4.7.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:27:22 +02:00
7376f25a40
Updating copyright for 2024.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:27:22 +02:00
aeb425b9a8
Merging upstream version 1.7.7.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:27:21 +02:00
b549a689a7
Releasing debian version 1.7.3-2.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:26:59 +02:00
9d4bc5bb2b
Adding manual depends to libwebsockets-evlib-uv (Closes: #1022217).
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:26:59 +02:00
0deb009df8
Releasing debian version 1.7.3-1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:26:52 +02:00
ae55a3d830
Updating to standards version 4.6.2.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:26:51 +02:00
b12f9f3fe0
Merging upstream version 1.7.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:26:51 +02:00
bd8b197726
Releasing debian version 1.7.2-1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:26:25 +02:00
cd5216f25e
Merging upstream version 1.7.2.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:26:25 +02:00
3612ff974b
Releasing debian version 1.7.1-1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:25:53 +02:00
fbea2ad4e6
Merging upstream version 1.7.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:25:53 +02:00
2f5f801caf
Releasing debian version 1.7.0-1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:25:37 +02:00
a970e2d5fa
Merging upstream version 1.7.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:25:37 +02:00
da35d1ca2c
Releasing debian version 1.6.3+20220719-4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:25:14 +02:00
5648f93669
Correcting port typo in apache2 config.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:25:14 +02:00
2489914c36
Releasing debian version 1.6.3+20220719-3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:25:07 +02:00
76bef99b97
Using 127.0.0.1 instead of localhost in apache2 config to work on ipv6-only systems too.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:25:07 +02:00
9218d27800
Correcting typo of ProxyPassReverse directive name in apache2 config.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:25:07 +02:00
b413c9f8c5
Releasing debian version 1.6.3+20220719-2.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:25:01 +02:00
fa8cd56f59
Removing superfluous slashes in apache2 config for convenience reasons.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:25:01 +02:00
4c6457f8d0
Releasing debian version 1.6.3+20220719-1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:24:55 +02:00
42c52e73d4
Adding TODO file.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:24:54 +02:00
c8e76972f7
Adding apache2 reverse-proxy configuration.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:24:54 +02:00
028c10bc5c
Updating to standards version 4.6.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:24:54 +02:00
57ea482fc3
Updating copyright for new upstream.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:24:54 +02:00
35118b8162
Merging upstream version 1.6.3+20220719.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:24:53 +02:00
155b3a2885
Releasing debian version 1.6.3+20210924-1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:24:34 +02:00
f66324fb7a
Merging upstream version 1.6.3+20210924.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:24:33 +02:00
a73d4a08e6
Releasing debian version 1.6.3-4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:24:16 +02:00
9a659fc6dd
Updating to standards version 4.6.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:24:16 +02:00
60d72c9cb8
Releasing debian version 1.6.3-3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:24:07 +02:00
bac1bee1cd
Restricting package to linux architectures.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:24:07 +02:00
316223a638
Adding /etc/default/ttyd to handle options used to start ttyd via systemd unit.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:24:06 +02:00
b7ac96d837
Releasing debian version 1.6.3-2.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:23:37 +02:00
51f96f61c8
Correcting path to executables in system service, thanks to Jonas Smedegaard <dr@jones.dk> (Closes: #983261).
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:23:35 +02:00
479ca7bea0
Adding README.Debian.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-22 17:23:31 +02:00
70 changed files with 30209 additions and 20417 deletions

View file

@ -1,3 +1,6 @@
BasedOnStyle: Google
Language: Cpp
ColumnLimit: 100
ColumnLimit: 120
IndentWidth: 2
TabWidth: 2
UseTab: Never

2
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,2 @@
github: tsl0922
patreon: tsl0922

7
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/html"
schedule:
interval: daily
open-pull-requests-limit: 10

View file

@ -13,40 +13,17 @@ on:
- "CMakeLists.txt"
- "src/*"
- "scripts/*"
workflow_call:
jobs:
build:
runs-on: ubuntu-18.04
strategy:
matrix:
lws-version: [4.1.6, 3.2.3, 2.4.2]
steps:
- name: Install packages
run: |
sudo apt-get update
sudo apt-get install build-essential cmake libjson-c-dev zlib1g-dev libssl-dev libuv1-dev
- name: Install libwebsockets-${{ matrix.lws-version }}
env:
LWS_VERSION: ${{ matrix.lws-version }}
run: |
cd $(mktemp -d)
curl -sLo- https://github.com/warmcat/libwebsockets/archive/v${LWS_VERSION}.tar.gz | tar xz
cd libwebsockets-${LWS_VERSION}
cmake -DLWS_WITH_LIBUV=ON -DLWS_UNIX_SOCK=ON -DLWS_IPV6=ON -DLWS_WITHOUT_TESTAPPS=ON -DCMAKE_BUILD_TYPE=RELEASE .
make && sudo make install && sudo ldconfig
- uses: actions/checkout@v2
- name: Build ttyd
run: |
cmake -DCMAKE_BUILD_TYPE=RELEASE .
make && sudo make install
ttyd -v
cross:
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
target: [i686, x86_64, arm, armhf, aarch64, mips, mipsel, mips64, mips64el]
target: [i686, x86_64, arm, armhf, aarch64, mips, mipsel, mips64, mips64el, s390x, win32]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install packages
run: |
sudo apt-get update
@ -55,7 +32,8 @@ jobs:
env:
BUILD_TARGET: ${{ matrix.target }}
run: ./scripts/cross-build.sh
- uses: actions/upload-artifact@v1
- uses: actions/upload-artifact@v4
with:
name: ttyd.${{ matrix.target }}
path: build/ttyd
path: build/ttyd*

59
.github/workflows/docker.yml vendored Normal file
View file

@ -0,0 +1,59 @@
name: docker
on:
push:
branches: main
tags: ["*"]
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install packages
run: |
sudo apt-get update
sudo apt-get install -y autoconf automake build-essential cmake curl file libtool
- name: Cross build multi-arch binary
run: |
mkdir dist
for arch in amd64 armv7 arm64 s390x; do
env BUILD_TARGET=$arch ./scripts/cross-build.sh
[ "$arch" = "armv7" ] && arch="arm"
mkdir -p dist/$arch && cp build/ttyd dist/$arch/ttyd
done
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Determine docker tags
id: docker_tag
run: |
case $GITHUB_REF in
refs/tags/*)
TAG_NAME=${GITHUB_REF#refs/tags/}
echo "DOCKER_TAG=tsl0922/ttyd:${TAG_NAME}" >> $GITHUB_ENV
echo "ALPINE_TAG=tsl0922/ttyd:${TAG_NAME}-alpine" >> $GITHUB_ENV
;;
*)
echo "DOCKER_TAG=tsl0922/ttyd:latest" >> $GITHUB_ENV
echo "ALPINE_TAG=tsl0922/ttyd:alpine" >> $GITHUB_ENV
esac
- name: build/push docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/s390x
push: true
tags: ${{ env.DOCKER_TAG }}
- name: build/push docker image (alpine)
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.alpine
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/s390x
push: true
tags: ${{ env.ALPINE_TAG }}

View file

@ -12,15 +12,17 @@ on:
jobs:
build:
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '12.x'
node-version: 18
- name: Run yarn install, check and build
run: |
cd html
corepack enable
corepack prepare yarn@stable --activate
yarn install
yarn run check
yarn run build
working-directory: html

View file

@ -2,49 +2,36 @@ name: release
on:
push:
tags:
- "*"
tags: ["*"]
jobs:
release:
runs-on: ubuntu-18.04
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
prerelease: false
build:
runs-on: ubuntu-18.04
needs: release
strategy:
matrix:
target: [i686, x86_64, arm, armhf, aarch64, mips, mipsel, mips64, mips64el]
uses: ./.github/workflows/backend.yml
publish:
needs: [build]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Install packages
- uses: actions/checkout@v4
- name: Check version bump
run: |
sudo apt-get update
sudo apt-get install -y autoconf automake build-essential cmake curl file libtool
- name: Cross build (${{ matrix.target }})
env:
BUILD_TARGET: ${{ matrix.target }}
run: ./scripts/cross-build.sh
- name: Upload assets
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG=$(git describe --tags --match "[0-9]*.[0-9]*.[0-9]*" --abbrev=8)
VERSION=$(grep project CMakeLists.txt| awk '{print $3}')
if [ "$TAG" != "$VERSION" ]; then
echo "=== Version in CMakeLists.txt and git tag does not match!"
echo "=== Git Tag: $TAG, Version: $VERSION"
exit 1
fi
- uses: actions/download-artifact@v4
- run: |
mkdir build
for file in ttyd.*/*; do
target=$(echo $file | awk -F/ '{print $1}')
[[ $file == *.exe ]] && target="$target.exe"
mv $file build/$target
done
pushd build; sha256sum ttyd.* > SHA256SUMS; popd
- uses: ncipollo/release-action@v1
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: build/ttyd
asset_name: ttyd.${{ matrix.target }}
asset_content_type: application/octet-stream
artifacts: build/*
allowUpdates: true
draft: true

3
.gitignore vendored
View file

@ -47,5 +47,8 @@ build
# Clion files
.idea/
# VSCode files
.vscode/
# Project files
!init.d

View file

@ -1,20 +1,19 @@
cmake_minimum_required(VERSION 2.8)
cmake_minimum_required(VERSION 3.12.0)
project(ttyd C)
set(PROJECT_VERSION "1.6.3")
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
find_package(Git)
if(GIT_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} --git-dir ${CMAKE_CURRENT_SOURCE_DIR}/.git rev-parse --short HEAD
RESULT_VARIABLE GIT_RET
OUTPUT_VARIABLE GIT_COMMIT
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if("${GIT_RET}" STREQUAL "0")
set(PROJECT_VERSION "${PROJECT_VERSION}-${GIT_COMMIT}")
project(ttyd VERSION 1.7.7 LANGUAGES C)
set(TTYD_VERSION "${PROJECT_VERSION}")
include(GetGitVersion)
get_git_version(GIT_VERSION SEM_VER)
get_git_head(GIT_COMMIT)
if("${SEM_VER}" VERSION_GREATER "${TTYD_VERSION}")
set(TTYD_VERSION "${SEM_VER}")
endif()
if(NOT "${GIT_COMMIT}" STREQUAL "")
set(TTYD_VERSION "${TTYD_VERSION}-${GIT_COMMIT}")
endif()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_GNU_SOURCE")
@ -28,7 +27,7 @@ else()
set(CMAKE_C_STANDARD 99)
endif()
set(SOURCE_FILES src/server.c src/http.c src/protocol.c src/terminal.c src/utils.c)
set(SOURCE_FILES src/utils.c src/pty.c src/protocol.c src/http.c src/server.c)
include(FindPackageHandleStandardArgs)
@ -43,7 +42,7 @@ endif()
find_path(JSON-C_INCLUDE_DIR NAMES json.h PATH_SUFFIXES json-c)
find_library(JSON-C_LIBRARY NAMES json-c)
find_package_handle_standard_args(JSON-C REQUIRED_VARS JSON-C_INCLUDE_DIR JSON-C_LIBRARY)
find_package_handle_standard_args(JSON-C REQUIRED_VARS JSON-C_LIBRARY JSON-C_INCLUDE_DIR)
mark_as_advanced(JSON-C_INCLUDE_DIR JSON-C_LIBRARY)
if(JSON-C_FOUND)
SET(JSON-C_INCLUDE_DIRS "${JSON-C_INCLUDE_DIR}")
@ -51,26 +50,19 @@ if(JSON-C_FOUND)
endif()
find_package(ZLIB REQUIRED)
find_package(Libwebsockets 1.7.0 QUIET)
if(NOT Libwebsockets_FOUND) # for libwebsockets-dev on ubuntu 16.04
find_path(LIBWEBSOCKETS_INCLUDE_DIR NAMES libwebsockets.h)
find_library(LIBWEBSOCKETS_LIBRARY NAMES websockets)
find_package_handle_standard_args(Libwebsockets REQUIRED_VARS LIBWEBSOCKETS_LIBRARY LIBWEBSOCKETS_INCLUDE_DIR)
mark_as_advanced(LIBWEBSOCKETS_INCLUDE_DIR LIBWEBSOCKETS_LIBRARY)
if(Libwebsockets_FOUND)
SET(LIBWEBSOCKETS_INCLUDE_DIRS "${LIBWEBSOCKETS_INCLUDE_DIR}")
SET(LIBWEBSOCKETS_LIBRARIES "${LIBWEBSOCKETS_LIBRARY}")
endif()
endif()
find_package(Libwebsockets 3.2.0 REQUIRED)
set(INCLUDE_DIRS ${ZLIB_INCLUDE_DIR} ${LIBWEBSOCKETS_INCLUDE_DIRS} ${JSON-C_INCLUDE_DIRS} ${LIBUV_INCLUDE_DIRS})
set(LINK_LIBS ${ZLIB_LIBRARIES} ${LIBWEBSOCKETS_LIBRARIES} ${JSON-C_LIBRARIES} ${LIBUV_LIBRARIES})
set (CMAKE_REQUIRED_INCLUDES ${INCLUDE_DIRS})
include(CheckSymbolExists)
check_symbol_exists(LWS_WITH_LIBUV "lws_config.h" LWS_WITH_LIBUV)
check_symbol_exists(LWS_OPENSSL_SUPPORT "lws_config.h" LWS_OPENSSL_ENABLED)
check_symbol_exists(LWS_WITH_MBEDTLS "lws_config.h" LWS_MBEDTLS_ENABLED)
if(NOT LWS_WITH_LIBUV)
message(FATAL_ERROR "libwebsockets was not build with libuv support (-DLWS_WITH_LIBUV=ON)")
endif()
if(LWS_OPENSSL_ENABLED AND NOT LWS_MBEDTLS_ENABLED)
find_package(OpenSSL REQUIRED)
list(APPEND INCLUDE_DIRS ${OPENSSL_INCLUDE_DIR})
@ -78,15 +70,23 @@ if(LWS_OPENSSL_ENABLED AND NOT LWS_MBEDTLS_ENABLED)
endif()
if(WIN32)
list(APPEND LINK_LIBS shell32)
elseif(NOT APPLE)
list(APPEND LINK_LIBS shell32 ws2_32)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/app.rc.in ${CMAKE_CURRENT_BINARY_DIR}/app.rc @ONLY)
list(APPEND SOURCE_FILES ${CMAKE_CURRENT_BINARY_DIR}/app.rc)
else()
find_library(LIBUTIL NAMES util)
if(LIBUTIL)
list(APPEND LINK_LIBS util)
endif()
endif()
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
target_include_directories(${PROJECT_NAME} PUBLIC ${INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME} ${LINK_LIBS})
target_compile_definitions(${PROJECT_NAME} PRIVATE TTYD_VERSION="${PROJECT_VERSION}")
target_compile_definitions(${PROJECT_NAME} PUBLIC
TTYD_VERSION="${TTYD_VERSION}"
$<$<PLATFORM_ID:Windows>:_WIN32_WINNT=0xa00 WINVER=0xa00>
)
include(GNUInstallDirs)

View file

@ -1,16 +1,15 @@
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y autoconf automake curl cmake git libtool make \
&& git clone --depth=1 https://github.com/tsl0922/ttyd.git /ttyd \
&& cd /ttyd && env BUILD_TARGET=x86_64 WITH_SSL=true ./scripts/cross-build.sh
FROM ubuntu:20.04
FROM ubuntu:18.04
COPY --from=0 /ttyd/build/ttyd /usr/bin/ttyd
ARG TARGETARCH
ADD https://github.com/krallin/tini/releases/download/v0.18.0/tini /sbin/tini
RUN chmod +x /sbin/tini
# Dependencies
RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/*
# Application
COPY ./dist/${TARGETARCH}/ttyd /usr/bin/ttyd
EXPOSE 7681
WORKDIR /root
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["ttyd", "bash"]
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["ttyd", "-W", "bash"]

View file

@ -1,14 +0,0 @@
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y autoconf automake curl cmake git libtool make \
&& git clone --depth=1 https://github.com/tsl0922/ttyd.git /ttyd \
&& cd /ttyd && env BUILD_TARGET=x86_64 WITH_SSL=true ./scripts/cross-build.sh
FROM alpine:3.12
COPY --from=0 /ttyd/build/ttyd /usr/bin/ttyd
RUN apk add --no-cache bash tini
EXPOSE 7681
WORKDIR /root
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["ttyd", "bash"]

15
Dockerfile.alpine Normal file
View file

@ -0,0 +1,15 @@
FROM alpine
ARG TARGETARCH
# Dependencies
RUN apk add --no-cache bash tini
# Application
COPY ./dist/${TARGETARCH}/ttyd /usr/bin/ttyd
EXPOSE 7681
WORKDIR /root
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["ttyd", "-W", "bash"]

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2016 Shuanglei Tao <tsl0922@gmail.com>
Copyright (c) 2016-2022 Shuanglei Tao <tsl0922@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,15 +1,23 @@
# ttyd - Share your terminal over the web ![backend](https://github.com/tsl0922/ttyd/workflows/backend/badge.svg) ![frontend](https://github.com/tsl0922/ttyd/workflows/frontend/badge.svg)
![backend](https://github.com/tsl0922/ttyd/workflows/backend/badge.svg)
![frontend](https://github.com/tsl0922/ttyd/workflows/frontend/badge.svg)
[![GitHub Releases](https://img.shields.io/github/downloads/tsl0922/ttyd/total)](https://github.com/tsl0922/ttyd/releases)
[![Docker Pulls](https://img.shields.io/docker/pulls/tsl0922/ttyd)](https://hub.docker.com/r/tsl0922/ttyd)
[![Packaging status](https://repology.org/badge/tiny-repos/ttyd.svg)](https://repology.org/project/ttyd/versions)
![GitHub](https://img.shields.io/github/license/tsl0922/ttyd)
# ttyd - Share your terminal over the web
ttyd is a simple command-line tool for sharing terminal over the web.
![screenshot](https://github.com/tsl0922/ttyd/raw/master/screenshot.gif)
![screenshot](https://github.com/tsl0922/ttyd/raw/main/screenshot.gif)
# Features
- Built on top of [Libwebsockets](https://libwebsockets.org) with [libuv](https://libuv.org) for speed
- Fully-featured terminal based on [Xterm.js](https://xtermjs.org) with [CJK](https://en.wikipedia.org/wiki/CJK_characters) and IME support
- Graphical [ZMODEM](https://en.wikipedia.org/wiki/ZMODEM) integration with [lrzsz](https://ohse.de/uwe/software/lrzsz.html) support
- SSL support based on [OpenSSL](https://www.openssl.org)
- Built on top of [libuv](https://libuv.org) and [WebGL2](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API) for speed
- Fully-featured terminal with [CJK](https://en.wikipedia.org/wiki/CJK_characters) and IME support
- [ZMODEM](https://en.wikipedia.org/wiki/ZMODEM) ([lrzsz](https://ohse.de/uwe/software/lrzsz.html)) / [trzsz](https://trzsz.github.io) file transfer support
- [Sixel](https://en.wikipedia.org/wiki/Sixel) image output support ([img2sixel](https://saitoha.github.io/libsixel) / [lsix](https://github.com/hackerb9/lsix))
- SSL support based on [OpenSSL](https://www.openssl.org) / [Mbed TLS](https://github.com/Mbed-TLS/mbedtls)
- Run any custom command with options
- Basic authentication support and many other custom options
- Cross platform: macOS, Linux, FreeBSD/OpenBSD, [OpenWrt](https://openwrt.org), Windows
@ -20,70 +28,64 @@ ttyd is a simple command-line tool for sharing terminal over the web.
## Install on macOS
Install with [homebrew](http://brew.sh):
```bash
brew install ttyd
```
- Install with [Homebrew](http://brew.sh): `brew install ttyd`
- Install with [MacPorts](https://www.macports.org): `sudo port install ttyd`
## Install on Linux
- Binary version (recommended): download from the [releases](https://github.com/tsl0922/ttyd/releases) page.
- Binary version (recommended): download from the [releases](https://github.com/tsl0922/ttyd/releases) page
- Install with [Homebrew](https://docs.brew.sh/Homebrew-on-Linux) : `brew install ttyd`
- Install the snap: `sudo snap install ttyd --classic`
- Build from source (debian/ubuntu):
```bash
sudo apt-get install build-essential cmake git libjson-c-dev libwebsockets-dev
sudo apt-get update
sudo apt-get install -y build-essential cmake git libjson-c-dev libwebsockets-dev
git clone https://github.com/tsl0922/ttyd.git
cd ttyd && mkdir build && cd build
cmake ..
make && sudo make install
```
You may also need to compile/install [libwebsockets](https://libwebsockets.org) from source if the `libwebsockets-dev` package is outdated.
- Install on OpenWrt: `opkg install ttyd`
- Install on Gentoo: clone the [repo](https://bitbucket.org/mgpagano/ttyd/src/master) and follow the directions [here](https://wiki.gentoo.org/wiki/Custom_repository#Creating_a_local_repository).
## Install on Windows
[Compile on Windows](https://github.com/tsl0922/ttyd/wiki/Compile-on-Windows).
## Install on OpenWrt
```bash
opkg install ttyd
```
- Binary version (recommended): download from the [releases](https://github.com/tsl0922/ttyd/releases) page
- Install with [WinGet](https://github.com/microsoft/winget-cli): `winget install tsl0922.ttyd`
- Install with [Scoop](https://scoop.sh/#/apps?q=ttyd&s=2&d=1&o=true): `scoop install ttyd`
- [Compile on Windows](https://github.com/tsl0922/ttyd/wiki/Compile-on-Windows)
# Usage
## Command-line Options
```
ttyd is a tool for sharing terminal over the web
USAGE:
ttyd [options] <command> [<arguments...>]
VERSION:
1.6.3
OPTIONS:
-p, --port Port to listen (default: 7681, use `0` for random port)
-i, --interface Network interface to bind (eg: eth0), or UNIX domain socket path (eg: /var/run/ttyd.sock)
-c, --credential Credential for Basic Authentication (format: username:password)
-U, --socket-owner User owner of the UNIX domain socket file, when enabled (eg: user:group)
-c, --credential Credential for basic authentication (format: username:password)
-H, --auth-header HTTP Header name for auth proxy, this will configure ttyd to let a HTTP reverse proxy handle authentication
-u, --uid User id to run with
-g, --gid Group id to run with
-s, --signal Signal to send to the command when exit it (default: 1, SIGHUP)
-w, --cwd Working directory to be set for the child program
-a, --url-arg Allow client to send command line arguments in URL (eg: http://localhost:7681?arg=foo&arg=bar)
-R, --readonly Do not allow clients to write to the TTY
-W, --writable Allow clients to write to the TTY (readonly by default)
-t, --client-option Send option to client (format: key=value), repeat to add more options
-T, --terminal-type Terminal type to report, default: xterm-256color
-O, --check-origin Do not allow websocket connection from different origin
-m, --max-clients Maximum clients to support (default: 0, no limit)
-o, --once Accept only one client and exit on disconnection
-q, --exit-no-conn Exit on all clients disconnection
-B, --browser Open terminal with the default system browser
-I, --index Custom index.html path
-b, --base-path Expected base path for requests coming from a reverse proxy (eg: /mounted/here)
-P, --ping-interval Websocket ping interval(sec) (default: 300)
-b, --base-path Expected base path for requests coming from a reverse proxy (eg: /mounted/here, max length: 128)
-P, --ping-interval Websocket ping interval(sec) (default: 5)
-6, --ipv6 Enable IPv6 support
-S, --ssl Enable SSL
-C, --ssl-cert SSL certificate file path
@ -92,8 +94,6 @@ OPTIONS:
-d, --debug Set log level (default: 7)
-v, --version Print the version and exit
-h, --help Print this text and exit
Visit https://github.com/tsl0922/ttyd to get more information and report bugs.
```
Read the example usage on the [wiki](https://github.com/tsl0922/ttyd/wiki/Example-Usage).

32
app.rc.in Normal file
View file

@ -0,0 +1,32 @@
#include <winver.h>
#define VERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
#define VERSION_STR "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.0\0"
VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION
PRODUCTVERSION VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS 0
FILEOS VOS__WINDOWS32
FILETYPE VFT_DLL
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "FileDescription", "ttyd\0"
VALUE "ProductName", "ttyd\0"
VALUE "ProductVersion", VERSION_STR
VALUE "FileVersion", VERSION_STR
VALUE "InternalName", "ttyd\0"
VALUE "OriginalFilename", "ttyd.exe\0"
VALUE "LegalCopyright", "Copyright (C) 2016-2022 Shuanglei Tao\0"
VALUE "Comment", "\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

47
cmake/GetGitVersion.cmake Normal file
View file

@ -0,0 +1,47 @@
find_package(Git)
function(get_git_version var1 var2)
if(GIT_EXECUTABLE)
execute_process(
COMMAND ${GIT_EXECUTABLE} describe --tags --match "[0-9]*.[0-9]*.[0-9]*" --abbrev=8
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
RESULT_VARIABLE status
OUTPUT_VARIABLE GIT_VERSION
)
if (${status})
set(GIT_VERSION "0.0.0")
else()
string(STRIP ${GIT_VERSION} GIT_VERSION)
string(REGEX REPLACE "-[0-9]+-g" "-" GIT_VERSION ${GIT_VERSION})
endif()
else()
set(GIT_VERSION "0.0.0")
endif()
string(REGEX MATCH "^[0-9]+.[0-9]+.[0-9]+" SEM_VER "${GIT_VERSION}")
message("-- Git Tag: ${GIT_VERSION}, Sem Ver: ${SEM_VER}")
set(${var1} ${GIT_VERSION} PARENT_SCOPE)
set(${var2} ${SEM_VER} PARENT_SCOPE)
endfunction()
function(get_git_head var1)
if(GIT_EXECUTABLE)
execute_process(
COMMAND ${GIT_EXECUTABLE} --git-dir ${CMAKE_CURRENT_SOURCE_DIR}/.git rev-parse --short HEAD
RESULT_VARIABLE status
OUTPUT_VARIABLE GIT_COMMIT
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(${status})
set(GIT_COMMIT "unknown")
endif()
message("-- Git Commit: ${GIT_COMMIT}")
set(${var1} ${GIT_COMMIT} PARENT_SCOPE)
endif()
endfunction()

8
debian/TODO vendored Normal file
View file

@ -0,0 +1,8 @@
ttyd
====
* write logs to own logfile
* add debconf and update-alternative handling to do
apache configuration (enable modules, create config, create htpasswd, etc.)
-- Daniel Baumann <daniel.baumann@progress-linux.org> Mon, 08 Feb 2021 14:20:24 +0100

123
debian/changelog vendored
View file

@ -1,3 +1,126 @@
ttyd (1.7.7-2) sid; urgency=medium
* Uploading to sid.
* Moving writable startup-option to the front as apparently order
matters.
-- Daniel Baumann <daniel.baumann@progress-linux.org> Mon, 08 Apr 2024 10:20:40 +0200
ttyd (1.7.7-1) sid; urgency=medium
* Uploading to sid.
* Merging upstream version 1.7.7.
* Updating copyright for 2024.
* Updating to standards-version 4.7.0.
* Starting ttyd by default in read-write mode (ttyd 1.7.4 changed to
read-only default which makes sense as a general default, in Debian
ttyd is by default started on localhost only and requireing login).
-- Daniel Baumann <daniel.baumann@progress-linux.org> Mon, 08 Apr 2024 10:17:09 +0200
ttyd (1.7.3-2) sid; urgency=medium
* Uploading to sid.
* Adding manual depends to libwebsockets-evlib-uv (Closes: #1022217).
-- Daniel Baumann <daniel.baumann@progress-linux.org> Wed, 01 Feb 2023 20:52:44 +0100
ttyd (1.7.3-1) sid; urgency=medium
* Uploading to sid.
* Merging upstream version 1.7.3.
* Updating to standards version 4.6.2.
-- Daniel Baumann <daniel.baumann@progress-linux.org> Mon, 23 Jan 2023 09:45:13 +0100
ttyd (1.7.2-1) sid; urgency=medium
* Uploading to sid.
* Merging upstream version 1.7.2.
-- Daniel Baumann <daniel.baumann@progress-linux.org> Fri, 21 Oct 2022 11:25:05 +0200
ttyd (1.7.1-1) sid; urgency=medium
* Uploading to sid.
* Merging upstream version 1.7.1.
-- Daniel Baumann <daniel.baumann@progress-linux.org> Fri, 02 Sep 2022 10:36:10 +0200
ttyd (1.7.0-1) sid; urgency=medium
* Uploading to sid.
* Merging upstream version 1.7.0.
-- Daniel Baumann <daniel.baumann@progress-linux.org> Mon, 15 Aug 2022 14:08:23 +0200
ttyd (1.6.3+20220719-4) sid; urgency=medium
* Uploading to sid.
* Correcting port typo in apache2 config.
-- Daniel Baumann <daniel.baumann@progress-linux.org> Thu, 21 Jul 2022 16:37:32 +0200
ttyd (1.6.3+20220719-3) sid; urgency=medium
* Uploading to sid.
* Correcting typo of ProxyPassReverse directive name in apache2 config.
* Using 127.0.0.1 instead of localhost in apache2 config to work on
ipv6-only systems too.
-- Daniel Baumann <daniel.baumann@progress-linux.org> Thu, 21 Jul 2022 07:14:33 +0200
ttyd (1.6.3+20220719-2) sid; urgency=medium
* Uploading to sid.
* Removing superfluous slashes in apache2 config for convenience
reasons.
-- Daniel Baumann <daniel.baumann@progress-linux.org> Wed, 20 Jul 2022 08:08:53 +0200
ttyd (1.6.3+20220719-1) sid; urgency=medium
* Uploading to sid.
* Merging upstream version 1.6.3+20220719.
* Updating copyright for new upstream.
* Updating to standards version 4.6.1.
* Adding apache2 reverse-proxy configuration.
* Adding TODO file.
-- Daniel Baumann <daniel.baumann@progress-linux.org> Tue, 19 Jul 2022 12:34:47 +0200
ttyd (1.6.3+20210924-1) sid; urgency=medium
* Uploading to sid.
* Merging upstream version 1.6.3+20210924.
-- Daniel Baumann <daniel.baumann@progress-linux.org> Sat, 09 Oct 2021 12:24:08 +0200
ttyd (1.6.3-4) sid; urgency=medium
* Uploading to sid.
* Updating to standards version 4.6.0.
-- Daniel Baumann <daniel.baumann@progress-linux.org> Sat, 09 Oct 2021 11:02:22 +0200
ttyd (1.6.3-3) sid; urgency=medium
* Uploading to sid.
* Adding /etc/default/ttyd to handle options used to start ttyd via systemd
unit.
* Restricting package to linux architectures.
-- Daniel Baumann <daniel.baumann@progress-linux.org> Fri, 26 Feb 2021 09:38:56 +0100
ttyd (1.6.3-2) sid; urgency=medium
* Uploading to sid.
* Adding README.Debian.
* Correcting path to executables in system service,
thanks to Jonas Smedegaard <dr@jones.dk> (Closes: #983261).
-- Daniel Baumann <daniel.baumann@progress-linux.org> Mon, 22 Feb 2021 06:50:22 +0100
ttyd (1.6.3-1) sid; urgency=medium
* Initial upload to sid (Closes: #972863).

7
debian/control vendored
View file

@ -9,16 +9,19 @@ Build-Depends:
libwebsockets-dev,
zlib1g-dev,
Rules-Requires-Root: no
Standards-Version: 4.5.1
Standards-Version: 4.7.0
Homepage: https://tsl0922.github.io/ttyd
Vcs-Browser: https://git.progress-linux.org/users/daniel.baumann/debian/packages/ttyd
Vcs-Git: https://git.progress-linux.org/users/daniel.baumann/debian/packages/ttyd
Package: ttyd
Section: web
Architecture: any
Architecture: linux-any
Depends:
libwebsockets-evlib-uv,
${misc:Depends},
${shlibs:Depends},
Suggests:
apache2,
Description: Share your terminal over the web
ttyd is a command-line tool for sharing a terminal over the web.

10
debian/copyright vendored
View file

@ -1,18 +1,14 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: ttyd
Upstream-Contact: Shuanglei Tao <tsl0922@gmail.com>
Source: https://github.com/tsl0922/ttyd/releases
Source: https://github.com/tsl0922/ttyd/tags
Files: *
Copyright: 2016-2021 Shuanglei Tao <tsl0922@gmail.com>
Copyright: 2016-2024 Shuanglei Tao <tsl0922@gmail.com>
License: MIT
Files: src/queue.h
Copyright: 1991-1993 The Regents of the University of California
License: BSD-3
Files: debian/*
Copyright: 2021 Daniel Baumann <daniel.baumann@progress-linux.org>
Copyright: 2021-2024 Daniel Baumann <daniel.baumann@progress-linux.org>
License: MIT
License: BSD-3

View file

@ -0,0 +1,10 @@
# /etc/ttyd/apache2-auth.conf
<IfFile /etc/ttyd/htpasswd>
AuthName "ttyd"
AuthBasicProvider file
AuthType basic
AuthUserFile /etc/ttyd/htpasswd
Require valid-user
</IfFile>

View file

@ -0,0 +1,8 @@
# /etc/ttyd/apache2-auth.conf
Order deny,allow
Deny from all
Allow from 10.0.0.0/8
Allow from 172.16.0.0/12
Allow from 192.168.0.0/16

View file

@ -0,0 +1,16 @@
# /etc/ttyd/apache2-auth.conf
AuthName "ttyd"
AuthBasicProvider ldap
AuthType basic
AuthLDAPURL "ldaps://ldap.example.net:636/dc=example,dc=net?uid?sub"
AuthLDAPBindDN cn=read-only,ou=srv-account,dc=example,dc=net
AuthLDAPBindPassword "examplePassword"
AuthLDAPRemoteUserAttribute uid
AuthLDAPRemoteUserIsDN off
AuthLDAPGroupAttribute memberUid
AuthLDAPGroupAttributeIsDN off
Require ldap-group cn=foo,ou=security,ou=groups,dc=example,dc=net

View file

@ -0,0 +1,16 @@
# /etc/ttyd/apache2-auth.conf
AuthName "ttyd"
AuthBasicProvider ldap
AuthType basic
AuthLDAPURL "ldaps://ldap.example.net:636/dc=example,dc=net?uid?sub"
AuthLDAPBindDN cn=read-only,ou=srv-account,dc=example,dc=net
AuthLDAPBindPassword "examplePassword"
AuthLDAPRemoteUserAttribute uid
AuthLDAPRemoteUserIsDN off
AuthLDAPGroupAttribute memberUid
AuthLDAPGroupAttributeIsDN off
Require ldap-user foo bar baz

18
debian/local/apache2/ttyd.conf vendored Normal file
View file

@ -0,0 +1,18 @@
# /etc/apache2/conf-available/ttyd.conf
<IfModule mod_proxy.c>
ProxyRequests Off
ProxyPreserveHost On
ProxyPass /ttyd/ws ws://127.0.0.1:7681/ws
ProxyPassReverse /ttyd/ws ws://127.0.0.1:7681/ws
ProxyPass /ttyd http://127.0.0.1:7681 keepalive=on
ProxyPassReverse /ttyd http://127.0.0.1:7681
<IfFile /etc/ttyd/apache2-auth.conf>
<Location /ttyd>
Include /etc/ttyd/apache2-auth.conf
</Location>
</IfFile>
</IfModule>

3
debian/local/default/ttyd vendored Normal file
View file

@ -0,0 +1,3 @@
# /etc/default/ttyd
TTYD_OPTIONS="-W -i lo -p 7681 -O login"

62
debian/ttyd.README.Debian vendored Normal file
View file

@ -0,0 +1,62 @@
ttyd for Debian
===============
1. Default configuration
------------------------
After installing ttyd it will by default listen on http://localhost:7681
in multi-user read-write "login"-mode:
* multi-user means that more than one user can connect at the same time.
* read-write means that anyone connecting to the website can input data.
* Login mode means that the user gets a login prompt (like getty) where
user and password has to be entered.
Edit /etc/default/ttyd and check the ttyd(1) manpage for more information
about available options.
2. Reverse proxy
----------------
To make ttyd accessible on the network, it is advised to hide it behind a
reverse proxy that does TLS and performs user authentication.
To enable the apache2 proxy configuration, the following modules and
configuration need to be enabled:
* sudo a2enmod proxy proxy_http proxy_http2 proxy_wstunnel
* sudo a2enconf ttyd
* sudo service apache2 reload
ttyd is then accessible as <http://example.org/ttyd>.
3. Apache authentication
------------------------
The apache reverse-proxy configuration automatically includes
/etc/ttyd/apache2-auth.conf, if existing, to protect access to '/ttyd'.
There are some examples in /usr/share/doc/ttyd/examples that can be
used as starting point.
To enable HTTP basic authentication, the following steps can be used:
* sudo mkdir -p /etc/ttyd
* sudo ln -s /usr/share/doc/ttyd/examples/apache2-authbasic-file.conf \
/etc/ttyd/apache2-auth.conf
* sudo htpasswd -c -b /etc/ttyd/htpasswd daniel password123
* sudo service apache2 reload
This will allow the user 'daniel' to access ttyd with the password
'password123'. Further users can be added, see htpasswd(1).
-- Daniel Baumann <daniel.baumann@progress-linux.org> Sun, 21 Feb 2021 17:19:20 +0100

1
debian/ttyd.examples vendored Normal file
View file

@ -0,0 +1 @@
debian/examples/*

2
debian/ttyd.install vendored Normal file
View file

@ -0,0 +1,2 @@
debian/local/apache2/* /etc/apache2/conf-available
debian/local/default/* /etc/default

5
debian/ttyd.service vendored
View file

@ -5,8 +5,9 @@ After=network.target systemd-tmpfiles-clean.service
[Service]
Type=simple
ExecStart=/bin/ttyd -i lo -p 7681 -O login
ExecReload=/bin/kill -HUP $MAINPID
EnvironmentFile=/etc/default/ttyd
ExecStart=/usr/bin/ttyd $TTYD_OPTIONS
ExecReload=/usr/bin/kill -HUP $MAINPID
KillMode=process
LimitNOFILE=512
LimitMEMLOCK=infinity

2
debian/watch vendored
View file

@ -1,3 +1,3 @@
version=4
opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/ttyd-$1\.tar\.gz/ \
https://github.com/tsl0922/ttyd/releases .*/v?(\d\S+)\.tar\.gz
https://github.com/tsl0922/ttyd/tags .*/v?(\d\S+)\.tar\.gz

1
html/.eslintignore Normal file
View file

@ -0,0 +1 @@
dist/

20
html/.eslintrc.json Normal file
View file

@ -0,0 +1,20 @@
{
"extends": "./node_modules/gts/",
"overrides": [
{
"files": ["**/*.ts", "**/*.tsx"],
"parserOptions": {
"jsxPragma": "h"
},
"rules": {
"@typescript-eslint/no-duplicate-enum-values": "off"
}
},
{
"files": ["gulpfile.js", "webpack.config.js"],
"rules": {
"node/no-unpublished-require": "off"
}
}
]
}

11
html/.gitignore vendored
View file

@ -1,4 +1,9 @@
node_modules
/build
/dist
/*.log
dist
*.log
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

6
html/.prettierrc.js Normal file
View file

@ -0,0 +1,6 @@
module.exports = {
...require('gts/.prettierrc.json'),
"bracketSpacing": true,
"tabWidth": 4,
"printWidth": 120,
}

View file

@ -0,0 +1,34 @@
diff --git a/src/zsession.js b/src/zsession.js
index 5f0b8f9d8afa6fba0acd6dd0477afa186f7aad9a..c7ea98e0f08c97d63d321f784a5dd8bf66888743 100644
--- a/src/zsession.js
+++ b/src/zsession.js
@@ -548,20 +548,17 @@ Zmodem.Session.Receive = class ZmodemReceiveSession extends Zmodem.Session {
if (this._got_ZFIN) {
if (this._input_buffer.length < 2) return;
- //if its OO, then set this._bytes_after_OO
- if (Zmodem.ZMLIB.find_subarray(this._input_buffer, OVER_AND_OUT) === 0) {
+ if (Zmodem.ZMLIB.find_subarray(this._input_buffer, OVER_AND_OUT) !== 0) {
+ console.warn( "PROTOCOL: Only thing after ZFIN should be “OO” (79,79), not: " + this._input_buffer.join() );
+ }
- //This doubles as an indication that the session has ended.
- //We need to set this right away so that handlers like
- //"session_end" will have access to it.
- this._bytes_after_OO = _trim_OO(this._bytes_being_consumed.slice(0));
- this._on_session_end();
+ //This doubles as an indication that the session has ended.
+ //We need to set this right away so that handlers like
+ //"session_end" will have access to it.
+ this._bytes_after_OO = _trim_OO(this._bytes_being_consumed.slice(0));
+ this._on_session_end();
- return;
- }
- else {
- throw( "PROTOCOL: Only thing after ZFIN should be “OO” (79,79), not: " + this._input_buffer.join() );
- }
+ return;
}
var parsed;

1
html/.yarnrc.yml Normal file
View file

@ -0,0 +1 @@
nodeLinker: node-modules

View file

@ -1,6 +1,8 @@
## Prerequisites
install [Yarn](https://yarnpkg.com), and run: `yarn install`.
> **NOTE:** yarn v2 is required.
Install [Yarn](https://yarnpkg.com/getting-started/install), and run: `yarn install`.
## Development

View file

@ -1,31 +1,31 @@
const { src, dest, task, series } = require("gulp");
const { src, dest, task, series } = require('gulp');
const clean = require('gulp-clean');
const gzip = require('gulp-gzip');
const inlineSource = require('gulp-inline-source');
const rename = require("gulp-rename");
const rename = require('gulp-rename');
const through2 = require('through2');
const genHeader = (size, buf, len) => {
let idx = 0;
let data = "unsigned char index_html[] = {\n ";
let data = 'unsigned char index_html[] = {\n ';
for (const value of buf) {
idx++;
let current = value < 0 ? value + 256 : value;
const current = value < 0 ? value + 256 : value;
data += "0x";
data += '0x';
data += (current >>> 4).toString(16);
data += (current & 0xF).toString(16);
data += (current & 0xf).toString(16);
if (idx === len) {
data += "\n";
data += '\n';
} else {
data += idx % 12 === 0 ? ",\n " : ", ";
data += idx % 12 === 0 ? ',\n ' : ', ';
}
}
data += "};\n";
data += '};\n';
data += `unsigned int index_html_len = ${len};\n`;
data += `unsigned int index_html_size = ${size};\n`;
return data;
@ -33,30 +33,36 @@ const genHeader = (size, buf, len) => {
let fileSize = 0;
task('clean', () => {
return src('dist', { read: false, allowEmpty: true })
.pipe(clean());
return src('dist', { read: false, allowEmpty: true }).pipe(clean());
});
task('inline', () => {
return src('dist/index.html')
.pipe(inlineSource())
.pipe(rename("inline.html"))
.pipe(dest('dist/'));
const options = {
compress: false,
};
return src('dist/index.html').pipe(inlineSource(options)).pipe(rename('inline.html')).pipe(dest('dist/'));
});
task('default', series('inline', () => {
task(
'default',
series('inline', () => {
return src('dist/inline.html')
.pipe(through2.obj((file, enc, cb) => {
.pipe(
through2.obj((file, enc, cb) => {
fileSize = file.contents.length;
return cb(null, file);
}))
})
)
.pipe(gzip())
.pipe(through2.obj((file, enc, cb) => {
.pipe(
through2.obj((file, enc, cb) => {
const buf = file.contents;
file.contents = Buffer.from(genHeader(fileSize, buf, buf.length));
return cb(null, file);
}))
.pipe(rename("html.h"))
})
)
.pipe(rename('html.h'))
.pipe(dest('../src/'));
}));
})
);

View file

@ -11,66 +11,63 @@
"license": "MIT",
"scripts": {
"prestart": "gulp clean",
"start": "webpack serve",
"start": "NODE_ENV=development && webpack serve",
"build": "NODE_ENV=production webpack && gulp",
"inline": "NODE_ENV=production webpack && gulp inline",
"check": "gts check",
"fix": "gts fix"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.ts": [
"gts fix",
"git add"
],
"src/**/*.scss": [
"scssfmt",
"git add"
]
"engines": {
"node": ">=12"
},
"devDependencies": {
"copy-webpack-plugin": "^6.4.1",
"css-loader": "^5.0.1",
"gts": "^1.1.2",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^6.10.0",
"css-minimizer-webpack-plugin": "^6.0.0",
"eslint": "^8.57.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-webpack-plugin": "^4.0.1",
"gts": "^5.2.0",
"gulp": "^4.0.2",
"gulp-clean": "^0.4.0",
"gulp-gzip": "^1.4.2",
"gulp-inline-source": "^4.0.0",
"gulp-rename": "^2.0.0",
"html-webpack-plugin": "^4.5.0",
"husky": "^4.3.6",
"lint-staged": "^10.5.3",
"mini-css-extract-plugin": "^1.3.3",
"node-sass": "^5.0.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"sass-loader": "^10.1.0",
"html-webpack-plugin": "^5.6.0",
"mini-css-extract-plugin": "^2.8.1",
"sass": "^1.71.1",
"sass-loader": "^14.1.1",
"scssfmt": "^1.0.7",
"style-loader": "^2.0.0",
"terser-webpack-plugin": "^4.2.3",
"style-loader": "^3.3.4",
"terser-webpack-plugin": "^5.3.10",
"through2": "^4.0.2",
"ts-loader": "^8.0.13",
"tslint": "^6.1.3",
"tslint-loader": "^3.5.4",
"typescript": "^3.9.7",
"webpack": "4.44.2",
"webpack-cli": "^4.3.1",
"webpack-dev-server": "^3.11.1",
"webpack-merge": "^5.7.3"
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",
"util": "^0.12.5",
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.2",
"webpack-merge": "^5.10.0"
},
"dependencies": {
"backoff": "^2.5.0",
"@xterm/addon-canvas": "^0.6.0",
"@xterm/addon-fit": "^0.9.0",
"@xterm/addon-image": "^0.7.0",
"@xterm/addon-unicode11": "^0.7.0",
"@xterm/addon-web-links": "^0.10.0",
"@xterm/addon-webgl": "^0.17.0",
"@xterm/xterm": "^5.4.0",
"decko": "^1.2.0",
"file-saver": "^2.0.5",
"preact": "^10.5.8",
"whatwg-fetch": "^3.5.0",
"xterm": "^4.9.0",
"xterm-addon-fit": "^0.4.0",
"xterm-addon-web-links": "^0.4.0",
"xterm-addon-webgl": "^0.9.0",
"preact": "^10.19.6",
"trzsz": "^1.1.5",
"whatwg-fetch": "^3.6.20",
"zmodem.js": "^0.1.10"
}
},
"resolutions": {
"zmodem.js@^0.1.10": "patch:zmodem.js@npm%3A0.1.10#./.yarn/patches/zmodem.js-npm-0.1.10-e5537fa2ed.patch"
},
"packageManager": "yarn@3.6.3"
}

View file

@ -1,6 +0,0 @@
module.exports = {
trailingComma: "es5",
tabWidth: 4,
printWidth: 120,
singleQuote: true,
};

View file

@ -1,26 +1,27 @@
import { h, Component } from 'preact';
import { ITerminalOptions, ITheme } from 'xterm';
import { ClientOptions, Xterm } from './terminal';
import { Terminal } from './terminal';
if ((module as any).hot) {
// tslint:disable-next-line:no-var-requires
require('preact/debug');
}
import type { ITerminalOptions, ITheme } from '@xterm/xterm';
import type { ClientOptions, FlowControl } from './terminal/xterm';
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const path = window.location.pathname.replace(/[\/]+$/, '');
const path = window.location.pathname.replace(/[/]+$/, '');
const wsUrl = [protocol, '//', window.location.host, path, '/ws', window.location.search].join('');
const tokenUrl = [window.location.protocol, '//', window.location.host, path, '/token'].join('');
const clientOptions = {
rendererType: 'webgl',
disableLeaveAlert: false,
disableResizeOverlay: false,
titleFixed: null,
enableZmodem: false,
enableTrzsz: false,
enableSixel: false,
isWindows: false,
unicodeVersion: '11',
} as ClientOptions;
const termOptions = {
fontSize: 13,
fontFamily: 'Menlo For Powerline,Consolas,Liberation Mono,Menlo,Courier,monospace',
fontFamily: 'Consolas,Liberation Mono,Menlo,Courier,monospace',
theme: {
foreground: '#d2d2d2',
background: '#2b2b2b',
@ -42,17 +43,24 @@ const termOptions = {
brightCyan: '#37e6e8',
brightWhite: '#f1f1f0',
} as ITheme,
allowProposedApi: true,
} as ITerminalOptions;
const flowControl = {
limit: 100000,
highWater: 10,
lowWater: 4,
} as FlowControl;
export class App extends Component {
render() {
return (
<Xterm
<Terminal
id="terminal-container"
wsUrl={wsUrl}
tokenUrl={tokenUrl}
clientOptions={clientOptions}
termOptions={termOptions}
flowControl={flowControl}
/>
);
}

View file

@ -1,372 +1,59 @@
import { bind } from 'decko';
import * as backoff from 'backoff';
import { Component, h } from 'preact';
import { ITerminalOptions, Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { WebglAddon } from 'xterm-addon-webgl';
import { WebLinksAddon } from 'xterm-addon-web-links';
import { Xterm, XtermOptions } from './xterm';
import { OverlayAddon } from './overlay';
import { ZmodemAddon, FlowControl } from '../zmodem';
import '@xterm/xterm/css/xterm.css';
import { Modal } from '../modal';
import 'xterm/css/xterm.css';
interface TtydTerminal extends Terminal {
fit(): void;
}
declare global {
interface Window {
term: TtydTerminal;
}
}
const enum Command {
// server side
OUTPUT = '0',
SET_WINDOW_TITLE = '1',
SET_PREFERENCES = '2',
// client side
INPUT = '0',
RESIZE_TERMINAL = '1',
PAUSE = '2',
RESUME = '3',
}
export interface ClientOptions {
rendererType: 'dom' | 'canvas' | 'webgl';
disableLeaveAlert: boolean;
disableResizeOverlay: boolean;
titleFixed: string;
}
interface Props {
interface Props extends XtermOptions {
id: string;
wsUrl: string;
tokenUrl: string;
clientOptions: ClientOptions;
termOptions: ITerminalOptions;
}
export class Xterm extends Component<Props> {
private textEncoder: TextEncoder;
private textDecoder: TextDecoder;
interface State {
modal: boolean;
}
export class Terminal extends Component<Props, State> {
private container: HTMLElement;
private terminal: Terminal;
private fitAddon: FitAddon;
private overlayAddon: OverlayAddon;
private zmodemAddon: ZmodemAddon;
private socket: WebSocket;
private token: string;
private title: string;
private titleFixed: string;
private resizeTimeout: number;
private resizeOverlay = true;
private backoff: backoff.Backoff;
private backoffLock = false;
private doBackoff = true;
private reconnect = false;
private xterm: Xterm;
constructor(props: Props) {
super(props);
this.textEncoder = new TextEncoder();
this.textDecoder = new TextDecoder();
this.fitAddon = new FitAddon();
this.overlayAddon = new OverlayAddon();
this.backoff = backoff.exponential({
initialDelay: 100,
maxDelay: 10000,
});
this.backoff.failAfter(15);
this.backoff.on('ready', () => {
this.backoffLock = false;
this.refreshToken().then(this.connect);
});
this.backoff.on('backoff', (_, delay: number) => {
console.log(`[ttyd] will attempt to reconnect websocket in ${delay}ms`);
this.backoffLock = true;
});
this.backoff.on('fail', () => {
this.backoffLock = true; // break backoff
});
super();
this.xterm = new Xterm(props, this.showModal);
}
async componentDidMount() {
await this.refreshToken();
this.openTerminal();
this.connect();
window.addEventListener('resize', this.onWindowResize);
window.addEventListener('beforeunload', this.onWindowUnload);
await this.xterm.refreshToken();
this.xterm.open(this.container);
this.xterm.connect();
}
componentWillUnmount() {
this.socket.close();
this.terminal.dispose();
window.removeEventListener('resize', this.onWindowResize);
window.removeEventListener('beforeunload', this.onWindowUnload);
this.xterm.dispose();
}
render({ id }: Props) {
const control = {
limit: 100000,
highWater: 10,
lowWater: 4,
pause: () => this.pause(),
resume: () => this.resume(),
} as FlowControl;
render({ id }: Props, { modal }: State) {
return (
<div id={id} ref={c => (this.container = c)}>
<ZmodemAddon ref={c => (this.zmodemAddon = c)} sender={this.sendData} control={control} />
<div id={id} ref={c => (this.container = c as HTMLElement)}>
<Modal show={modal}>
<label class="file-label">
<input onChange={this.sendFile} class="file-input" type="file" multiple />
<span class="file-cta">Choose files</span>
</label>
</Modal>
</div>
);
}
@bind
private pause() {
const { textEncoder, socket } = this;
socket.send(textEncoder.encode(Command.PAUSE));
showModal() {
this.setState({ modal: true });
}
@bind
private resume() {
const { textEncoder, socket } = this;
socket.send(textEncoder.encode(Command.RESUME));
}
@bind
private sendData(data: ArrayLike<number>) {
const { socket } = this;
const payload = new Uint8Array(data.length + 1);
payload[0] = Command.INPUT.charCodeAt(0);
payload.set(data, 1);
socket.send(payload);
}
@bind
private async refreshToken() {
try {
const resp = await fetch(this.props.tokenUrl);
if (resp.ok) {
const json = await resp.json();
this.token = json.token;
}
} catch (e) {
console.error(`[ttyd] fetch ${this.props.tokenUrl}: `, e);
}
}
@bind
private onWindowResize() {
const { fitAddon } = this;
clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => fitAddon.fit(), 250) as any;
}
@bind
private onWindowUnload(event: BeforeUnloadEvent): any {
const { socket } = this;
if (socket && socket.readyState === WebSocket.OPEN) {
const message = 'Close terminal? this will also terminate the command.';
event.returnValue = message;
return message;
}
event.preventDefault();
}
@bind
private openTerminal() {
this.terminal = new Terminal(this.props.termOptions);
const { terminal, container, fitAddon, overlayAddon } = this;
window.term = terminal as TtydTerminal;
window.term.fit = () => {
this.fitAddon.fit();
};
terminal.loadAddon(fitAddon);
terminal.loadAddon(overlayAddon);
terminal.loadAddon(new WebLinksAddon());
terminal.loadAddon(this.zmodemAddon);
terminal.onTitleChange(data => {
if (data && data !== '' && !this.titleFixed) {
document.title = data + ' | ' + this.title;
}
});
terminal.onData(this.onTerminalData);
terminal.onResize(this.onTerminalResize);
if (document.queryCommandSupported && document.queryCommandSupported('copy')) {
terminal.onSelectionChange(() => {
if (terminal.getSelection() === '') return;
overlayAddon.showOverlay('\u2702', 200);
document.execCommand('copy');
});
}
terminal.open(container);
}
@bind
private connect() {
this.socket = new WebSocket(this.props.wsUrl, ['tty']);
const { socket } = this;
socket.binaryType = 'arraybuffer';
socket.onopen = this.onSocketOpen;
socket.onmessage = this.onSocketData;
socket.onclose = this.onSocketClose;
socket.onerror = this.onSocketError;
}
@bind
private applyOptions(options: any) {
const { terminal, fitAddon } = this;
const isWebGL2Available = () => {
try {
const canvas = document.createElement('canvas');
return !!(window.WebGL2RenderingContext && canvas.getContext('webgl2'));
} catch (e) {
return false;
}
};
Object.keys(options).forEach(key => {
const value = options[key];
switch (key) {
case 'rendererType':
if (value === 'webgl' && isWebGL2Available()) {
terminal.loadAddon(new WebglAddon());
console.log(`[ttyd] WebGL renderer enabled`);
}
break;
case 'disableLeaveAlert':
if (value) {
window.removeEventListener('beforeunload', this.onWindowUnload);
console.log('[ttyd] Leave site alert disabled');
}
break;
case 'disableResizeOverlay':
if (value) {
console.log(`[ttyd] Resize overlay disabled`);
this.resizeOverlay = false;
}
break;
case 'disableReconnect':
if (value) {
console.log(`[ttyd] Reconnect disabled`);
this.doBackoff = false;
}
break;
case 'titleFixed':
if (!value || value === '') return;
console.log(`[ttyd] setting fixed title: ${value}`);
this.titleFixed = value;
document.title = value;
break;
default:
console.log(`[ttyd] option: ${key}=${value}`);
terminal.setOption(key, value);
if (key.indexOf('font') === 0) fitAddon.fit();
break;
}
});
}
@bind
private onSocketOpen() {
console.log('[ttyd] websocket connection opened');
this.backoff.reset();
const { socket, textEncoder, terminal, fitAddon, overlayAddon } = this;
socket.send(textEncoder.encode(JSON.stringify({ AuthToken: this.token })));
if (this.reconnect) {
const dims = fitAddon.proposeDimensions();
terminal.reset();
terminal.resize(dims.cols, dims.rows);
this.onTerminalResize(dims); // may not be triggered by terminal.resize
overlayAddon.showOverlay('Reconnected', 300);
} else {
this.reconnect = true;
fitAddon.fit();
}
this.applyOptions(this.props.clientOptions);
terminal.focus();
}
@bind
private onSocketClose(event: CloseEvent) {
console.log(`[ttyd] websocket connection closed with code: ${event.code}`);
const { backoff, doBackoff, backoffLock, overlayAddon } = this;
overlayAddon.showOverlay('Connection Closed', null);
// 1000: CLOSE_NORMAL
if (event.code !== 1000 && doBackoff && !backoffLock) {
backoff.backoff();
}
}
@bind
private onSocketError(event: Event) {
console.error('[ttyd] websocket connection error: ', event);
const { backoff, doBackoff, backoffLock } = this;
if (doBackoff && !backoffLock) {
backoff.backoff();
}
}
@bind
private onSocketData(event: MessageEvent) {
const { textDecoder, zmodemAddon } = this;
const rawData = event.data as ArrayBuffer;
const cmd = String.fromCharCode(new Uint8Array(rawData)[0]);
const data = rawData.slice(1);
switch (cmd) {
case Command.OUTPUT:
zmodemAddon.consume(data);
break;
case Command.SET_WINDOW_TITLE:
this.title = textDecoder.decode(data);
document.title = this.title;
break;
case Command.SET_PREFERENCES:
this.applyOptions(JSON.parse(textDecoder.decode(data)));
break;
default:
console.warn(`[ttyd] unknown command: ${cmd}`);
break;
}
}
@bind
private onTerminalResize(size: { cols: number; rows: number }) {
const { overlayAddon, socket, textEncoder, resizeOverlay } = this;
if (socket.readyState === WebSocket.OPEN) {
const msg = JSON.stringify({ columns: size.cols, rows: size.rows });
socket.send(textEncoder.encode(Command.RESIZE_TERMINAL + msg));
}
if (resizeOverlay) {
setTimeout(() => {
overlayAddon.showOverlay(`${size.cols}x${size.rows}`);
}, 500);
}
}
@bind
private onTerminalData(data: string) {
const { socket, textEncoder } = this;
if (socket.readyState === WebSocket.OPEN) {
socket.send(textEncoder.encode(Command.INPUT + data));
}
sendFile(event: Event) {
this.setState({ modal: false });
const files = (event.target as HTMLInputElement).files;
if (files) this.xterm.sendFile(files);
}
}

View file

@ -1,11 +1,12 @@
// ported from hterm.Terminal.prototype.showOverlay
// https://chromium.googlesource.com/apps/libapps/+/master/hterm/js/hterm_terminal.js
import { ITerminalAddon, Terminal } from 'xterm';
import { bind } from 'decko';
import { ITerminalAddon, Terminal } from '@xterm/xterm';
export class OverlayAddon implements ITerminalAddon {
private terminal: Terminal | undefined;
private overlayNode: HTMLElement | null;
private overlayTimeout: number | null;
private terminal: Terminal;
private overlayNode: HTMLElement;
private overlayTimeout?: number;
constructor() {
this.overlayNode = document.createElement('div');
@ -35,8 +36,10 @@ position: absolute;
dispose(): void {}
@bind
showOverlay(msg: string, timeout?: number): void {
const { terminal, overlayNode } = this;
if (!terminal.element) return;
overlayNode.style.color = '#101010';
overlayNode.style.backgroundColor = '#f0f0f0';
@ -53,23 +56,18 @@ position: absolute;
overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px';
overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px';
if (this.overlayTimeout) {
clearTimeout(this.overlayTimeout);
}
if (timeout === null) {
return;
}
if (this.overlayTimeout) clearTimeout(this.overlayTimeout);
if (!timeout) return;
const self = this;
self.overlayTimeout = setTimeout(() => {
this.overlayTimeout = window.setTimeout(() => {
overlayNode.style.opacity = '0';
self.overlayTimeout = setTimeout(() => {
this.overlayTimeout = window.setTimeout(() => {
if (overlayNode.parentNode) {
overlayNode.parentNode.removeChild(overlayNode);
}
self.overlayTimeout = null;
this.overlayTimeout = undefined;
overlayNode.style.opacity = '0.75';
}, 200) as any;
}, timeout || 1500) as any;
}, 200);
}, timeout || 1500);
}
}

View file

@ -0,0 +1,182 @@
import { bind } from 'decko';
import { saveAs } from 'file-saver';
import { IDisposable, ITerminalAddon, Terminal } from '@xterm/xterm';
import * as Zmodem from 'zmodem.js/src/zmodem_browser';
import { TrzszFilter } from 'trzsz';
export interface ZmodeOptions {
zmodem: boolean;
trzsz: boolean;
windows: boolean;
trzszDragInitTimeout: number;
onSend: () => void;
sender: (data: string | Uint8Array) => void;
writer: (data: string | Uint8Array) => void;
}
export class ZmodemAddon implements ITerminalAddon {
private disposables: IDisposable[] = [];
private terminal: Terminal;
private sentry: Zmodem.Sentry;
private session: Zmodem.Session;
private denier: () => void;
private trzszFilter: TrzszFilter;
constructor(private options: ZmodeOptions) {}
activate(terminal: Terminal) {
this.terminal = terminal;
if (this.options.zmodem) this.zmodemInit();
if (this.options.trzsz) this.trzszInit();
}
dispose() {
for (const d of this.disposables) {
d.dispose();
}
this.disposables.length = 0;
}
consume(data: ArrayBuffer) {
try {
if (this.options.trzsz) {
this.trzszFilter.processServerOutput(data);
} else {
this.sentry.consume(data);
}
} catch (e) {
console.error('[ttyd] zmodem consume: ', e);
this.reset();
}
}
@bind
private reset() {
this.terminal.options.disableStdin = false;
this.terminal.focus();
}
private addDisposableListener(target: EventTarget, type: string, listener: EventListener) {
target.addEventListener(type, listener);
this.disposables.push({ dispose: () => target.removeEventListener(type, listener) });
}
@bind
private trzszInit() {
const { terminal } = this;
const { sender, writer, zmodem } = this.options;
this.trzszFilter = new TrzszFilter({
writeToTerminal: data => {
if (!this.trzszFilter.isTransferringFiles() && zmodem) {
this.sentry.consume(data);
} else {
writer(typeof data === 'string' ? data : new Uint8Array(data as ArrayBuffer));
}
},
sendToServer: data => sender(data),
terminalColumns: terminal.cols,
isWindowsShell: this.options.windows,
dragInitTimeout: this.options.trzszDragInitTimeout,
});
const element = terminal.element as EventTarget;
this.addDisposableListener(element, 'dragover', event => event.preventDefault());
this.addDisposableListener(element, 'drop', event => {
event.preventDefault();
this.trzszFilter
.uploadFiles((event as DragEvent).dataTransfer?.items as DataTransferItemList)
.then(() => console.log('[ttyd] upload success'))
.catch(err => console.log('[ttyd] upload failed: ' + err));
});
this.disposables.push(terminal.onResize(size => this.trzszFilter.setTerminalColumns(size.cols)));
}
@bind
private zmodemInit() {
const { sender, writer } = this.options;
const { terminal, reset, zmodemDetect } = this;
this.session = null;
this.sentry = new Zmodem.Sentry({
to_terminal: octets => writer(new Uint8Array(octets)),
sender: octets => sender(new Uint8Array(octets)),
on_retract: () => reset(),
on_detect: detection => zmodemDetect(detection),
});
this.disposables.push(
terminal.onKey(e => {
const event = e.domEvent;
if (event.ctrlKey && event.key === 'c') {
if (this.denier) this.denier();
}
})
);
}
@bind
private zmodemDetect(detection: Zmodem.Detection): void {
const { terminal, receiveFile } = this;
terminal.options.disableStdin = true;
this.denier = () => detection.deny();
this.session = detection.confirm();
this.session.on('session_end', () => this.reset());
if (this.session.type === 'send') {
this.options.onSend();
} else {
receiveFile();
}
}
@bind
public sendFile(files: FileList) {
const { session, writeProgress } = this;
Zmodem.Browser.send_files(session, files, {
on_progress: (_, offer) => writeProgress(offer),
})
.then(() => session.close())
.catch(() => this.reset());
}
@bind
private receiveFile() {
const { session, writeProgress } = this;
session.on('offer', offer => {
offer.on('input', () => writeProgress(offer));
offer
.accept()
.then(payloads => {
const blob = new Blob(payloads, { type: 'application/octet-stream' });
saveAs(blob, offer.get_details().name);
})
.catch(() => this.reset());
});
session.start();
}
@bind
private writeProgress(offer: Zmodem.Offer) {
const { bytesHuman } = this;
const file = offer.get_details();
const name = file.name;
const size = file.size;
const offset = offer.get_offset();
const percent = ((100 * offset) / size).toFixed(2);
this.options.writer(`${name} ${percent}% ${bytesHuman(offset, 2)}/${bytesHuman(size, 2)}\r`);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private bytesHuman(bytes: any, precision: number): string {
if (!/^([-+])?|(\.\d+)(\d+(\.\d+)?|(\d+\.)|Infinity)$/.test(bytes)) {
return '-';
}
if (bytes === 0) return '0';
if (typeof precision === 'undefined') precision = 1;
const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
const num = Math.floor(Math.log(bytes) / Math.log(1024));
const value = (bytes / Math.pow(1024, Math.floor(num))).toFixed(precision);
return `${value} ${units[num]}`;
}
}

View file

@ -0,0 +1,519 @@
import { bind } from 'decko';
import type { IDisposable, ITerminalOptions } from '@xterm/xterm';
import { Terminal } from '@xterm/xterm';
import { CanvasAddon } from '@xterm/addon-canvas';
import { WebglAddon } from '@xterm/addon-webgl';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import { ImageAddon } from '@xterm/addon-image';
import { Unicode11Addon } from '@xterm/addon-unicode11';
import { OverlayAddon } from './addons/overlay';
import { ZmodemAddon } from './addons/zmodem';
import '@xterm/xterm/css/xterm.css';
interface TtydTerminal extends Terminal {
fit(): void;
}
declare global {
interface Window {
term: TtydTerminal;
}
}
enum Command {
// server side
OUTPUT = '0',
SET_WINDOW_TITLE = '1',
SET_PREFERENCES = '2',
// client side
INPUT = '0',
RESIZE_TERMINAL = '1',
PAUSE = '2',
RESUME = '3',
}
type Preferences = ITerminalOptions & ClientOptions;
export type RendererType = 'dom' | 'canvas' | 'webgl';
export interface ClientOptions {
rendererType: RendererType;
disableLeaveAlert: boolean;
disableResizeOverlay: boolean;
enableZmodem: boolean;
enableTrzsz: boolean;
enableSixel: boolean;
titleFixed?: string;
isWindows: boolean;
trzszDragInitTimeout: number;
unicodeVersion: string;
}
export interface FlowControl {
limit: number;
highWater: number;
lowWater: number;
}
export interface XtermOptions {
wsUrl: string;
tokenUrl: string;
flowControl: FlowControl;
clientOptions: ClientOptions;
termOptions: ITerminalOptions;
}
function toDisposable(f: () => void): IDisposable {
return { dispose: f };
}
function addEventListener(target: EventTarget, type: string, listener: EventListener): IDisposable {
target.addEventListener(type, listener);
return toDisposable(() => target.removeEventListener(type, listener));
}
export class Xterm {
private disposables: IDisposable[] = [];
private textEncoder = new TextEncoder();
private textDecoder = new TextDecoder();
private written = 0;
private pending = 0;
private terminal: Terminal;
private fitAddon = new FitAddon();
private overlayAddon = new OverlayAddon();
private webglAddon?: WebglAddon;
private canvasAddon?: CanvasAddon;
private zmodemAddon?: ZmodemAddon;
private socket?: WebSocket;
private token: string;
private opened = false;
private title?: string;
private titleFixed?: string;
private resizeOverlay = true;
private reconnect = true;
private doReconnect = true;
private writeFunc = (data: ArrayBuffer) => this.writeData(new Uint8Array(data));
constructor(
private options: XtermOptions,
private sendCb: () => void
) {}
dispose() {
for (const d of this.disposables) {
d.dispose();
}
this.disposables.length = 0;
}
@bind
private register<T extends IDisposable>(d: T): T {
this.disposables.push(d);
return d;
}
@bind
public sendFile(files: FileList) {
this.zmodemAddon?.sendFile(files);
}
@bind
public async refreshToken() {
try {
const resp = await fetch(this.options.tokenUrl);
if (resp.ok) {
const json = await resp.json();
this.token = json.token;
}
} catch (e) {
console.error(`[ttyd] fetch ${this.options.tokenUrl}: `, e);
}
}
@bind
private onWindowUnload(event: BeforeUnloadEvent) {
event.preventDefault();
if (this.socket?.readyState === WebSocket.OPEN) {
const message = 'Close terminal? this will also terminate the command.';
event.returnValue = message;
return message;
}
return undefined;
}
@bind
public open(parent: HTMLElement) {
this.terminal = new Terminal(this.options.termOptions);
const { terminal, fitAddon, overlayAddon } = this;
window.term = terminal as TtydTerminal;
window.term.fit = () => {
this.fitAddon.fit();
};
terminal.loadAddon(fitAddon);
terminal.loadAddon(overlayAddon);
terminal.loadAddon(new WebLinksAddon());
terminal.open(parent);
fitAddon.fit();
}
@bind
private initListeners() {
const { terminal, fitAddon, overlayAddon, register, sendData } = this;
register(
terminal.onTitleChange(data => {
if (data && data !== '' && !this.titleFixed) {
document.title = data + ' | ' + this.title;
}
})
);
register(terminal.onData(data => sendData(data)));
register(terminal.onBinary(data => sendData(Uint8Array.from(data, v => v.charCodeAt(0)))));
register(
terminal.onResize(({ cols, rows }) => {
const msg = JSON.stringify({ columns: cols, rows: rows });
this.socket?.send(this.textEncoder.encode(Command.RESIZE_TERMINAL + msg));
if (this.resizeOverlay) overlayAddon.showOverlay(`${cols}x${rows}`, 300);
})
);
register(
terminal.onSelectionChange(() => {
if (this.terminal.getSelection() === '') return;
try {
document.execCommand('copy');
} catch (e) {
return;
}
this.overlayAddon?.showOverlay('\u2702', 200);
})
);
register(addEventListener(window, 'resize', () => fitAddon.fit()));
register(addEventListener(window, 'beforeunload', this.onWindowUnload));
}
@bind
public writeData(data: string | Uint8Array) {
const { terminal, textEncoder } = this;
const { limit, highWater, lowWater } = this.options.flowControl;
this.written += data.length;
if (this.written > limit) {
terminal.write(data, () => {
this.pending = Math.max(this.pending - 1, 0);
if (this.pending < lowWater) {
this.socket?.send(textEncoder.encode(Command.RESUME));
}
});
this.pending++;
this.written = 0;
if (this.pending > highWater) {
this.socket?.send(textEncoder.encode(Command.PAUSE));
}
} else {
terminal.write(data);
}
}
@bind
public sendData(data: string | Uint8Array) {
const { socket, textEncoder } = this;
if (socket?.readyState !== WebSocket.OPEN) return;
if (typeof data === 'string') {
const payload = new Uint8Array(data.length * 3 + 1);
payload[0] = Command.INPUT.charCodeAt(0);
const stats = textEncoder.encodeInto(data, payload.subarray(1));
socket.send(payload.subarray(0, (stats.written as number) + 1));
} else {
const payload = new Uint8Array(data.length + 1);
payload[0] = Command.INPUT.charCodeAt(0);
payload.set(data, 1);
socket.send(payload);
}
}
@bind
public connect() {
this.socket = new WebSocket(this.options.wsUrl, ['tty']);
const { socket, register } = this;
socket.binaryType = 'arraybuffer';
register(addEventListener(socket, 'open', this.onSocketOpen));
register(addEventListener(socket, 'message', this.onSocketData as EventListener));
register(addEventListener(socket, 'close', this.onSocketClose as EventListener));
register(addEventListener(socket, 'error', () => (this.doReconnect = false)));
}
@bind
private onSocketOpen() {
console.log('[ttyd] websocket connection opened');
const { textEncoder, terminal, overlayAddon } = this;
const msg = JSON.stringify({ AuthToken: this.token, columns: terminal.cols, rows: terminal.rows });
this.socket?.send(textEncoder.encode(msg));
if (this.opened) {
terminal.reset();
terminal.options.disableStdin = false;
overlayAddon.showOverlay('Reconnected', 300);
} else {
this.opened = true;
}
this.doReconnect = this.reconnect;
this.initListeners();
terminal.focus();
}
@bind
private onSocketClose(event: CloseEvent) {
console.log(`[ttyd] websocket connection closed with code: ${event.code}`);
const { refreshToken, connect, doReconnect, overlayAddon } = this;
overlayAddon.showOverlay('Connection Closed');
this.dispose();
// 1000: CLOSE_NORMAL
if (event.code !== 1000 && doReconnect) {
overlayAddon.showOverlay('Reconnecting...');
refreshToken().then(connect);
} else {
const { terminal } = this;
const keyDispose = terminal.onKey(e => {
const event = e.domEvent;
if (event.key === 'Enter') {
keyDispose.dispose();
overlayAddon.showOverlay('Reconnecting...');
refreshToken().then(connect);
}
});
overlayAddon.showOverlay('Press ⏎ to Reconnect');
}
}
@bind
private parseOptsFromUrlQuery(query: string): Preferences {
const { terminal } = this;
const { clientOptions } = this.options;
const prefs = {} as Preferences;
const queryObj = Array.from(new URLSearchParams(query) as unknown as Iterable<[string, string]>);
for (const [k, queryVal] of queryObj) {
let v = clientOptions[k];
if (v === undefined) v = terminal.options[k];
switch (typeof v) {
case 'boolean':
prefs[k] = queryVal === 'true' || queryVal === '1';
break;
case 'number':
case 'bigint':
prefs[k] = Number.parseInt(queryVal, 10);
break;
case 'string':
prefs[k] = queryVal;
break;
case 'object':
prefs[k] = JSON.parse(queryVal);
break;
default:
console.warn(`[ttyd] maybe unknown option: ${k}=${queryVal}, treating as string`);
prefs[k] = queryVal;
break;
}
}
return prefs;
}
@bind
private onSocketData(event: MessageEvent) {
const { textDecoder } = this;
const rawData = event.data as ArrayBuffer;
const cmd = String.fromCharCode(new Uint8Array(rawData)[0]);
const data = rawData.slice(1);
switch (cmd) {
case Command.OUTPUT:
this.writeFunc(data);
break;
case Command.SET_WINDOW_TITLE:
this.title = textDecoder.decode(data);
document.title = this.title;
break;
case Command.SET_PREFERENCES:
this.applyPreferences({
...this.options.clientOptions,
...JSON.parse(textDecoder.decode(data)),
...this.parseOptsFromUrlQuery(window.location.search),
} as Preferences);
break;
default:
console.warn(`[ttyd] unknown command: ${cmd}`);
break;
}
}
@bind
private applyPreferences(prefs: Preferences) {
const { terminal, fitAddon, register } = this;
if (prefs.enableZmodem || prefs.enableTrzsz) {
this.zmodemAddon = new ZmodemAddon({
zmodem: prefs.enableZmodem,
trzsz: prefs.enableTrzsz,
windows: prefs.isWindows,
trzszDragInitTimeout: prefs.trzszDragInitTimeout,
onSend: this.sendCb,
sender: this.sendData,
writer: this.writeData,
});
this.writeFunc = data => this.zmodemAddon?.consume(data);
terminal.loadAddon(register(this.zmodemAddon));
}
for (const [key, value] of Object.entries(prefs)) {
switch (key) {
case 'rendererType':
this.setRendererType(value);
break;
case 'disableLeaveAlert':
if (value) {
window.removeEventListener('beforeunload', this.onWindowUnload);
console.log('[ttyd] Leave site alert disabled');
}
break;
case 'disableResizeOverlay':
if (value) {
console.log('[ttyd] Resize overlay disabled');
this.resizeOverlay = false;
}
break;
case 'disableReconnect':
if (value) {
console.log('[ttyd] Reconnect disabled');
this.reconnect = false;
this.doReconnect = false;
}
break;
case 'enableZmodem':
if (value) console.log('[ttyd] Zmodem enabled');
break;
case 'enableTrzsz':
if (value) console.log('[ttyd] trzsz enabled');
break;
case 'trzszDragInitTimeout':
if (value) console.log(`[ttyd] trzsz drag init timeout: ${value}`);
break;
case 'enableSixel':
if (value) {
terminal.loadAddon(register(new ImageAddon()));
console.log('[ttyd] Sixel enabled');
}
break;
case 'titleFixed':
if (!value || value === '') return;
console.log(`[ttyd] setting fixed title: ${value}`);
this.titleFixed = value;
document.title = value;
break;
case 'isWindows':
if (value) console.log('[ttyd] is windows');
break;
case 'unicodeVersion':
switch (value) {
case 6:
case '6':
console.log('[ttyd] setting Unicode version: 6');
break;
case 11:
case '11':
default:
console.log('[ttyd] setting Unicode version: 11');
terminal.loadAddon(new Unicode11Addon());
terminal.unicode.activeVersion = '11';
break;
}
break;
default:
console.log(`[ttyd] option: ${key}=${JSON.stringify(value)}`);
if (terminal.options[key] instanceof Object) {
terminal.options[key] = Object.assign({}, terminal.options[key], value);
} else {
terminal.options[key] = value;
}
if (key.indexOf('font') === 0) fitAddon.fit();
break;
}
}
}
@bind
private setRendererType(value: RendererType) {
const { terminal } = this;
const disposeCanvasRenderer = () => {
try {
this.canvasAddon?.dispose();
} catch {
// ignore
}
this.canvasAddon = undefined;
};
const disposeWebglRenderer = () => {
try {
this.webglAddon?.dispose();
} catch {
// ignore
}
this.webglAddon = undefined;
};
const enableCanvasRenderer = () => {
if (this.canvasAddon) return;
this.canvasAddon = new CanvasAddon();
disposeWebglRenderer();
try {
this.terminal.loadAddon(this.canvasAddon);
console.log('[ttyd] canvas renderer loaded');
} catch (e) {
console.log('[ttyd] canvas renderer could not be loaded, falling back to dom renderer', e);
disposeCanvasRenderer();
}
};
const enableWebglRenderer = () => {
if (this.webglAddon) return;
this.webglAddon = new WebglAddon();
disposeCanvasRenderer();
try {
this.webglAddon.onContextLoss(() => {
this.webglAddon?.dispose();
});
terminal.loadAddon(this.webglAddon);
console.log('[ttyd] WebGL renderer loaded');
} catch (e) {
console.log('[ttyd] WebGL renderer could not be loaded, falling back to canvas renderer', e);
disposeWebglRenderer();
enableCanvasRenderer();
}
};
switch (value) {
case 'canvas':
enableCanvasRenderer();
break;
case 'webgl':
enableWebglRenderer();
break;
case 'dom':
disposeWebglRenderer();
disposeCanvasRenderer();
console.log('[ttyd] dom renderer loaded');
break;
default:
break;
}
}
}

View file

@ -1,209 +0,0 @@
import { bind } from 'decko';
import { h, Component } from 'preact';
import { saveAs } from 'file-saver';
import { IDisposable, ITerminalAddon, Terminal } from 'xterm';
import * as Zmodem from 'zmodem.js/src/zmodem_browser';
import { Modal } from '../modal';
export interface FlowControl {
limit: number;
highWater: number;
lowWater: number;
pause: () => void;
resume: () => void;
}
interface Props {
sender: (data: ArrayLike<number>) => void;
control: FlowControl;
}
interface State {
modal: boolean;
}
export class ZmodemAddon extends Component<Props, State> implements ITerminalAddon {
private terminal: Terminal | undefined;
private keyDispose: IDisposable | undefined;
private sentry: Zmodem.Sentry;
private session: Zmodem.Session;
private written = 0;
private pending = 0;
constructor(props: Props) {
super(props);
this.zmodemInit();
}
render(_, { modal }: State) {
return (
<Modal show={modal}>
<label class="file-label">
<input onChange={this.sendFile} class="file-input" type="file" multiple />
<span class="file-cta">Choose files</span>
</label>
</Modal>
);
}
activate(terminal: Terminal): void {
this.terminal = terminal;
}
dispose(): void {}
consume(data: ArrayBuffer) {
const { sentry, handleError } = this;
try {
sentry.consume(data);
} catch (e) {
handleError(e, 'consume');
}
}
@bind
private handleError(e: Error, reason: string) {
console.error(`[ttyd] zmodem ${reason}: `, e);
this.zmodemReset();
}
@bind
private zmodemInit() {
this.session = null;
this.sentry = new Zmodem.Sentry({
to_terminal: (octets: ArrayBuffer) => this.zmodemWrite(octets),
sender: (octets: ArrayLike<number>) => this.zmodemSend(octets),
on_retract: () => this.zmodemReset(),
on_detect: (detection: Zmodem.Detection) => this.zmodemDetect(detection),
});
}
@bind
private zmodemReset() {
this.terminal.setOption('disableStdin', false);
if (this.keyDispose) {
this.keyDispose.dispose();
this.keyDispose = null;
}
this.zmodemInit();
this.terminal.focus();
}
@bind
private zmodemWrite(data: ArrayBuffer): void {
const { limit, highWater, lowWater, pause, resume } = this.props.control;
const { terminal } = this;
const rawData = new Uint8Array(data);
this.written += rawData.length;
if (this.written > limit) {
terminal.write(rawData, () => {
this.pending = Math.max(this.pending - 1, 0);
if (this.pending < lowWater) {
resume();
}
});
this.pending++;
this.written = 0;
if (this.pending > highWater) {
pause();
}
} else {
terminal.write(rawData);
}
}
@bind
private zmodemSend(data: ArrayLike<number>): void {
this.props.sender(data);
}
@bind
private zmodemDetect(detection: Zmodem.Detection): void {
const { terminal, receiveFile, zmodemReset } = this;
terminal.setOption('disableStdin', true);
this.keyDispose = terminal.onKey(e => {
const event = e.domEvent;
if (event.ctrlKey && event.key === 'c') {
detection.deny();
}
});
this.session = detection.confirm();
this.session.on('session_end', zmodemReset);
if (this.session.type === 'send') {
this.setState({ modal: true });
} else {
receiveFile();
}
}
@bind
private sendFile(event: Event) {
this.setState({ modal: false });
const { session, writeProgress, handleError } = this;
const files: FileList = (event.target as HTMLInputElement).files;
Zmodem.Browser.send_files(session, files, {
on_progress: (_, offer: Zmodem.Offer) => writeProgress(offer),
})
.then(() => session.close())
.catch(e => handleError(e, 'send'));
}
@bind
private receiveFile() {
const { session, writeProgress, handleError } = this;
session.on('offer', (offer: Zmodem.Offer) => {
const fileBuffer = [];
offer.on('input', payload => {
writeProgress(offer);
fileBuffer.push(new Uint8Array(payload));
});
offer
.accept()
.then(() => {
const blob = new Blob(fileBuffer, { type: 'application/octet-stream' });
saveAs(blob, offer.get_details().name);
})
.catch(e => handleError(e, 'receive'));
});
session.start();
}
@bind
private writeProgress(offer: Zmodem.Offer) {
const { terminal, bytesHuman } = this;
const file = offer.get_details();
const name = file.name;
const size = file.size;
const offset = offer.get_offset();
const percent = ((100 * offset) / size).toFixed(2);
terminal.write(`${name} ${percent}% ${bytesHuman(offset, 2)}/${bytesHuman(size, 2)}\r`);
}
private bytesHuman(bytes: any, precision: number): string {
if (!/^([-+])?|(\.\d+)(\d+(\.\d+)?|(\d+\.)|Infinity)$/.test(bytes)) {
return '-';
}
if (bytes === 0) return '0';
if (typeof precision === 'undefined') precision = 1;
const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
const num = Math.floor(Math.log(bytes) / Math.log(1024));
const value = (bytes / Math.pow(1024, Math.floor(num))).toFixed(precision);
return `${value} ${units[num]}`;
}
}

View file

@ -1,3 +1,6 @@
if (process.env.NODE_ENV === 'development') {
require('preact/debug');
}
import 'whatwg-fetch';
import { h, render } from 'preact';
import { App } from './components/app';

View file

@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<title><%= htmlWebpackPlugin.options.title %></title>
<link inline rel="icon" type="image/png" href="favicon.png">
<% for (const css in htmlWebpackPlugin.files.css) { %>

View file

@ -1,20 +1,16 @@
{
"extends": "./node_modules/gts/tsconfig-google.json",
"compilerOptions": {
"target": "es2015",
"module": "es2015",
"lib": [
"es2015",
"dom"
],
"allowJs": true,
"jsx": "react",
"jsxFactory": "h",
"sourceMap": true,
"moduleResolution": "node",
"esModuleInterop": true,
"jsx": "react",
"jsxFactory": "h",
"allowJs": true,
"noImplicitAny": false,
"declaration": false,
"experimentalDecorators": true,
"noImplicitReturns": true,
"noUnusedParameters": true
"strictPropertyInitialization": false,
"lib": ["es2019", "dom"],
},
"include": [
"src/**/*.tsx",

View file

@ -1,7 +0,0 @@
{
"extends": "gts/tslint.json",
"rules": {
"deprecation": false,
"no-any": false
}
}

View file

@ -1,9 +1,10 @@
const path = require('path');
const { merge } = require('webpack-merge');
const ESLintPlugin = require('eslint-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const devMode = process.env.NODE_ENV !== 'production';
@ -11,46 +12,39 @@ const devMode = process.env.NODE_ENV !== 'production';
const baseConfig = {
context: path.resolve(__dirname, 'src'),
entry: {
app: './index.tsx'
app: './index.tsx',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: devMode ? '[name].js' : '[name].[hash].js',
filename: devMode ? '[name].js' : '[name].[contenthash].js',
},
module: {
rules: [
{
test: /\.ts$/,
enforce: 'pre',
use: 'tslint-loader',
},
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
exclude: /node_modules/,
},
{
test: /\.s?[ac]ss$/,
use: [
devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
],
use: [devMode ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
},
]
],
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
extensions: ['.tsx', '.ts', '.js'],
},
plugins: [
new ESLintPlugin({
context: path.resolve(__dirname, '.'),
extensions: ['js', 'jsx', 'ts', 'tsx'],
}),
new CopyWebpackPlugin({
patterns:[
{ from: './favicon.png', to: '.' }
],
patterns: [{ from: './favicon.png', to: '.' }],
}),
new MiniCssExtractPlugin({
filename: devMode ? '[name].css' : '[name].[hash].css',
chunkFilename: devMode ? '[id].css' : '[id].[hash].css',
filename: devMode ? '[name].css' : '[name].[contenthash].css',
chunkFilename: devMode ? '[id].css' : '[id].[contenthash].css',
}),
new HtmlWebpackPlugin({
inject: false,
@ -59,25 +53,39 @@ const baseConfig = {
collapseWhitespace: true,
},
title: 'ttyd - Terminal',
template: './template.html'
})
template: './template.html',
}),
],
performance: {
hints : false
hints: false,
},
};
const devConfig = {
mode: 'development',
devServer: {
contentBase: path.join(__dirname, 'dist'),
static: path.join(__dirname, 'dist'),
compress: true,
port: 9000,
proxy: [{
client: {
overlay: {
errors: true,
warnings: false,
},
},
proxy: [
{
context: ['/token', '/ws'],
target: 'http://localhost:7681',
ws: true
}]
ws: true,
},
],
webSocketServer: {
type: 'sockjs',
options: {
path: '/sockjs-node',
},
},
},
devtool: 'inline-source-map',
};
@ -85,22 +93,9 @@ const devConfig = {
const prodConfig = {
mode: 'production',
optimization: {
minimizer: [
new TerserPlugin({
sourceMap: true
}),
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
map: {
inline: false,
annotation: true
}
}
}),
]
minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
},
devtool: 'source-map',
};
module.exports = merge(baseConfig, devMode ? devConfig : prodConfig);

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,9 @@
.nh
.TH ttyd 1 "September 2016" ttyd "User Manual"
.SH NAME
.PP
ttyd \- Share your terminal over the web
ttyd - Share your terminal over the web
.SH SYNOPSIS
@ -10,18 +11,20 @@ ttyd \- Share your terminal over the web
\fBttyd\fP [options] <command> [<arguments...>]
.SH Description
.SH DESCRIPTION
.PP
ttyd is a command\-line tool for sharing terminal over the web that runs in *nix and windows systems, with the following features:
ttyd is a command-line tool for sharing terminal over the web that runs in *nix and windows systems, with the following features:
.RS
.IP \(bu 2
Built on top of Libwebsockets with libuv for speed
.IP \(bu 2
Fully\-featured terminal based on Xterm.js with CJK (Chinese, Japanese, Korean) and IME support
Fully-featured terminal based on Xterm.js with CJK (Chinese, Japanese, Korean) and IME support
.IP \(bu 2
Graphical ZMODEM integration with lrzsz support
.IP \(bu 2
Sixel image output support
.IP \(bu 2
SSL support based on OpenSSL
.IP \(bu 2
Run any custom command with options
@ -35,123 +38,206 @@ Cross platform: macOS, Linux, FreeBSD/OpenBSD, OpenWrt/LEDE, Windows
.SH OPTIONS
.PP
\-p, \-\-port <port>
-p, --port
Port to listen (default: 7681, use \fB\fC0\fR for random port)
.PP
\-i, \-\-interface <interface>
-i, --interface
Network interface to bind (eg: eth0), or UNIX domain socket path (eg: /var/run/ttyd.sock)
.PP
\-c, \-\-credential USER[:PASSWORD]
-U, --socket-owner
User owner of the UNIX domain socket file, when enabled (eg: user:group)
.PP
-c, --credential USER[:PASSWORD]
Credential for Basic Authentication (format: username:password)
.PP
\-u, \-\-uid <uid>
-H, --auth-header
HTTP Header name for auth proxy, this will configure ttyd to let a HTTP reverse proxy handle authentication
.PP
-u, --uid
User id to run with
.PP
\-g, \-\-gid <gid>
-g, --gid
Group id to run with
.PP
\-s, \-\-signal <signal string>
-s, --signal
Signal to send to the command when exit it (default: 1, SIGHUP)
.PP
\-a, \-\-url\-arg
Allow client to send command line arguments in URL (eg:
\[la]http://localhost:7681?arg=foo&arg=bar\[ra])
-w, --cwd
Working directory to be set for the child program
.PP
\-R, \-\-readonly
Do not allow clients to write to the TTY
-a, --url-arg
Allow client to send command line arguments in URL (eg: http://localhost:7681?arg=foo&arg=bar)
.PP
\-t, \-\-client\-option <key=value>
Send option to client (format: key=value), repeat to add more options
-W, --writable
Allow clients to write to the TTY (readonly by default)
.PP
\-T, \-\-terminal\-type
Terminal type to report, default: xterm\-256color
-t, --client-option
Send option to client (format: key=value), repeat to add more options, see \fBCLIENT OPTIONS\fP for details
.PP
\-O, \-\-check\-origin
-T, --terminal-type
Terminal type to report, default: xterm-256color
.PP
-O, --check-origin
Do not allow websocket connection from different origin
.PP
\-m, \-\-max\-clients
-m, --max-clients
Maximum clients to support (default: 0, no limit)
.PP
\-o, \-\-once
-o, --once
Accept only one client and exit on disconnection
.PP
\-B, \-\-browser
-q, --exit-no-conn
Exit on all clients disconnection
.PP
-B, --browser
Open terminal with the default system browser
.PP
\-I, \-\-index <index file>
-I, --index
Custom index.html path
.PP
\-b, \-\-base\-path
Expected base path for requests coming from a reverse proxy (eg: /mounted/here)
-b, --base-path
Expected base path for requests coming from a reverse proxy (eg: /mounted/here, max length: 128)
.PP
\-P, \-\-ping\-interval
Websocket ping interval(sec) (default: 300)
-P, --ping-interval
Websocket ping interval(sec) (default: 5)
.PP
\-6, \-\-ipv6
-6, --ipv6
Enable IPv6 support
.PP
\-S, \-\-ssl
-S, --ssl
Enable SSL
.PP
\-C, \-\-ssl\-cert <cert path>
-C, --ssl-cert
SSL certificate file path
.PP
\-K, \-\-ssl\-key <key path>
-K, --ssl-key
SSL key file path
.PP
\-A, \-\-ssl\-ca <ca path>
-A, --ssl-ca
SSL CA file path for client certificate verification
.PP
\-d, \-\-debug <level>
-d, --debug
Set log level (default: 7)
.PP
\-v, \-\-version
-v, --version
Print the version and exit
.PP
\-h, \-\-help
-h, --help
Print this text and exit
.SH Examples
.SH CLIENT OPTIONS
.PP
ttyd starts web server at port 7681 by default, you can use the \-p option to change it, the command will be started with arguments as options. For example, run:
ttyd has a mechanism to pass server side command-line arguments to the browser page which is called \fBclient options\fP:
.PP
.RS
.nf
ttyd \-p 8080 bash \-x
-t, --client-option Send option to client (format: key=value), repeat to add more options
.fi
.RE
.SH Basic usage
.RS
.IP \(bu 2
\fB\fC-t rendererType=canvas\fR: use the \fB\fCcanvas\fR renderer for xterm.js (default: \fB\fCwebgl\fR)
.IP \(bu 2
\fB\fC-t disableLeaveAlert=true\fR: disable the leave page alert
.IP \(bu 2
\fB\fC-t disableResizeOverlay=true\fR: disable the terminal resize overlay
.IP \(bu 2
\fB\fC-t disableReconnect=true\fR: prevent the terminal from reconnecting on connection error/close
.IP \(bu 2
\fB\fC-t enableZmodem=true\fR: enable ZMODEM
\[la]https://en.wikipedia.org/wiki/ZMODEM\[ra] / lrzsz
\[la]https://ohse.de/uwe/software/lrzsz.html\[ra] file transfer support
.IP \(bu 2
\fB\fC-t enableTrzsz=true\fR: enable trzsz
\[la]https://trzsz.github.io\[ra] file transfer support
.IP \(bu 2
\fB\fC-t enableSixel=true\fR: enable Sixel
\[la]https://en.wikipedia.org/wiki/Sixel\[ra] image output support (Usage
\[la]https://saitoha.github.io/libsixel/\[ra])
.IP \(bu 2
\fB\fC-t titleFixed=hello\fR: set a fixed title for the browser window
.IP \(bu 2
\fB\fC-t fontSize=20\fR: change the font size of the terminal
.RE
.SH Advanced usage
.PP
You can use the client option to change all the settings of xterm defined in ITerminalOptions
\[la]https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/\[ra], examples:
.RS
.IP \(bu 2
\fB\fC-t cursorStyle=bar\fR: set cursor style to \fB\fCbar\fR
.IP \(bu 2
\fB\fC-t lineHeight=1.5\fR: set line-height to \fB\fC1.5\fR
.IP \(bu 2
\fB\fC-t 'theme={"background": "green"}'\fR: set background color to \fB\fCgreen\fR
.RE
.PP
to try the example options above, run:
.PP
.RS
.nf
ttyd -t cursorStyle=bar -t lineHeight=1.5 -t 'theme={"background": "green"}' bash
.fi
.RE
.SH EXAMPLES
.PP
ttyd starts web server at port 7681 by default, you can use the -p option to change it, the command will be started with arguments as options. For example, run:
.PP
.RS
.nf
ttyd -p 8080 bash -x
.fi
.RE
.PP
Then open
\[la]http://localhost:8080\[ra] with a browser, you will get a bash shell with debug mode enabled. More examples:
Then open http://localhost:8080 with a browser, you will get a bash shell with debug mode enabled. More examples:
.RS
.IP \(bu 2
@ -159,12 +245,12 @@ If you want to login with your system accounts on the web browser, run \fB\fCtty
.IP \(bu 2
You can even run a none shell command like vim, try: \fB\fCttyd vim\fR, the web browser will show you a vim editor.
.IP \(bu 2
Sharing single process with multiple clients: \fB\fCttyd tmux new \-A \-s ttyd vim\fR, run \fB\fCtmux new \-A \-s ttyd\fR to connect to the tmux session from terminal.
Sharing single process with multiple clients: \fB\fCttyd tmux new -A -s ttyd vim\fR, run \fB\fCtmux new -A -s ttyd\fR to connect to the tmux session from terminal.
.RE
.SH SSL how\-to
.SH SSL how-to
.PP
Generate SSL CA and self signed server/client certificates:
@ -173,18 +259,18 @@ Generate SSL CA and self signed server/client certificates:
.nf
# CA certificate (FQDN must be different from server/client)
openssl genrsa \-out ca.key 2048
openssl req \-new \-x509 \-days 365 \-key ca.key \-subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=Acme Root CA" \-out ca.crt
openssl genrsa -out ca.key 2048
openssl req -new -x509 -days 365 -key ca.key -subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=Acme Root CA" -out ca.crt
# server certificate (for multiple domains, change subjectAltName to: DNS:example.com,DNS:www.example.com)
openssl req \-newkey rsa:2048 \-nodes \-keyout server.key \-subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=localhost" \-out server.csr
openssl x509 \-sha256 \-req \-extfile <(printf "subjectAltName=DNS:localhost") \-days 365 \-in server.csr \-CA ca.crt \-CAkey ca.key \-CAcreateserial \-out server.crt
openssl req -newkey rsa:2048 -nodes -keyout server.key -subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=localhost" -out server.csr
openssl x509 -sha256 -req -extfile <(printf "subjectAltName=DNS:localhost") -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt
# client certificate (the p12/pem format may be useful for some clients)
openssl req \-newkey rsa:2048 \-nodes \-keyout client.key \-subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=client" \-out client.csr
openssl x509 \-req \-days 365 \-in client.csr \-CA ca.crt \-CAkey ca.key \-CAcreateserial \-out client.crt
openssl pkcs12 \-export \-clcerts \-in client.crt \-inkey client.key \-out client.p12
openssl pkcs12 \-in client.p12 \-out client.pem \-clcerts
openssl req -newkey rsa:2048 -nodes -keyout client.key -subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=client" -out client.csr
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12
openssl pkcs12 -in client.p12 -out client.pem -clcerts
.fi
.RE
@ -196,7 +282,7 @@ Then start ttyd:
.RS
.nf
ttyd \-\-ssl \-\-ssl\-cert server.crt \-\-ssl\-key server.key \-\-ssl\-ca ca.crt bash
ttyd --ssl --ssl-cert server.crt --ssl-key server.key --ssl-ca ca.crt bash
.fi
.RE
@ -208,13 +294,13 @@ You may want to test the client certificate verification with \fIcurl\fP(1):
.RS
.nf
curl \-\-insecure \-\-cert client.p12[:password] \-v https://localhost:7681
curl --insecure --cert client.p12[:password] -v https://localhost:7681
.fi
.RE
.PP
If you don't want to enable client certificate verification, remove the \fB\fC\-\-ssl\-ca\fR option.
If you don't want to enable client certificate verification, remove the \fB\fC--ssl-ca\fR option.
.SH Docker and ttyd
@ -223,9 +309,9 @@ Docker containers are jailed environments which are more secure, this is useful
.RS
.IP \(bu 2
Sharing single docker container with multiple clients: docker run \-it \-\-rm \-p 7681:7681 tsl0922/ttyd.
Sharing single docker container with multiple clients: docker run -it --rm -p 7681:7681 tsl0922/ttyd.
.IP \(bu 2
Creating new docker container for each client: ttyd docker run \-it \-\-rm ubuntu.
Creating new docker container for each client: ttyd docker run -it --rm ubuntu.
.RE
@ -238,14 +324,14 @@ Sample config to proxy ttyd under the \fB\fC/ttyd\fR path:
.RS
.nf
location \~ ^/ttyd(.*)$ {
proxy\_http\_version 1.1;
proxy\_set\_header Host $host;
proxy\_set\_header X\-Forwarded\-Proto $scheme;
proxy\_set\_header X\-Forwarded\-For $proxy\_add\_x\_forwarded\_for;
proxy\_set\_header Upgrade $http\_upgrade;
proxy\_set\_header Connection "upgrade";
proxy\_pass http://127.0.0.1:7681/$1;
location ~ ^/ttyd(.*)$ {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:7681/$1;
}
.fi
@ -254,5 +340,4 @@ location \~ ^/ttyd(.*)$ {
.SH AUTHOR
.PP
Shuanglei Tao <tsl0922@gmail.com> Visit
\[la]https://github.com/tsl0922/ttyd\[ra] to get more information and report bugs.
Shuanglei Tao <tsl0922@gmail.com> Visit https://github.com/tsl0922/ttyd to get more information and report bugs.

View file

@ -7,12 +7,13 @@ ttyd 1 "September 2016" ttyd "User Manual"
# SYNOPSIS
**ttyd** [options] \<command\> [\<arguments...\>]
# Description
# DESCRIPTION
ttyd is a command-line tool for sharing terminal over the web that runs in *nix and windows systems, with the following features:
- Built on top of Libwebsockets with libuv for speed
- Fully-featured terminal based on Xterm.js with CJK (Chinese, Japanese, Korean) and IME support
- Graphical ZMODEM integration with lrzsz support
- Sixel image output support
- SSL support based on OpenSSL
- Run any custom command with options
- Basic authentication support and many other custom options
@ -25,9 +26,15 @@ ttyd 1 "September 2016" ttyd "User Manual"
-i, --interface <interface>
Network interface to bind (eg: eth0), or UNIX domain socket path (eg: /var/run/ttyd.sock)
-U, --socket-owner
User owner of the UNIX domain socket file, when enabled (eg: user:group)
-c, --credential USER[:PASSWORD]
Credential for Basic Authentication (format: username:password)
-H, --auth-header <name>
HTTP Header name for auth proxy, this will configure ttyd to let a HTTP reverse proxy handle authentication
-u, --uid <uid>
User id to run with
@ -37,14 +44,17 @@ ttyd 1 "September 2016" ttyd "User Manual"
-s, --signal <signal string>
Signal to send to the command when exit it (default: 1, SIGHUP)
-w, --cwd <path>
Working directory to be set for the child program
-a, --url-arg
Allow client to send command line arguments in URL (eg: http://localhost:7681?arg=foo&arg=bar)
-R, --readonly
Do not allow clients to write to the TTY
-W, --writable
Allow clients to write to the TTY (readonly by default)
-t, --client-option <key=value>
Send option to client (format: key=value), repeat to add more options
Send option to client (format: key=value), repeat to add more options, see **CLIENT OPTIONS** for details
-T, --terminal-type
Terminal type to report, default: xterm-256color
@ -58,6 +68,9 @@ ttyd 1 "September 2016" ttyd "User Manual"
-o, --once
Accept only one client and exit on disconnection
-q, --exit-no-conn
Exit on all clients disconnection
-B, --browser
Open terminal with the default system browser
@ -65,10 +78,10 @@ ttyd 1 "September 2016" ttyd "User Manual"
Custom index.html path
-b, --base-path
Expected base path for requests coming from a reverse proxy (eg: /mounted/here)
Expected base path for requests coming from a reverse proxy (eg: /mounted/here, max length: 128)
-P, --ping-interval
Websocket ping interval(sec) (default: 300)
Websocket ping interval(sec) (default: 5)
-6, --ipv6
Enable IPv6 support
@ -94,7 +107,40 @@ ttyd 1 "September 2016" ttyd "User Manual"
-h, --help
Print this text and exit
# Examples
# CLIENT OPTIONS
ttyd has a mechanism to pass server side command-line arguments to the browser page which is called **client options**:
```bash
-t, --client-option Send option to client (format: key=value), repeat to add more options
```
## Basic usage
- `-t rendererType=canvas`: use the `canvas` renderer for xterm.js (default: `webgl`)
- `-t disableLeaveAlert=true`: disable the leave page alert
- `-t disableResizeOverlay=true`: disable the terminal resize overlay
- `-t disableReconnect=true`: prevent the terminal from reconnecting on connection error/close
- `-t enableZmodem=true`: enable [ZMODEM](https://en.wikipedia.org/wiki/ZMODEM) / [lrzsz](https://ohse.de/uwe/software/lrzsz.html) file transfer support
- `-t enableTrzsz=true`: enable [trzsz](https://trzsz.github.io) file transfer support
- `-t enableSixel=true`: enable [Sixel](https://en.wikipedia.org/wiki/Sixel) image output support ([Usage](https://saitoha.github.io/libsixel/))
- `-t titleFixed=hello`: set a fixed title for the browser window
- `-t fontSize=20`: change the font size of the terminal
## Advanced usage
You can use the client option to change all the settings of xterm defined in [ITerminalOptions](https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/), examples:
- `-t cursorStyle=bar`: set cursor style to `bar`
- `-t lineHeight=1.5`: set line-height to `1.5`
- `-t 'theme={"background": "green"}'`: set background color to `green`
to try the example options above, run:
```bash
ttyd -t cursorStyle=bar -t lineHeight=1.5 -t 'theme={"background": "green"}' bash
```
# EXAMPLES
ttyd starts web server at port 7681 by default, you can use the -p option to change it, the command will be started with arguments as options. For example, run:
```

View file

@ -10,15 +10,15 @@ STAGE_ROOT="${STAGE_ROOT:-/opt/stage}"
BUILD_ROOT="${BUILD_ROOT:-/opt/build}"
BUILD_TARGET="${BUILD_TARGET:-x86_64}"
ZLIB_VERSION="${ZLIB_VERSION:-1.2.11}"
JSON_C_VERSION="${JSON_C_VERSION:-0.15}"
MBEDTLS_VERSION="${MBEDTLS_VERSION:-2.16.8}"
LIBUV_VERSION="${LIBUV_VERSION:-1.40.0}"
LIBWEBSOCKETS_VERSION="${LIBWEBSOCKETS_VERSION:-4.1.6}"
ZLIB_VERSION="${ZLIB_VERSION:-1.3.1}"
JSON_C_VERSION="${JSON_C_VERSION:-0.17}"
MBEDTLS_VERSION="${MBEDTLS_VERSION:-2.28.5}"
LIBUV_VERSION="${LIBUV_VERSION:-1.44.2}"
LIBWEBSOCKETS_VERSION="${LIBWEBSOCKETS_VERSION:-4.3.3}"
build_zlib() {
echo "=== Building zlib-${ZLIB_VERSION} (${TARGET})..."
curl -sLo- "https://zlib.net/zlib-${ZLIB_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
curl -fSsLo- "https://zlib.net/zlib-${ZLIB_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
pushd "${BUILD_DIR}"/zlib-"${ZLIB_VERSION}"
env CHOST="${TARGET}" ./configure --static --archs="-fPIC" --prefix="${STAGE_DIR}"
make -j"$(nproc)" install
@ -27,13 +27,15 @@ build_zlib() {
build_json-c() {
echo "=== Building json-c-${JSON_C_VERSION} (${TARGET})..."
curl -sLo- "https://s3.amazonaws.com/json-c_releases/releases/json-c-${JSON_C_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
curl -fSsLo- "https://s3.amazonaws.com/json-c_releases/releases/json-c-${JSON_C_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
pushd "${BUILD_DIR}/json-c-${JSON_C_VERSION}"
rm -rf build && mkdir -p build && cd build
cmake -DCMAKE_TOOLCHAIN_FILE="${BUILD_DIR}/cross-${TARGET}.cmake" \
-DCMAKE_BUILD_TYPE=RELEASE \
-DCMAKE_INSTALL_PREFIX="${STAGE_DIR}" \
-DBUILD_SHARED_LIBS=OFF \
-DBUILD_TESTING=OFF \
-DDISABLE_THREAD_LOCAL_STORAGE=ON \
..
make -j"$(nproc)" install
popd
@ -41,7 +43,7 @@ build_json-c() {
build_mbedtls() {
echo "=== Building mbedtls-${MBEDTLS_VERSION} (${TARGET})..."
curl -sLo- "https://github.com/ARMmbed/mbedtls/archive/v${MBEDTLS_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
curl -fSsLo- "https://github.com/ARMmbed/mbedtls/archive/v${MBEDTLS_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
pushd "${BUILD_DIR}/mbedtls-${MBEDTLS_VERSION}"
rm -rf build && mkdir -p build && cd build
cmake -DCMAKE_TOOLCHAIN_FILE="${BUILD_DIR}/cross-${TARGET}.cmake" \
@ -55,7 +57,7 @@ build_mbedtls() {
build_libuv() {
echo "=== Building libuv-${LIBUV_VERSION} (${TARGET})..."
curl -sLo- "https://dist.libuv.org/dist/v${LIBUV_VERSION}/libuv-v${LIBUV_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
curl -fSsLo- "https://dist.libuv.org/dist/v${LIBUV_VERSION}/libuv-v${LIBUV_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
pushd "${BUILD_DIR}/libuv-v${LIBUV_VERSION}"
./autogen.sh
env CFLAGS=-fPIC ./configure --disable-shared --enable-static --prefix="${STAGE_DIR}" --host="${TARGET}"
@ -65,7 +67,7 @@ build_libuv() {
install_cmake_cross_file() {
cat << EOF > "${BUILD_DIR}/cross-${TARGET}.cmake"
set(CMAKE_SYSTEM_NAME Linux)
SET(CMAKE_SYSTEM_NAME $1)
set(CMAKE_C_COMPILER "${TARGET}-gcc")
set(CMAKE_CXX_COMPILER "${TARGET}-g++")
@ -81,9 +83,10 @@ EOF
build_libwebsockets() {
echo "=== Building libwebsockets-${LIBWEBSOCKETS_VERSION} (${TARGET})..."
curl -sLo- "https://github.com/warmcat/libwebsockets/archive/v${LIBWEBSOCKETS_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
curl -fSsLo- "https://github.com/warmcat/libwebsockets/archive/v${LIBWEBSOCKETS_VERSION}.tar.gz" | tar xz -C "${BUILD_DIR}"
pushd "${BUILD_DIR}/libwebsockets-${LIBWEBSOCKETS_VERSION}"
sed -i 's/ websockets_shared//g' cmake/libwebsockets-config.cmake.in
sed -i 's/ OR PC_OPENSSL_FOUND//g' lib/tls/CMakeLists.txt
sed -i '/PC_OPENSSL/d' lib/tls/CMakeLists.txt
rm -rf build && mkdir -p build && cd build
cmake -DCMAKE_TOOLCHAIN_FILE="${BUILD_DIR}/cross-${TARGET}.cmake" \
@ -99,14 +102,14 @@ build_libwebsockets() {
-DLWS_UNIX_SOCK=ON \
-DLWS_IPV6=ON \
-DLWS_ROLE_RAW_FILE=OFF \
-DLWS_WITH_HTTP2=OFF \
-DLWS_WITH_HTTP2=ON \
-DLWS_WITH_HTTP_BASIC_AUTH=OFF \
-DLWS_WITH_UDP=OFF \
-DLWS_WITHOUT_CLIENT=ON \
-DLWS_WITHOUT_EXTENSIONS=OFF \
-DLWS_WITH_LEJP=OFF \
-DLWS_WITH_LEJP_CONF=OFF \
-DLWS_WITH_LWSAC=OFF \
-DLWS_WITH_CUSTOM_HEADERS=OFF \
-DLWS_WITH_SEQUENCER=OFF \
..
make -j"$(nproc)" install
@ -131,11 +134,19 @@ build() {
ALIAS="$2"
STAGE_DIR="${STAGE_ROOT}/${TARGET}"
BUILD_DIR="${BUILD_ROOT}/${TARGET}"
MUSL_CC_URL="https://github.com/tsl0922/musl-toolchains/releases/download/2021-11-23"
COMPONENTS="1"
SYSTEM="Linux"
if [ "$ALIAS" = "win32" ]; then
COMPONENTS=2
SYSTEM="Windows"
fi
echo "=== Installing toolchain ${ALIAS} (${TARGET})..."
mkdir -p "${CROSS_ROOT}" && export PATH="${PATH}:/opt/cross/bin"
curl -sLo- "https://musl.cc/${TARGET}-cross.tgz" | tar xz -C "${CROSS_ROOT}" --strip-components 1
mkdir -p "${CROSS_ROOT}" && export PATH="${PATH}:${CROSS_ROOT}/bin"
curl -fSsLo- "${MUSL_CC_URL}/${TARGET}-cross.tgz" | tar xz -C "${CROSS_ROOT}" --strip-components=${COMPONENTS}
echo "=== Building target ${ALIAS} (${TARGET})..."
@ -143,7 +154,7 @@ build() {
mkdir -p "${STAGE_DIR}" "${BUILD_DIR}"
export PKG_CONFIG_PATH="${STAGE_DIR}/lib/pkgconfig"
install_cmake_cross_file
install_cmake_cross_file ${SYSTEM}
build_zlib
build_json-c
@ -154,15 +165,27 @@ build() {
}
case ${BUILD_TARGET} in
i686|x86_64|aarch64|mips|mipsel|mips64|mips64el)
amd64) BUILD_TARGET="x86_64" ;;
arm64) BUILD_TARGET="aarch64" ;;
armv7) BUILD_TARGET="armv7l" ;;
esac
case ${BUILD_TARGET} in
i686|x86_64|aarch64|mips|mipsel|mips64|mips64el|s390x)
build "${BUILD_TARGET}-linux-musl" "${BUILD_TARGET}"
;;
arm)
build arm-linux-musleabi "${BUILD_TARGET}"
build "${BUILD_TARGET}-linux-musleabi" "${BUILD_TARGET}"
;;
armhf)
build arm-linux-musleabihf "${BUILD_TARGET}"
;;
armv7l)
build armv7l-linux-musleabihf "${BUILD_TARGET}"
;;
win32)
build x86_64-w64-mingw32 "${BUILD_TARGET}"
;;
*)
echo "unknown cross target: ${BUILD_TARGET}" && exit 1
esac

27
scripts/mingw-build.sh Normal file
View file

@ -0,0 +1,27 @@
#!/bin/bash
set -eo pipefail
build_libwebsockets() {
svn co https://github.com/msys2/MINGW-packages/trunk/mingw-w64-libwebsockets
sed -i 's/openssl/mbedtls/' mingw-w64-libwebsockets/PKGBUILD
sed -i '/-DCMAKE_INSTALL_PREFIX=${MINGW_PREFIX}/a \ -DLWS_WITH_MBEDTLS=ON \\' mingw-w64-libwebsockets/PKGBUILD
sed -i '/-DCMAKE_INSTALL_PREFIX=${MINGW_PREFIX}/a \ -DLWS_WITH_LIBUV=ON \\' mingw-w64-libwebsockets/PKGBUILD
pushd mingw-w64-libwebsockets
makepkg-mingw --cleanbuild --syncdeps --force --noconfirm
pacman -U *.pkg.tar.zst --noconfirm
popd
}
build_libwebsockets
# workaround for the lib name change
cp ${MINGW_PREFIX}/lib/libuv_a.a ${MINGW_PREFIX}/lib/libuv.a
rm -rf build && mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=RELEASE \
-DCMAKE_FIND_LIBRARY_SUFFIXES=".a" \
-DCMAKE_C_FLAGS="-Os -ffunction-sections -fdata-sections -fno-unwind-tables -fno-asynchronous-unwind-tables -flto" \
-DCMAKE_EXE_LINKER_FLAGS="-static -no-pie -Wl,-s -Wl,-Bsymbolic -Wl,--gc-sections" \
..
cmake --build .

49
snap/snapcraft.yaml Normal file
View file

@ -0,0 +1,49 @@
name: ttyd
adopt-info: ttyd
summary: Share your terminal over the web
description: |
ttyd is a simple command-line tool for sharing terminal over the web
grade: stable
confinement: classic
base: core20
compression: lzo
license: MIT
assumes:
- command-chain
apps:
ttyd:
command: usr/bin/ttyd
command-chain:
- bin/homeishome-launch
parts:
ttyd:
source: https://github.com/tsl0922/ttyd
source-type: git
plugin: cmake
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
build-environment:
- LDFLAGS: "-pthread"
override-pull: |
snapcraftctl pull
snapcraftctl set-version "$(git describe --tags | sed 's/^v//' | cut -d "-" -f1)"
build-packages:
- build-essential
- libjson-c-dev
- libwebsockets-dev
stage-packages:
- libjson-c4
- libwebsockets15
homeishome-launch:
plugin: nil
stage-snaps:
- homeishome-launch

25354
src/html.h generated

File diff suppressed because it is too large Load diff

View file

@ -6,61 +6,45 @@
#include "server.h"
#include "utils.h"
#if LWS_LIBRARY_VERSION_MAJOR < 2
#define HTTP_STATUS_FOUND 302
#endif
enum { AUTH_OK, AUTH_FAIL, AUTH_ERROR };
static char *html_cache = NULL;
static size_t html_cache_len = 0;
static int check_auth(struct lws *wsi, struct pss_http *pss) {
if (server->credential == NULL) return AUTH_OK;
int hdr_length = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_AUTHORIZATION);
char buf[hdr_length + 1];
int len = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_AUTHORIZATION);
if (len > 0) {
// extract base64 text from authorization header
char *ptr = &buf[0];
char *token, *b64_text = NULL;
int i = 1;
while ((token = strsep(&ptr, " ")) != NULL) {
if (strlen(token) == 0) continue;
if (i++ == 2) {
b64_text = token;
break;
}
}
if (b64_text != NULL && !strcmp(b64_text, server->credential)) return AUTH_OK;
}
static int send_unauthorized(struct lws *wsi, unsigned int code, enum lws_token_indexes header) {
unsigned char buffer[1024 + LWS_PRE], *p, *end;
p = buffer + LWS_PRE;
end = p + sizeof(buffer) - LWS_PRE;
char *body = strdup("401 Unauthorized\n");
size_t n = strlen(body);
if (lws_add_http_header_status(wsi, HTTP_STATUS_UNAUTHORIZED, &p, end) ||
lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_WWW_AUTHENTICATE,
(unsigned char *)"Basic realm=\"ttyd\"", 18, &p, end) ||
lws_add_http_header_content_length(wsi, n, &p, end) ||
lws_finalize_http_header(wsi, &p, end) ||
if (lws_add_http_header_status(wsi, code, &p, end) ||
lws_add_http_header_by_token(wsi, header, (unsigned char *)"Basic realm=\"ttyd\"", 18, &p, end) ||
lws_add_http_header_content_length(wsi, 0, &p, end) || lws_finalize_http_header(wsi, &p, end) ||
lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0)
return AUTH_ERROR;
pss->buffer = pss->ptr = body;
pss->len = n;
lws_callback_on_writable(wsi);
return AUTH_FAIL;
return lws_http_transaction_completed(wsi) ? AUTH_FAIL : AUTH_ERROR;
}
static int check_auth(struct lws *wsi, struct pss_http *pss) {
if (server->auth_header != NULL) {
if (lws_hdr_custom_length(wsi, server->auth_header, strlen(server->auth_header)) > 0) return AUTH_OK;
return send_unauthorized(wsi, HTTP_STATUS_PROXY_AUTH_REQUIRED, WSI_TOKEN_HTTP_PROXY_AUTHENTICATE);
}
if(server->credential != NULL) {
char buf[256];
int len = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_AUTHORIZATION);
if (len >= 7 && strstr(buf, "Basic ")) {
if (!strcmp(buf + 6, server->credential)) return AUTH_OK;
}
return send_unauthorized(wsi, HTTP_STATUS_UNAUTHORIZED, WSI_TOKEN_HTTP_WWW_AUTHENTICATE);
}
return AUTH_OK;
}
static bool accept_gzip(struct lws *wsi) {
int hdr_length = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_ACCEPT_ENCODING);
char buf[hdr_length + 1];
char buf[256];
int len = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_ACCEPT_ENCODING);
return len > 0 && strstr(buf, "gzip") != NULL;
}
@ -102,17 +86,11 @@ static void pss_buffer_free(struct pss_http *pss) {
static void access_log(struct lws *wsi, const char *path) {
char rip[50];
#if LWS_LIBRARY_VERSION_NUMBER >= 2004000
lws_get_peer_simple(lws_get_network_wsi(wsi), rip, sizeof(rip));
#else
char name[100];
lws_get_peer_addresses(wsi, lws_get_socket_fd(wsi), name, sizeof(name), rip, sizeof(rip));
#endif
lwsl_notice("HTTP %s - %s\n", path, rip);
}
int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in,
size_t len) {
int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) {
struct pss_http *pss = (struct pss_http *)user;
unsigned char buffer[4096 + LWS_PRE], *p, *end;
char buf[256];
@ -140,8 +118,7 @@ int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user,
size_t n = sprintf(buf, "{\"token\": \"%s\"}", credential);
if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end) ||
lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE,
(unsigned char *)"application/json;charset=utf-8", 30, &p,
end) ||
(unsigned char *)"application/json;charset=utf-8", 30, &p, end) ||
lws_add_http_header_content_length(wsi, (unsigned long)n, &p, end) ||
lws_finalize_http_header(wsi, &p, end) ||
lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0)
@ -156,11 +133,9 @@ int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user,
// redirects `/base-path` to `/base-path/`
if (strcmp(pss->path, endpoints.parent) == 0) {
if (lws_add_http_header_status(wsi, HTTP_STATUS_FOUND, &p, end) ||
lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_LOCATION,
(unsigned char *)endpoints.index,
lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_LOCATION, (unsigned char *)endpoints.index,
(int)strlen(endpoints.index), &p, end) ||
lws_add_http_header_content_length(wsi, 0, &p, end) ||
lws_finalize_http_header(wsi, &p, end) ||
lws_add_http_header_content_length(wsi, 0, &p, end) || lws_finalize_http_header(wsi, &p, end) ||
lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0)
return 1;
goto try_to_reuse;
@ -179,15 +154,14 @@ int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user,
char *output = (char *)index_html;
size_t output_len = index_html_len;
if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end) ||
lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE,
(const unsigned char *)content_type, 9, &p, end))
lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (const unsigned char *)content_type, 9, &p,
end))
return 1;
#ifdef LWS_WITH_HTTP_STREAM_COMPRESSION
if (!uncompress_html(&output, &output_len)) return 1;
#else
if (accept_gzip(wsi)) {
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_ENCODING,
(unsigned char *)"gzip", 4, &p, end))
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_ENCODING, (unsigned char *)"gzip", 4, &p, end))
return 1;
} else {
if (!uncompress_html(&output, &output_len)) return 1;
@ -199,14 +173,9 @@ int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user,
lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0)
return 1;
#if LWS_LIBRARY_VERSION_MAJOR < 2
if (lws_write_http(wsi, output, output_len) < 0) return 1;
goto try_to_reuse;
#else
pss->buffer = pss->ptr = output;
pss->len = output_len;
lws_callback_on_writable(wsi);
#endif
}
break;

View file

@ -1,15 +1,13 @@
#include <errno.h>
#include <json.h>
#include <libwebsockets.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include "pty.h"
#include "server.h"
#include "terminal.h"
#include "utils.h"
// initial message list
@ -37,37 +35,23 @@ static int send_initial_message(struct lws *wsi, int index) {
return lws_write(wsi, p, (size_t)n, LWS_WRITE_BINARY);
}
static bool parse_window_size(struct pss_tty *pss, int *cols, int *rows) {
char json[pss->len];
strncpy(json, pss->buffer + 1, pss->len - 1);
json[pss->len - 1] = '\0';
json_object *obj = json_tokener_parse(json);
static json_object *parse_window_size(const char *buf, size_t len, uint16_t *cols, uint16_t *rows) {
json_tokener *tok = json_tokener_new();
json_object *obj = json_tokener_parse_ex(tok, buf, len);
struct json_object *o = NULL;
if (!json_object_object_get_ex(obj, "columns", &o)) {
lwsl_err("columns field not exists, json: %s\n", json);
return false;
}
*cols = json_object_get_int(o);
if (!json_object_object_get_ex(obj, "rows", &o)) {
lwsl_err("rows field not exists, json: %s\n", json);
return false;
}
*rows = json_object_get_int(o);
json_object_put(obj);
if (json_object_object_get_ex(obj, "columns", &o)) *cols = (uint16_t)json_object_get_int(o);
if (json_object_object_get_ex(obj, "rows", &o)) *rows = (uint16_t)json_object_get_int(o);
return true;
json_tokener_free(tok);
return obj;
}
static bool check_host_origin(struct lws *wsi) {
int origin_length = lws_hdr_total_length(wsi, WSI_TOKEN_ORIGIN);
char buf[origin_length + 1];
char buf[256];
memset(buf, 0, sizeof(buf));
int len = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_ORIGIN);
if (len <= 0) {
return false;
}
int len = lws_hdr_copy(wsi, buf, (int)sizeof(buf), WSI_TOKEN_ORIGIN);
if (len <= 0) return false;
const char *prot, *address, *path;
int port;
@ -78,154 +62,140 @@ static bool check_host_origin(struct lws *wsi) {
sprintf(buf, "%s:%d", address, port);
}
int host_length = lws_hdr_total_length(wsi, WSI_TOKEN_HOST);
if (host_length != strlen(buf)) return false;
char host_buf[host_length + 1];
char host_buf[256];
memset(host_buf, 0, sizeof(host_buf));
len = lws_hdr_copy(wsi, host_buf, sizeof(host_buf), WSI_TOKEN_HOST);
len = lws_hdr_copy(wsi, host_buf, (int)sizeof(host_buf), WSI_TOKEN_HOST);
return len > 0 && strcasecmp(buf, host_buf) == 0;
}
static void close_cb(uv_handle_t *handle) {
struct pty_proc *proc = container_of((uv_pipe_t *)handle, struct pty_proc, pipe);
free(proc);
static pty_ctx_t *pty_ctx_init(struct pss_tty *pss) {
pty_ctx_t *ctx = xmalloc(sizeof(pty_ctx_t));
ctx->pss = pss;
ctx->ws_closed = false;
return ctx;
}
static void pty_proc_free(struct pty_proc *proc) {
uv_read_stop((uv_stream_t *)&proc->pipe);
close(proc->pty);
if (proc->pty_buffer != NULL) {
free(proc->pty_buffer);
proc->pty_buffer = NULL;
}
for (int i = 0; i < proc->argc; i++) {
free(proc->args[i]);
}
uv_close((uv_handle_t *)&proc->pipe, close_cb);
static void pty_ctx_free(pty_ctx_t *ctx) { free(ctx); }
static void process_read_cb(pty_process *process, pty_buf_t *buf, bool eof) {
pty_ctx_t *ctx = (pty_ctx_t *)process->ctx;
if (ctx->ws_closed) {
pty_buf_free(buf);
return;
}
static void alloc_cb(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
buf->base = xmalloc(suggested_size);
buf->len = suggested_size;
if (eof && !process_running(process))
ctx->pss->lws_close_status = process->exit_code == 0 ? 1000 : 1006;
else
ctx->pss->pty_buf = buf;
lws_callback_on_writable(ctx->pss->wsi);
}
static void read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
struct pss_tty *pss = (struct pss_tty *)stream->data;
struct pty_proc *proc = pss->proc;
proc->pty_len = nread;
uv_read_stop(stream);
if (nread == UV_ENOBUFS || nread == 0) return;
if (nread > 0) {
proc->pty_buffer = xmalloc(LWS_PRE + 1 + (size_t)nread);
memcpy(proc->pty_buffer + LWS_PRE + 1, buf->base, (size_t)nread);
} else {
proc->pty_buffer = NULL;
if (nread != UV_EOF) {
proc->err_count++;
lwsl_err("read_cb: %s (%s)\n", uv_err_name(nread), uv_strerror(nread));
}
}
free(buf->base);
lws_callback_on_writable(pss->wsi);
static void process_exit_cb(pty_process *process) {
pty_ctx_t *ctx = (pty_ctx_t *)process->ctx;
if (ctx->ws_closed) {
lwsl_notice("process killed with signal %d, pid: %d\n", process->exit_signal, process->pid);
goto done;
}
static void child_cb(uv_signal_t *handle, int signum) {
pid_t pid;
int stat;
lwsl_notice("process exited with code %d, pid: %d\n", process->exit_code, process->pid);
ctx->pss->process = NULL;
ctx->pss->lws_close_status = process->exit_code == 0 ? 1000 : 1006;
lws_callback_on_writable(ctx->pss->wsi);
struct pty_proc *proc;
LIST_HEAD(proc, pty_proc) *procs = handle->data;
LIST_FOREACH(proc, procs, entry) {
do
pid = waitpid(proc->pid, &stat, WNOHANG);
while (pid == -1 && errno == EINTR);
if (pid <= 0) continue;
if (WIFEXITED(stat)) {
proc->status = WEXITSTATUS(stat);
lwsl_notice("process exited with code %d, pid: %d\n", proc->status, proc->pid);
} else if (WIFSIGNALED(stat)) {
int sig = WTERMSIG(stat);
char sig_name[20];
proc->status = 128 + sig;
get_sig_name(sig, sig_name, sizeof(sig_name));
lwsl_notice("process killed with signal %d (%s), pid: %d\n", sig, sig_name, proc->pid);
done:
pty_ctx_free(ctx);
}
LIST_REMOVE(proc, entry);
if (proc->state == STATE_KILL) {
pty_proc_free(proc);
} else {
proc->state = STATE_EXIT;
}
}
}
static int spawn_process(struct pss_tty *pss) {
struct pty_proc *proc = pss->proc;
// append url args to arguments
char *argv[server->argc + proc->argc + 1];
static char **build_args(struct pss_tty *pss) {
int i, n = 0;
char **argv = xmalloc((server->argc + pss->argc + 1) * sizeof(char *));
for (i = 0; i < server->argc; i++) {
argv[n++] = server->argv[i];
}
for (i = 0; i < proc->argc; i++) {
argv[n++] = proc->args[i];
for (i = 0; i < pss->argc; i++) {
argv[n++] = pss->args[i];
}
argv[n] = NULL;
uv_signal_start(&server->watcher, child_cb, SIGCHLD);
// ensure the lws socket fd close-on-exec
fd_set_cloexec(lws_get_socket_fd(pss->wsi));
// create process with pseudo-tty
proc->pid = pty_fork(&proc->pty, argv[0], argv, server->terminal_type);
if (proc->pid < 0) {
lwsl_err("pty_fork: %d (%s)\n", errno, strerror(errno));
return 1;
return argv;
}
lwsl_notice("started process, pid: %d\n", proc->pid);
static char **build_env(struct pss_tty *pss) {
int i = 0, n = 2;
char **envp = xmalloc(n * sizeof(char *));
proc->pipe.data = pss;
uv_pipe_open(&proc->pipe, proc->pty);
// TERM
envp[i] = xmalloc(36);
snprintf(envp[i], 36, "TERM=%s", server->terminal_type);
i++;
// TTYD_USER
if (strlen(pss->user) > 0) {
envp = xrealloc(envp, (++n) * sizeof(char *));
envp[i] = xmalloc(40);
snprintf(envp[i], 40, "TTYD_USER=%s", pss->user);
i++;
}
envp[i] = NULL;
return envp;
}
static bool spawn_process(struct pss_tty *pss, uint16_t columns, uint16_t rows) {
pty_process *process = process_init((void *)pty_ctx_init(pss), server->loop, build_args(pss), build_env(pss));
if (server->cwd != NULL) process->cwd = strdup(server->cwd);
if (columns > 0) process->columns = columns;
if (rows > 0) process->rows = rows;
if (pty_spawn(process, process_read_cb, process_exit_cb) != 0) {
lwsl_err("pty_spawn: %d (%s)\n", errno, strerror(errno));
process_free(process);
return false;
}
lwsl_notice("started process, pid: %d\n", process->pid);
pss->process = process;
lws_callback_on_writable(pss->wsi);
return 0;
return true;
}
static void kill_process(struct pty_proc *proc) {
if (proc->pid <= 0) return;
static void wsi_output(struct lws *wsi, pty_buf_t *buf) {
if (buf == NULL) return;
char *message = xmalloc(LWS_PRE + 1 + buf->len);
char *ptr = message + LWS_PRE;
pid_t pid = proc->pid;
int sig = server->sig_code;
char *sig_name = server->sig_name;
*ptr = OUTPUT;
memcpy(ptr + 1, buf->base, buf->len);
size_t n = buf->len + 1;
lwsl_notice("killing process %d with signal: %d (%s)\n", pid, sig, sig_name);
int pgid = getpgid(pid);
if (uv_kill(pgid > 0 ? -pgid : pid, sig) != 0) {
lwsl_err("kill: %d, errno: %d (%s)\n", pid, errno, strerror(errno));
}
if (lws_write(wsi, (unsigned char *)ptr, n, LWS_WRITE_BINARY) < n) {
lwsl_err("write OUTPUT to WS\n");
}
static void write_cb(uv_write_t *req, int status) {
if (status != 0) lwsl_warn("uv_write callback returned status: %d\n", status);
free(req->data);
free(req);
free(message);
}
int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in,
size_t len) {
static bool check_auth(struct lws *wsi, struct pss_tty *pss) {
if (server->auth_header != NULL) {
return lws_hdr_custom_copy(wsi, pss->user, sizeof(pss->user), server->auth_header, strlen(server->auth_header)) > 0;
}
if (server->credential != NULL) {
char buf[256];
size_t n = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_AUTHORIZATION);
return n >= 7 && strstr(buf, "Basic ") && !strcmp(buf + 6, server->credential);
}
return true;
}
int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) {
struct pss_tty *pss = (struct pss_tty *)user;
struct pty_proc *proc;
char buf[256];
size_t n = 0;
@ -239,13 +209,14 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user,
lwsl_warn("refuse to serve WS client due to the --max-clients option.\n");
return 1;
}
if (!check_auth(wsi, pss)) return 1;
n = lws_hdr_copy(wsi, pss->path, sizeof(pss->path), WSI_TOKEN_GET_URI);
#if defined(LWS_ROLE_H2)
if (n <= 0) n = lws_hdr_copy(wsi, pss->path, sizeof(pss->path), WSI_TOKEN_HTTP_COLON_PATH);
#endif
if (strncmp(pss->path, endpoints.ws, n) != 0) {
lwsl_warn("refuse to serve WS client for illegal ws path: %s\n", buf);
lwsl_warn("refuse to serve WS client for illegal ws path: %s\n", pss->path);
return 1;
}
@ -259,46 +230,31 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user,
case LWS_CALLBACK_ESTABLISHED:
pss->initialized = false;
pss->initial_cmd_index = 0;
pss->authenticated = false;
pss->wsi = wsi;
pss->buffer = NULL;
pss->proc = proc = xmalloc(sizeof(struct pty_proc));
memset(proc, 0, sizeof(struct pty_proc));
proc->status = -1;
proc->state = STATE_INIT;
uv_pipe_init(server->loop, &proc->pipe, 0);
pss->lws_close_status = LWS_CLOSE_STATUS_NOSTATUS;
if (server->url_arg) {
while (lws_hdr_copy_fragment(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_URI_ARGS, n++) > 0) {
if (strncmp(buf, "arg=", 4) == 0) {
proc->args = xrealloc(proc->args, (proc->argc + 1) * sizeof(char *));
proc->args[proc->argc] = strdup(&buf[4]);
proc->argc++;
pss->args = xrealloc(pss->args, (pss->argc + 1) * sizeof(char *));
pss->args[pss->argc] = strdup(&buf[4]);
pss->argc++;
}
}
}
LIST_INSERT_HEAD(&server->procs, proc, entry);
server->client_count++;
#if LWS_LIBRARY_VERSION_NUMBER >= 2004000
lws_get_peer_simple(lws_get_network_wsi(wsi), pss->address, sizeof(pss->address));
#else
char name[100];
lws_get_peer_addresses(wsi, lws_get_socket_fd(wsi), name, sizeof(name), pss->address,
sizeof(pss->address));
#endif
lwsl_notice("WS %s - %s, clients: %d\n", pss->path, pss->address, server->client_count);
break;
case LWS_CALLBACK_SERVER_WRITEABLE:
proc = pss->proc;
if (!pss->initialized) {
if (pss->initial_cmd_index == sizeof(initial_cmds)) {
pss->initialized = true;
uv_read_start((uv_stream_t *)&proc->pipe, alloc_cb, read_cb);
pty_resume(pss->process);
break;
}
if (send_initial_message(wsi, pss->initial_cmd_index) < 0) {
@ -311,26 +267,17 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user,
break;
}
// read error or client exited, close connection
if (proc->status == 0 || proc->pty_len == UV_EOF) {
lws_close_reason(wsi, LWS_CLOSE_STATUS_NORMAL, NULL, 0);
if (pss->lws_close_status > LWS_CLOSE_STATUS_NOSTATUS) {
lws_close_reason(wsi, pss->lws_close_status, NULL, 0);
return 1;
} else if (proc->status > 0 || (proc->pty_len < 0 && proc->err_count == MAX_READ_RETRY)) {
lws_close_reason(wsi, LWS_CLOSE_STATUS_UNEXPECTED_CONDITION, NULL, 0);
return -1;
}
if (proc->pty_buffer != NULL && proc->pty_len > 0) {
proc->pty_buffer[LWS_PRE] = OUTPUT;
n = (size_t)(proc->pty_len + 1);
if (lws_write(wsi, (unsigned char *)proc->pty_buffer + LWS_PRE, n, LWS_WRITE_BINARY) < n) {
lwsl_err("write OUTPUT to WS\n");
if (pss->pty_buf != NULL) {
wsi_output(wsi, pss->pty_buf);
pty_buf_free(pss->pty_buf);
pss->pty_buf = NULL;
pty_resume(pss->process);
}
free(proc->pty_buffer);
proc->pty_buffer = NULL;
}
uv_read_start((uv_stream_t *)&proc->pipe, alloc_cb, read_cb);
break;
case LWS_CALLBACK_RECEIVE:
@ -357,49 +304,33 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user,
return 0;
}
proc = pss->proc;
switch (command) {
case INPUT:
if (proc->pty == 0) break;
if (server->readonly) break;
char *data = xmalloc(pss->len - 1);
memcpy(data, pss->buffer + 1, pss->len - 1);
uv_buf_t b = {data, pss->len - 1};
uv_write_t *req = xmalloc(sizeof(uv_write_t));
req->data = data;
int err = uv_write(req, (uv_stream_t *)&proc->pipe, &b, 1, write_cb);
if (!server->writable) break;
int err = pty_write(pss->process, pty_buf_init(pss->buffer + 1, pss->len - 1));
if (err) {
lwsl_err("uv_write: %s (%s)\n", uv_err_name(err), uv_strerror(err));
return -1;
}
break;
case RESIZE_TERMINAL: {
int cols, rows;
if (parse_window_size(pss, &cols, &rows)) {
if (pty_resize(proc->pty, cols, rows) < 0) {
lwsl_err("pty_resize: %d (%s)\n", errno, strerror(errno));
}
}
} break;
case RESIZE_TERMINAL:
if (pss->process == NULL) break;
json_object_put(
parse_window_size(pss->buffer + 1, pss->len - 1, &pss->process->columns, &pss->process->rows));
pty_resize(pss->process);
break;
case PAUSE:
if (proc->state == STATE_INIT) {
uv_read_stop((uv_stream_t *)&proc->pipe);
proc->state = STATE_PAUSE;
}
pty_pause(pss->process);
break;
case RESUME:
if (proc->state == STATE_PAUSE) {
uv_read_start((uv_stream_t *)&proc->pipe, alloc_cb, read_cb);
proc->state = STATE_INIT;
}
pty_resume(pss->process);
break;
case JSON_DATA:
if (proc->pid > 0) break;
if (pss->process != NULL) break;
uint16_t columns = 0;
uint16_t rows = 0;
json_object *obj = parse_window_size(pss->buffer, pss->len, &columns, &rows);
if (server->credential != NULL) {
json_object *obj = json_tokener_parse(pss->buffer);
struct json_object *o = NULL;
if (json_object_object_get_ex(obj, "AuthToken", &o)) {
const char *token = json_object_get_string(o);
@ -409,11 +340,13 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user,
lwsl_warn("WS authentication failed with token: %s\n", token);
}
if (!pss->authenticated) {
json_object_put(obj);
lws_close_reason(wsi, LWS_CLOSE_STATUS_POLICY_VIOLATION, NULL, 0);
return -1;
}
}
if (spawn_process(pss) != 0) return 1;
json_object_put(obj);
if (!spawn_process(pss, columns, rows)) return 1;
break;
default:
lwsl_warn("ignored unknown message type: %c\n", command);
@ -431,21 +364,23 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user,
server->client_count--;
lwsl_notice("WS closed from %s, clients: %d\n", pss->address, server->client_count);
if (pss->buffer != NULL) {
free(pss->buffer);
if (pss->buffer != NULL) free(pss->buffer);
if (pss->pty_buf != NULL) pty_buf_free(pss->pty_buf);
for (int i = 0; i < pss->argc; i++) {
free(pss->args[i]);
}
proc = pss->proc;
if (proc->state == STATE_EXIT) {
pty_proc_free(proc);
} else {
proc->state = STATE_KILL;
uv_read_stop((uv_stream_t *)&proc->pipe);
kill_process(proc);
if (pss->process != NULL) {
((pty_ctx_t *)pss->process->ctx)->ws_closed = true;
if (process_running(pss->process)) {
pty_pause(pss->process);
lwsl_notice("killing process, pid: %d\n", pss->process->pid);
pty_kill(pss->process, server->sig_code);
}
}
if (server->once && server->client_count == 0) {
lwsl_notice("exiting due to the --once option.\n");
if ((server->once || server->exit_no_conn) && server->client_count == 0) {
lwsl_notice("exiting due to the --once/--exit-no-conn option.\n");
force_exit = true;
lws_cancel_service(context);
exit(0);

485
src/pty.c Normal file
View file

@ -0,0 +1,485 @@
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#ifndef _WIN32
#include <sys/ioctl.h>
#include <sys/wait.h>
#if defined(__OpenBSD__) || defined(__APPLE__)
#include <util.h>
#elif defined(__FreeBSD__)
#include <libutil.h>
#else
#include <pty.h>
#endif
#if defined(__APPLE__)
#include <crt_externs.h>
#define environ (*_NSGetEnviron())
#else
extern char **environ;
#endif
#endif
#include "pty.h"
#include "utils.h"
#ifdef _WIN32
HRESULT (WINAPI *pCreatePseudoConsole)(COORD, HANDLE, HANDLE, DWORD, HPCON *);
HRESULT (WINAPI *pResizePseudoConsole)(HPCON, COORD);
void (WINAPI *pClosePseudoConsole)(HPCON);
#endif
static void alloc_cb(uv_handle_t *unused, size_t suggested_size, uv_buf_t *buf) {
buf->base = xmalloc(suggested_size);
buf->len = suggested_size;
}
static void close_cb(uv_handle_t *handle) { free(handle); }
static void async_free_cb(uv_handle_t *handle) {
free((uv_async_t *) handle -> data);
}
pty_buf_t *pty_buf_init(char *base, size_t len) {
pty_buf_t *buf = xmalloc(sizeof(pty_buf_t));
buf->base = xmalloc(len);
memcpy(buf->base, base, len);
buf->len = len;
return buf;
}
void pty_buf_free(pty_buf_t *buf) {
if (buf == NULL) return;
if (buf->base != NULL) free(buf->base);
free(buf);
}
static void read_cb(uv_stream_t *stream, ssize_t n, const uv_buf_t *buf) {
uv_read_stop(stream);
pty_process *process = (pty_process *) stream->data;
if (n <= 0) {
if (n == UV_ENOBUFS || n == 0) return;
process->read_cb(process, NULL, true);
goto done;
}
process->read_cb(process, pty_buf_init(buf->base, (size_t) n), false);
done:
free(buf->base);
}
static void write_cb(uv_write_t *req, int unused) {
pty_buf_t *buf = (pty_buf_t *) req->data;
pty_buf_free(buf);
free(req);
}
pty_process *process_init(void *ctx, uv_loop_t *loop, char *argv[], char *envp[]) {
pty_process *process = xmalloc(sizeof(pty_process));
memset(process, 0, sizeof(pty_process));
process->ctx = ctx;
process->loop = loop;
process->argv = argv;
process->envp = envp;
process->columns = 80;
process->rows = 24;
process->exit_code = -1;
return process;
}
bool process_running(pty_process *process) {
return process != NULL && process->pid > 0 && uv_kill(process->pid, 0) == 0;
}
void process_free(pty_process *process) {
if (process == NULL) return;
#ifdef _WIN32
if (process->si.lpAttributeList != NULL) {
DeleteProcThreadAttributeList(process->si.lpAttributeList);
free(process->si.lpAttributeList);
}
if (process->pty != NULL) pClosePseudoConsole(process->pty);
if (process->handle != NULL) CloseHandle(process->handle);
#else
close(process->pty);
uv_thread_join(&process->tid);
#endif
if (process->in != NULL) uv_close((uv_handle_t *) process->in, close_cb);
if (process->out != NULL) uv_close((uv_handle_t *) process->out, close_cb);
if (process->argv != NULL) free(process->argv);
if (process->cwd != NULL) free(process->cwd);
char **p = process->envp;
for (; *p; p++) free(*p);
free(process->envp);
}
void pty_pause(pty_process *process) {
if (process == NULL) return;
if (process->paused) return;
uv_read_stop((uv_stream_t *) process->out);
}
void pty_resume(pty_process *process) {
if (process == NULL) return;
if (!process->paused) return;
process->out->data = process;
uv_read_start((uv_stream_t *) process->out, alloc_cb, read_cb);
}
int pty_write(pty_process *process, pty_buf_t *buf) {
if (process == NULL) {
pty_buf_free(buf);
return UV_ESRCH;
}
uv_buf_t b = uv_buf_init(buf->base, buf->len);
uv_write_t *req = xmalloc(sizeof(uv_write_t));
req->data = buf;
return uv_write(req, (uv_stream_t *) process->in, &b, 1, write_cb);
}
bool pty_resize(pty_process *process) {
if (process == NULL) return false;
if (process->columns <= 0 || process->rows <= 0) return false;
#ifdef _WIN32
COORD size = {(int16_t) process->columns, (int16_t) process->rows};
return pResizePseudoConsole(process->pty, size) == S_OK;
#else
struct winsize size = {process->rows, process->columns, 0, 0};
return ioctl(process->pty, TIOCSWINSZ, &size) == 0;
#endif
}
bool pty_kill(pty_process *process, int sig) {
if (process == NULL) return false;
#ifdef _WIN32
return TerminateProcess(process->handle, 1) != 0;
#else
return uv_kill(-process->pid, sig) == 0;
#endif
}
#ifdef _WIN32
bool conpty_init() {
uv_lib_t kernel;
if (uv_dlopen("kernel32.dll", &kernel)) {
uv_dlclose(&kernel);
return false;
}
static struct {
char *name;
FARPROC *ptr;
} conpty_entry[] = {{"CreatePseudoConsole", (FARPROC *) &pCreatePseudoConsole},
{"ResizePseudoConsole", (FARPROC *) &pResizePseudoConsole},
{"ClosePseudoConsole", (FARPROC *) &pClosePseudoConsole},
{NULL, NULL}};
for (int i = 0; conpty_entry[i].name != NULL && conpty_entry[i].ptr != NULL; i++) {
if (uv_dlsym(&kernel, conpty_entry[i].name, (void **) conpty_entry[i].ptr)) {
uv_dlclose(&kernel);
return false;
}
}
return true;
}
static WCHAR *to_utf16(char *str) {
int len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
if (len <= 0) return NULL;
WCHAR *wstr = xmalloc((len + 1) * sizeof(WCHAR));
if (len != MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, len)) {
free(wstr);
return NULL;
}
wstr[len] = L'\0';
return wstr;
}
// convert argv to cmdline for CreateProcessW
static WCHAR *join_args(char **argv) {
char args[256] = {0};
char **ptr = argv;
for (; *ptr; ptr++) {
char *quoted = (char *) quote_arg(*ptr);
size_t arg_len = strlen(args) + 1;
size_t quoted_len = strlen(quoted);
if (arg_len == 1) memset(args, 0, 2);
if (arg_len != 1) strcat(args, " ");
strncat(args, quoted, quoted_len);
if (quoted != *ptr) free(quoted);
}
if (args[255] != '\0') args[255] = '\0'; // truncate
return to_utf16(args);
}
static bool conpty_setup(HPCON *hnd, COORD size, STARTUPINFOEXW *si_ex, char **in_name, char **out_name) {
static int count = 0;
char buf[256];
HPCON pty = INVALID_HANDLE_VALUE;
SECURITY_ATTRIBUTES sa = {0};
HANDLE in_pipe = INVALID_HANDLE_VALUE;
HANDLE out_pipe = INVALID_HANDLE_VALUE;
const DWORD open_mode = PIPE_ACCESS_INBOUND | PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE;
const DWORD pipe_mode = PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT;
DWORD pid = GetCurrentProcessId();
bool ret = false;
sa.nLength = sizeof(sa);
snprintf(buf, sizeof(buf), "\\\\.\\pipe\\ttyd-term-in-%d-%d", pid, count);
*in_name = strdup(buf);
snprintf(buf, sizeof(buf), "\\\\.\\pipe\\ttyd-term-out-%d-%d", pid, count);
*out_name = strdup(buf);
in_pipe = CreateNamedPipeA(*in_name, open_mode, pipe_mode, 1, 0, 0, 30000, &sa);
out_pipe = CreateNamedPipeA(*out_name, open_mode, pipe_mode, 1, 0, 0, 30000, &sa);
if (in_pipe == INVALID_HANDLE_VALUE || out_pipe == INVALID_HANDLE_VALUE) {
print_error("CreateNamedPipeA");
goto failed;
}
HRESULT hr = pCreatePseudoConsole(size, in_pipe, out_pipe, 0, &pty);
if (FAILED(hr)) {
print_error("CreatePseudoConsole");
goto failed;
}
si_ex->StartupInfo.cb = sizeof(STARTUPINFOEXW);
si_ex->StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
si_ex->StartupInfo.hStdError = NULL;
si_ex->StartupInfo.hStdInput = NULL;
si_ex->StartupInfo.hStdOutput = NULL;
size_t bytes_required;
InitializeProcThreadAttributeList(NULL, 1, 0, &bytes_required);
si_ex->lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST) xmalloc(bytes_required);
if (!InitializeProcThreadAttributeList(si_ex->lpAttributeList, 1, 0, &bytes_required)) {
print_error("InitializeProcThreadAttributeList");
goto failed;
}
if (!UpdateProcThreadAttribute(si_ex->lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, pty, sizeof(HPCON),
NULL, NULL)) {
print_error("UpdateProcThreadAttribute");
goto failed;
}
count++;
*hnd = pty;
ret = true;
goto done;
failed:
ret = false;
free(*in_name);
*in_name = NULL;
free(*out_name);
*out_name = NULL;
done:
if (in_pipe != INVALID_HANDLE_VALUE) CloseHandle(in_pipe);
if (out_pipe != INVALID_HANDLE_VALUE) CloseHandle(out_pipe);
return ret;
}
static void connect_cb(uv_connect_t *req, int status) { free(req); }
static void CALLBACK conpty_exit(void *context, BOOLEAN unused) {
pty_process *process = (pty_process *) context;
uv_async_send(&process->async);
}
static void async_cb(uv_async_t *async) {
pty_process *process = (pty_process *) async->data;
UnregisterWait(process->wait);
DWORD exit_code;
GetExitCodeProcess(process->handle, &exit_code);
process->exit_code = (int) exit_code;
process->exit_signal = 1;
process->exit_cb(process);
uv_close((uv_handle_t *) async, async_free_cb);
process_free(process);
}
int pty_spawn(pty_process *process, pty_read_cb read_cb, pty_exit_cb exit_cb) {
char *in_name = NULL;
char *out_name = NULL;
DWORD flags = EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT;
COORD size = {(int16_t) process->columns, (int16_t) process->rows};
if (!conpty_setup(&process->pty, size, &process->si, &in_name, &out_name)) return 1;
SetConsoleCtrlHandler(NULL, FALSE);
int status = 1;
process->in = xmalloc(sizeof(uv_pipe_t));
process->out = xmalloc(sizeof(uv_pipe_t));
uv_pipe_init(process->loop, process->in, 0);
uv_pipe_init(process->loop, process->out, 0);
uv_connect_t *in_req = xmalloc(sizeof(uv_connect_t));
uv_connect_t *out_req = xmalloc(sizeof(uv_connect_t));
uv_pipe_connect(in_req, process->in, in_name, connect_cb);
uv_pipe_connect(out_req, process->out, out_name, connect_cb);
PROCESS_INFORMATION pi = {0};
WCHAR *cmdline, *cwd;
cmdline = join_args(process->argv);
if (cmdline == NULL) goto cleanup;
if (process->envp != NULL) {
char **p = process->envp;
for (; *p; p++) {
WCHAR *env = to_utf16(*p);
if (env == NULL) goto cleanup;
_wputenv(env);
free(env);
}
}
if (process->cwd != NULL) {
cwd = to_utf16(process->cwd);
if (cwd == NULL) goto cleanup;
}
if (!CreateProcessW(NULL, cmdline, NULL, NULL, FALSE, flags, NULL, cwd, &process->si.StartupInfo, &pi)) {
print_error("CreateProcessW");
DWORD exitCode = 0;
if (GetExitCodeProcess(pi.hProcess, &exitCode)) printf("== exit code: %d\n", exitCode);
goto cleanup;
}
process->pid = pi.dwProcessId;
process->handle = pi.hProcess;
process->paused = true;
process->read_cb = read_cb;
process->exit_cb = exit_cb;
process->async.data = process;
uv_async_init(process->loop, &process->async, async_cb);
if (!RegisterWaitForSingleObject(&process->wait, pi.hProcess, conpty_exit, process, INFINITE, WT_EXECUTEONLYONCE)) {
print_error("RegisterWaitForSingleObject");
goto cleanup;
}
status = 0;
cleanup:
if (in_name != NULL) free(in_name);
if (out_name != NULL) free(out_name);
if (cmdline != NULL) free(cmdline);
if (cwd != NULL) free(cwd);
return status;
}
#else
static bool fd_set_cloexec(const int fd) {
int flags = fcntl(fd, F_GETFD);
if (flags < 0) return false;
return (flags & FD_CLOEXEC) == 0 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != -1;
}
static bool fd_duplicate(int fd, uv_pipe_t *pipe) {
int fd_dup = dup(fd);
if (fd_dup < 0) return false;
if (!fd_set_cloexec(fd_dup)) return false;
int status = uv_pipe_open(pipe, fd_dup);
if (status) close(fd_dup);
return status == 0;
}
static void wait_cb(void *arg) {
pty_process *process = (pty_process *) arg;
pid_t pid;
int stat;
do
pid = waitpid(process->pid, &stat, 0);
while (pid != process->pid && errno == EINTR);
if (WIFEXITED(stat)) {
process->exit_code = WEXITSTATUS(stat);
}
if (WIFSIGNALED(stat)) {
int sig = WTERMSIG(stat);
process->exit_code = 128 + sig;
process->exit_signal = sig;
}
uv_async_send(&process->async);
}
static void async_cb(uv_async_t *async) {
pty_process *process = (pty_process *) async->data;
process->exit_cb(process);
uv_close((uv_handle_t *) async, async_free_cb);
process_free(process);
}
int pty_spawn(pty_process *process, pty_read_cb read_cb, pty_exit_cb exit_cb) {
int status = 0;
uv_disable_stdio_inheritance();
int master, pid;
struct winsize size = {process->rows, process->columns, 0, 0};
pid = forkpty(&master, NULL, NULL, &size);
if (pid < 0) {
status = -errno;
return status;
} else if (pid == 0) {
setsid();
if (process->cwd != NULL) chdir(process->cwd);
if (process->envp != NULL) {
char **p = process->envp;
for (; *p; p++) putenv(*p);
}
int ret = execvp(process->argv[0], process->argv);
if (ret < 0) {
perror("execvp failed\n");
_exit(-errno);
}
}
int flags = fcntl(master, F_GETFL);
if (flags == -1) {
status = -errno;
goto error;
}
if (fcntl(master, F_SETFL, flags | O_NONBLOCK) == -1) {
status = -errno;
goto error;
}
if (!fd_set_cloexec(master)) {
status = -errno;
goto error;
}
process->in = xmalloc(sizeof(uv_pipe_t));
process->out = xmalloc(sizeof(uv_pipe_t));
uv_pipe_init(process->loop, process->in, 0);
uv_pipe_init(process->loop, process->out, 0);
if (!fd_duplicate(master, process->in) || !fd_duplicate(master, process->out)) {
status = -errno;
goto error;
}
process->pty = master;
process->pid = pid;
process->paused = true;
process->read_cb = read_cb;
process->exit_cb = exit_cb;
process->async.data = process;
uv_async_init(process->loop, &process->async, async_cb);
uv_thread_create(&process->tid, wait_cb, process);
return 0;
error:
close(master);
uv_kill(pid, SIGKILL);
waitpid(pid, NULL, 0);
return status;
}
#endif

68
src/pty.h Normal file
View file

@ -0,0 +1,68 @@
#ifndef TTYD_PTY_H
#define TTYD_PTY_H
#include <stdbool.h>
#include <stdint.h>
#include <uv.h>
#ifdef _WIN32
#ifndef HPCON
#define HPCON VOID *
#endif
#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
#define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 0x00020016
#endif
bool conpty_init();
#endif
typedef struct {
char *base;
size_t len;
} pty_buf_t;
struct pty_process_;
typedef struct pty_process_ pty_process;
typedef void (*pty_read_cb)(pty_process *, pty_buf_t *, bool);
typedef void (*pty_exit_cb)(pty_process *);
struct pty_process_ {
int pid, exit_code, exit_signal;
uint16_t columns, rows;
#ifdef _WIN32
STARTUPINFOEXW si;
HPCON pty;
HANDLE handle;
HANDLE wait;
#else
pid_t pty;
uv_thread_t tid;
#endif
char **argv;
char **envp;
char *cwd;
uv_loop_t *loop;
uv_async_t async;
uv_pipe_t *in;
uv_pipe_t *out;
bool paused;
pty_read_cb read_cb;
pty_exit_cb exit_cb;
void *ctx;
};
pty_buf_t *pty_buf_init(char *base, size_t len);
void pty_buf_free(pty_buf_t *buf);
pty_process *process_init(void *ctx, uv_loop_t *loop, char *argv[], char *envp[]);
bool process_running(pty_process *process);
void process_free(pty_process *process);
int pty_spawn(pty_process *process, pty_read_cb read_cb, pty_exit_cb exit_cb);
void pty_pause(pty_process *process);
void pty_resume(pty_process *process);
int pty_write(pty_process *process, pty_buf_t *buf);
bool pty_resize(pty_process *process);
bool pty_kill(pty_process *process, int sig);
#endif // TTYD_PTY_H

View file

@ -1,574 +0,0 @@
/*
* Copyright (c) 1991, 1993
* The Regents of the University of California. 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.
* 3. Neither the name of the University 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 REGENTS 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 REGENTS 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.
*
* @(#)queue.h 8.5 (Berkeley) 8/20/94
*/
#ifndef _SYS_QUEUE_H_
#define _SYS_QUEUE_H_
/*
* This file defines five types of data structures: singly-linked lists,
* lists, simple queues, tail queues, and circular queues.
*
* A singly-linked list is headed by a single forward pointer. The
* elements are singly linked for minimum space and pointer manipulation
* overhead at the expense of O(n) removal for arbitrary elements. New
* elements can be added to the list after an existing element or at the
* head of the list. Elements being removed from the head of the list
* should use the explicit macro for this purpose for optimum
* efficiency. A singly-linked list may only be traversed in the forward
* direction. Singly-linked lists are ideal for applications with large
* datasets and few or no removals or for implementing a LIFO queue.
*
* A list is headed by a single forward pointer (or an array of forward
* pointers for a hash table header). The elements are doubly linked
* so that an arbitrary element can be removed without a need to
* traverse the list. New elements can be added to the list before
* or after an existing element or at the head of the list. A list
* may only be traversed in the forward direction.
*
* A simple queue is headed by a pair of pointers, one the head of the
* list and the other to the tail of the list. The elements are singly
* linked to save space, so elements can only be removed from the
* head of the list. New elements can be added to the list after
* an existing element, at the head of the list, or at the end of the
* list. A simple queue may only be traversed in the forward direction.
*
* A tail queue is headed by a pair of pointers, one to the head of the
* list and the other to the tail of the list. The elements are doubly
* linked so that an arbitrary element can be removed without a need to
* traverse the list. New elements can be added to the list before or
* after an existing element, at the head of the list, or at the end of
* the list. A tail queue may be traversed in either direction.
*
* A circle queue is headed by a pair of pointers, one to the head of the
* list and the other to the tail of the list. The elements are doubly
* linked so that an arbitrary element can be removed without a need to
* traverse the list. New elements can be added to the list before or after
* an existing element, at the head of the list, or at the end of the list.
* A circle queue may be traversed in either direction, but has a more
* complex end of list detection.
*
* For details on the use of these macros, see the queue(3) manual page.
*/
/*
* List definitions.
*/
#define LIST_HEAD(name, type) \
struct name { \
struct type *lh_first; /* first element */ \
}
#define LIST_HEAD_INITIALIZER(head) \
{ NULL }
#define LIST_ENTRY(type) \
struct { \
struct type *le_next; /* next element */ \
struct type **le_prev; /* address of previous next element */ \
}
/*
* List functions.
*/
#define LIST_INIT(head) do { \
(head)->lh_first = NULL; \
} while (/*CONSTCOND*/0)
#define LIST_INSERT_AFTER(listelm, elm, field) do { \
if (((elm)->field.le_next = (listelm)->field.le_next) != NULL) \
(listelm)->field.le_next->field.le_prev = \
&(elm)->field.le_next; \
(listelm)->field.le_next = (elm); \
(elm)->field.le_prev = &(listelm)->field.le_next; \
} while (/*CONSTCOND*/0)
#define LIST_INSERT_BEFORE(listelm, elm, field) do { \
(elm)->field.le_prev = (listelm)->field.le_prev; \
(elm)->field.le_next = (listelm); \
*(listelm)->field.le_prev = (elm); \
(listelm)->field.le_prev = &(elm)->field.le_next; \
} while (/*CONSTCOND*/0)
#define LIST_INSERT_HEAD(head, elm, field) do { \
if (((elm)->field.le_next = (head)->lh_first) != NULL) \
(head)->lh_first->field.le_prev = &(elm)->field.le_next;\
(head)->lh_first = (elm); \
(elm)->field.le_prev = &(head)->lh_first; \
} while (/*CONSTCOND*/0)
#define LIST_REMOVE(elm, field) do { \
if ((elm)->field.le_next != NULL) \
(elm)->field.le_next->field.le_prev = \
(elm)->field.le_prev; \
*(elm)->field.le_prev = (elm)->field.le_next; \
} while (/*CONSTCOND*/0)
#define LIST_FOREACH(var, head, field) \
for ((var) = ((head)->lh_first); \
(var); \
(var) = ((var)->field.le_next))
/*
* List access methods.
*/
#define LIST_EMPTY(head) ((head)->lh_first == NULL)
#define LIST_FIRST(head) ((head)->lh_first)
#define LIST_NEXT(elm, field) ((elm)->field.le_next)
/*
* Singly-linked List definitions.
*/
#define SLIST_HEAD(name, type) \
struct name { \
struct type *slh_first; /* first element */ \
}
#define SLIST_HEAD_INITIALIZER(head) \
{ NULL }
#define SLIST_ENTRY(type) \
struct { \
struct type *sle_next; /* next element */ \
}
/*
* Singly-linked List functions.
*/
#define SLIST_INIT(head) do { \
(head)->slh_first = NULL; \
} while (/*CONSTCOND*/0)
#define SLIST_INSERT_AFTER(slistelm, elm, field) do { \
(elm)->field.sle_next = (slistelm)->field.sle_next; \
(slistelm)->field.sle_next = (elm); \
} while (/*CONSTCOND*/0)
#define SLIST_INSERT_HEAD(head, elm, field) do { \
(elm)->field.sle_next = (head)->slh_first; \
(head)->slh_first = (elm); \
} while (/*CONSTCOND*/0)
#define SLIST_REMOVE_HEAD(head, field) do { \
(head)->slh_first = (head)->slh_first->field.sle_next; \
} while (/*CONSTCOND*/0)
#define SLIST_REMOVE(head, elm, type, field) do { \
if ((head)->slh_first == (elm)) { \
SLIST_REMOVE_HEAD((head), field); \
} \
else { \
struct type *curelm = (head)->slh_first; \
while(curelm->field.sle_next != (elm)) \
curelm = curelm->field.sle_next; \
curelm->field.sle_next = \
curelm->field.sle_next->field.sle_next; \
} \
} while (/*CONSTCOND*/0)
#define SLIST_FOREACH(var, head, field) \
for((var) = (head)->slh_first; (var); (var) = (var)->field.sle_next)
/*
* Singly-linked List access methods.
*/
#define SLIST_EMPTY(head) ((head)->slh_first == NULL)
#define SLIST_FIRST(head) ((head)->slh_first)
#define SLIST_NEXT(elm, field) ((elm)->field.sle_next)
/*
* Singly-linked Tail queue declarations.
*/
#define STAILQ_HEAD(name, type) \
struct name { \
struct type *stqh_first; /* first element */ \
struct type **stqh_last; /* addr of last next element */ \
}
#define STAILQ_HEAD_INITIALIZER(head) \
{ NULL, &(head).stqh_first }
#define STAILQ_ENTRY(type) \
struct { \
struct type *stqe_next; /* next element */ \
}
/*
* Singly-linked Tail queue functions.
*/
#define STAILQ_INIT(head) do { \
(head)->stqh_first = NULL; \
(head)->stqh_last = &(head)->stqh_first; \
} while (/*CONSTCOND*/0)
#define STAILQ_INSERT_HEAD(head, elm, field) do { \
if (((elm)->field.stqe_next = (head)->stqh_first) == NULL) \
(head)->stqh_last = &(elm)->field.stqe_next; \
(head)->stqh_first = (elm); \
} while (/*CONSTCOND*/0)
#define STAILQ_INSERT_TAIL(head, elm, field) do { \
(elm)->field.stqe_next = NULL; \
*(head)->stqh_last = (elm); \
(head)->stqh_last = &(elm)->field.stqe_next; \
} while (/*CONSTCOND*/0)
#define STAILQ_INSERT_AFTER(head, listelm, elm, field) do { \
if (((elm)->field.stqe_next = (listelm)->field.stqe_next) == NULL)\
(head)->stqh_last = &(elm)->field.stqe_next; \
(listelm)->field.stqe_next = (elm); \
} while (/*CONSTCOND*/0)
#define STAILQ_REMOVE_HEAD(head, field) do { \
if (((head)->stqh_first = (head)->stqh_first->field.stqe_next) == NULL) \
(head)->stqh_last = &(head)->stqh_first; \
} while (/*CONSTCOND*/0)
#define STAILQ_REMOVE(head, elm, type, field) do { \
if ((head)->stqh_first == (elm)) { \
STAILQ_REMOVE_HEAD((head), field); \
} else { \
struct type *curelm = (head)->stqh_first; \
while (curelm->field.stqe_next != (elm)) \
curelm = curelm->field.stqe_next; \
if ((curelm->field.stqe_next = \
curelm->field.stqe_next->field.stqe_next) == NULL) \
(head)->stqh_last = &(curelm)->field.stqe_next; \
} \
} while (/*CONSTCOND*/0)
#define STAILQ_FOREACH(var, head, field) \
for ((var) = ((head)->stqh_first); \
(var); \
(var) = ((var)->field.stqe_next))
#define STAILQ_CONCAT(head1, head2) do { \
if (!STAILQ_EMPTY((head2))) { \
*(head1)->stqh_last = (head2)->stqh_first; \
(head1)->stqh_last = (head2)->stqh_last; \
STAILQ_INIT((head2)); \
} \
} while (/*CONSTCOND*/0)
/*
* Singly-linked Tail queue access methods.
*/
#define STAILQ_EMPTY(head) ((head)->stqh_first == NULL)
#define STAILQ_FIRST(head) ((head)->stqh_first)
#define STAILQ_NEXT(elm, field) ((elm)->field.stqe_next)
/*
* Simple queue definitions.
*/
#define SIMPLEQ_HEAD(name, type) \
struct name { \
struct type *sqh_first; /* first element */ \
struct type **sqh_last; /* addr of last next element */ \
}
#define SIMPLEQ_HEAD_INITIALIZER(head) \
{ NULL, &(head).sqh_first }
#define SIMPLEQ_ENTRY(type) \
struct { \
struct type *sqe_next; /* next element */ \
}
/*
* Simple queue functions.
*/
#define SIMPLEQ_INIT(head) do { \
(head)->sqh_first = NULL; \
(head)->sqh_last = &(head)->sqh_first; \
} while (/*CONSTCOND*/0)
#define SIMPLEQ_INSERT_HEAD(head, elm, field) do { \
if (((elm)->field.sqe_next = (head)->sqh_first) == NULL) \
(head)->sqh_last = &(elm)->field.sqe_next; \
(head)->sqh_first = (elm); \
} while (/*CONSTCOND*/0)
#define SIMPLEQ_INSERT_TAIL(head, elm, field) do { \
(elm)->field.sqe_next = NULL; \
*(head)->sqh_last = (elm); \
(head)->sqh_last = &(elm)->field.sqe_next; \
} while (/*CONSTCOND*/0)
#define SIMPLEQ_INSERT_AFTER(head, listelm, elm, field) do { \
if (((elm)->field.sqe_next = (listelm)->field.sqe_next) == NULL)\
(head)->sqh_last = &(elm)->field.sqe_next; \
(listelm)->field.sqe_next = (elm); \
} while (/*CONSTCOND*/0)
#define SIMPLEQ_REMOVE_HEAD(head, field) do { \
if (((head)->sqh_first = (head)->sqh_first->field.sqe_next) == NULL) \
(head)->sqh_last = &(head)->sqh_first; \
} while (/*CONSTCOND*/0)
#define SIMPLEQ_REMOVE(head, elm, type, field) do { \
if ((head)->sqh_first == (elm)) { \
SIMPLEQ_REMOVE_HEAD((head), field); \
} else { \
struct type *curelm = (head)->sqh_first; \
while (curelm->field.sqe_next != (elm)) \
curelm = curelm->field.sqe_next; \
if ((curelm->field.sqe_next = \
curelm->field.sqe_next->field.sqe_next) == NULL) \
(head)->sqh_last = &(curelm)->field.sqe_next; \
} \
} while (/*CONSTCOND*/0)
#define SIMPLEQ_FOREACH(var, head, field) \
for ((var) = ((head)->sqh_first); \
(var); \
(var) = ((var)->field.sqe_next))
/*
* Simple queue access methods.
*/
#define SIMPLEQ_EMPTY(head) ((head)->sqh_first == NULL)
#define SIMPLEQ_FIRST(head) ((head)->sqh_first)
#define SIMPLEQ_NEXT(elm, field) ((elm)->field.sqe_next)
/*
* Tail queue definitions.
*/
#define _TAILQ_HEAD(name, type, qual) \
struct name { \
qual type *tqh_first; /* first element */ \
qual type *qual *tqh_last; /* addr of last next element */ \
}
#define TAILQ_HEAD(name, type) _TAILQ_HEAD(name, struct type,)
#define TAILQ_HEAD_INITIALIZER(head) \
{ NULL, &(head).tqh_first }
#define _TAILQ_ENTRY(type, qual) \
struct { \
qual type *tqe_next; /* next element */ \
qual type *qual *tqe_prev; /* address of previous next element */\
}
#define TAILQ_ENTRY(type) _TAILQ_ENTRY(struct type,)
/*
* Tail queue functions.
*/
#define TAILQ_INIT(head) do { \
(head)->tqh_first = NULL; \
(head)->tqh_last = &(head)->tqh_first; \
} while (/*CONSTCOND*/0)
#define TAILQ_INSERT_HEAD(head, elm, field) do { \
if (((elm)->field.tqe_next = (head)->tqh_first) != NULL) \
(head)->tqh_first->field.tqe_prev = \
&(elm)->field.tqe_next; \
else \
(head)->tqh_last = &(elm)->field.tqe_next; \
(head)->tqh_first = (elm); \
(elm)->field.tqe_prev = &(head)->tqh_first; \
} while (/*CONSTCOND*/0)
#define TAILQ_INSERT_TAIL(head, elm, field) do { \
(elm)->field.tqe_next = NULL; \
(elm)->field.tqe_prev = (head)->tqh_last; \
*(head)->tqh_last = (elm); \
(head)->tqh_last = &(elm)->field.tqe_next; \
} while (/*CONSTCOND*/0)
#define TAILQ_INSERT_AFTER(head, listelm, elm, field) do { \
if (((elm)->field.tqe_next = (listelm)->field.tqe_next) != NULL)\
(elm)->field.tqe_next->field.tqe_prev = \
&(elm)->field.tqe_next; \
else \
(head)->tqh_last = &(elm)->field.tqe_next; \
(listelm)->field.tqe_next = (elm); \
(elm)->field.tqe_prev = &(listelm)->field.tqe_next; \
} while (/*CONSTCOND*/0)
#define TAILQ_INSERT_BEFORE(listelm, elm, field) do { \
(elm)->field.tqe_prev = (listelm)->field.tqe_prev; \
(elm)->field.tqe_next = (listelm); \
*(listelm)->field.tqe_prev = (elm); \
(listelm)->field.tqe_prev = &(elm)->field.tqe_next; \
} while (/*CONSTCOND*/0)
#define TAILQ_REMOVE(head, elm, field) do { \
if (((elm)->field.tqe_next) != NULL) \
(elm)->field.tqe_next->field.tqe_prev = \
(elm)->field.tqe_prev; \
else \
(head)->tqh_last = (elm)->field.tqe_prev; \
*(elm)->field.tqe_prev = (elm)->field.tqe_next; \
} while (/*CONSTCOND*/0)
#define TAILQ_FOREACH(var, head, field) \
for ((var) = ((head)->tqh_first); \
(var); \
(var) = ((var)->field.tqe_next))
#define TAILQ_FOREACH_REVERSE(var, head, headname, field) \
for ((var) = (*(((struct headname *)((head)->tqh_last))->tqh_last)); \
(var); \
(var) = (*(((struct headname *)((var)->field.tqe_prev))->tqh_last)))
#define TAILQ_CONCAT(head1, head2, field) do { \
if (!TAILQ_EMPTY(head2)) { \
*(head1)->tqh_last = (head2)->tqh_first; \
(head2)->tqh_first->field.tqe_prev = (head1)->tqh_last; \
(head1)->tqh_last = (head2)->tqh_last; \
TAILQ_INIT((head2)); \
} \
} while (/*CONSTCOND*/0)
/*
* Tail queue access methods.
*/
#define TAILQ_EMPTY(head) ((head)->tqh_first == NULL)
#define TAILQ_FIRST(head) ((head)->tqh_first)
#define TAILQ_NEXT(elm, field) ((elm)->field.tqe_next)
#define TAILQ_LAST(head, headname) \
(*(((struct headname *)((head)->tqh_last))->tqh_last))
#define TAILQ_PREV(elm, headname, field) \
(*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))
/*
* Circular queue definitions.
*/
#define CIRCLEQ_HEAD(name, type) \
struct name { \
struct type *cqh_first; /* first element */ \
struct type *cqh_last; /* last element */ \
}
#define CIRCLEQ_HEAD_INITIALIZER(head) \
{ (void *)&head, (void *)&head }
#define CIRCLEQ_ENTRY(type) \
struct { \
struct type *cqe_next; /* next element */ \
struct type *cqe_prev; /* previous element */ \
}
/*
* Circular queue functions.
*/
#define CIRCLEQ_INIT(head) do { \
(head)->cqh_first = (void *)(head); \
(head)->cqh_last = (void *)(head); \
} while (/*CONSTCOND*/0)
#define CIRCLEQ_INSERT_AFTER(head, listelm, elm, field) do { \
(elm)->field.cqe_next = (listelm)->field.cqe_next; \
(elm)->field.cqe_prev = (listelm); \
if ((listelm)->field.cqe_next == (void *)(head)) \
(head)->cqh_last = (elm); \
else \
(listelm)->field.cqe_next->field.cqe_prev = (elm); \
(listelm)->field.cqe_next = (elm); \
} while (/*CONSTCOND*/0)
#define CIRCLEQ_INSERT_BEFORE(head, listelm, elm, field) do { \
(elm)->field.cqe_next = (listelm); \
(elm)->field.cqe_prev = (listelm)->field.cqe_prev; \
if ((listelm)->field.cqe_prev == (void *)(head)) \
(head)->cqh_first = (elm); \
else \
(listelm)->field.cqe_prev->field.cqe_next = (elm); \
(listelm)->field.cqe_prev = (elm); \
} while (/*CONSTCOND*/0)
#define CIRCLEQ_INSERT_HEAD(head, elm, field) do { \
(elm)->field.cqe_next = (head)->cqh_first; \
(elm)->field.cqe_prev = (void *)(head); \
if ((head)->cqh_last == (void *)(head)) \
(head)->cqh_last = (elm); \
else \
(head)->cqh_first->field.cqe_prev = (elm); \
(head)->cqh_first = (elm); \
} while (/*CONSTCOND*/0)
#define CIRCLEQ_INSERT_TAIL(head, elm, field) do { \
(elm)->field.cqe_next = (void *)(head); \
(elm)->field.cqe_prev = (head)->cqh_last; \
if ((head)->cqh_first == (void *)(head)) \
(head)->cqh_first = (elm); \
else \
(head)->cqh_last->field.cqe_next = (elm); \
(head)->cqh_last = (elm); \
} while (/*CONSTCOND*/0)
#define CIRCLEQ_REMOVE(head, elm, field) do { \
if ((elm)->field.cqe_next == (void *)(head)) \
(head)->cqh_last = (elm)->field.cqe_prev; \
else \
(elm)->field.cqe_next->field.cqe_prev = \
(elm)->field.cqe_prev; \
if ((elm)->field.cqe_prev == (void *)(head)) \
(head)->cqh_first = (elm)->field.cqe_next; \
else \
(elm)->field.cqe_prev->field.cqe_next = \
(elm)->field.cqe_next; \
} while (/*CONSTCOND*/0)
#define CIRCLEQ_FOREACH(var, head, field) \
for ((var) = ((head)->cqh_first); \
(var) != (const void *)(head); \
(var) = ((var)->field.cqe_next))
#define CIRCLEQ_FOREACH_REVERSE(var, head, field) \
for ((var) = ((head)->cqh_last); \
(var) != (const void *)(head); \
(var) = ((var)->field.cqe_prev))
/*
* Circular queue access methods.
*/
#define CIRCLEQ_EMPTY(head) ((head)->cqh_first == (void *)(head))
#define CIRCLEQ_FIRST(head) ((head)->cqh_first)
#define CIRCLEQ_LAST(head) ((head)->cqh_last)
#define CIRCLEQ_NEXT(elm, field) ((elm)->field.cqe_next)
#define CIRCLEQ_PREV(elm, field) ((elm)->field.cqe_prev)
#define CIRCLEQ_LOOP_NEXT(head, elm, field) \
(((elm)->field.cqe_next == (void *)(head)) \
? ((head)->cqh_first) \
: (elm->field.cqe_next))
#define CIRCLEQ_LOOP_PREV(head, elm, field) \
(((elm)->field.cqe_prev == (void *)(head)) \
? ((head)->cqh_last) \
: (elm->field.cqe_prev))
#endif /* sys/queue.h */

View file

@ -22,22 +22,18 @@ struct lws_context *context;
struct server *server;
struct endpoints endpoints = {"/ws", "/", "/token", ""};
extern int callback_http(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len);
extern int callback_tty(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len);
extern int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len);
extern int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len);
// websocket protocols
static const struct lws_protocols protocols[] = {
{"http-only", callback_http, sizeof(struct pss_http), 0},
static const struct lws_protocols protocols[] = {{"http-only", callback_http, sizeof(struct pss_http), 0},
{"tty", callback_tty, sizeof(struct pss_tty), 0},
{NULL, NULL, 0, 0}};
#ifndef LWS_WITHOUT_EXTENSIONS
// websocket extensions
static const struct lws_extension extensions[] = {
{"permessage-deflate", lws_extension_callback_pm_deflate,
"permessage-deflate"},
{"permessage-deflate", lws_extension_callback_pm_deflate, "permessage-deflate"},
{"deflate-frame", lws_extension_callback_pm_deflate, "deflate_frame"},
{NULL, NULL, NULL}};
#endif
@ -48,20 +44,22 @@ static lws_retry_bo_t retry = {
.retry_ms_table = backoff_ms,
.retry_ms_table_count = LWS_ARRAY_SIZE(backoff_ms),
.conceal_count = LWS_ARRAY_SIZE(backoff_ms),
.secs_since_valid_ping = 300,
.secs_since_valid_hangup = 300 + 7,
.secs_since_valid_ping = 5,
.secs_since_valid_hangup = 10,
.jitter_percent = 0,
};
#endif
// command line options
static const struct option options[] = {
{"port", required_argument, NULL, 'p'},
static const struct option options[] = {{"port", required_argument, NULL, 'p'},
{"interface", required_argument, NULL, 'i'},
{"socket-owner", required_argument, NULL, 'U'},
{"credential", required_argument, NULL, 'c'},
{"auth-header", required_argument, NULL, 'H'},
{"uid", required_argument, NULL, 'u'},
{"gid", required_argument, NULL, 'g'},
{"signal", required_argument, NULL, 's'},
{"cwd", required_argument, NULL, 'w'},
{"index", required_argument, NULL, 'I'},
{"base-path", required_argument, NULL, 'b'},
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000
@ -73,24 +71,19 @@ static const struct option options[] = {
{"ssl-key", required_argument, NULL, 'K'},
{"ssl-ca", required_argument, NULL, 'A'},
{"url-arg", no_argument, NULL, 'a'},
{"readonly", no_argument, NULL, 'R'},
{"writable", no_argument, NULL, 'W'},
{"terminal-type", required_argument, NULL, 'T'},
{"client-option", required_argument, NULL, 't'},
{"check-origin", no_argument, NULL, 'O'},
{"max-clients", required_argument, NULL, 'm'},
{"once", no_argument, NULL, 'o'},
{"exit-no-conn", no_argument, NULL, 'q'},
{"browser", no_argument, NULL, 'B'},
{"debug", required_argument, NULL, 'd'},
{"version", no_argument, NULL, 'v'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, 0, 0}};
#if LWS_LIBRARY_VERSION_NUMBER < 4000000
static const char *opt_string = "p:i:c:u:g:s:I:b:6aSC:K:A:Rt:T:Om:oBd:vh";
#endif
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000
static const char *opt_string = "p:i:c:u:g:s:I:b:P:6aSC:K:A:Rt:T:Om:oBd:vh";
#endif
static const char *opt_string = "p:i:U:c:H:u:g:s:w:I:b:P:6aSC:K:A:Wt:T:Om:oqBd:vh";
static void print_help() {
// clang-format off
@ -102,22 +95,26 @@ static void print_help() {
"OPTIONS:\n"
" -p, --port Port to listen (default: 7681, use `0` for random port)\n"
" -i, --interface Network interface to bind (eg: eth0), or UNIX domain socket path (eg: /var/run/ttyd.sock)\n"
" -c, --credential Credential for Basic Authentication (format: username:password)\n"
" -U, --socket-owner User owner of the UNIX domain socket file, when enabled (eg: user:group)\n"
" -c, --credential Credential for basic authentication (format: username:password)\n"
" -H, --auth-header HTTP Header name for auth proxy, this will configure ttyd to let a HTTP reverse proxy handle authentication\n"
" -u, --uid User id to run with\n"
" -g, --gid Group id to run with\n"
" -s, --signal Signal to send to the command when exit it (default: 1, SIGHUP)\n"
" -w, --cwd Working directory to be set for the child program\n"
" -a, --url-arg Allow client to send command line arguments in URL (eg: http://localhost:7681?arg=foo&arg=bar)\n"
" -R, --readonly Do not allow clients to write to the TTY\n"
" -W, --writable Allow clients to write to the TTY (readonly by default)\n"
" -t, --client-option Send option to client (format: key=value), repeat to add more options\n"
" -T, --terminal-type Terminal type to report, default: xterm-256color\n"
" -O, --check-origin Do not allow websocket connection from different origin\n"
" -m, --max-clients Maximum clients to support (default: 0, no limit)\n"
" -o, --once Accept only one client and exit on disconnection\n"
" -q, --exit-no-conn Exit on all clients disconnection\n"
" -B, --browser Open terminal with the default system browser\n"
" -I, --index Custom index.html path\n"
" -b, --base-path Expected base path for requests coming from a reverse proxy (eg: /mounted/here)\n"
" -b, --base-path Expected base path for requests coming from a reverse proxy (eg: /mounted/here, max length: 128)\n"
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000
" -P, --ping-interval Websocket ping interval(sec) (default: 300)\n"
" -P, --ping-interval Websocket ping interval(sec) (default: 5)\n"
#endif
#ifdef LWS_WITH_IPV6
" -6, --ipv6 Enable IPv6 support\n"
@ -137,6 +134,30 @@ static void print_help() {
// clang-format on
}
static void print_config() {
lwsl_notice("tty configuration:\n");
if (server->credential != NULL) lwsl_notice(" credential: %s\n", server->credential);
lwsl_notice(" start command: %s\n", server->command);
lwsl_notice(" close signal: %s (%d)\n", server->sig_name, server->sig_code);
lwsl_notice(" terminal type: %s\n", server->terminal_type);
if (endpoints.parent[0]) {
lwsl_notice("endpoints:\n");
lwsl_notice(" base-path: %s\n", endpoints.parent);
lwsl_notice(" index : %s\n", endpoints.index);
lwsl_notice(" token : %s\n", endpoints.token);
lwsl_notice(" websocket: %s\n", endpoints.ws);
}
if (server->auth_header != NULL) lwsl_notice(" auth header: %s\n", server->auth_header);
if (server->check_origin) lwsl_notice(" check origin: true\n");
if (server->url_arg) lwsl_notice(" allow url arg: true\n");
if (server->max_clients > 0) lwsl_notice(" max clients: %d\n", server->max_clients);
if (server->once) lwsl_notice(" once: true\n");
if (server->exit_no_conn) lwsl_notice(" exit_no_conn: true\n");
if (server->index != NULL) lwsl_notice(" custom index.html: %s\n", server->index);
if (server->cwd != NULL) lwsl_notice(" working directory: %s\n", server->cwd);
if (!server->writable) lwsl_notice("The --writable option is not set, will start in readonly mode");
}
static struct server *server_new(int argc, char **argv, int start) {
struct server *ts;
size_t cmd_len = 0;
@ -144,7 +165,6 @@ static struct server *server_new(int argc, char **argv, int start) {
ts = xmalloc(sizeof(struct server));
memset(ts, 0, sizeof(struct server));
LIST_INIT(&ts->procs);
ts->client_count = 0;
ts->sig_code = SIGHUP;
sprintf(ts->terminal_type, "%s", "xterm-256color");
@ -167,7 +187,8 @@ static struct server *server_new(int argc, char **argv, int start) {
ts->command = xmalloc(cmd_len + 1);
char *ptr = ts->command;
for (int i = 0; i < cmd_argc; i++) {
ptr = stpcpy(ptr, ts->argv[i]);
size_t len = strlen(ts->argv[i]);
ptr = memcpy(ptr, ts->argv[i], len + 1) + len;
if (i != cmd_argc - 1) {
*ptr++ = ' ';
}
@ -176,8 +197,6 @@ static struct server *server_new(int argc, char **argv, int start) {
ts->loop = xmalloc(sizeof *ts->loop);
uv_loop_init(ts->loop);
uv_signal_init(ts->loop, &ts->watcher);
ts->watcher.data = &ts->procs;
return ts;
}
@ -185,22 +204,25 @@ static struct server *server_new(int argc, char **argv, int start) {
static void server_free(struct server *ts) {
if (ts == NULL) return;
if (ts->credential != NULL) free(ts->credential);
if (ts->auth_header != NULL) free(ts->auth_header);
if (ts->index != NULL) free(ts->index);
if (ts->cwd != NULL) free(ts->cwd);
free(ts->command);
free(ts->prefs_json);
int i = 0;
do {
free(ts->argv[i++]);
} while (ts->argv[i] != NULL);
char **p = ts->argv;
for (; *p; p++) free(*p);
free(ts->argv);
if (strlen(ts->socket_path) > 0) {
struct stat st;
if (!stat(ts->socket_path, &st)) {
unlink(ts->socket_path);
}
}
uv_signal_stop(&ts->watcher);
uv_loop_close(ts->loop);
free(ts->loop);
free(ts);
}
@ -212,8 +234,7 @@ static void signal_cb(uv_signal_t *watcher, int signum) {
case SIGINT:
case SIGTERM:
get_sig_name(watcher->signum, sig_name, sizeof(sig_name));
lwsl_notice("received signal: %s (%d), exiting...\n", sig_name,
watcher->signum);
lwsl_notice("received signal: %s (%d), exiting...\n", sig_name, watcher->signum);
break;
default:
signal(SIGABRT, SIG_DFL);
@ -222,14 +243,22 @@ static void signal_cb(uv_signal_t *watcher, int signum) {
if (force_exit) exit(EXIT_FAILURE);
force_exit = true;
lws_cancel_service(context);
#if LWS_LIBRARY_VERSION_MAJOR >= 3
uv_stop(server->loop);
lwsl_notice("send ^C to force exit.\n");
#else
lws_libuv_stop(context);
exit(EXIT_SUCCESS);
#endif
}
static int parse_int(char *name, char *str) {
char *endptr;
errno = 0;
long val = strtol(str, &endptr, 0);
if (errno != 0 || endptr == str) {
fprintf(stderr, "ttyd: invalid value for %s: %s\n", name, str);
exit(EXIT_FAILURE);
}
return (int)val;
}
static int calc_command_start(int argc, char **argv) {
@ -274,6 +303,12 @@ int main(int argc, char **argv) {
print_help();
return 0;
}
#ifdef _WIN32
if (!conpty_init()) {
fprintf(stderr, "ERROR: ConPTY init failed! Make sure you are on Windows 10 1809 or later.");
return 1;
}
#endif
int start = calc_command_start(argc, argv);
server = server_new(argc, argv, start);
@ -286,15 +321,15 @@ int main(int argc, char **argv) {
info.gid = -1;
info.uid = -1;
info.max_http_header_pool = 16;
info.options = LWS_SERVER_OPTION_LIBUV | LWS_SERVER_OPTION_VALIDATE_UTF8 |
LWS_SERVER_OPTION_DISABLE_IPV6;
info.options = LWS_SERVER_OPTION_LIBUV | LWS_SERVER_OPTION_VALIDATE_UTF8 | LWS_SERVER_OPTION_DISABLE_IPV6;
#ifndef LWS_WITHOUT_EXTENSIONS
info.extensions = extensions;
#endif
info.max_http_header_data = 20480;
info.max_http_header_data = 65535;
int debug_level = LLL_ERR | LLL_WARN | LLL_NOTICE;
char iface[128] = "";
char socket_owner[128] = "";
bool browser = false;
bool ssl = false;
char cert_path[1024] = "";
@ -303,6 +338,10 @@ int main(int argc, char **argv) {
struct json_object *client_prefs = json_object_new_object();
#ifdef _WIN32
json_object_object_add(client_prefs, "isWindows", json_object_new_boolean(true));
#endif
// parse command line options
int c;
while ((c = getopt_long(start, argv, opt_string, options, NULL)) != -1) {
@ -314,28 +353,31 @@ int main(int argc, char **argv) {
printf("ttyd version %s\n", TTYD_VERSION);
return 0;
case 'd':
debug_level = atoi(optarg);
debug_level = parse_int("debug", optarg);
break;
case 'a':
server->url_arg = true;
break;
case 'R':
server->readonly = true;
case 'W':
server->writable = true;
break;
case 'O':
server->check_origin = true;
break;
case 'm':
server->max_clients = atoi(optarg);
server->max_clients = parse_int("max-clients", optarg);
break;
case 'o':
server->once = true;
break;
case 'q':
server->exit_no_conn = true;
break;
case 'B':
browser = true;
break;
case 'p':
info.port = atoi(optarg);
info.port = parse_int("port", optarg);
if (info.port < 0) {
fprintf(stderr, "ttyd: invalid port: %s\n", optarg);
return -1;
@ -345,20 +387,27 @@ int main(int argc, char **argv) {
strncpy(iface, optarg, sizeof(iface) - 1);
iface[sizeof(iface) - 1] = '\0';
break;
case 'U':
strncpy(socket_owner, optarg, sizeof(socket_owner) - 1);
socket_owner[sizeof(socket_owner) - 1] = '\0';
break;
case 'c':
if (strchr(optarg, ':') == NULL) {
fprintf(stderr,
"ttyd: invalid credential, format: username:password\n");
fprintf(stderr, "ttyd: invalid credential, format: username:password\n");
return -1;
}
server->credential =
base64_encode((const unsigned char *)optarg, strlen(optarg));
char b64_text[256];
lws_b64_encode_string(optarg, strlen(optarg), b64_text, sizeof(b64_text));
server->credential = strdup(b64_text);
break;
case 'H':
server->auth_header = strdup(optarg);
break;
case 'u':
info.uid = atoi(optarg);
info.uid = parse_int("uid", optarg);
break;
case 'g':
info.gid = atoi(optarg);
info.gid = parse_int("gid", optarg);
break;
case 's': {
int sig = get_sig(optarg);
@ -370,6 +419,9 @@ int main(int argc, char **argv) {
return -1;
}
} break;
case 'w':
server->cwd = strdup(optarg);
break;
case 'I':
if (!strncmp(optarg, "~/", 2)) {
const char *home = getenv("HOME");
@ -380,13 +432,11 @@ int main(int argc, char **argv) {
}
struct stat st;
if (stat(server->index, &st) == -1) {
fprintf(stderr, "Can not stat index.html: %s, error: %s\n",
server->index, strerror(errno));
fprintf(stderr, "Can not stat index.html: %s, error: %s\n", server->index, strerror(errno));
return -1;
}
if (S_ISDIR(st.st_mode)) {
fprintf(stderr, "Invalid index.html path: %s, is it a dir?\n",
server->index);
fprintf(stderr, "Invalid index.html path: %s, is it a dir?\n", server->index);
return -1;
}
break;
@ -403,15 +453,15 @@ int main(int argc, char **argv) {
#undef sc
} break;
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000
case 'P':
if (atoi(optarg) <= 0) {
case 'P': {
int interval = parse_int("ping-interval", optarg);
if (interval < 0) {
fprintf(stderr, "ttyd: invalid ping interval: %s\n", optarg);
return -1;
}
retry.secs_since_valid_ping = atoi(optarg);
retry.secs_since_valid_hangup = atoi(optarg) + 7;
info.retry_and_idle_policy = &retry;
break;
retry.secs_since_valid_ping = interval;
retry.secs_since_valid_hangup = interval + 7;
} break;
#endif
case '6':
info.options &= ~(LWS_SERVER_OPTION_DISABLE_IPV6);
@ -434,8 +484,7 @@ int main(int argc, char **argv) {
break;
#endif
case 'T':
strncpy(server->terminal_type, optarg,
sizeof(server->terminal_type) - 1);
strncpy(server->terminal_type, optarg, sizeof(server->terminal_type) - 1);
server->terminal_type[sizeof(server->terminal_type) - 1] = '\0';
break;
case '?':
@ -443,26 +492,19 @@ int main(int argc, char **argv) {
case 't':
optind--;
for (; optind < start && *argv[optind] != '-'; optind++) {
char *option = strdup(optarg);
char *option = optarg;
char *key = strsep(&option, "=");
if (key == NULL) {
fprintf(stderr,
"ttyd: invalid client option: %s, format: key=value\n",
optarg);
fprintf(stderr, "ttyd: invalid client option: %s, format: key=value\n", optarg);
return -1;
}
char *value = strsep(&option, "=");
if (value == NULL) {
fprintf(stderr,
"ttyd: invalid client option: %s, format: key=value\n",
optarg);
fprintf(stderr, "ttyd: invalid client option: %s, format: key=value\n", optarg);
return -1;
}
free(option);
struct json_object *obj = json_tokener_parse(value);
json_object_object_add(
client_prefs, key,
obj != NULL ? obj : json_object_new_string(value));
json_object_object_add(client_prefs, key, obj != NULL ? obj : json_object_new_string(value));
}
break;
default:
@ -480,14 +522,14 @@ int main(int argc, char **argv) {
lws_set_log_level(debug_level, NULL);
#if LWS_LIBRARY_VERSION_MAJOR >= 2
char server_hdr[128] = "";
sprintf(server_hdr, "ttyd/%s (libwebsockets/%s)", TTYD_VERSION,
LWS_LIBRARY_VERSION);
sprintf(server_hdr, "ttyd/%s (libwebsockets/%s)", TTYD_VERSION, LWS_LIBRARY_VERSION);
info.server_string = server_hdr;
#endif
#if LWS_LIBRARY_VERSION_NUMBER >= 2001000 && LWS_LIBRARY_VERSION_NUMBER < 4000000
#if LWS_LIBRARY_VERSION_NUMBER < 4000000
info.ws_ping_pong_interval = 5;
#else
info.retry_and_idle_policy = &retry;
#endif
if (strlen(iface) > 0) {
@ -497,9 +539,11 @@ int main(int argc, char **argv) {
info.options |= LWS_SERVER_OPTION_UNIX_SOCK;
info.port = 0; // warmcat/libwebsockets#1985
strncpy(server->socket_path, info.iface, sizeof(server->socket_path) - 1);
if (strlen(socket_owner) > 0) {
info.unix_socket_perms = socket_owner;
}
#else
fprintf(stderr,
"libwebsockets is not compiled with UNIX domain socket support");
fprintf(stderr, "libwebsockets is not compiled with UNIX domain socket support");
return -1;
#endif
}
@ -509,46 +553,32 @@ int main(int argc, char **argv) {
if (ssl) {
info.ssl_cert_filepath = cert_path;
info.ssl_private_key_filepath = key_path;
if (strlen(ca_path) > 0)
#ifndef LWS_WITH_MBEDTLS
info.ssl_options_set = SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1;
#endif
if (strlen(ca_path) > 0) {
info.ssl_ca_filepath = ca_path;
info.options |= LWS_SERVER_OPTION_REQUIRE_VALID_OPENSSL_CLIENT_CERT;
#if LWS_LIBRARY_VERSION_MAJOR >= 2
info.options |= LWS_SERVER_OPTION_REDIRECT_HTTP_TO_HTTPS;
#endif
}
info.options |= LWS_SERVER_OPTION_ALLOW_NON_SSL_ON_SSL_PORT | LWS_SERVER_OPTION_REDIRECT_HTTP_TO_HTTPS;
}
#endif
lwsl_notice("ttyd %s (libwebsockets %s)\n", TTYD_VERSION,
LWS_LIBRARY_VERSION);
lwsl_notice("tty configuration:\n");
if (server->credential != NULL)
lwsl_notice(" credential: %s\n", server->credential);
lwsl_notice(" start command: %s\n", server->command);
lwsl_notice(" close signal: %s (%d)\n", server->sig_name, server->sig_code);
lwsl_notice(" terminal type: %s\n", server->terminal_type);
if (endpoints.parent[0]) {
lwsl_notice("endpoints:\n");
lwsl_notice(" base-path: %s\n", endpoints.parent);
lwsl_notice(" index : %s\n", endpoints.index);
lwsl_notice(" token : %s\n", endpoints.token);
lwsl_notice(" websocket: %s\n", endpoints.ws);
}
if (server->check_origin) lwsl_notice(" check origin: true\n");
if (server->url_arg) lwsl_notice(" allow url arg: true\n");
if (server->readonly) lwsl_notice(" readonly: true\n");
if (server->max_clients > 0)
lwsl_notice(" max clients: %d\n", server->max_clients);
if (server->once) lwsl_notice(" once: true\n");
if (server->index != NULL) {
lwsl_notice(" custom index.html: %s\n", server->index);
lwsl_notice("ttyd %s (libwebsockets %s)\n", TTYD_VERSION, LWS_LIBRARY_VERSION);
print_config();
// lws custom header requires lower case name, and terminating :
if (server->auth_header != NULL) {
size_t auth_header_len = strlen(server->auth_header);
server->auth_header = xrealloc(server->auth_header, auth_header_len + 2);
strcat(server->auth_header + auth_header_len, ":");
lowercase(server->auth_header);
}
#if LWS_LIBRARY_VERSION_MAJOR >= 3
void *foreign_loops[1];
foreign_loops[0] = server->loop;
info.foreign_loops = foreign_loops;
info.options |= LWS_SERVER_OPTION_EXPLICIT_VHOSTS;
#endif
context = lws_create_context(&info);
if (context == NULL) {
@ -556,16 +586,12 @@ int main(int argc, char **argv) {
return 1;
}
#if LWS_LIBRARY_VERSION_MAJOR >= 3
struct lws_vhost *vhost = lws_create_vhost(context, &info);
if (vhost == NULL) {
lwsl_err("libwebsockets vhost creation failed\n");
return 1;
}
int port = lws_get_vhost_listen_port(vhost);
#else
int port = info.port;
#endif
lwsl_notice(" Listening on port: %d\n", port);
if (browser) {
@ -574,29 +600,20 @@ int main(int argc, char **argv) {
open_uri(url);
}
#if LWS_LIBRARY_VERSION_MAJOR >= 3
#define sig_count 2
int sig_nums[] = {SIGINT, SIGTERM};
int ns = sizeof(sig_nums) / sizeof(sig_nums[0]);
uv_signal_t signals[ns];
for (int i = 0; i < ns; i++) {
uv_signal_t signals[sig_count];
for (int i = 0; i < sig_count; i++) {
uv_signal_init(server->loop, &signals[i]);
uv_signal_start(&signals[i], signal_cb, sig_nums[i]);
}
lws_service(context, 0);
for (int i = 0; i < ns; i++) {
for (int i = 0; i < sig_count; i++) {
uv_signal_stop(&signals[i]);
}
#else
#if LWS_LIBRARY_VERSION_MAJOR < 2
lws_uv_initloop(context, server->loop, signal_cb, 0);
#else
lws_uv_sigint_cfg(context, 1, signal_cb);
lws_uv_initloop(context, server->loop, 0);
#endif
lws_libuv_run(context, 0);
#endif
#undef sig_count
lws_context_destroy(context);

View file

@ -1,7 +1,8 @@
#include <libwebsockets.h>
#include <stdbool.h>
#include <uv.h>
#include "queue.h"
#include "pty.h"
// client message
#define INPUT '0'
@ -15,8 +16,6 @@
#define SET_WINDOW_TITLE '1'
#define SET_PREFERENCES '2'
#define MAX_READ_RETRY 2
// url paths
struct endpoints {
char *ws;
@ -30,8 +29,6 @@ extern struct lws_context *context;
extern struct server *server;
extern struct endpoints endpoints;
typedef enum { STATE_INIT, STATE_PAUSE, STATE_KILL, STATE_EXIT } proc_state;
struct pss_http {
char path[128];
char *buffer;
@ -39,58 +36,51 @@ struct pss_http {
size_t len;
};
struct pty_proc {
char **args;
int argc;
pid_t pid;
int status;
proc_state state;
int pty;
char *pty_buffer;
ssize_t pty_len;
int err_count;
uv_pipe_t pipe;
LIST_ENTRY(pty_proc) entry;
};
struct pss_tty {
bool initialized;
int initial_cmd_index;
bool authenticated;
char user[30];
char address[50];
char path[20];
char path[128];
char **args;
int argc;
struct lws *wsi;
char *buffer;
size_t len;
struct pty_proc *proc;
pty_process *process;
pty_buf_t *pty_buf;
int lws_close_status;
};
typedef struct {
struct pss_tty *pss;
bool ws_closed;
} pty_ctx_t;
struct server {
int client_count; // client count
char *prefs_json; // client preferences
char *credential; // encoded basic auth credential
char *auth_header; // header name used for auth proxy
char *index; // custom index.html
char *command; // full command line
char **argv; // command with arguments
int argc; // command + arguments count
char *cwd; // working directory
int sig_code; // close signal
char sig_name[20]; // human readable signal string
bool url_arg; // allow client to send cli arguments in URL
bool readonly; // whether not allow clients to write to the TTY
bool writable; // whether clients to write to the TTY
bool check_origin; // whether allow websocket connection from different origin
int max_clients; // maximum clients to support
bool once; // whether accept only one client and exit on disconnection
bool exit_no_conn; // whether exit on all clients disconnection
char socket_path[255]; // UNIX domain socket path
char terminal_type[30]; // terminal type to report
uv_loop_t *loop; // the libuv event loop
uv_signal_t watcher; // SIGCHLD watcher
LIST_HEAD(proc, pty_proc) procs; // started process list
};

View file

@ -1,53 +0,0 @@
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <unistd.h>
#if defined(__OpenBSD__) || defined(__APPLE__)
#include <util.h>
#elif defined(__FreeBSD__)
#include <libutil.h>
#else
#include <pty.h>
#endif
#include "utils.h"
pid_t pty_fork(int *pty, const char *file, char *const argv[], const char *term) {
pid_t pid = forkpty(pty, NULL, NULL, NULL);
if (pid < 0) {
return pid;
} else if (pid == 0) {
setenv("TERM", term, true);
int ret = execvp(file, argv);
if (ret < 0) {
perror("execvp failed\n");
_exit(-errno);
}
}
// set the file descriptor non blocking
int flags = fcntl(*pty, F_GETFL);
if (flags != -1) {
fcntl(*pty, F_SETFD, flags | O_NONBLOCK);
}
// set the file descriptor close-on-exec
fd_set_cloexec(*pty);
return pid;
}
int pty_resize(int pty, int cols, int rows) {
struct winsize size;
size.ws_col = (unsigned short)cols;
size.ws_row = (unsigned short)rows;
size.ws_xpixel = 0;
size.ws_ypixel = 0;
return ioctl(pty, TIOCSWINSZ, &size);
}

View file

@ -1,8 +0,0 @@
#ifndef TTYD_TERMINAL_H
#define TTYD_TERMINAL_H
int pty_fork(int *pty, const char *file, char *const argv[], const char *term);
int pty_resize(int pty, int cols, int rows);
#endif // TTYD_TERMINAL_H

View file

@ -7,26 +7,20 @@
#include <string.h>
#if defined(__linux__) && !defined(__ANDROID__)
// https://github.com/karelzak/util-linux/blob/master/misc-utils/kill.c
const char *sys_signame[NSIG] = {
"zero", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "UNUSED", "FPE",
"KILL", "USR1", "SEGV", "USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD",
"CONT", "STOP", "TSTP", "TTIN", "TTOU", "URG", "XCPU", "XFSZ", "VTALRM",
"PROF", "WINCH", "IO", "PWR", "SYS", NULL};
"zero", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "UNUSED", "FPE", "KILL", "USR1",
"SEGV", "USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN",
"TTOU", "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR", "SYS", NULL};
#endif
#if defined(_WIN32) || defined(__CYGWIN__)
#include <shellapi.h>
#include <windows.h>
// https://github.com/mirror/newlib-cygwin/blob/master/winsup/cygwin/strsig.cc
#ifndef NSIG
#undef NSIG
#define NSIG 33
#endif
const char *sys_signame[NSIG] = {
"zero", "HUP", "INT", "QUIT", "ILL", "TRAP", "IOT", "EMT", "FPE",
"KILL", "BUS", "SEGV", "SYS", "PIPE", "ALRM", "TERM", "URG", "STOP",
"TSTP", "CONT", "CHLD", "TTIN", "TTOU", "IO", "XCPU", "XFSZ", "VTALRM",
"PROF", "WINCH", "PWR", "USR1", "USR2", NULL};
"zero", "HUP", "INT", "QUIT", "ILL", "TRAP", "IOT", "EMT", "FPE", "KILL", "BUS",
"SEGV", "SYS", "PIPE", "ALRM", "TERM", "URG", "STOP", "TSTP", "CONT", "CHLD", "TTIN",
"TTOU", "IO", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "PWR", "USR1", "USR2", NULL};
#endif
void *xmalloc(size_t size) {
@ -43,12 +37,20 @@ void *xrealloc(void *p, size_t size) {
return p;
}
char *uppercase(char *str) {
int i = 0;
do {
str[i] = (char)toupper(str[i]);
} while (str[i++] != '\0');
return str;
char *uppercase(char *s) {
while(*s) {
*s = (char)toupper((int)*s);
s++;
}
return s;
}
char *lowercase(char *s) {
while(*s) {
*s = (char)tolower((int)*s);
s++;
}
return s;
}
bool endswith(const char *str, const char *suffix) {
@ -58,8 +60,7 @@ bool endswith(const char *str, const char *suffix) {
}
int get_sig_name(int sig, char *buf, size_t len) {
int n =
snprintf(buf, len, "SIG%s", sig < NSIG ? sys_signame[sig] : "unknown");
int n = snprintf(buf, len, "SIG%s", sig < NSIG ? sys_signame[sig] : "unknown");
uppercase(buf);
return n;
}
@ -67,27 +68,19 @@ int get_sig_name(int sig, char *buf, size_t len) {
int get_sig(const char *sig_name) {
for (int sig = 1; sig < NSIG; sig++) {
const char *name = sys_signame[sig];
if (name != NULL && (strcasecmp(name, sig_name) == 0 ||
strcasecmp(name, sig_name + 3) == 0))
if (name != NULL && (strcasecmp(name, sig_name) == 0 || strcasecmp(name, sig_name + 3) == 0))
return sig;
}
return atoi(sig_name);
}
bool fd_set_cloexec(const int fd) {
int flags = fcntl(fd, F_GETFD);
if (flags < 0) return false;
return (flags & FD_CLOEXEC) == 0 ||
fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != -1;
}
int open_uri(char *uri) {
#ifdef __APPLE__
char command[256];
sprintf(command, "open %s > /dev/null 2>&1", uri);
return system(command);
#elif defined(_WIN32) || defined(__CYGWIN__)
return ShellExecute(0, 0, uri, 0, 0, SW_SHOW) > 32 ? 0 : 1;
return ShellExecute(0, 0, uri, 0, 0, SW_SHOW) > (HINSTANCE)32 ? 0 : 1;
#else
// check if X server is running
if (system("xset -q > /dev/null 2>&1")) return 1;
@ -97,28 +90,74 @@ int open_uri(char *uri) {
#endif
}
// https://github.com/darkk/redsocks/blob/master/base64.c
char *base64_encode(const unsigned char *buffer, size_t length) {
static const char b64[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
char *ret, *dst;
unsigned i_bits = 0;
int i_shift = 0;
int bytes_remaining = (int)length;
ret = dst = xmalloc((size_t)(((length + 2) / 3 * 4) + 1));
while (bytes_remaining) {
i_bits = (i_bits << 8) + *buffer++;
bytes_remaining--;
i_shift += 8;
do {
*dst++ = b64[(i_bits << 6 >> i_shift) & 0x3f];
i_shift -= 6;
} while (i_shift > 6 || (bytes_remaining == 0 && i_shift > 0));
#ifdef _WIN32
char *strsep(char **sp, char *sep) {
char *p, *s;
if (sp == NULL || *sp == NULL || **sp == '\0') return (NULL);
s = *sp;
p = s + strcspn(s, sep);
if (*p != '\0') *p++ = '\0';
*sp = p;
return s;
}
while ((dst - ret) & 3) *dst++ = '=';
*dst = '\0';
return ret;
const char *quote_arg(const char *arg) {
int len = 0, n = 0;
int force_quotes = 0;
char *q, *d;
const char *p = arg;
if (!*p) force_quotes = 1;
while (*p) {
if (isspace(*p) || *p == '*' || *p == '?' || *p == '{' || *p == '\'')
force_quotes = 1;
else if (*p == '"')
n++;
else if (*p == '\\') {
int count = 0;
while (*p == '\\') {
count++;
p++;
len++;
}
if (*p == '"' || !*p) n += count * 2 + 1;
continue;
}
len++;
p++;
}
if (!force_quotes && n == 0) return arg;
d = q = xmalloc(len + n + 3);
*d++ = '"';
while (*arg) {
if (*arg == '"')
*d++ = '\\';
else if (*arg == '\\') {
int count = 0;
while (*arg == '\\') {
count++;
*d++ = *arg++;
}
if (*arg == '"' || !*arg) {
while (count-- > 0) *d++ = '\\';
if (!*arg) break;
*d++ = '\\';
}
}
*d++ = *arg++;
}
*d++ = '"';
*d++ = '\0';
return q;
}
void print_error(char *func) {
LPVOID buffer;
DWORD dw = GetLastError();
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&buffer, 0, NULL);
wprintf(L"== %s failed with error %d: %s", func, dw, buffer);
LocalFree(buffer);
}
#endif

View file

@ -14,7 +14,10 @@ void *xmalloc(size_t size);
void *xrealloc(void *p, size_t size);
// Convert a string to upper case
char *uppercase(char *str);
char *uppercase(char *s);
// Convert a string to lower case
char *lowercase(char *s);
// Check whether str ends with suffix
bool endswith(const char *str, const char *suffix);
@ -25,13 +28,12 @@ int get_sig_name(int sig, char *buf, size_t len);
// Get signal code from string like SIGHUP
int get_sig(const char *sig_name);
// Set the given file descriptor close-on-exec
bool fd_set_cloexec(const int fd);
// Open uri with the default application of system
int open_uri(char *uri);
// Encode text to base64, the caller should free the returned string
char *base64_encode(const unsigned char *buffer, size_t length);
#ifdef _WIN32
char *strsep(char **sp, char *sep);
const char *quote_arg(const char *arg);
void print_error(char *func);
#endif
#endif // TTYD_UTIL_H