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 BasedOnStyle: Google
Language: Cpp 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" - "CMakeLists.txt"
- "src/*" - "src/*"
- "scripts/*" - "scripts/*"
workflow_call:
jobs: 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: cross:
runs-on: ubuntu-18.04 runs-on: ubuntu-22.04
strategy: strategy:
fail-fast: false
matrix: 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: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Install packages - name: Install packages
run: | run: |
sudo apt-get update sudo apt-get update
@ -55,7 +32,8 @@ jobs:
env: env:
BUILD_TARGET: ${{ matrix.target }} BUILD_TARGET: ${{ matrix.target }}
run: ./scripts/cross-build.sh run: ./scripts/cross-build.sh
- uses: actions/upload-artifact@v1 - uses: actions/upload-artifact@v4
with: with:
name: ttyd.${{ matrix.target }} 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: jobs:
build: build:
runs-on: ubuntu-18.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-node@v1 - uses: actions/setup-node@v4
with: with:
node-version: '12.x' node-version: 18
- name: Run yarn install, check and build - name: Run yarn install, check and build
run: | run: |
cd html corepack enable
corepack prepare yarn@stable --activate
yarn install yarn install
yarn run check yarn run check
yarn run build yarn run build
working-directory: html

View file

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

3
.gitignore vendored
View file

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

View file

@ -1,20 +1,19 @@
cmake_minimum_required(VERSION 2.8) cmake_minimum_required(VERSION 3.12.0)
project(ttyd C) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
set(PROJECT_VERSION "1.6.3")
find_package(Git) project(ttyd VERSION 1.7.7 LANGUAGES C)
if(GIT_FOUND)
execute_process( set(TTYD_VERSION "${PROJECT_VERSION}")
COMMAND ${GIT_EXECUTABLE} --git-dir ${CMAKE_CURRENT_SOURCE_DIR}/.git rev-parse --short HEAD
RESULT_VARIABLE GIT_RET include(GetGitVersion)
OUTPUT_VARIABLE GIT_COMMIT get_git_version(GIT_VERSION SEM_VER)
OUTPUT_STRIP_TRAILING_WHITESPACE get_git_head(GIT_COMMIT)
ERROR_QUIET if("${SEM_VER}" VERSION_GREATER "${TTYD_VERSION}")
) set(TTYD_VERSION "${SEM_VER}")
if("${GIT_RET}" STREQUAL "0") endif()
set(PROJECT_VERSION "${PROJECT_VERSION}-${GIT_COMMIT}") if(NOT "${GIT_COMMIT}" STREQUAL "")
endif() set(TTYD_VERSION "${TTYD_VERSION}-${GIT_COMMIT}")
endif() endif()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_GNU_SOURCE") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_GNU_SOURCE")
@ -28,7 +27,7 @@ else()
set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD 99)
endif() 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) include(FindPackageHandleStandardArgs)
@ -43,7 +42,7 @@ endif()
find_path(JSON-C_INCLUDE_DIR NAMES json.h PATH_SUFFIXES json-c) find_path(JSON-C_INCLUDE_DIR NAMES json.h PATH_SUFFIXES json-c)
find_library(JSON-C_LIBRARY NAMES 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) mark_as_advanced(JSON-C_INCLUDE_DIR JSON-C_LIBRARY)
if(JSON-C_FOUND) if(JSON-C_FOUND)
SET(JSON-C_INCLUDE_DIRS "${JSON-C_INCLUDE_DIR}") SET(JSON-C_INCLUDE_DIRS "${JSON-C_INCLUDE_DIR}")
@ -51,26 +50,19 @@ if(JSON-C_FOUND)
endif() endif()
find_package(ZLIB REQUIRED) find_package(ZLIB REQUIRED)
find_package(Libwebsockets 1.7.0 QUIET) find_package(Libwebsockets 3.2.0 REQUIRED)
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()
set(INCLUDE_DIRS ${ZLIB_INCLUDE_DIR} ${LIBWEBSOCKETS_INCLUDE_DIRS} ${JSON-C_INCLUDE_DIRS} ${LIBUV_INCLUDE_DIRS}) 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(LINK_LIBS ${ZLIB_LIBRARIES} ${LIBWEBSOCKETS_LIBRARIES} ${JSON-C_LIBRARIES} ${LIBUV_LIBRARIES})
set (CMAKE_REQUIRED_INCLUDES ${INCLUDE_DIRS}) set (CMAKE_REQUIRED_INCLUDES ${INCLUDE_DIRS})
include(CheckSymbolExists) 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_OPENSSL_SUPPORT "lws_config.h" LWS_OPENSSL_ENABLED)
check_symbol_exists(LWS_WITH_MBEDTLS "lws_config.h" LWS_MBEDTLS_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) if(LWS_OPENSSL_ENABLED AND NOT LWS_MBEDTLS_ENABLED)
find_package(OpenSSL REQUIRED) find_package(OpenSSL REQUIRED)
list(APPEND INCLUDE_DIRS ${OPENSSL_INCLUDE_DIR}) list(APPEND INCLUDE_DIRS ${OPENSSL_INCLUDE_DIR})
@ -78,15 +70,23 @@ if(LWS_OPENSSL_ENABLED AND NOT LWS_MBEDTLS_ENABLED)
endif() endif()
if(WIN32) if(WIN32)
list(APPEND LINK_LIBS shell32) list(APPEND LINK_LIBS shell32 ws2_32)
elseif(NOT APPLE) 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) list(APPEND LINK_LIBS util)
endif()
endif() endif()
add_executable(${PROJECT_NAME} ${SOURCE_FILES}) add_executable(${PROJECT_NAME} ${SOURCE_FILES})
target_include_directories(${PROJECT_NAME} PUBLIC ${INCLUDE_DIRS}) target_include_directories(${PROJECT_NAME} PUBLIC ${INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME} ${LINK_LIBS}) 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) include(GNUInstallDirs)

View file

@ -1,16 +1,15 @@
FROM ubuntu:18.04 FROM ubuntu:20.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:18.04 ARG TARGETARCH
COPY --from=0 /ttyd/build/ttyd /usr/bin/ttyd
ADD https://github.com/krallin/tini/releases/download/v0.18.0/tini /sbin/tini # Dependencies
RUN chmod +x /sbin/tini 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 EXPOSE 7681
WORKDIR /root WORKDIR /root
ENTRYPOINT ["/sbin/tini", "--"] ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["ttyd", "bash"] 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 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal 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. 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 # Features
- Built on top of [Libwebsockets](https://libwebsockets.org) with [libuv](https://libuv.org) for speed - 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 based on [Xterm.js](https://xtermjs.org) with [CJK](https://en.wikipedia.org/wiki/CJK_characters) and IME support - Fully-featured terminal 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 - [ZMODEM](https://en.wikipedia.org/wiki/ZMODEM) ([lrzsz](https://ohse.de/uwe/software/lrzsz.html)) / [trzsz](https://trzsz.github.io) file transfer support
- SSL support based on [OpenSSL](https://www.openssl.org) - [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 - Run any custom command with options
- Basic authentication support and many other custom options - Basic authentication support and many other custom options
- Cross platform: macOS, Linux, FreeBSD/OpenBSD, [OpenWrt](https://openwrt.org), Windows - 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 on macOS
Install with [homebrew](http://brew.sh): - Install with [Homebrew](http://brew.sh): `brew install ttyd`
- Install with [MacPorts](https://www.macports.org): `sudo port install ttyd`
```bash
brew install ttyd
```
## Install on Linux ## 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): - Build from source (debian/ubuntu):
```bash ```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 git clone https://github.com/tsl0922/ttyd.git
cd ttyd && mkdir build && cd build cd ttyd && mkdir build && cd build
cmake .. cmake ..
make && sudo make install make && sudo make install
``` ```
You may also need to compile/install [libwebsockets](https://libwebsockets.org) from source if the `libwebsockets-dev` package is outdated. 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 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 ## Install on Windows
[Compile on Windows](https://github.com/tsl0922/ttyd/wiki/Compile-on-Windows). - 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 on OpenWrt - 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)
```bash
opkg install ttyd
```
# Usage # Usage
## Command-line Options ## Command-line Options
``` ```
ttyd is a tool for sharing terminal over the web
USAGE: USAGE:
ttyd [options] <command> [<arguments...>] ttyd [options] <command> [<arguments...>]
VERSION:
1.6.3
OPTIONS: OPTIONS:
-p, --port Port to listen (default: 7681, use `0` for random port) -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) -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 -u, --uid User id to run with
-g, --gid Group 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) -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) -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, --client-option Send option to client (format: key=value), repeat to add more options
-T, --terminal-type Terminal type to report, default: xterm-256color -T, --terminal-type Terminal type to report, default: xterm-256color
-O, --check-origin Do not allow websocket connection from different origin -O, --check-origin Do not allow websocket connection from different origin
-m, --max-clients Maximum clients to support (default: 0, no limit) -m, --max-clients Maximum clients to support (default: 0, no limit)
-o, --once Accept only one client and exit on disconnection -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 -B, --browser Open terminal with the default system browser
-I, --index Custom index.html path -I, --index Custom index.html path
-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)
-P, --ping-interval Websocket ping interval(sec) (default: 300) -P, --ping-interval Websocket ping interval(sec) (default: 5)
-6, --ipv6 Enable IPv6 support -6, --ipv6 Enable IPv6 support
-S, --ssl Enable SSL -S, --ssl Enable SSL
-C, --ssl-cert SSL certificate file path -C, --ssl-cert SSL certificate file path
@ -92,8 +94,6 @@ OPTIONS:
-d, --debug Set log level (default: 7) -d, --debug Set log level (default: 7)
-v, --version Print the version and exit -v, --version Print the version and exit
-h, --help Print this text 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). 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 ttyd (1.6.3-1) sid; urgency=medium
* Initial upload to sid (Closes: #972863). * Initial upload to sid (Closes: #972863).

7
debian/control vendored
View file

@ -9,16 +9,19 @@ Build-Depends:
libwebsockets-dev, libwebsockets-dev,
zlib1g-dev, zlib1g-dev,
Rules-Requires-Root: no Rules-Requires-Root: no
Standards-Version: 4.5.1 Standards-Version: 4.7.0
Homepage: https://tsl0922.github.io/ttyd Homepage: https://tsl0922.github.io/ttyd
Vcs-Browser: https://git.progress-linux.org/users/daniel.baumann/debian/packages/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 Vcs-Git: https://git.progress-linux.org/users/daniel.baumann/debian/packages/ttyd
Package: ttyd Package: ttyd
Section: web Section: web
Architecture: any Architecture: linux-any
Depends: Depends:
libwebsockets-evlib-uv,
${misc:Depends}, ${misc:Depends},
${shlibs:Depends}, ${shlibs:Depends},
Suggests:
apache2,
Description: Share your terminal over the web Description: Share your terminal over the web
ttyd is a command-line tool for sharing a 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/ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: ttyd Upstream-Name: ttyd
Upstream-Contact: Shuanglei Tao <tsl0922@gmail.com> Upstream-Contact: Shuanglei Tao <tsl0922@gmail.com>
Source: https://github.com/tsl0922/ttyd/releases Source: https://github.com/tsl0922/ttyd/tags
Files: * Files: *
Copyright: 2016-2021 Shuanglei Tao <tsl0922@gmail.com> Copyright: 2016-2024 Shuanglei Tao <tsl0922@gmail.com>
License: MIT License: MIT
Files: src/queue.h
Copyright: 1991-1993 The Regents of the University of California
License: BSD-3
Files: debian/* Files: debian/*
Copyright: 2021 Daniel Baumann <daniel.baumann@progress-linux.org> Copyright: 2021-2024 Daniel Baumann <daniel.baumann@progress-linux.org>
License: MIT License: MIT
License: BSD-3 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] [Service]
Type=simple Type=simple
ExecStart=/bin/ttyd -i lo -p 7681 -O login EnvironmentFile=/etc/default/ttyd
ExecReload=/bin/kill -HUP $MAINPID ExecStart=/usr/bin/ttyd $TTYD_OPTIONS
ExecReload=/usr/bin/kill -HUP $MAINPID
KillMode=process KillMode=process
LimitNOFILE=512 LimitNOFILE=512
LimitMEMLOCK=infinity LimitMEMLOCK=infinity

2
debian/watch vendored
View file

@ -1,3 +1,3 @@
version=4 version=4
opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/ttyd-$1\.tar\.gz/ \ 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 node_modules
/build dist
/dist *.log
/*.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 ## 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 ## 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 clean = require('gulp-clean');
const gzip = require('gulp-gzip'); const gzip = require('gulp-gzip');
const inlineSource = require('gulp-inline-source'); const inlineSource = require('gulp-inline-source');
const rename = require("gulp-rename"); const rename = require('gulp-rename');
const through2 = require('through2'); const through2 = require('through2');
const genHeader = (size, buf, len) => { const genHeader = (size, buf, len) => {
let idx = 0; let idx = 0;
let data = "unsigned char index_html[] = {\n "; let data = 'unsigned char index_html[] = {\n ';
for (const value of buf) { for (const value of buf) {
idx++; 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 >>> 4).toString(16);
data += (current & 0xF).toString(16); data += (current & 0xf).toString(16);
if (idx === len) { if (idx === len) {
data += "\n"; data += '\n';
} else { } 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_len = ${len};\n`;
data += `unsigned int index_html_size = ${size};\n`; data += `unsigned int index_html_size = ${size};\n`;
return data; return data;
@ -33,30 +33,36 @@ const genHeader = (size, buf, len) => {
let fileSize = 0; let fileSize = 0;
task('clean', () => { task('clean', () => {
return src('dist', { read: false, allowEmpty: true }) return src('dist', { read: false, allowEmpty: true }).pipe(clean());
.pipe(clean());
}); });
task('inline', () => { task('inline', () => {
return src('dist/index.html') const options = {
.pipe(inlineSource()) compress: false,
.pipe(rename("inline.html")) };
.pipe(dest('dist/'));
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') return src('dist/inline.html')
.pipe(through2.obj((file, enc, cb) => { .pipe(
through2.obj((file, enc, cb) => {
fileSize = file.contents.length; fileSize = file.contents.length;
return cb(null, file); return cb(null, file);
})) })
)
.pipe(gzip()) .pipe(gzip())
.pipe(through2.obj((file, enc, cb) => { .pipe(
through2.obj((file, enc, cb) => {
const buf = file.contents; const buf = file.contents;
file.contents = Buffer.from(genHeader(fileSize, buf, buf.length)); file.contents = Buffer.from(genHeader(fileSize, buf, buf.length));
return cb(null, file); return cb(null, file);
})) })
.pipe(rename("html.h")) )
.pipe(rename('html.h'))
.pipe(dest('../src/')); .pipe(dest('../src/'));
})); })
);

View file

@ -11,66 +11,63 @@
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"prestart": "gulp clean", "prestart": "gulp clean",
"start": "webpack serve", "start": "NODE_ENV=development && webpack serve",
"build": "NODE_ENV=production webpack && gulp", "build": "NODE_ENV=production webpack && gulp",
"inline": "NODE_ENV=production webpack && gulp inline", "inline": "NODE_ENV=production webpack && gulp inline",
"check": "gts check", "check": "gts check",
"fix": "gts fix" "fix": "gts fix"
}, },
"husky": { "engines": {
"hooks": { "node": ">=12"
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.ts": [
"gts fix",
"git add"
],
"src/**/*.scss": [
"scssfmt",
"git add"
]
}, },
"devDependencies": { "devDependencies": {
"copy-webpack-plugin": "^6.4.1", "@typescript-eslint/eslint-plugin": "^7.1.1",
"css-loader": "^5.0.1", "copy-webpack-plugin": "^12.0.2",
"gts": "^1.1.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": "^4.0.2",
"gulp-clean": "^0.4.0", "gulp-clean": "^0.4.0",
"gulp-gzip": "^1.4.2", "gulp-gzip": "^1.4.2",
"gulp-inline-source": "^4.0.0", "gulp-inline-source": "^4.0.0",
"gulp-rename": "^2.0.0", "gulp-rename": "^2.0.0",
"html-webpack-plugin": "^4.5.0", "html-webpack-plugin": "^5.6.0",
"husky": "^4.3.6", "mini-css-extract-plugin": "^2.8.1",
"lint-staged": "^10.5.3", "sass": "^1.71.1",
"mini-css-extract-plugin": "^1.3.3", "sass-loader": "^14.1.1",
"node-sass": "^5.0.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"sass-loader": "^10.1.0",
"scssfmt": "^1.0.7", "scssfmt": "^1.0.7",
"style-loader": "^2.0.0", "style-loader": "^3.3.4",
"terser-webpack-plugin": "^4.2.3", "terser-webpack-plugin": "^5.3.10",
"through2": "^4.0.2", "through2": "^4.0.2",
"ts-loader": "^8.0.13", "ts-loader": "^9.5.1",
"tslint": "^6.1.3", "typescript": "^5.3.3",
"tslint-loader": "^3.5.4", "util": "^0.12.5",
"typescript": "^3.9.7", "webpack": "^5.90.3",
"webpack": "4.44.2", "webpack-cli": "^5.1.4",
"webpack-cli": "^4.3.1", "webpack-dev-server": "^5.0.2",
"webpack-dev-server": "^3.11.1", "webpack-merge": "^5.10.0"
"webpack-merge": "^5.7.3"
}, },
"dependencies": { "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", "decko": "^1.2.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"preact": "^10.5.8", "preact": "^10.19.6",
"whatwg-fetch": "^3.5.0", "trzsz": "^1.1.5",
"xterm": "^4.9.0", "whatwg-fetch": "^3.6.20",
"xterm-addon-fit": "^0.4.0",
"xterm-addon-web-links": "^0.4.0",
"xterm-addon-webgl": "^0.9.0",
"zmodem.js": "^0.1.10" "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 { h, Component } from 'preact';
import { ITerminalOptions, ITheme } from 'xterm'; import { Terminal } from './terminal';
import { ClientOptions, Xterm } from './terminal';
if ((module as any).hot) { import type { ITerminalOptions, ITheme } from '@xterm/xterm';
// tslint:disable-next-line:no-var-requires import type { ClientOptions, FlowControl } from './terminal/xterm';
require('preact/debug');
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; 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 wsUrl = [protocol, '//', window.location.host, path, '/ws', window.location.search].join('');
const tokenUrl = [window.location.protocol, '//', window.location.host, path, '/token'].join(''); const tokenUrl = [window.location.protocol, '//', window.location.host, path, '/token'].join('');
const clientOptions = { const clientOptions = {
rendererType: 'webgl', rendererType: 'webgl',
disableLeaveAlert: false, disableLeaveAlert: false,
disableResizeOverlay: false, disableResizeOverlay: false,
titleFixed: null, enableZmodem: false,
enableTrzsz: false,
enableSixel: false,
isWindows: false,
unicodeVersion: '11',
} as ClientOptions; } as ClientOptions;
const termOptions = { const termOptions = {
fontSize: 13, fontSize: 13,
fontFamily: 'Menlo For Powerline,Consolas,Liberation Mono,Menlo,Courier,monospace', fontFamily: 'Consolas,Liberation Mono,Menlo,Courier,monospace',
theme: { theme: {
foreground: '#d2d2d2', foreground: '#d2d2d2',
background: '#2b2b2b', background: '#2b2b2b',
@ -42,17 +43,24 @@ const termOptions = {
brightCyan: '#37e6e8', brightCyan: '#37e6e8',
brightWhite: '#f1f1f0', brightWhite: '#f1f1f0',
} as ITheme, } as ITheme,
allowProposedApi: true,
} as ITerminalOptions; } as ITerminalOptions;
const flowControl = {
limit: 100000,
highWater: 10,
lowWater: 4,
} as FlowControl;
export class App extends Component { export class App extends Component {
render() { render() {
return ( return (
<Xterm <Terminal
id="terminal-container" id="terminal-container"
wsUrl={wsUrl} wsUrl={wsUrl}
tokenUrl={tokenUrl} tokenUrl={tokenUrl}
clientOptions={clientOptions} clientOptions={clientOptions}
termOptions={termOptions} termOptions={termOptions}
flowControl={flowControl}
/> />
); );
} }

View file

@ -1,372 +1,59 @@
import { bind } from 'decko'; import { bind } from 'decko';
import * as backoff from 'backoff';
import { Component, h } from 'preact'; import { Component, h } from 'preact';
import { ITerminalOptions, Terminal } from 'xterm'; import { Xterm, XtermOptions } from './xterm';
import { FitAddon } from 'xterm-addon-fit';
import { WebglAddon } from 'xterm-addon-webgl';
import { WebLinksAddon } from 'xterm-addon-web-links';
import { OverlayAddon } from './overlay'; import '@xterm/xterm/css/xterm.css';
import { ZmodemAddon, FlowControl } from '../zmodem'; import { Modal } from '../modal';
import 'xterm/css/xterm.css'; interface Props extends XtermOptions {
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 {
id: string; id: string;
wsUrl: string;
tokenUrl: string;
clientOptions: ClientOptions;
termOptions: ITerminalOptions;
} }
export class Xterm extends Component<Props> { interface State {
private textEncoder: TextEncoder; modal: boolean;
private textDecoder: TextDecoder; }
export class Terminal extends Component<Props, State> {
private container: HTMLElement; private container: HTMLElement;
private terminal: Terminal; private xterm: Xterm;
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;
constructor(props: Props) { constructor(props: Props) {
super(props); super();
this.xterm = new Xterm(props, this.showModal);
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
});
} }
async componentDidMount() { async componentDidMount() {
await this.refreshToken(); await this.xterm.refreshToken();
this.openTerminal(); this.xterm.open(this.container);
this.connect(); this.xterm.connect();
window.addEventListener('resize', this.onWindowResize);
window.addEventListener('beforeunload', this.onWindowUnload);
} }
componentWillUnmount() { componentWillUnmount() {
this.socket.close(); this.xterm.dispose();
this.terminal.dispose();
window.removeEventListener('resize', this.onWindowResize);
window.removeEventListener('beforeunload', this.onWindowUnload);
} }
render({ id }: Props) { render({ id }: Props, { modal }: State) {
const control = {
limit: 100000,
highWater: 10,
lowWater: 4,
pause: () => this.pause(),
resume: () => this.resume(),
} as FlowControl;
return ( return (
<div id={id} ref={c => (this.container = c)}> <div id={id} ref={c => (this.container = c as HTMLElement)}>
<ZmodemAddon ref={c => (this.zmodemAddon = c)} sender={this.sendData} control={control} /> <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> </div>
); );
} }
@bind @bind
private pause() { showModal() {
const { textEncoder, socket } = this; this.setState({ modal: true });
socket.send(textEncoder.encode(Command.PAUSE));
} }
@bind @bind
private resume() { sendFile(event: Event) {
const { textEncoder, socket } = this; this.setState({ modal: false });
socket.send(textEncoder.encode(Command.RESUME)); const files = (event.target as HTMLInputElement).files;
} if (files) this.xterm.sendFile(files);
@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));
}
} }
} }

View file

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

View file

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

View file

@ -1,20 +1,16 @@
{ {
"extends": "./node_modules/gts/tsconfig-google.json",
"compilerOptions": { "compilerOptions": {
"target": "es2015",
"module": "es2015",
"lib": [
"es2015",
"dom"
],
"allowJs": true,
"jsx": "react",
"jsxFactory": "h",
"sourceMap": true,
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,
"jsx": "react",
"jsxFactory": "h",
"allowJs": true,
"noImplicitAny": false,
"declaration": false,
"experimentalDecorators": true, "experimentalDecorators": true,
"noImplicitReturns": true, "strictPropertyInitialization": false,
"noUnusedParameters": true "lib": ["es2019", "dom"],
}, },
"include": [ "include": [
"src/**/*.tsx", "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 path = require('path');
const { merge } = require('webpack-merge'); const { merge } = require('webpack-merge');
const ESLintPlugin = require('eslint-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
const devMode = process.env.NODE_ENV !== 'production'; const devMode = process.env.NODE_ENV !== 'production';
@ -11,46 +12,39 @@ const devMode = process.env.NODE_ENV !== 'production';
const baseConfig = { const baseConfig = {
context: path.resolve(__dirname, 'src'), context: path.resolve(__dirname, 'src'),
entry: { entry: {
app: './index.tsx' app: './index.tsx',
}, },
output: { output: {
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
filename: devMode ? '[name].js' : '[name].[hash].js', filename: devMode ? '[name].js' : '[name].[contenthash].js',
}, },
module: { module: {
rules: [ rules: [
{
test: /\.ts$/,
enforce: 'pre',
use: 'tslint-loader',
},
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: 'ts-loader', use: 'ts-loader',
exclude: /node_modules/ exclude: /node_modules/,
}, },
{ {
test: /\.s?[ac]ss$/, test: /\.s?[ac]ss$/,
use: [ use: [devMode ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
],
}, },
] ],
}, },
resolve: { resolve: {
extensions: [ '.tsx', '.ts', '.js' ] extensions: ['.tsx', '.ts', '.js'],
}, },
plugins: [ plugins: [
new ESLintPlugin({
context: path.resolve(__dirname, '.'),
extensions: ['js', 'jsx', 'ts', 'tsx'],
}),
new CopyWebpackPlugin({ new CopyWebpackPlugin({
patterns:[ patterns: [{ from: './favicon.png', to: '.' }],
{ from: './favicon.png', to: '.' }
],
}), }),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: devMode ? '[name].css' : '[name].[hash].css', filename: devMode ? '[name].css' : '[name].[contenthash].css',
chunkFilename: devMode ? '[id].css' : '[id].[hash].css', chunkFilename: devMode ? '[id].css' : '[id].[contenthash].css',
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
inject: false, inject: false,
@ -59,25 +53,39 @@ const baseConfig = {
collapseWhitespace: true, collapseWhitespace: true,
}, },
title: 'ttyd - Terminal', title: 'ttyd - Terminal',
template: './template.html' template: './template.html',
}) }),
], ],
performance : { performance: {
hints : false hints: false,
}, },
}; };
const devConfig = { const devConfig = {
mode: 'development', mode: 'development',
devServer: { devServer: {
contentBase: path.join(__dirname, 'dist'), static: path.join(__dirname, 'dist'),
compress: true, compress: true,
port: 9000, port: 9000,
proxy: [{ client: {
overlay: {
errors: true,
warnings: false,
},
},
proxy: [
{
context: ['/token', '/ws'], context: ['/token', '/ws'],
target: 'http://localhost:7681', target: 'http://localhost:7681',
ws: true ws: true,
}] },
],
webSocketServer: {
type: 'sockjs',
options: {
path: '/sockjs-node',
},
},
}, },
devtool: 'inline-source-map', devtool: 'inline-source-map',
}; };
@ -85,22 +93,9 @@ const devConfig = {
const prodConfig = { const prodConfig = {
mode: 'production', mode: 'production',
optimization: { optimization: {
minimizer: [ minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
new TerserPlugin({
sourceMap: true
}),
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
map: {
inline: false,
annotation: true
}
}
}),
]
}, },
devtool: 'source-map', devtool: 'source-map',
}; };
module.exports = merge(baseConfig, devMode ? devConfig : prodConfig); 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" .TH ttyd 1 "September 2016" ttyd "User Manual"
.SH NAME .SH NAME
.PP .PP
ttyd \- Share your terminal over the web ttyd - Share your terminal over the web
.SH SYNOPSIS .SH SYNOPSIS
@ -10,18 +11,20 @@ ttyd \- Share your terminal over the web
\fBttyd\fP [options] <command> [<arguments...>] \fBttyd\fP [options] <command> [<arguments...>]
.SH Description .SH DESCRIPTION
.PP .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 .RS
.IP \(bu 2 .IP \(bu 2
Built on top of Libwebsockets with libuv for speed Built on top of Libwebsockets with libuv for speed
.IP \(bu 2 .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 .IP \(bu 2
Graphical ZMODEM integration with lrzsz support Graphical ZMODEM integration with lrzsz support
.IP \(bu 2 .IP \(bu 2
Sixel image output support
.IP \(bu 2
SSL support based on OpenSSL SSL support based on OpenSSL
.IP \(bu 2 .IP \(bu 2
Run any custom command with options Run any custom command with options
@ -35,123 +38,206 @@ Cross platform: macOS, Linux, FreeBSD/OpenBSD, OpenWrt/LEDE, Windows
.SH OPTIONS .SH OPTIONS
.PP .PP
\-p, \-\-port <port> -p, --port
Port to listen (default: 7681, use \fB\fC0\fR for random port) Port to listen (default: 7681, use \fB\fC0\fR for random port)
.PP .PP
\-i, \-\-interface <interface> -i, --interface
Network interface to bind (eg: eth0), or UNIX domain socket path (eg: /var/run/ttyd.sock) Network interface to bind (eg: eth0), or UNIX domain socket path (eg: /var/run/ttyd.sock)
.PP .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) Credential for Basic Authentication (format: username:password)
.PP .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 User id to run with
.PP .PP
\-g, \-\-gid <gid> -g, --gid
Group id to run with Group id to run with
.PP .PP
\-s, \-\-signal <signal string> -s, --signal
Signal to send to the command when exit it (default: 1, SIGHUP) Signal to send to the command when exit it (default: 1, SIGHUP)
.PP .PP
\-a, \-\-url\-arg -w, --cwd
Allow client to send command line arguments in URL (eg: Working directory to be set for the child program
\[la]http://localhost:7681?arg=foo&arg=bar\[ra])
.PP .PP
\-R, \-\-readonly -a, --url-arg
Do not allow clients to write to the TTY Allow client to send command line arguments in URL (eg: http://localhost:7681?arg=foo&arg=bar)
.PP .PP
\-t, \-\-client\-option <key=value> -W, --writable
Send option to client (format: key=value), repeat to add more options Allow clients to write to the TTY (readonly by default)
.PP .PP
\-T, \-\-terminal\-type -t, --client-option
Terminal type to report, default: xterm\-256color Send option to client (format: key=value), repeat to add more options, see \fBCLIENT OPTIONS\fP for details
.PP .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 Do not allow websocket connection from different origin
.PP .PP
\-m, \-\-max\-clients -m, --max-clients
Maximum clients to support (default: 0, no limit) Maximum clients to support (default: 0, no limit)
.PP .PP
\-o, \-\-once -o, --once
Accept only one client and exit on disconnection Accept only one client and exit on disconnection
.PP .PP
\-B, \-\-browser -q, --exit-no-conn
Exit on all clients disconnection
.PP
-B, --browser
Open terminal with the default system browser Open terminal with the default system browser
.PP .PP
\-I, \-\-index <index file> -I, --index
Custom index.html path Custom index.html path
.PP .PP
\-b, \-\-base\-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)
.PP .PP
\-P, \-\-ping\-interval -P, --ping-interval
Websocket ping interval(sec) (default: 300) Websocket ping interval(sec) (default: 5)
.PP .PP
\-6, \-\-ipv6 -6, --ipv6
Enable IPv6 support Enable IPv6 support
.PP .PP
\-S, \-\-ssl -S, --ssl
Enable SSL Enable SSL
.PP .PP
\-C, \-\-ssl\-cert <cert path> -C, --ssl-cert
SSL certificate file path SSL certificate file path
.PP .PP
\-K, \-\-ssl\-key <key path> -K, --ssl-key
SSL key file path SSL key file path
.PP .PP
\-A, \-\-ssl\-ca <ca path> -A, --ssl-ca
SSL CA file path for client certificate verification SSL CA file path for client certificate verification
.PP .PP
\-d, \-\-debug <level> -d, --debug
Set log level (default: 7) Set log level (default: 7)
.PP .PP
\-v, \-\-version -v, --version
Print the version and exit Print the version and exit
.PP .PP
\-h, \-\-help -h, --help
Print this text and exit Print this text and exit
.SH Examples .SH CLIENT OPTIONS
.PP .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 .PP
.RS .RS
.nf .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 .fi
.RE .RE
.PP .PP
Then open Then open http://localhost:8080 with a browser, you will get a bash shell with debug mode enabled. More examples:
\[la]http://localhost:8080\[ra] with a browser, you will get a bash shell with debug mode enabled. More examples:
.RS .RS
.IP \(bu 2 .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 .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. 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 .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 .RE
.SH SSL how\-to .SH SSL how-to
.PP .PP
Generate SSL CA and self signed server/client certificates: Generate SSL CA and self signed server/client certificates:
@ -173,18 +259,18 @@ Generate SSL CA and self signed server/client certificates:
.nf .nf
# CA certificate (FQDN must be different from server/client) # CA certificate (FQDN must be different from server/client)
openssl genrsa \-out ca.key 2048 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 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) # 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 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 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) # 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 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 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 -export -clcerts -in client.crt -inkey client.key -out client.p12
openssl pkcs12 \-in client.p12 \-out client.pem \-clcerts openssl pkcs12 -in client.p12 -out client.pem -clcerts
.fi .fi
.RE .RE
@ -196,7 +282,7 @@ Then start ttyd:
.RS .RS
.nf .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 .fi
.RE .RE
@ -208,13 +294,13 @@ You may want to test the client certificate verification with \fIcurl\fP(1):
.RS .RS
.nf .nf
curl \-\-insecure \-\-cert client.p12[:password] \-v https://localhost:7681 curl --insecure --cert client.p12[:password] -v https://localhost:7681
.fi .fi
.RE .RE
.PP .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 .SH Docker and ttyd
@ -223,9 +309,9 @@ Docker containers are jailed environments which are more secure, this is useful
.RS .RS
.IP \(bu 2 .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 .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 .RE
@ -238,14 +324,14 @@ Sample config to proxy ttyd under the \fB\fC/ttyd\fR path:
.RS .RS
.nf .nf
location \~ ^/ttyd(.*)$ { location ~ ^/ttyd(.*)$ {
proxy\_http\_version 1.1; proxy_http_version 1.1;
proxy\_set\_header Host $host; proxy_set_header Host $host;
proxy\_set\_header X\-Forwarded\-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy\_set\_header X\-Forwarded\-For $proxy\_add\_x\_forwarded\_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy\_set\_header Upgrade $http\_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy\_set\_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy\_pass http://127.0.0.1:7681/$1; proxy_pass http://127.0.0.1:7681/$1;
} }
.fi .fi
@ -254,5 +340,4 @@ location \~ ^/ttyd(.*)$ {
.SH AUTHOR .SH AUTHOR
.PP .PP
Shuanglei Tao <tsl0922@gmail.com> Visit Shuanglei Tao <tsl0922@gmail.com> Visit https://github.com/tsl0922/ttyd to get more information and report bugs.
\[la]https://github.com/tsl0922/ttyd\[ra] to get more information and report bugs.

View file

@ -7,12 +7,13 @@ ttyd 1 "September 2016" ttyd "User Manual"
# SYNOPSIS # SYNOPSIS
**ttyd** [options] \<command\> [\<arguments...\>] **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: 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 - Built on top of Libwebsockets with libuv for speed
- 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
- Graphical ZMODEM integration with lrzsz support - Graphical ZMODEM integration with lrzsz support
- Sixel image output support
- SSL support based on OpenSSL - SSL support based on OpenSSL
- Run any custom command with options - Run any custom command with options
- Basic authentication support and many other custom options - Basic authentication support and many other custom options
@ -25,9 +26,15 @@ ttyd 1 "September 2016" ttyd "User Manual"
-i, --interface <interface> -i, --interface <interface>
Network interface to bind (eg: eth0), or UNIX domain socket path (eg: /var/run/ttyd.sock) 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] -c, --credential USER[:PASSWORD]
Credential for Basic Authentication (format: username: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> -u, --uid <uid>
User id to run with User id to run with
@ -37,14 +44,17 @@ ttyd 1 "September 2016" ttyd "User Manual"
-s, --signal <signal string> -s, --signal <signal string>
Signal to send to the command when exit it (default: 1, SIGHUP) 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 -a, --url-arg
Allow client to send command line arguments in URL (eg: http://localhost:7681?arg=foo&arg=bar) Allow client to send command line arguments in URL (eg: http://localhost:7681?arg=foo&arg=bar)
-R, --readonly -W, --writable
Do not allow clients to write to the TTY Allow clients to write to the TTY (readonly by default)
-t, --client-option <key=value> -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 -T, --terminal-type
Terminal type to report, default: xterm-256color Terminal type to report, default: xterm-256color
@ -58,6 +68,9 @@ ttyd 1 "September 2016" ttyd "User Manual"
-o, --once -o, --once
Accept only one client and exit on disconnection Accept only one client and exit on disconnection
-q, --exit-no-conn
Exit on all clients disconnection
-B, --browser -B, --browser
Open terminal with the default system browser Open terminal with the default system browser
@ -65,10 +78,10 @@ ttyd 1 "September 2016" ttyd "User Manual"
Custom index.html path Custom index.html path
-b, --base-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 -P, --ping-interval
Websocket ping interval(sec) (default: 300) Websocket ping interval(sec) (default: 5)
-6, --ipv6 -6, --ipv6
Enable IPv6 support Enable IPv6 support
@ -94,7 +107,40 @@ ttyd 1 "September 2016" ttyd "User Manual"
-h, --help -h, --help
Print this text and exit 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: 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_ROOT="${BUILD_ROOT:-/opt/build}"
BUILD_TARGET="${BUILD_TARGET:-x86_64}" BUILD_TARGET="${BUILD_TARGET:-x86_64}"
ZLIB_VERSION="${ZLIB_VERSION:-1.2.11}" ZLIB_VERSION="${ZLIB_VERSION:-1.3.1}"
JSON_C_VERSION="${JSON_C_VERSION:-0.15}" JSON_C_VERSION="${JSON_C_VERSION:-0.17}"
MBEDTLS_VERSION="${MBEDTLS_VERSION:-2.16.8}" MBEDTLS_VERSION="${MBEDTLS_VERSION:-2.28.5}"
LIBUV_VERSION="${LIBUV_VERSION:-1.40.0}" LIBUV_VERSION="${LIBUV_VERSION:-1.44.2}"
LIBWEBSOCKETS_VERSION="${LIBWEBSOCKETS_VERSION:-4.1.6}" LIBWEBSOCKETS_VERSION="${LIBWEBSOCKETS_VERSION:-4.3.3}"
build_zlib() { build_zlib() {
echo "=== Building zlib-${ZLIB_VERSION} (${TARGET})..." 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}" pushd "${BUILD_DIR}"/zlib-"${ZLIB_VERSION}"
env CHOST="${TARGET}" ./configure --static --archs="-fPIC" --prefix="${STAGE_DIR}" env CHOST="${TARGET}" ./configure --static --archs="-fPIC" --prefix="${STAGE_DIR}"
make -j"$(nproc)" install make -j"$(nproc)" install
@ -27,13 +27,15 @@ build_zlib() {
build_json-c() { build_json-c() {
echo "=== Building json-c-${JSON_C_VERSION} (${TARGET})..." 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}" pushd "${BUILD_DIR}/json-c-${JSON_C_VERSION}"
rm -rf build && mkdir -p build && cd build rm -rf build && mkdir -p build && cd build
cmake -DCMAKE_TOOLCHAIN_FILE="${BUILD_DIR}/cross-${TARGET}.cmake" \ cmake -DCMAKE_TOOLCHAIN_FILE="${BUILD_DIR}/cross-${TARGET}.cmake" \
-DCMAKE_BUILD_TYPE=RELEASE \ -DCMAKE_BUILD_TYPE=RELEASE \
-DCMAKE_INSTALL_PREFIX="${STAGE_DIR}" \ -DCMAKE_INSTALL_PREFIX="${STAGE_DIR}" \
-DBUILD_SHARED_LIBS=OFF \ -DBUILD_SHARED_LIBS=OFF \
-DBUILD_TESTING=OFF \
-DDISABLE_THREAD_LOCAL_STORAGE=ON \
.. ..
make -j"$(nproc)" install make -j"$(nproc)" install
popd popd
@ -41,7 +43,7 @@ build_json-c() {
build_mbedtls() { build_mbedtls() {
echo "=== Building mbedtls-${MBEDTLS_VERSION} (${TARGET})..." 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}" pushd "${BUILD_DIR}/mbedtls-${MBEDTLS_VERSION}"
rm -rf build && mkdir -p build && cd build rm -rf build && mkdir -p build && cd build
cmake -DCMAKE_TOOLCHAIN_FILE="${BUILD_DIR}/cross-${TARGET}.cmake" \ cmake -DCMAKE_TOOLCHAIN_FILE="${BUILD_DIR}/cross-${TARGET}.cmake" \
@ -55,7 +57,7 @@ build_mbedtls() {
build_libuv() { build_libuv() {
echo "=== Building libuv-${LIBUV_VERSION} (${TARGET})..." 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}" pushd "${BUILD_DIR}/libuv-v${LIBUV_VERSION}"
./autogen.sh ./autogen.sh
env CFLAGS=-fPIC ./configure --disable-shared --enable-static --prefix="${STAGE_DIR}" --host="${TARGET}" env CFLAGS=-fPIC ./configure --disable-shared --enable-static --prefix="${STAGE_DIR}" --host="${TARGET}"
@ -65,7 +67,7 @@ build_libuv() {
install_cmake_cross_file() { install_cmake_cross_file() {
cat << EOF > "${BUILD_DIR}/cross-${TARGET}.cmake" 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_C_COMPILER "${TARGET}-gcc")
set(CMAKE_CXX_COMPILER "${TARGET}-g++") set(CMAKE_CXX_COMPILER "${TARGET}-g++")
@ -81,9 +83,10 @@ EOF
build_libwebsockets() { build_libwebsockets() {
echo "=== Building libwebsockets-${LIBWEBSOCKETS_VERSION} (${TARGET})..." 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}" pushd "${BUILD_DIR}/libwebsockets-${LIBWEBSOCKETS_VERSION}"
sed -i 's/ websockets_shared//g' cmake/libwebsockets-config.cmake.in 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 sed -i '/PC_OPENSSL/d' lib/tls/CMakeLists.txt
rm -rf build && mkdir -p build && cd build rm -rf build && mkdir -p build && cd build
cmake -DCMAKE_TOOLCHAIN_FILE="${BUILD_DIR}/cross-${TARGET}.cmake" \ cmake -DCMAKE_TOOLCHAIN_FILE="${BUILD_DIR}/cross-${TARGET}.cmake" \
@ -99,14 +102,14 @@ build_libwebsockets() {
-DLWS_UNIX_SOCK=ON \ -DLWS_UNIX_SOCK=ON \
-DLWS_IPV6=ON \ -DLWS_IPV6=ON \
-DLWS_ROLE_RAW_FILE=OFF \ -DLWS_ROLE_RAW_FILE=OFF \
-DLWS_WITH_HTTP2=OFF \ -DLWS_WITH_HTTP2=ON \
-DLWS_WITH_HTTP_BASIC_AUTH=OFF \ -DLWS_WITH_HTTP_BASIC_AUTH=OFF \
-DLWS_WITH_UDP=OFF \ -DLWS_WITH_UDP=OFF \
-DLWS_WITHOUT_CLIENT=ON \ -DLWS_WITHOUT_CLIENT=ON \
-DLWS_WITHOUT_EXTENSIONS=OFF \
-DLWS_WITH_LEJP=OFF \ -DLWS_WITH_LEJP=OFF \
-DLWS_WITH_LEJP_CONF=OFF \ -DLWS_WITH_LEJP_CONF=OFF \
-DLWS_WITH_LWSAC=OFF \ -DLWS_WITH_LWSAC=OFF \
-DLWS_WITH_CUSTOM_HEADERS=OFF \
-DLWS_WITH_SEQUENCER=OFF \ -DLWS_WITH_SEQUENCER=OFF \
.. ..
make -j"$(nproc)" install make -j"$(nproc)" install
@ -131,11 +134,19 @@ build() {
ALIAS="$2" ALIAS="$2"
STAGE_DIR="${STAGE_ROOT}/${TARGET}" STAGE_DIR="${STAGE_ROOT}/${TARGET}"
BUILD_DIR="${BUILD_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})..." echo "=== Installing toolchain ${ALIAS} (${TARGET})..."
mkdir -p "${CROSS_ROOT}" && export PATH="${PATH}:/opt/cross/bin" mkdir -p "${CROSS_ROOT}" && export PATH="${PATH}:${CROSS_ROOT}/bin"
curl -sLo- "https://musl.cc/${TARGET}-cross.tgz" | tar xz -C "${CROSS_ROOT}" --strip-components 1 curl -fSsLo- "${MUSL_CC_URL}/${TARGET}-cross.tgz" | tar xz -C "${CROSS_ROOT}" --strip-components=${COMPONENTS}
echo "=== Building target ${ALIAS} (${TARGET})..." echo "=== Building target ${ALIAS} (${TARGET})..."
@ -143,7 +154,7 @@ build() {
mkdir -p "${STAGE_DIR}" "${BUILD_DIR}" mkdir -p "${STAGE_DIR}" "${BUILD_DIR}"
export PKG_CONFIG_PATH="${STAGE_DIR}/lib/pkgconfig" export PKG_CONFIG_PATH="${STAGE_DIR}/lib/pkgconfig"
install_cmake_cross_file install_cmake_cross_file ${SYSTEM}
build_zlib build_zlib
build_json-c build_json-c
@ -154,15 +165,27 @@ build() {
} }
case ${BUILD_TARGET} in 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}" build "${BUILD_TARGET}-linux-musl" "${BUILD_TARGET}"
;; ;;
arm) arm)
build arm-linux-musleabi "${BUILD_TARGET}" build "${BUILD_TARGET}-linux-musleabi" "${BUILD_TARGET}"
;; ;;
armhf) armhf)
build arm-linux-musleabihf "${BUILD_TARGET}" 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 echo "unknown cross target: ${BUILD_TARGET}" && exit 1
esac 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 "server.h"
#include "utils.h" #include "utils.h"
#if LWS_LIBRARY_VERSION_MAJOR < 2
#define HTTP_STATUS_FOUND 302
#endif
enum { AUTH_OK, AUTH_FAIL, AUTH_ERROR }; enum { AUTH_OK, AUTH_FAIL, AUTH_ERROR };
static char *html_cache = NULL; static char *html_cache = NULL;
static size_t html_cache_len = 0; static size_t html_cache_len = 0;
static int check_auth(struct lws *wsi, struct pss_http *pss) { static int send_unauthorized(struct lws *wsi, unsigned int code, enum lws_token_indexes header) {
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;
}
unsigned char buffer[1024 + LWS_PRE], *p, *end; unsigned char buffer[1024 + LWS_PRE], *p, *end;
p = buffer + LWS_PRE; p = buffer + LWS_PRE;
end = p + sizeof(buffer) - LWS_PRE; end = p + sizeof(buffer) - LWS_PRE;
char *body = strdup("401 Unauthorized\n"); if (lws_add_http_header_status(wsi, code, &p, end) ||
size_t n = strlen(body); 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) ||
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) ||
lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0) 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 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) { static bool accept_gzip(struct lws *wsi) {
int hdr_length = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_ACCEPT_ENCODING); char buf[256];
char buf[hdr_length + 1];
int len = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_ACCEPT_ENCODING); int len = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_ACCEPT_ENCODING);
return len > 0 && strstr(buf, "gzip") != NULL; 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) { static void access_log(struct lws *wsi, const char *path) {
char rip[50]; char rip[50];
#if LWS_LIBRARY_VERSION_NUMBER >= 2004000
lws_get_peer_simple(lws_get_network_wsi(wsi), rip, sizeof(rip)); 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); lwsl_notice("HTTP %s - %s\n", path, rip);
} }
int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) {
size_t len) {
struct pss_http *pss = (struct pss_http *)user; struct pss_http *pss = (struct pss_http *)user;
unsigned char buffer[4096 + LWS_PRE], *p, *end; unsigned char buffer[4096 + LWS_PRE], *p, *end;
char buf[256]; 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); size_t n = sprintf(buf, "{\"token\": \"%s\"}", credential);
if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end) || if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end) ||
lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE,
(unsigned char *)"application/json;charset=utf-8", 30, &p, (unsigned char *)"application/json;charset=utf-8", 30, &p, end) ||
end) ||
lws_add_http_header_content_length(wsi, (unsigned long)n, &p, end) || lws_add_http_header_content_length(wsi, (unsigned long)n, &p, end) ||
lws_finalize_http_header(wsi, &p, end) || lws_finalize_http_header(wsi, &p, end) ||
lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0) 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/` // redirects `/base-path` to `/base-path/`
if (strcmp(pss->path, endpoints.parent) == 0) { if (strcmp(pss->path, endpoints.parent) == 0) {
if (lws_add_http_header_status(wsi, HTTP_STATUS_FOUND, &p, end) || if (lws_add_http_header_status(wsi, HTTP_STATUS_FOUND, &p, end) ||
lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_LOCATION, lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_LOCATION, (unsigned char *)endpoints.index,
(unsigned char *)endpoints.index,
(int)strlen(endpoints.index), &p, end) || (int)strlen(endpoints.index), &p, end) ||
lws_add_http_header_content_length(wsi, 0, &p, end) || lws_add_http_header_content_length(wsi, 0, &p, end) || lws_finalize_http_header(wsi, &p, end) ||
lws_finalize_http_header(wsi, &p, end) ||
lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0) lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0)
return 1; return 1;
goto try_to_reuse; 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; char *output = (char *)index_html;
size_t output_len = index_html_len; size_t output_len = index_html_len;
if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end) || if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end) ||
lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (const unsigned char *)content_type, 9, &p,
(const unsigned char *)content_type, 9, &p, end)) end))
return 1; return 1;
#ifdef LWS_WITH_HTTP_STREAM_COMPRESSION #ifdef LWS_WITH_HTTP_STREAM_COMPRESSION
if (!uncompress_html(&output, &output_len)) return 1; if (!uncompress_html(&output, &output_len)) return 1;
#else #else
if (accept_gzip(wsi)) { if (accept_gzip(wsi)) {
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_ENCODING, if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_ENCODING, (unsigned char *)"gzip", 4, &p, end))
(unsigned char *)"gzip", 4, &p, end))
return 1; return 1;
} else { } else {
if (!uncompress_html(&output, &output_len)) return 1; 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) lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0)
return 1; 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->buffer = pss->ptr = output;
pss->len = output_len; pss->len = output_len;
lws_callback_on_writable(wsi); lws_callback_on_writable(wsi);
#endif
} }
break; break;

View file

@ -1,15 +1,13 @@
#include <errno.h> #include <errno.h>
#include <json.h> #include <json.h>
#include <libwebsockets.h> #include <libwebsockets.h>
#include <signal.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/wait.h>
#include "pty.h"
#include "server.h" #include "server.h"
#include "terminal.h"
#include "utils.h" #include "utils.h"
// initial message list // 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); return lws_write(wsi, p, (size_t)n, LWS_WRITE_BINARY);
} }
static bool parse_window_size(struct pss_tty *pss, int *cols, int *rows) { static json_object *parse_window_size(const char *buf, size_t len, uint16_t *cols, uint16_t *rows) {
char json[pss->len]; json_tokener *tok = json_tokener_new();
strncpy(json, pss->buffer + 1, pss->len - 1); json_object *obj = json_tokener_parse_ex(tok, buf, len);
json[pss->len - 1] = '\0';
json_object *obj = json_tokener_parse(json);
struct json_object *o = NULL; struct json_object *o = NULL;
if (!json_object_object_get_ex(obj, "columns", &o)) { if (json_object_object_get_ex(obj, "columns", &o)) *cols = (uint16_t)json_object_get_int(o);
lwsl_err("columns field not exists, json: %s\n", json); if (json_object_object_get_ex(obj, "rows", &o)) *rows = (uint16_t)json_object_get_int(o);
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);
return true; json_tokener_free(tok);
return obj;
} }
static bool check_host_origin(struct lws *wsi) { static bool check_host_origin(struct lws *wsi) {
int origin_length = lws_hdr_total_length(wsi, WSI_TOKEN_ORIGIN); char buf[256];
char buf[origin_length + 1];
memset(buf, 0, sizeof(buf)); memset(buf, 0, sizeof(buf));
int len = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_ORIGIN); int len = lws_hdr_copy(wsi, buf, (int)sizeof(buf), WSI_TOKEN_ORIGIN);
if (len <= 0) { if (len <= 0) return false;
return false;
}
const char *prot, *address, *path; const char *prot, *address, *path;
int port; int port;
@ -78,154 +62,140 @@ static bool check_host_origin(struct lws *wsi) {
sprintf(buf, "%s:%d", address, port); sprintf(buf, "%s:%d", address, port);
} }
int host_length = lws_hdr_total_length(wsi, WSI_TOKEN_HOST); char host_buf[256];
if (host_length != strlen(buf)) return false;
char host_buf[host_length + 1];
memset(host_buf, 0, sizeof(host_buf)); 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; return len > 0 && strcasecmp(buf, host_buf) == 0;
} }
static void close_cb(uv_handle_t *handle) { static pty_ctx_t *pty_ctx_init(struct pss_tty *pss) {
struct pty_proc *proc = container_of((uv_pipe_t *)handle, struct pty_proc, pipe); pty_ctx_t *ctx = xmalloc(sizeof(pty_ctx_t));
free(proc); ctx->pss = pss;
ctx->ws_closed = false;
return ctx;
} }
static void pty_proc_free(struct pty_proc *proc) { static void pty_ctx_free(pty_ctx_t *ctx) { free(ctx); }
uv_read_stop((uv_stream_t *)&proc->pipe);
close(proc->pty); static void process_read_cb(pty_process *process, pty_buf_t *buf, bool eof) {
if (proc->pty_buffer != NULL) { pty_ctx_t *ctx = (pty_ctx_t *)process->ctx;
free(proc->pty_buffer); if (ctx->ws_closed) {
proc->pty_buffer = NULL; pty_buf_free(buf);
return;
} }
for (int i = 0; i < proc->argc; i++) {
free(proc->args[i]); if (eof && !process_running(process))
} ctx->pss->lws_close_status = process->exit_code == 0 ? 1000 : 1006;
uv_close((uv_handle_t *)&proc->pipe, close_cb); else
ctx->pss->pty_buf = buf;
lws_callback_on_writable(ctx->pss->wsi);
} }
static void alloc_cb(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) { static void process_exit_cb(pty_process *process) {
buf->base = xmalloc(suggested_size); pty_ctx_t *ctx = (pty_ctx_t *)process->ctx;
buf->len = suggested_size; if (ctx->ws_closed) {
lwsl_notice("process killed with signal %d, pid: %d\n", process->exit_signal, process->pid);
goto done;
}
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);
done:
pty_ctx_free(ctx);
} }
static void read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { static char **build_args(struct pss_tty *pss) {
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 child_cb(uv_signal_t *handle, int signum) {
pid_t pid;
int stat;
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);
}
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];
int i, n = 0; int i, n = 0;
char **argv = xmalloc((server->argc + pss->argc + 1) * sizeof(char *));
for (i = 0; i < server->argc; i++) { for (i = 0; i < server->argc; i++) {
argv[n++] = server->argv[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; argv[n] = NULL;
uv_signal_start(&server->watcher, child_cb, SIGCHLD); return argv;
}
// ensure the lws socket fd close-on-exec static char **build_env(struct pss_tty *pss) {
fd_set_cloexec(lws_get_socket_fd(pss->wsi)); int i = 0, n = 2;
char **envp = xmalloc(n * sizeof(char *));
// create process with pseudo-tty // TERM
proc->pid = pty_fork(&proc->pty, argv[0], argv, server->terminal_type); envp[i] = xmalloc(36);
if (proc->pid < 0) { snprintf(envp[i], 36, "TERM=%s", server->terminal_type);
lwsl_err("pty_fork: %d (%s)\n", errno, strerror(errno)); i++;
return 1;
// 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++;
} }
lwsl_notice("started process, pid: %d\n", proc->pid); envp[i] = NULL;
proc->pipe.data = pss; return envp;
uv_pipe_open(&proc->pipe, proc->pty); }
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); lws_callback_on_writable(pss->wsi);
return 0; return true;
} }
static void kill_process(struct pty_proc *proc) { static void wsi_output(struct lws *wsi, pty_buf_t *buf) {
if (proc->pid <= 0) return; if (buf == NULL) return;
char *message = xmalloc(LWS_PRE + 1 + buf->len);
char *ptr = message + LWS_PRE;
pid_t pid = proc->pid; *ptr = OUTPUT;
int sig = server->sig_code; memcpy(ptr + 1, buf->base, buf->len);
char *sig_name = server->sig_name; size_t n = buf->len + 1;
lwsl_notice("killing process %d with signal: %d (%s)\n", pid, sig, sig_name); if (lws_write(wsi, (unsigned char *)ptr, n, LWS_WRITE_BINARY) < n) {
int pgid = getpgid(pid); lwsl_err("write OUTPUT to WS\n");
if (uv_kill(pgid > 0 ? -pgid : pid, sig) != 0) {
lwsl_err("kill: %d, errno: %d (%s)\n", pid, errno, strerror(errno));
} }
free(message);
} }
static void write_cb(uv_write_t *req, int status) { static bool check_auth(struct lws *wsi, struct pss_tty *pss) {
if (status != 0) lwsl_warn("uv_write callback returned status: %d\n", status); if (server->auth_header != NULL) {
free(req->data); return lws_hdr_custom_copy(wsi, pss->user, sizeof(pss->user), server->auth_header, strlen(server->auth_header)) > 0;
free(req); }
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, int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) {
size_t len) {
struct pss_tty *pss = (struct pss_tty *)user; struct pss_tty *pss = (struct pss_tty *)user;
struct pty_proc *proc;
char buf[256]; char buf[256];
size_t n = 0; 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"); lwsl_warn("refuse to serve WS client due to the --max-clients option.\n");
return 1; return 1;
} }
if (!check_auth(wsi, pss)) return 1;
n = lws_hdr_copy(wsi, pss->path, sizeof(pss->path), WSI_TOKEN_GET_URI); n = lws_hdr_copy(wsi, pss->path, sizeof(pss->path), WSI_TOKEN_GET_URI);
#if defined(LWS_ROLE_H2) #if defined(LWS_ROLE_H2)
if (n <= 0) n = lws_hdr_copy(wsi, pss->path, sizeof(pss->path), WSI_TOKEN_HTTP_COLON_PATH); if (n <= 0) n = lws_hdr_copy(wsi, pss->path, sizeof(pss->path), WSI_TOKEN_HTTP_COLON_PATH);
#endif #endif
if (strncmp(pss->path, endpoints.ws, n) != 0) { 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; return 1;
} }
@ -259,46 +230,31 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user,
case LWS_CALLBACK_ESTABLISHED: case LWS_CALLBACK_ESTABLISHED:
pss->initialized = false; pss->initialized = false;
pss->initial_cmd_index = 0;
pss->authenticated = false; pss->authenticated = false;
pss->wsi = wsi; pss->wsi = wsi;
pss->buffer = NULL; pss->lws_close_status = LWS_CLOSE_STATUS_NOSTATUS;
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);
if (server->url_arg) { if (server->url_arg) {
while (lws_hdr_copy_fragment(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_URI_ARGS, n++) > 0) { while (lws_hdr_copy_fragment(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_URI_ARGS, n++) > 0) {
if (strncmp(buf, "arg=", 4) == 0) { if (strncmp(buf, "arg=", 4) == 0) {
proc->args = xrealloc(proc->args, (proc->argc + 1) * sizeof(char *)); pss->args = xrealloc(pss->args, (pss->argc + 1) * sizeof(char *));
proc->args[proc->argc] = strdup(&buf[4]); pss->args[pss->argc] = strdup(&buf[4]);
proc->argc++; pss->argc++;
} }
} }
} }
LIST_INSERT_HEAD(&server->procs, proc, entry);
server->client_count++; server->client_count++;
#if LWS_LIBRARY_VERSION_NUMBER >= 2004000
lws_get_peer_simple(lws_get_network_wsi(wsi), pss->address, sizeof(pss->address)); 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); lwsl_notice("WS %s - %s, clients: %d\n", pss->path, pss->address, server->client_count);
break; break;
case LWS_CALLBACK_SERVER_WRITEABLE: case LWS_CALLBACK_SERVER_WRITEABLE:
proc = pss->proc;
if (!pss->initialized) { if (!pss->initialized) {
if (pss->initial_cmd_index == sizeof(initial_cmds)) { if (pss->initial_cmd_index == sizeof(initial_cmds)) {
pss->initialized = true; pss->initialized = true;
uv_read_start((uv_stream_t *)&proc->pipe, alloc_cb, read_cb); pty_resume(pss->process);
break; break;
} }
if (send_initial_message(wsi, pss->initial_cmd_index) < 0) { 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; break;
} }
// read error or client exited, close connection if (pss->lws_close_status > LWS_CLOSE_STATUS_NOSTATUS) {
if (proc->status == 0 || proc->pty_len == UV_EOF) { lws_close_reason(wsi, pss->lws_close_status, NULL, 0);
lws_close_reason(wsi, LWS_CLOSE_STATUS_NORMAL, NULL, 0);
return 1; 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) { if (pss->pty_buf != NULL) {
proc->pty_buffer[LWS_PRE] = OUTPUT; wsi_output(wsi, pss->pty_buf);
n = (size_t)(proc->pty_len + 1); pty_buf_free(pss->pty_buf);
if (lws_write(wsi, (unsigned char *)proc->pty_buffer + LWS_PRE, n, LWS_WRITE_BINARY) < n) { pss->pty_buf = NULL;
lwsl_err("write OUTPUT to WS\n"); 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; break;
case LWS_CALLBACK_RECEIVE: case LWS_CALLBACK_RECEIVE:
@ -357,49 +304,33 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user,
return 0; return 0;
} }
proc = pss->proc;
switch (command) { switch (command) {
case INPUT: case INPUT:
if (proc->pty == 0) break; if (!server->writable) break;
if (server->readonly) break; int err = pty_write(pss->process, pty_buf_init(pss->buffer + 1, pss->len - 1));
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 (err) { if (err) {
lwsl_err("uv_write: %s (%s)\n", uv_err_name(err), uv_strerror(err)); lwsl_err("uv_write: %s (%s)\n", uv_err_name(err), uv_strerror(err));
return -1; return -1;
} }
break; break;
case RESIZE_TERMINAL: { case RESIZE_TERMINAL:
int cols, rows; if (pss->process == NULL) break;
if (parse_window_size(pss, &cols, &rows)) { json_object_put(
if (pty_resize(proc->pty, cols, rows) < 0) { parse_window_size(pss->buffer + 1, pss->len - 1, &pss->process->columns, &pss->process->rows));
lwsl_err("pty_resize: %d (%s)\n", errno, strerror(errno)); pty_resize(pss->process);
} break;
}
} break;
case PAUSE: case PAUSE:
if (proc->state == STATE_INIT) { pty_pause(pss->process);
uv_read_stop((uv_stream_t *)&proc->pipe);
proc->state = STATE_PAUSE;
}
break; break;
case RESUME: case RESUME:
if (proc->state == STATE_PAUSE) { pty_resume(pss->process);
uv_read_start((uv_stream_t *)&proc->pipe, alloc_cb, read_cb);
proc->state = STATE_INIT;
}
break; break;
case JSON_DATA: 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) { if (server->credential != NULL) {
json_object *obj = json_tokener_parse(pss->buffer);
struct json_object *o = NULL; struct json_object *o = NULL;
if (json_object_object_get_ex(obj, "AuthToken", &o)) { if (json_object_object_get_ex(obj, "AuthToken", &o)) {
const char *token = json_object_get_string(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); lwsl_warn("WS authentication failed with token: %s\n", token);
} }
if (!pss->authenticated) { if (!pss->authenticated) {
json_object_put(obj);
lws_close_reason(wsi, LWS_CLOSE_STATUS_POLICY_VIOLATION, NULL, 0); lws_close_reason(wsi, LWS_CLOSE_STATUS_POLICY_VIOLATION, NULL, 0);
return -1; return -1;
} }
} }
if (spawn_process(pss) != 0) return 1; json_object_put(obj);
if (!spawn_process(pss, columns, rows)) return 1;
break; break;
default: default:
lwsl_warn("ignored unknown message type: %c\n", command); 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--; server->client_count--;
lwsl_notice("WS closed from %s, clients: %d\n", pss->address, server->client_count); lwsl_notice("WS closed from %s, clients: %d\n", pss->address, server->client_count);
if (pss->buffer != NULL) { if (pss->buffer != NULL) free(pss->buffer);
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 (pss->process != NULL) {
if (proc->state == STATE_EXIT) { ((pty_ctx_t *)pss->process->ctx)->ws_closed = true;
pty_proc_free(proc); if (process_running(pss->process)) {
} else { pty_pause(pss->process);
proc->state = STATE_KILL; lwsl_notice("killing process, pid: %d\n", pss->process->pid);
uv_read_stop((uv_stream_t *)&proc->pipe); pty_kill(pss->process, server->sig_code);
kill_process(proc); }
} }
if (server->once && server->client_count == 0) { if ((server->once || server->exit_no_conn) && server->client_count == 0) {
lwsl_notice("exiting due to the --once option.\n"); lwsl_notice("exiting due to the --once/--exit-no-conn option.\n");
force_exit = true; force_exit = true;
lws_cancel_service(context); lws_cancel_service(context);
exit(0); 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,46 +22,44 @@ struct lws_context *context;
struct server *server; struct server *server;
struct endpoints endpoints = {"/ws", "/", "/token", ""}; struct endpoints endpoints = {"/ws", "/", "/token", ""};
extern int callback_http(struct lws *wsi, enum lws_callback_reasons reason, extern int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len);
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_tty(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len);
// websocket protocols // websocket protocols
static const struct lws_protocols protocols[] = { static const struct lws_protocols protocols[] = {{"http-only", callback_http, sizeof(struct pss_http), 0},
{"http-only", callback_http, sizeof(struct pss_http), 0},
{"tty", callback_tty, sizeof(struct pss_tty), 0}, {"tty", callback_tty, sizeof(struct pss_tty), 0},
{NULL, NULL, 0, 0}}; {NULL, NULL, 0, 0}};
#ifndef LWS_WITHOUT_EXTENSIONS #ifndef LWS_WITHOUT_EXTENSIONS
// websocket extensions // websocket extensions
static const struct lws_extension extensions[] = { static const struct lws_extension extensions[] = {
{"permessage-deflate", lws_extension_callback_pm_deflate, {"permessage-deflate", lws_extension_callback_pm_deflate, "permessage-deflate"},
"permessage-deflate"},
{"deflate-frame", lws_extension_callback_pm_deflate, "deflate_frame"}, {"deflate-frame", lws_extension_callback_pm_deflate, "deflate_frame"},
{NULL, NULL, NULL}}; {NULL, NULL, NULL}};
#endif #endif
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000 #if LWS_LIBRARY_VERSION_NUMBER >= 4000000
static const uint32_t backoff_ms[] = { 1000, 2000, 3000, 4000, 5000 }; static const uint32_t backoff_ms[] = {1000, 2000, 3000, 4000, 5000};
static lws_retry_bo_t retry = { static lws_retry_bo_t retry = {
.retry_ms_table = backoff_ms, .retry_ms_table = backoff_ms,
.retry_ms_table_count = LWS_ARRAY_SIZE(backoff_ms), .retry_ms_table_count = LWS_ARRAY_SIZE(backoff_ms),
.conceal_count = LWS_ARRAY_SIZE(backoff_ms), .conceal_count = LWS_ARRAY_SIZE(backoff_ms),
.secs_since_valid_ping = 300, .secs_since_valid_ping = 5,
.secs_since_valid_hangup = 300 + 7, .secs_since_valid_hangup = 10,
.jitter_percent = 0, .jitter_percent = 0,
}; };
#endif #endif
// command line options // command line options
static const struct option options[] = { static const struct option options[] = {{"port", required_argument, NULL, 'p'},
{"port", required_argument, NULL, 'p'},
{"interface", required_argument, NULL, 'i'}, {"interface", required_argument, NULL, 'i'},
{"socket-owner", required_argument, NULL, 'U'},
{"credential", required_argument, NULL, 'c'}, {"credential", required_argument, NULL, 'c'},
{"auth-header", required_argument, NULL, 'H'},
{"uid", required_argument, NULL, 'u'}, {"uid", required_argument, NULL, 'u'},
{"gid", required_argument, NULL, 'g'}, {"gid", required_argument, NULL, 'g'},
{"signal", required_argument, NULL, 's'}, {"signal", required_argument, NULL, 's'},
{"cwd", required_argument, NULL, 'w'},
{"index", required_argument, NULL, 'I'}, {"index", required_argument, NULL, 'I'},
{"base-path", required_argument, NULL, 'b'}, {"base-path", required_argument, NULL, 'b'},
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000 #if LWS_LIBRARY_VERSION_NUMBER >= 4000000
@ -73,24 +71,19 @@ static const struct option options[] = {
{"ssl-key", required_argument, NULL, 'K'}, {"ssl-key", required_argument, NULL, 'K'},
{"ssl-ca", required_argument, NULL, 'A'}, {"ssl-ca", required_argument, NULL, 'A'},
{"url-arg", no_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'}, {"terminal-type", required_argument, NULL, 'T'},
{"client-option", required_argument, NULL, 't'}, {"client-option", required_argument, NULL, 't'},
{"check-origin", no_argument, NULL, 'O'}, {"check-origin", no_argument, NULL, 'O'},
{"max-clients", required_argument, NULL, 'm'}, {"max-clients", required_argument, NULL, 'm'},
{"once", no_argument, NULL, 'o'}, {"once", no_argument, NULL, 'o'},
{"exit-no-conn", no_argument, NULL, 'q'},
{"browser", no_argument, NULL, 'B'}, {"browser", no_argument, NULL, 'B'},
{"debug", required_argument, NULL, 'd'}, {"debug", required_argument, NULL, 'd'},
{"version", no_argument, NULL, 'v'}, {"version", no_argument, NULL, 'v'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{NULL, 0, 0, 0}}; {NULL, 0, 0, 0}};
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";
#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 void print_help() { static void print_help() {
// clang-format off // clang-format off
@ -102,22 +95,26 @@ static void print_help() {
"OPTIONS:\n" "OPTIONS:\n"
" -p, --port Port to listen (default: 7681, use `0` for random port)\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" " -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" " -u, --uid User id to run with\n"
" -g, --gid Group 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" " -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" " -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, --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" " -T, --terminal-type Terminal type to report, default: xterm-256color\n"
" -O, --check-origin Do not allow websocket connection from different origin\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" " -m, --max-clients Maximum clients to support (default: 0, no limit)\n"
" -o, --once Accept only one client and exit on disconnection\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" " -B, --browser Open terminal with the default system browser\n"
" -I, --index Custom index.html path\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 #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 #endif
#ifdef LWS_WITH_IPV6 #ifdef LWS_WITH_IPV6
" -6, --ipv6 Enable IPv6 support\n" " -6, --ipv6 Enable IPv6 support\n"
@ -137,6 +134,30 @@ static void print_help() {
// clang-format on // 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) { static struct server *server_new(int argc, char **argv, int start) {
struct server *ts; struct server *ts;
size_t cmd_len = 0; 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)); ts = xmalloc(sizeof(struct server));
memset(ts, 0, sizeof(struct server)); memset(ts, 0, sizeof(struct server));
LIST_INIT(&ts->procs);
ts->client_count = 0; ts->client_count = 0;
ts->sig_code = SIGHUP; ts->sig_code = SIGHUP;
sprintf(ts->terminal_type, "%s", "xterm-256color"); 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); ts->command = xmalloc(cmd_len + 1);
char *ptr = ts->command; char *ptr = ts->command;
for (int i = 0; i < cmd_argc; i++) { 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) { if (i != cmd_argc - 1) {
*ptr++ = ' '; *ptr++ = ' ';
} }
@ -176,8 +197,6 @@ static struct server *server_new(int argc, char **argv, int start) {
ts->loop = xmalloc(sizeof *ts->loop); ts->loop = xmalloc(sizeof *ts->loop);
uv_loop_init(ts->loop); uv_loop_init(ts->loop);
uv_signal_init(ts->loop, &ts->watcher);
ts->watcher.data = &ts->procs;
return ts; return ts;
} }
@ -185,22 +204,25 @@ static struct server *server_new(int argc, char **argv, int start) {
static void server_free(struct server *ts) { static void server_free(struct server *ts) {
if (ts == NULL) return; if (ts == NULL) return;
if (ts->credential != NULL) free(ts->credential); 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->index != NULL) free(ts->index);
if (ts->cwd != NULL) free(ts->cwd);
free(ts->command); free(ts->command);
free(ts->prefs_json); free(ts->prefs_json);
int i = 0;
do { char **p = ts->argv;
free(ts->argv[i++]); for (; *p; p++) free(*p);
} while (ts->argv[i] != NULL);
free(ts->argv); free(ts->argv);
if (strlen(ts->socket_path) > 0) { if (strlen(ts->socket_path) > 0) {
struct stat st; struct stat st;
if (!stat(ts->socket_path, &st)) { if (!stat(ts->socket_path, &st)) {
unlink(ts->socket_path); unlink(ts->socket_path);
} }
} }
uv_signal_stop(&ts->watcher);
uv_loop_close(ts->loop); uv_loop_close(ts->loop);
free(ts->loop); free(ts->loop);
free(ts); free(ts);
} }
@ -212,8 +234,7 @@ static void signal_cb(uv_signal_t *watcher, int signum) {
case SIGINT: case SIGINT:
case SIGTERM: case SIGTERM:
get_sig_name(watcher->signum, sig_name, sizeof(sig_name)); get_sig_name(watcher->signum, sig_name, sizeof(sig_name));
lwsl_notice("received signal: %s (%d), exiting...\n", sig_name, lwsl_notice("received signal: %s (%d), exiting...\n", sig_name, watcher->signum);
watcher->signum);
break; break;
default: default:
signal(SIGABRT, SIG_DFL); signal(SIGABRT, SIG_DFL);
@ -222,14 +243,22 @@ static void signal_cb(uv_signal_t *watcher, int signum) {
if (force_exit) exit(EXIT_FAILURE); if (force_exit) exit(EXIT_FAILURE);
force_exit = true; force_exit = true;
lws_cancel_service(context); lws_cancel_service(context);
#if LWS_LIBRARY_VERSION_MAJOR >= 3
uv_stop(server->loop); uv_stop(server->loop);
lwsl_notice("send ^C to force exit.\n"); lwsl_notice("send ^C to force exit.\n");
#else }
lws_libuv_stop(context);
exit(EXIT_SUCCESS); static int parse_int(char *name, char *str) {
#endif 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) { static int calc_command_start(int argc, char **argv) {
@ -274,6 +303,12 @@ int main(int argc, char **argv) {
print_help(); print_help();
return 0; 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); int start = calc_command_start(argc, argv);
server = server_new(argc, argv, start); server = server_new(argc, argv, start);
@ -286,15 +321,15 @@ int main(int argc, char **argv) {
info.gid = -1; info.gid = -1;
info.uid = -1; info.uid = -1;
info.max_http_header_pool = 16; info.max_http_header_pool = 16;
info.options = LWS_SERVER_OPTION_LIBUV | LWS_SERVER_OPTION_VALIDATE_UTF8 | info.options = LWS_SERVER_OPTION_LIBUV | LWS_SERVER_OPTION_VALIDATE_UTF8 | LWS_SERVER_OPTION_DISABLE_IPV6;
LWS_SERVER_OPTION_DISABLE_IPV6;
#ifndef LWS_WITHOUT_EXTENSIONS #ifndef LWS_WITHOUT_EXTENSIONS
info.extensions = extensions; info.extensions = extensions;
#endif #endif
info.max_http_header_data = 20480; info.max_http_header_data = 65535;
int debug_level = LLL_ERR | LLL_WARN | LLL_NOTICE; int debug_level = LLL_ERR | LLL_WARN | LLL_NOTICE;
char iface[128] = ""; char iface[128] = "";
char socket_owner[128] = "";
bool browser = false; bool browser = false;
bool ssl = false; bool ssl = false;
char cert_path[1024] = ""; char cert_path[1024] = "";
@ -303,6 +338,10 @@ int main(int argc, char **argv) {
struct json_object *client_prefs = json_object_new_object(); 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 // parse command line options
int c; int c;
while ((c = getopt_long(start, argv, opt_string, options, NULL)) != -1) { 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); printf("ttyd version %s\n", TTYD_VERSION);
return 0; return 0;
case 'd': case 'd':
debug_level = atoi(optarg); debug_level = parse_int("debug", optarg);
break; break;
case 'a': case 'a':
server->url_arg = true; server->url_arg = true;
break; break;
case 'R': case 'W':
server->readonly = true; server->writable = true;
break; break;
case 'O': case 'O':
server->check_origin = true; server->check_origin = true;
break; break;
case 'm': case 'm':
server->max_clients = atoi(optarg); server->max_clients = parse_int("max-clients", optarg);
break; break;
case 'o': case 'o':
server->once = true; server->once = true;
break; break;
case 'q':
server->exit_no_conn = true;
break;
case 'B': case 'B':
browser = true; browser = true;
break; break;
case 'p': case 'p':
info.port = atoi(optarg); info.port = parse_int("port", optarg);
if (info.port < 0) { if (info.port < 0) {
fprintf(stderr, "ttyd: invalid port: %s\n", optarg); fprintf(stderr, "ttyd: invalid port: %s\n", optarg);
return -1; return -1;
@ -345,20 +387,27 @@ int main(int argc, char **argv) {
strncpy(iface, optarg, sizeof(iface) - 1); strncpy(iface, optarg, sizeof(iface) - 1);
iface[sizeof(iface) - 1] = '\0'; iface[sizeof(iface) - 1] = '\0';
break; break;
case 'U':
strncpy(socket_owner, optarg, sizeof(socket_owner) - 1);
socket_owner[sizeof(socket_owner) - 1] = '\0';
break;
case 'c': case 'c':
if (strchr(optarg, ':') == NULL) { if (strchr(optarg, ':') == NULL) {
fprintf(stderr, fprintf(stderr, "ttyd: invalid credential, format: username:password\n");
"ttyd: invalid credential, format: username:password\n");
return -1; return -1;
} }
server->credential = char b64_text[256];
base64_encode((const unsigned char *)optarg, strlen(optarg)); 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; break;
case 'u': case 'u':
info.uid = atoi(optarg); info.uid = parse_int("uid", optarg);
break; break;
case 'g': case 'g':
info.gid = atoi(optarg); info.gid = parse_int("gid", optarg);
break; break;
case 's': { case 's': {
int sig = get_sig(optarg); int sig = get_sig(optarg);
@ -370,6 +419,9 @@ int main(int argc, char **argv) {
return -1; return -1;
} }
} break; } break;
case 'w':
server->cwd = strdup(optarg);
break;
case 'I': case 'I':
if (!strncmp(optarg, "~/", 2)) { if (!strncmp(optarg, "~/", 2)) {
const char *home = getenv("HOME"); const char *home = getenv("HOME");
@ -380,13 +432,11 @@ int main(int argc, char **argv) {
} }
struct stat st; struct stat st;
if (stat(server->index, &st) == -1) { if (stat(server->index, &st) == -1) {
fprintf(stderr, "Can not stat index.html: %s, error: %s\n", fprintf(stderr, "Can not stat index.html: %s, error: %s\n", server->index, strerror(errno));
server->index, strerror(errno));
return -1; return -1;
} }
if (S_ISDIR(st.st_mode)) { if (S_ISDIR(st.st_mode)) {
fprintf(stderr, "Invalid index.html path: %s, is it a dir?\n", fprintf(stderr, "Invalid index.html path: %s, is it a dir?\n", server->index);
server->index);
return -1; return -1;
} }
break; break;
@ -403,15 +453,15 @@ int main(int argc, char **argv) {
#undef sc #undef sc
} break; } break;
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000 #if LWS_LIBRARY_VERSION_NUMBER >= 4000000
case 'P': case 'P': {
if (atoi(optarg) <= 0) { int interval = parse_int("ping-interval", optarg);
if (interval < 0) {
fprintf(stderr, "ttyd: invalid ping interval: %s\n", optarg); fprintf(stderr, "ttyd: invalid ping interval: %s\n", optarg);
return -1; return -1;
} }
retry.secs_since_valid_ping = atoi(optarg); retry.secs_since_valid_ping = interval;
retry.secs_since_valid_hangup = atoi(optarg) + 7; retry.secs_since_valid_hangup = interval + 7;
info.retry_and_idle_policy = &retry; } break;
break;
#endif #endif
case '6': case '6':
info.options &= ~(LWS_SERVER_OPTION_DISABLE_IPV6); info.options &= ~(LWS_SERVER_OPTION_DISABLE_IPV6);
@ -434,8 +484,7 @@ int main(int argc, char **argv) {
break; break;
#endif #endif
case 'T': case 'T':
strncpy(server->terminal_type, optarg, strncpy(server->terminal_type, optarg, sizeof(server->terminal_type) - 1);
sizeof(server->terminal_type) - 1);
server->terminal_type[sizeof(server->terminal_type) - 1] = '\0'; server->terminal_type[sizeof(server->terminal_type) - 1] = '\0';
break; break;
case '?': case '?':
@ -443,26 +492,19 @@ int main(int argc, char **argv) {
case 't': case 't':
optind--; optind--;
for (; optind < start && *argv[optind] != '-'; optind++) { for (; optind < start && *argv[optind] != '-'; optind++) {
char *option = strdup(optarg); char *option = optarg;
char *key = strsep(&option, "="); char *key = strsep(&option, "=");
if (key == NULL) { if (key == NULL) {
fprintf(stderr, fprintf(stderr, "ttyd: invalid client option: %s, format: key=value\n", optarg);
"ttyd: invalid client option: %s, format: key=value\n",
optarg);
return -1; return -1;
} }
char *value = strsep(&option, "="); char *value = strsep(&option, "=");
if (value == NULL) { if (value == NULL) {
fprintf(stderr, fprintf(stderr, "ttyd: invalid client option: %s, format: key=value\n", optarg);
"ttyd: invalid client option: %s, format: key=value\n",
optarg);
return -1; return -1;
} }
free(option);
struct json_object *obj = json_tokener_parse(value); struct json_object *obj = json_tokener_parse(value);
json_object_object_add( json_object_object_add(client_prefs, key, obj != NULL ? obj : json_object_new_string(value));
client_prefs, key,
obj != NULL ? obj : json_object_new_string(value));
} }
break; break;
default: default:
@ -480,14 +522,14 @@ int main(int argc, char **argv) {
lws_set_log_level(debug_level, NULL); lws_set_log_level(debug_level, NULL);
#if LWS_LIBRARY_VERSION_MAJOR >= 2
char server_hdr[128] = ""; char server_hdr[128] = "";
sprintf(server_hdr, "ttyd/%s (libwebsockets/%s)", TTYD_VERSION, sprintf(server_hdr, "ttyd/%s (libwebsockets/%s)", TTYD_VERSION, LWS_LIBRARY_VERSION);
LWS_LIBRARY_VERSION);
info.server_string = server_hdr; 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; info.ws_ping_pong_interval = 5;
#else
info.retry_and_idle_policy = &retry;
#endif #endif
if (strlen(iface) > 0) { if (strlen(iface) > 0) {
@ -497,9 +539,11 @@ int main(int argc, char **argv) {
info.options |= LWS_SERVER_OPTION_UNIX_SOCK; info.options |= LWS_SERVER_OPTION_UNIX_SOCK;
info.port = 0; // warmcat/libwebsockets#1985 info.port = 0; // warmcat/libwebsockets#1985
strncpy(server->socket_path, info.iface, sizeof(server->socket_path) - 1); strncpy(server->socket_path, info.iface, sizeof(server->socket_path) - 1);
if (strlen(socket_owner) > 0) {
info.unix_socket_perms = socket_owner;
}
#else #else
fprintf(stderr, fprintf(stderr, "libwebsockets is not compiled with UNIX domain socket support");
"libwebsockets is not compiled with UNIX domain socket support");
return -1; return -1;
#endif #endif
} }
@ -509,46 +553,32 @@ int main(int argc, char **argv) {
if (ssl) { if (ssl) {
info.ssl_cert_filepath = cert_path; info.ssl_cert_filepath = cert_path;
info.ssl_private_key_filepath = key_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.ssl_ca_filepath = ca_path;
info.options |= LWS_SERVER_OPTION_REQUIRE_VALID_OPENSSL_CLIENT_CERT; 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; info.options |= LWS_SERVER_OPTION_ALLOW_NON_SSL_ON_SSL_PORT | LWS_SERVER_OPTION_REDIRECT_HTTP_TO_HTTPS;
#endif
} }
#endif #endif
lwsl_notice("ttyd %s (libwebsockets %s)\n", TTYD_VERSION, lwsl_notice("ttyd %s (libwebsockets %s)\n", TTYD_VERSION, LWS_LIBRARY_VERSION);
LWS_LIBRARY_VERSION); print_config();
lwsl_notice("tty configuration:\n");
if (server->credential != NULL) // lws custom header requires lower case name, and terminating :
lwsl_notice(" credential: %s\n", server->credential); if (server->auth_header != NULL) {
lwsl_notice(" start command: %s\n", server->command); size_t auth_header_len = strlen(server->auth_header);
lwsl_notice(" close signal: %s (%d)\n", server->sig_name, server->sig_code); server->auth_header = xrealloc(server->auth_header, auth_header_len + 2);
lwsl_notice(" terminal type: %s\n", server->terminal_type); strcat(server->auth_header + auth_header_len, ":");
if (endpoints.parent[0]) { lowercase(server->auth_header);
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);
} }
#if LWS_LIBRARY_VERSION_MAJOR >= 3
void *foreign_loops[1]; void *foreign_loops[1];
foreign_loops[0] = server->loop; foreign_loops[0] = server->loop;
info.foreign_loops = foreign_loops; info.foreign_loops = foreign_loops;
info.options |= LWS_SERVER_OPTION_EXPLICIT_VHOSTS; info.options |= LWS_SERVER_OPTION_EXPLICIT_VHOSTS;
#endif
context = lws_create_context(&info); context = lws_create_context(&info);
if (context == NULL) { if (context == NULL) {
@ -556,16 +586,12 @@ int main(int argc, char **argv) {
return 1; return 1;
} }
#if LWS_LIBRARY_VERSION_MAJOR >= 3
struct lws_vhost *vhost = lws_create_vhost(context, &info); struct lws_vhost *vhost = lws_create_vhost(context, &info);
if (vhost == NULL) { if (vhost == NULL) {
lwsl_err("libwebsockets vhost creation failed\n"); lwsl_err("libwebsockets vhost creation failed\n");
return 1; return 1;
} }
int port = lws_get_vhost_listen_port(vhost); int port = lws_get_vhost_listen_port(vhost);
#else
int port = info.port;
#endif
lwsl_notice(" Listening on port: %d\n", port); lwsl_notice(" Listening on port: %d\n", port);
if (browser) { if (browser) {
@ -574,29 +600,20 @@ int main(int argc, char **argv) {
open_uri(url); open_uri(url);
} }
#if LWS_LIBRARY_VERSION_MAJOR >= 3 #define sig_count 2
int sig_nums[] = {SIGINT, SIGTERM}; int sig_nums[] = {SIGINT, SIGTERM};
int ns = sizeof(sig_nums) / sizeof(sig_nums[0]); uv_signal_t signals[sig_count];
uv_signal_t signals[ns]; for (int i = 0; i < sig_count; i++) {
for (int i = 0; i < ns; i++) {
uv_signal_init(server->loop, &signals[i]); uv_signal_init(server->loop, &signals[i]);
uv_signal_start(&signals[i], signal_cb, sig_nums[i]); uv_signal_start(&signals[i], signal_cb, sig_nums[i]);
} }
lws_service(context, 0); lws_service(context, 0);
for (int i = 0; i < ns; i++) { for (int i = 0; i < sig_count; i++) {
uv_signal_stop(&signals[i]); uv_signal_stop(&signals[i]);
} }
#else #undef sig_count
#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
lws_context_destroy(context); lws_context_destroy(context);

View file

@ -1,7 +1,8 @@
#include <libwebsockets.h>
#include <stdbool.h> #include <stdbool.h>
#include <uv.h> #include <uv.h>
#include "queue.h" #include "pty.h"
// client message // client message
#define INPUT '0' #define INPUT '0'
@ -15,8 +16,6 @@
#define SET_WINDOW_TITLE '1' #define SET_WINDOW_TITLE '1'
#define SET_PREFERENCES '2' #define SET_PREFERENCES '2'
#define MAX_READ_RETRY 2
// url paths // url paths
struct endpoints { struct endpoints {
char *ws; char *ws;
@ -30,8 +29,6 @@ extern struct lws_context *context;
extern struct server *server; extern struct server *server;
extern struct endpoints endpoints; extern struct endpoints endpoints;
typedef enum { STATE_INIT, STATE_PAUSE, STATE_KILL, STATE_EXIT } proc_state;
struct pss_http { struct pss_http {
char path[128]; char path[128];
char *buffer; char *buffer;
@ -39,58 +36,51 @@ struct pss_http {
size_t len; 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 { struct pss_tty {
bool initialized; bool initialized;
int initial_cmd_index; int initial_cmd_index;
bool authenticated; bool authenticated;
char user[30];
char address[50]; char address[50];
char path[20]; char path[128];
char **args;
int argc;
struct lws *wsi; struct lws *wsi;
char *buffer; char *buffer;
size_t len; 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 { struct server {
int client_count; // client count int client_count; // client count
char *prefs_json; // client preferences char *prefs_json; // client preferences
char *credential; // encoded basic auth credential char *credential; // encoded basic auth credential
char *auth_header; // header name used for auth proxy
char *index; // custom index.html char *index; // custom index.html
char *command; // full command line char *command; // full command line
char **argv; // command with arguments char **argv; // command with arguments
int argc; // command + arguments count int argc; // command + arguments count
char *cwd; // working directory
int sig_code; // close signal int sig_code; // close signal
char sig_name[20]; // human readable signal string char sig_name[20]; // human readable signal string
bool url_arg; // allow client to send cli arguments in URL 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 bool check_origin; // whether allow websocket connection from different origin
int max_clients; // maximum clients to support int max_clients; // maximum clients to support
bool once; // whether accept only one client and exit on disconnection 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 socket_path[255]; // UNIX domain socket path
char terminal_type[30]; // terminal type to report char terminal_type[30]; // terminal type to report
uv_loop_t *loop; // the libuv event loop 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> #include <string.h>
#if defined(__linux__) && !defined(__ANDROID__) #if defined(__linux__) && !defined(__ANDROID__)
// https://github.com/karelzak/util-linux/blob/master/misc-utils/kill.c
const char *sys_signame[NSIG] = { const char *sys_signame[NSIG] = {
"zero", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "UNUSED", "FPE", "zero", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "UNUSED", "FPE", "KILL", "USR1",
"KILL", "USR1", "SEGV", "USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "SEGV", "USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN",
"CONT", "STOP", "TSTP", "TTIN", "TTOU", "URG", "XCPU", "XFSZ", "VTALRM", "TTOU", "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR", "SYS", NULL};
"PROF", "WINCH", "IO", "PWR", "SYS", NULL};
#endif #endif
#if defined(_WIN32) || defined(__CYGWIN__) #if defined(_WIN32) || defined(__CYGWIN__)
#include <shellapi.h>
#include <windows.h> #include <windows.h>
// https://github.com/mirror/newlib-cygwin/blob/master/winsup/cygwin/strsig.cc #undef NSIG
#ifndef NSIG
#define NSIG 33 #define NSIG 33
#endif
const char *sys_signame[NSIG] = { const char *sys_signame[NSIG] = {
"zero", "HUP", "INT", "QUIT", "ILL", "TRAP", "IOT", "EMT", "FPE", "zero", "HUP", "INT", "QUIT", "ILL", "TRAP", "IOT", "EMT", "FPE", "KILL", "BUS",
"KILL", "BUS", "SEGV", "SYS", "PIPE", "ALRM", "TERM", "URG", "STOP", "SEGV", "SYS", "PIPE", "ALRM", "TERM", "URG", "STOP", "TSTP", "CONT", "CHLD", "TTIN",
"TSTP", "CONT", "CHLD", "TTIN", "TTOU", "IO", "XCPU", "XFSZ", "VTALRM", "TTOU", "IO", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "PWR", "USR1", "USR2", NULL};
"PROF", "WINCH", "PWR", "USR1", "USR2", NULL};
#endif #endif
void *xmalloc(size_t size) { void *xmalloc(size_t size) {
@ -43,12 +37,20 @@ void *xrealloc(void *p, size_t size) {
return p; return p;
} }
char *uppercase(char *str) { char *uppercase(char *s) {
int i = 0; while(*s) {
do { *s = (char)toupper((int)*s);
str[i] = (char)toupper(str[i]); s++;
} while (str[i++] != '\0'); }
return str; return s;
}
char *lowercase(char *s) {
while(*s) {
*s = (char)tolower((int)*s);
s++;
}
return s;
} }
bool endswith(const char *str, const char *suffix) { 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 get_sig_name(int sig, char *buf, size_t len) {
int n = int n = snprintf(buf, len, "SIG%s", sig < NSIG ? sys_signame[sig] : "unknown");
snprintf(buf, len, "SIG%s", sig < NSIG ? sys_signame[sig] : "unknown");
uppercase(buf); uppercase(buf);
return n; return n;
} }
@ -67,27 +68,19 @@ int get_sig_name(int sig, char *buf, size_t len) {
int get_sig(const char *sig_name) { int get_sig(const char *sig_name) {
for (int sig = 1; sig < NSIG; sig++) { for (int sig = 1; sig < NSIG; sig++) {
const char *name = sys_signame[sig]; const char *name = sys_signame[sig];
if (name != NULL && (strcasecmp(name, sig_name) == 0 || if (name != NULL && (strcasecmp(name, sig_name) == 0 || strcasecmp(name, sig_name + 3) == 0))
strcasecmp(name, sig_name + 3) == 0))
return sig; return sig;
} }
return atoi(sig_name); 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) { int open_uri(char *uri) {
#ifdef __APPLE__ #ifdef __APPLE__
char command[256]; char command[256];
sprintf(command, "open %s > /dev/null 2>&1", uri); sprintf(command, "open %s > /dev/null 2>&1", uri);
return system(command); return system(command);
#elif defined(_WIN32) || defined(__CYGWIN__) #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 #else
// check if X server is running // check if X server is running
if (system("xset -q > /dev/null 2>&1")) return 1; if (system("xset -q > /dev/null 2>&1")) return 1;
@ -97,28 +90,74 @@ int open_uri(char *uri) {
#endif #endif
} }
// https://github.com/darkk/redsocks/blob/master/base64.c #ifdef _WIN32
char *base64_encode(const unsigned char *buffer, size_t length) { char *strsep(char **sp, char *sep) {
static const char b64[] = char *p, *s;
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; if (sp == NULL || *sp == NULL || **sp == '\0') return (NULL);
char *ret, *dst; s = *sp;
unsigned i_bits = 0; p = s + strcspn(s, sep);
int i_shift = 0; if (*p != '\0') *p++ = '\0';
int bytes_remaining = (int)length; *sp = p;
return s;
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));
}
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); void *xrealloc(void *p, size_t size);
// Convert a string to upper case // 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 // Check whether str ends with suffix
bool endswith(const char *str, const char *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 // Get signal code from string like SIGHUP
int get_sig(const char *sig_name); 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 // Open uri with the default application of system
int open_uri(char *uri); int open_uri(char *uri);
// Encode text to base64, the caller should free the returned string #ifdef _WIN32
char *base64_encode(const unsigned char *buffer, size_t length); char *strsep(char **sp, char *sep);
const char *quote_arg(const char *arg);
void print_error(char *func);
#endif
#endif // TTYD_UTIL_H #endif // TTYD_UTIL_H