Compare commits

...

109 commits
v0.3.0 ... main

Author SHA1 Message Date
3f6edc7662 Hubris 2024-08-17 08:30:41 -07:00
aad9c74a63 Vanity 2024-08-17 08:29:27 -07:00
eede5b0e50 Support showing and hiding anonymous ports
I'm still not convinced that showing a big list of disabled ports is
the right thing to do so here's the ability to turn it off.
2024-08-17 08:03:41 -07:00
3430cae957 Add fwd-browse to the debian package 2024-08-16 10:36:56 -07:00
a4df8fc588 This is a better name for the build 2024-08-16 10:29:37 -07:00
9e8fa4d0a6 Add debian build to release matrix 2024-08-16 10:26:50 -07:00
940e573468 Support for debian packaging 2024-08-16 10:23:57 -07:00
f13139e79b Remove older documentation notes 2024-08-16 10:23:44 -07:00
ff92002dcf Update year I guess 2024-08-16 10:22:53 -07:00
2d1c8a4ceb Set the github token for the release action 2024-08-15 11:59:00 -07:00
241e8e1eea This is broken because I have consummate vs 2024-08-15 11:52:50 -07:00
666456e456 More release stuff (tools) 2024-08-15 11:50:44 -07:00
73126ba770 Update the release workflow
Use the python automation script instead
2024-08-15 11:43:53 -07:00
9c9f7cfa82 Release automation
There are a lot of steps in preparing the release and so I'm trying to
make sure that we're in a place where I can iterate on it locally.
2024-08-15 11:40:04 -07:00
7a40326719 Re-work config code
Add raw description as a possible config for a port, and update the
documentation appropriately.
2024-08-15 10:14:43 -07:00
74e2da2f29 Man page edits 2024-08-14 11:24:23 -07:00
cfde429786 A man page, somewhat 2024-08-14 11:22:50 -07:00
afa13bf920 This description is out of date
Given the introduction of anonymous ports
2024-08-14 10:52:19 -07:00
38fbfbd918 Move config file to ~/.config/fwd/config.toml
Presumably this also works for MacOS and windows.

While doing this, move away from xdg and home and use this
directories-next crate instead. Reverse connections still seem to
work.
2024-08-14 10:51:19 -07:00
663ce42016 tempdir -> tempfile
According to the documentation of the tempdir crate
2024-08-13 10:59:47 -07:00
e44d4dea7a Also update the fuzzing targets, I guess 2024-08-13 10:56:29 -07:00
4fe255e7d2 Fix colors in the help box
When the lines of the help box overlap with disabled or error'd ports
you might notice that those lines are dark grey or red. That's
surprising!

The bug is that Style::default() means "don't change anything", just
continue being whatever color the current cell is, which is deeply
surprising. What we really want here is `Style::reset()`, which means
"reset the colors to whatever the terminal would show by default."
2024-08-13 10:52:20 -07:00
b381f71692 Move from tui to ratatui
Tui is no longer supported, ratatui is the new hotness. Fortunately
there is very little difference between the two, except I've noticed a
fun new bug in the help screen. (Maybe it's been there the whole time?)
2024-08-13 10:44:58 -07:00
7e047626df Bump to the next version 2024-08-13 07:24:29 -07:00
68f3c4fa4e Experimental updates to release workflow 2024-08-13 07:23:59 -07:00
df914e68f2 I *think* I need something other than macos-12 for aarch64
https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories
2024-08-12 17:57:36 -07:00
a7202010d0 Probably we should run tests as part of release? 2024-08-12 17:43:17 -07:00
df0ca4ce31 Remove "users" crate, call libc directly
This is all I actually needed anyways
2024-08-12 17:37:34 -07:00
35dcf93971 Add fuzzing based on serde_json
This test ensures that we can parse anything that serde_json can
produce, which *ought* to ensure reasonable coverage?
2024-08-12 17:18:26 -07:00
43f6b75762 Rename fuzz target to something more meaningful 2024-08-12 11:44:02 -07:00
542127f723 Handle transfer-encoding chunked in docker responses
Yeah, OK, thanks HTTP.
2024-08-12 11:28:59 -07:00
665fccf753 Add trace logging to the docker refresh
That way we can see what's going on with docker responses if they're weird.
2024-08-12 10:07:42 -07:00
e27b788e8f Fuzzing for the json decoder
Hey it seems like it's working!
2024-08-12 09:43:56 -07:00
77cbf1700f Check for unterminated strings properly
Also, public to enable fuzzing. This was the first catch!
2024-08-12 09:41:22 -07:00
9b0a39fa90 Bump crate version 2024-08-12 09:14:22 -07:00
4647226ee7 Handle blank input a little more cleanly 2024-08-12 09:11:21 -07:00
03de4a4661 Yet another tweak to git process for cargo publish 2024-08-10 09:00:20 -07:00
9ad55c903f Speculative changes to github workflows 2024-08-10 08:13:45 -07:00
a9bbd29f9f Fix enable/disable state changes
Enabled -> Broken -> Disabled -> Enabled

etc
2024-08-10 07:59:46 -07:00
a4745c92e2 Anonymous ports
This is the other way to allow ports to work when the processes
themselves cannot be enumerated: just report the port with an empty
description. We need to do some work to make sure this is safe for the
client; see comments.
2024-08-10 07:44:06 -07:00
69b9bc9824 Sometimes you can't get a clipboard, don't fail
This should fix CI as well, but there are possibly folks out there
that don't have a functioning clipboard and still want to run.
2024-08-10 07:29:54 -07:00
bb8c87bad9 Anonymous ports are described with a fixed string 2024-08-10 07:02:46 -07:00
b86a09131b Don't enable anonymous ports by default 2024-08-10 06:58:22 -07:00
de06612eb1 Respect server-wide auto setting 2024-08-10 06:54:26 -07:00
cc004df6e8 Clippy 2024-08-10 06:48:24 -07:00
e32f27494a Tweak git status *again*
Turns out that in order to go from clean to dirty you need to
watch *everything*. Caching is hard, man.
2024-08-09 14:05:07 -07:00
c6aa657b4c Fix warning in windows 2024-08-08 21:13:14 -07:00
0ad0fb1a56 That was completely wrong
Look the whole point of the ports in the config is to have them
enabled *even if the server doesn't show them to you.* The other
behavior was just completely wrong in that respect.
2024-08-08 18:21:47 -07:00
8f12945d83 Fix up port state interactions in the face of configuration 2024-08-08 14:26:41 -07:00
6736cdd431 Additional tests for configuration 2024-08-08 10:26:36 -07:00
e1768a0433 Honor the configured description for a port, if any
Get rid of that dumb warning, can't believe I forgot to wire this for
so long.
2024-08-08 10:15:42 -07:00
2a582e25a8 Many fixes for the clipboard and others
- Reverse connections must be maintained to ensure messages are
  processed in order. (Whoops!)
- The clipboard context must remain live in order for the data to
  remain available for applications, at least on X11. (And it couldn't
  hurt elsewhere, either, I guess.)
- Print out the server version at startup time, so we can be sure what
  we're talking to.
- Print out the full details of the error when something goes wrong
  with `browse` or `clip`.
2024-08-08 10:07:57 -07:00
a3fa032500 Fix git status parsing 2024-08-08 07:18:13 -07:00
b8fe678ff0 Repository information in version 2024-08-07 12:14:17 -07:00
8a60f89110 Fix small JSON bugs, bring in test suite
Just decided to "harden" the JSON parser a little bit with the test
suite from https://github.com/nst/JSONTestSuite. Now I'm pretty sure
that we can handle whatever JSON docker throws at us.
2024-08-06 09:22:11 -07:00
9ef5515f01 Clippy 2024-08-06 06:51:03 -07:00
1f19792c58 Merge branch 'main' into quodlibetor/support-server-logs 2024-08-06 06:39:14 -07:00
Brandon W Maister
b983595049 feat: Show errored ports as an error state 2024-08-05 12:16:47 -07:00
5e96b37f5b Docker support
This is the most basic kind of docker querying you will find. Does not
support HTTPS. Seems to work for local docker engines. Has not been
tested against remote docker engines, or full URLs.

Note that if you want this to work you'll have to configure docker to
allow manipulation without being root, i.e., the user you connect as
will need to be in the `docker` group.

This was done instead of pulling in the `bollard` crate. Maybe I'm
being silly, but `bollard` uses a whole lot of other crates in the
name of being general and robust. These crates, however, add an
unacceptable size to the final binary. (In the experiment I ran, on a
release build, the binary size went from 2904696 to 4840968 bytes: an
increase of 1.8 MB. With this patch the release binary is 2986360
bytes, which is an increase of 80k.)

I wanted to see exactly what I could get away with when it came to
talking to docker. This here actually seems like a fine compromise:
HTTP is very simple if you only have to worry about one specific
server, and JSON is not very hard to parse if you don't care too much
about error handling, or are willing to play fast and loose with
punctuation (which I am).
2024-08-05 12:04:26 -07:00
Brandon W Maister
18da61ed32 make server logging show messages in the frontend
now you can use the log crate and get messages in the frontend.
2024-08-05 16:59:41 +01:00
8135f163f2 Refresh is async
Now we are ready for more asyncery (e.g., docker)
2024-08-04 08:29:33 -07:00
c2c57289cf Refactor refresh
Moving different mechanisms into different conditionally-compiled
modules. This way it can be extended, e.g. with docker lookups, MacOS
support, etc.
2024-08-04 08:14:19 -07:00
75343dbea2 Fix build break for windows (oops)
Really need to have better cross-building infrastructure for my own
personal garbage.
2024-08-04 08:11:00 -07:00
6335944591 One more test 2024-07-31 15:07:27 -07:00
3b1847d882 Require a file name for 'clip', use '-' for stdin
This makes argument parsing more reliable: to `fwd` to a server named
`clip` just leave off the file name.
2024-07-31 15:06:20 -07:00
46bd840bc0 Another test for clip garbage 2024-07-31 14:58:11 -07:00
604f31d8e6 Fix argument parsing (whoops)
This whole command line thing is actually busted; probably should make
it a little bit more robust.
2024-07-31 14:56:50 -07:00
3cb40bc2f4 Clippy 2024-07-31 14:45:20 -07:00
a40a493d39 Initial implementation of clipboard forwarding 2024-06-23 08:54:41 -07:00
fb86cbd0de Bump crate version for unreleased 2024-06-22 07:32:43 -07:00
3eba65f6e6 Refactor in prep for clip 2024-06-22 07:32:11 -07:00
3f7afc5b78 More clippy 2024-06-22 06:13:09 -07:00
08a41492b8 Clippy I guess 2024-06-21 08:26:56 -07:00
1e33561d92 Allow manual trigger of workflow dispatch 2024-04-14 06:51:26 -07:00
e11b6e025e Maybe release for aarch64? Who can say. 2024-04-13 14:59:19 -07:00
7766feafd4 Bump the version in the crate
Forgot to do this on release, whoops
2024-03-01 06:15:56 -08:00
10984034fa Supply the error message when connect fails
This might be too ugly
2024-02-29 13:16:16 -08:00
0368074ea0 Explicit tokio features
Somehow I thought this would make my binary smaller lol
2023-11-25 08:04:40 -08:00
2684d7f009 Upgrade dependencies 2023-11-25 07:58:00 -08:00
00daedeb95 Some silly refactoring 2023-11-25 07:57:52 -08:00
519b7bc415 Display the URL we're trying to open when it fails
This means that it is not lost forever and you can, I don't know,
click it in your terminal window or something.

(Thanks @quodlibetor for the patch!)
2023-11-25 07:33:44 -08:00
9671da9750 Make notes about the future 2023-08-28 09:25:46 -07:00
fd02779ba0 Bump version to 0.7.0 2023-04-30 07:33:55 -07:00
b85e3fa9a6 Fix crash on changing selection with no ports
No ports always means no selection
2023-04-30 07:33:06 -07:00
815ee5e86e Remove explanation for what fwd does
You probably know already, and it feels clunky here.
2023-04-30 07:24:18 -07:00
ec130d38b9 Support running server side with sudo
This allows forwarding ports that you would otherwise not be able to
see. More dangerous, probably not what you want most of the time, but
OK for now.

(I continue to resist adding clap as a dependency.)
2023-04-30 06:51:37 -07:00
d3d7b4f137 Bump cargo version to v0.6.2 2023-04-26 12:01:17 -07:00
59b8b8f3dc Cleanup some help text 2023-04-26 12:00:01 -07:00
0f8486d418 Upgrade dependencies 2023-04-26 11:53:15 -07:00
b74bf4aa9a The release that we create should be a draft
A human should have to publish it.
2023-04-26 11:47:48 -07:00
7deb8489e4 Bump the cargo version
Whoops I forgot.
2023-04-26 11:44:21 -07:00
a3c4c3ea5e Track the selection across refreshes by port number
The next step would obviously be to track the selection only by port
number, so that we remember the selection if the port goes away and
comes back.
2023-03-23 19:40:42 -07:00
99d377d4ce Make help modal
I don't like that ENTER and the arrow keys still manipulate the list
while help is showing. Ignore other keypresses while the help screen
is shown.

Also, make the spelling/capitalization a little cleaner.
2023-03-21 23:17:16 -07:00
34340e2575 Simplify centering math, don't crash if the rectangle clips
Also, tests
2023-03-21 23:07:08 -07:00
Brandon W Maister
290dcff9b6 tui: Add a help popup 2023-03-21 22:53:35 -07:00
Brandon W Maister
381c008665 tui: Add a fwd column with a ✓ to show that ports are being forwarded
I didn't realize that everything was forwarded by default, this makes it more
obvious.
2023-03-21 22:53:35 -07:00
7410ec5143 Add tests for keypresses 2023-03-21 22:48:06 -07:00
f174f364a4 Re-add assertions and simplify wrapping 2023-03-21 19:45:17 -07:00
Brandon W Maister
3060032a95 ui: Wrap around list when at the endoflife
Pressing "up"/"k" at the top of the UI will now go to the end of the list. And
same in reverse.
2023-02-18 11:27:58 -08:00
Brandon W Maister
85dc2f0707 ui: Switch j and k keys to match vi -- j means down, k means up 2023-02-18 11:27:23 -08:00
5fb0410eee Make README a little more clear
Thanks Digant!
2023-02-03 15:32:11 -08:00
Digant C Kasundra
19d6095c86
Make the README a little more clear 2023-02-03 18:21:35 -05:00
19e7baf89c Update dependencies
Thanks dependabot, I guess
2023-02-03 15:18:26 -08:00
766aad15f4 Update the version number for new args 2023-01-24 09:14:31 -08:00
fc144162b9 Better command line parsing, version, usage
Also tests for same
2023-01-24 09:13:53 -08:00
52fe5523b2 Update version to 0.4.0 for a clean release 2023-01-20 13:34:53 -08:00
e6a9caa1b5 Remove version from release archive
Makes downloading easier probably?
2023-01-20 13:27:00 -08:00
505eed6ea5 More stuff; use the cross compiler and stuff 2023-01-20 13:18:51 -08:00
33adbd3e27 Whoops bad YAML 2023-01-20 13:01:41 -08:00
156 changed files with 6682 additions and 812 deletions

View file

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose

View file

@ -1,105 +1,101 @@
# From https://github.com/BurntSushi/ripgrep/blob/master/.github/workflows/release.yml
# Which is also via https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/
# ...both of which are very good.
#
# I'm sure I don't need half the stuff I have in here (around cargo
# customization and whatnot) but.
#
name: release
on:
workflow_dispatch:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
permissions:
contents: write
jobs:
create_release:
name: Create release
runs-on: ubuntu-22.04
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
fwd_version: ${{ env.FWD_VERSION }}
runs-on: ubuntu-latest
steps:
- name: Get the release version from the tag
shell: bash
- uses: actions/checkout@v4
- name: Get the release version
if: env.VERSION == ''
run: echo "VERSION=${{ github.ref_name }}" >> $GITHUB_ENV
- name: Show the version
run: |
# Apparently, this is the right way to get a tag name. Really?
#
# See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
echo "FWD_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
echo "version is: ${{ env.FWD_VERSION }}"
echo "version is: $VERSION"
- name: Create GitHub release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.FWD_VERSION }}
release_name: ${{ env.FWD_VERSION }}
run: gh release create $VERSION --draft --verify-tag --title $VERSION
release_assets:
name: Release assets
needs: ['create_release'] # We need to know the upload URL
runs-on: ${{ matrix.config.os }} # We run many different builds
outputs:
version: ${{ env.VERSION }}
build_release:
name: Build all the stuff
needs: ['create_release'] # We need to know the upload URL
runs-on: ${{ matrix.os }} # We run many different builds
env:
# For some builds, we use cross to test on 32-bit and big-endian
# systems.
CARGO: cargo
# When CARGO is set to CROSS, this is set to `--target matrix.target`.
TARGET_FLAGS: ""
# When CARGO is set to CROSS, TARGET_DIR includes matrix.target.
TARGET_DIR: ./target
# Emit backtraces on panics.
RUST_BACKTRACE: 1
# Build static releases with PCRE2.
PCRE2_SYS_STATIC: 1
strategy:
# just an example matrix
fail-fast: false
matrix:
build: ['linux', 'macos', 'windows']
build: ['linux', 'debian', 'macos', 'arm-macos', 'windows']
include:
- build: linux
os: ubuntu-22.04
rust: nightly
target: x86_64-unknown-linux-musl
packages: apt
- build: debian
os: ubuntu-22.04
target: x86_64-unknown-linux-musl
packages: apt
- build: macos
os: macos-12
rust: nightly
os: macos-latest
target: x86_64-apple-darwin
packages: brew
- build: arm-macos
os: macos-latest
target: aarch64-apple-darwin
packages: brew
- build: windows
os: windows-2022
rust: nightly
target: x86_64-pc-windows-msvc
packages: none
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@master
- name: Install packages (linux)
if: matrix.packages == 'apt'
run: |
sudo apt-get update
sudo apt-get install -y pandoc
- name: Install packages (macos)
if: matrix.packages == 'brew'
run: |
brew update
brew install pandoc
- name: Install rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
- name: Build release binary
run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }}
- name: Strip release binary (linux and macos)
if: matrix.build == 'linux' || matrix.build == 'macos'
run: strip "target/${{ matrix.target }}/release/fwd"
- name: Upload release assets
uses: actions/upload-release-asset@v1
- name: Run the release automation
shell: bash
env:
RELEASE_TAG: ${{ needs.create_release.outputs.version }}
BUILD: ${{ matrix.build }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create_release.outputs.upload_url }}
asset_name: fwd-${{needs.create_release.outputs.fwd_version }}-${{ matrix.target }}
asset_path: "./target/${{ matrix.target }}/release/fwd"
asset_content_type: application/octet-stream
run: python3 release.py

1462
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,14 @@
[package]
name = "fwd"
version = "0.3.0"
version = "0.9.2"
authors = ["John Doty <john@d0ty.me>"]
edition = "2021"
license = "MIT"
description = "Automatically forward ports to a remote server over ssh"
description = "Automatically forward ports to a remote server"
readme = "README.md"
documentation = "https://github.com/DeCarabas/fwd"
homepage = "https://github.com/DeCarabas/fwd"
repository = "https://github.com/DeCarabas/fwd"
readme = "README.md"
[[bin]]
name = "fwd-browse"
@ -16,23 +18,47 @@ bench = false
[dependencies]
anyhow = "1.0"
bytes = "1"
crossterm = { version = "0.25", features = ["event-stream"] }
home = "0.5.4"
copypasta = "0.10.1"
crossterm = { version = "0.28.1", features = ["event-stream"] }
directories-next = "2"
env_logger = { version = "0.11.5", default-features = false }
indoc = "1"
log = { version = "0.4", features = ["std"] }
open = "3"
rand = "0.8.5"
ratatui = "0.28.0"
thiserror = "1.0"
tokio = { version = "1", features = ["full"] }
tokio = { version = "1", features = ["io-std", "io-util", "macros", "net", "process", "rt", "rt-multi-thread", "fs"] }
tokio-stream = "0.1"
toml = "0.5"
tui = "0.19"
xdg = "2"
[dev-dependencies]
assert_matches = "1"
tempdir = "0.3"
pretty_assertions = "1"
tempfile = "3"
[target.'cfg(target_os="linux")'.dependencies]
procfs = "0.14.1"
[target.'cfg(target_family="unix")'.dependencies]
users = "0.11"
libc = "0.2.155"
[package.metadata.deb]
section = "net"
depends = [] # No auto deps?
assets = [
["target/release/fwd", "usr/bin/", "755"],
["target/release/fwd-browse", "usr/bin/", "755"],
["LICENSE", "usr/share/doc/fwd/", "644"],
["README.md", "usr/share/doc/fwd/README", "644"],
# The man page is automatically generated by fwd's build process. See
# release.py for details.
["target/release/fwd.1", "usr/share/man/man1/fwd.1", "644"],
]
extended-description = """\
fwd enumerates the listening ports the remote server and automatically listens
for connections on the same ports on the local machine. When fwd receives a
connection on the local machine, it automatically forwards that connection to
the remote machine.
"""

View file

@ -1,4 +1,4 @@
Copyright 2022 John Doty
Copyright 2024 John Doty
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -3,16 +3,19 @@
A port-forwarding utility.
Here's how it works:
1. You install `fwd` on the server.
2. You install `fwd` on the client.
3. You run `fwd` on the client to connect to the server, like so:
1. Get the latest [release](https://github.com/DeCarabas/fwd/releases) of `fwd`
2. You install `fwd` on the server somewhere in your `$PATH` (like `/usr/bin/`, or `.local/bin`)
3. You install `fwd` on the client (like your laptop)
4. You run `fwd` on the client to connect to the server, like so:
```bash
doty@my.laptop$ fwd some.server
```
`fwd` will connect to `some.server` via ssh, and then show you a screen listing all of the ports that the server is listening on locally.
<img width="1337" src="doc/screenshot-01.png" alt="A terminal displaying a list of ports and descriptions. Some are dimmed and one is highlighted." />
Use the up and down arrow keys (or `j`/`k`) to select the port you're interested in and press `e` to toggle forwarding of that port.
Now, connections to that port locally will be forwarded to the remote server.

148
build.rs Normal file
View file

@ -0,0 +1,148 @@
use std::io::Write;
use std::path::{absolute, Path, PathBuf};
/// Fetch the contents of the given file, and also tell cargo that we looked
/// in there.
fn file_contents<P: AsRef<Path>>(path: P) -> String {
let path =
absolute(path.as_ref()).expect("Unable to make the path absolute");
let mut stdout = std::io::stdout();
stdout
.write_all(b"cargo::rerun-if-changed=")
.expect("Unable to write stdout");
stdout
.write_all(path.as_os_str().as_encoded_bytes())
.expect("Unable to write path to stdout");
stdout
.write_all(b"\n")
.expect("Unable to write newline to stdout");
std::fs::read_to_string(path).expect("Unable to read file")
}
fn git_rel<P: AsRef<Path>>(path: P) -> PathBuf {
let output = std::process::Command::new("git")
.arg("rev-parse")
.arg("--show-toplevel")
.output()
.expect("Error launching git rev-parse");
if !output.status.success() {
let stderr = std::str::from_utf8(&output.stderr)
.expect("git failed and stderr was not utf8");
eprintln!("`git rev-parse --show-toplevel` failed, stderr: {stderr}");
panic!("`git rev-parse --show-toplevel` failed");
}
let mut root = PathBuf::from(
std::str::from_utf8(&output.stdout)
.expect("Output was not utf-8")
.trim(),
);
root.push(path);
root
}
/// Emit the current git commit.
fn emit_git_commit() {
// Fetch the current commit from the head. We do it this way instead of
// asking `git rev-parse` to do it for us because we want to reliably
// tell cargo which files it should monitor for changes.
let head = file_contents(git_rel(".git/HEAD"));
let rev = if let Some(r) = head.strip_prefix("ref: ") {
let mut ref_path = git_rel(".git/");
ref_path.push(r.trim());
file_contents(ref_path)
} else {
head
};
// But *now* we ask git rev-parse to make this into a short hash (a) to
// make sure we got it right and (b) because git knows how to quickly
// determine how much of a commit is required to be unique. We don't need
// to tell cargo anything here, no file that git consults will be
// mutable.
let output = std::process::Command::new("git")
.arg("rev-parse")
.arg("--short")
.arg(rev.trim())
.output()
.expect("could not spawn `git` to get the hash");
if !output.status.success() {
let stderr = std::str::from_utf8(&output.stderr)
.expect("git failed and stderr was not utf8");
eprintln!("`git rev-parse --short HEAD` failed, stderr: {stderr}");
panic!("`git rev-parse --short HEAD` failed");
}
let rev = std::str::from_utf8(&output.stdout)
.expect("git did not output utf8")
.trim();
println!("cargo::rustc-env=REPO_REV={rev}");
}
fn emit_git_dirty() {
// Here is the way to see if anything is up with the repository: run `git
// status --porcelain=v1`. The status output in the v1 porcelain format
// has one line for every file that's modified in some way: staged,
// changed but unstaged, untracked, you name it. Files in the working
// tree that are up to date with the repository are not emitted. This is
// exactly what we want.
//
// (Yes, I want to track untracked files, because they can mess with the
// build too. The only good build is a clean build!)
let output = std::process::Command::new("git")
.arg("status")
.arg("-z")
.arg("--porcelain=v1")
.output()
.expect("could not spawn `git` to get repository status");
if !output.status.success() {
let stderr = std::str::from_utf8(&output.stderr)
.expect("git failed and stderr was not utf8");
eprintln!("`git status` failed, stderr: {stderr}");
panic!("`git status` failed");
}
let output =
std::str::from_utf8(&output.stdout).expect("git did not output utf8");
// Emit the repository status.
let dirty = if output.trim().is_empty() {
""
} else {
" *dirty*"
};
println!("cargo::rustc-env=REPO_DIRTY={dirty}");
// NOW: The output here has to do with *all* of the files in the git
// respository. (Because if nothing was modified, but then *becomes*
// modified, we need to rerun the script to notice the dirty bit.)
// `git-ls-files` is the way to do that.
let output = std::process::Command::new("git")
.arg("ls-files")
.arg("-z")
.arg("--cached")
.arg("--deleted")
.arg("--modified")
.arg("--others")
.arg("--exclude-standard")
.output()
.expect("could not spawn `git` to get repository status");
if !output.status.success() {
let stderr = std::str::from_utf8(&output.stderr)
.expect("git failed and stderr was not utf-8");
eprintln!("`git ls-files` failed, stderr: {stderr}");
panic!("`git ls-files` failed");
}
let output =
std::str::from_utf8(&output.stdout).expect("git did not output utf8");
for fname in output.split_terminator("\0") {
println!("cargo::rerun-if-changed={fname}");
}
}
fn main() {
emit_git_commit();
emit_git_dirty();
}

156
doc/fwd.man.md Normal file
View file

@ -0,0 +1,156 @@
% fwd(1)
% John Doty <john@d0ty.me>
% August 2024
# NAME
fwd - Automatically forward connections to remote machines
# SYNOPSIS
**fwd** [OPTIONS] SERVER
**fwd** [OPTIONS] browse URL
**fwd** [OPTIONS] clip FILE
**fwd-browse** URL
# DESCRIPTION
**fwd** enumerates the listening ports the remote server and automatically listens for connections on the same ports on the local machine.
When **fwd** receives a connection on the local machine, it automatically forwards that connection to the remote machine.
**-s**, **-\-sudo**
: Run the server side of fwd with `sudo`.
: This allows the client to forward ports that are open by processes being run under other accounts (e.g., docker containers being run as root), but requires sudo access on the server and *might* end up forwarding ports that you do not want forwarded (e.g., port 22 for sshd, or port 53 for systemd.)
**-\-log-filter** **FILTER**
: Set remote server's log level. Default is `warn`.
: Supports all of Rust's env_logger filter syntax, e.g. `--log-filter=fwd::trace`.
**-\-version**
: Print the version of fwd and exit.
# INTERACTIVE COMMANDS
Once **fwd** is connected, it displays an interactive list of the ports available on the remote server.
- Ports that **fwd** is listening on are displayed in the default terminal color.
- Ports that **fwd** is aware of but which are disabled are displayed in dark gray.
- Ports that **fwd** has tried to listen on but which have failed are displayed in red.
Details on the error may be found in the log window.
Disabling and re-enabling the port will cause **fwd** to try again.
The following commands are available while **fwd** is connected:
**Esc, q, Ctrl-C**
: Exit **fwd**.
**?, h**
: Display the help window.
**Up, k**
: Select the previous port in the list.
**Down, j**
: Select the next port in the list.
**Enter**
: Attempt to browse to localhost on the specified port with the default browser.
**a**
: Hide or show anonymous ports.
: (See "identifying ports" below for more information on anonymous ports.)
**e**
: Enable or disable the selected port.
**l**
: Show or hide the log window.
# IDENTIFYING PORTS
**fwd** enumerates all of the ports that the remote server is listening on, and attempts to identify the process that is listening on each port.
It can identify ports in the following ways:
*docker*
: **fwd** will attempt to find and connect to a docker engine on the remote machine.
: If successful, it will list all of the forwarded ports, and identify each port as belonging to that docker container.
*procfs*
: On Linux, the listening ports are found by reading procfs and mapping them back to process command lines.
: **fwd** can only identify processes that the user it is connected as has permissions to read on the remote machine.
(Earlier methods take precedence over later methods.)
If **fwd** cannot identify the process that is listening on a given port, then the port is *anonymous*.
Anonymous ports are not enabled by default, but can be enabled manually, either with the UI or by configuration.
# OPENING BROWSERS
**fwd** can be used to open URLs in the default browser on the local machine.
Run **fwd browse URL** on the remote server to open the `URL` in the default browser on the local machine.
This only works if **fwd** is connected, and if the user running **fwd browse** is the same as the user that connected the **fwd** session.
The **fwd-browse** program acts as a wrapper around **fwd browse**, to be used with configurations that can't handle a browser being a program with an argument.
# CLIPBOARD
**fwd** can be used from the remote machine to place text on the clipboard of the local machine.
Run **fwd clip FILE** to copy the contents of the named file to the clipboard.
If **FILE** is **-**, this reads text from stdin instead.
# CONFIGURATION
**fwd** can be configured with a configuration file.
- On Windows, the config file will be in your roaming AppData folder.
(e.g., *c:\\Users\\Winifred\\AppData\\Roaming\\fwd\\config\\config.toml*)
- On MacOS, the config file will be in *$HOME/Library/Application Support/fwd/config.toml*.
(e.g., /Users/Margarie/Library/Application Support/fwd/config.toml)
- On XDG-ish systems (like Linux), the config file is in *~/.config/fwd/config.toml*.
(e.g., */home/lynette/.config/fwd/config.toml*)
The following is an example of a *config.toml* file:
```
auto=true # should `fwd` should enable identified ports (default true)
[servers.foo] # Server-specific settings for foo
auto=true # defaults to the global setting
ports=[1080, 1082] # ports that are always present
[servers.bar.ports] # `ports` can also be a table with port numbers as keys
1080=true # the values can be booleans (for enabled)...
1081="My program" # or strings (for descriptions).
[servers.bar.ports.1082] # port values can also be tables
enabled=true
description="A humble python"
```
Ports that are specified in the configuration file will always be present in the list of ports for a given server, even if no process is listening on that port.
# TROUBLESHOOTING
Connections are made via the **ssh** command.
Your **ssh** must:
- Be on your path, so that **fwd** can find it to invoke it
- Be able to authenticate you to the remote server.
(Interactive authentication is fine.)
- Understand the **-D** command line option, to operate as a SOCKS5 server
- Be able to start the **fwd** command on the remote server
A typical ssh invocation from **fwd** looks like:
```bash
ssh -T -D XXXX me@server FWD_LOG=warning FWD_SEND_ANONYMOUS=1 fwd --server
```
**fwd** only enumerates ports that are listening on loopback addresses (e.g., 127.0.0.1) or on all addresses (e.g., 0.0.0.0).
If it cannot find a particular port, check to make sure that the process listening on that port is accessible via localhost.
# SEE ALSO
ssh(1)

BIN
doc/screenshot-01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

4
fuzz/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
target
corpus
artifacts
coverage

1735
fuzz/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

30
fuzz/Cargo.toml Normal file
View file

@ -0,0 +1,30 @@
[package]
name = "fwd-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
arbitrary = { version = "1.3.2", features = ["derive"] }
libfuzzer-sys = "0.4"
serde_json = "1.0.124"
[dependencies.fwd]
path = ".."
[[bin]]
name = "json_raw_input"
path = "fuzz_targets/json_raw_input.rs"
test = false
doc = false
bench = false
[[bin]]
name = "json_only_valid_serde"
path = "fuzz_targets/json_only_valid_serde.rs"
test = false
doc = false
bench = false

View file

@ -0,0 +1,77 @@
#![no_main]
use arbitrary::{Arbitrary, Error, Unstructured};
use libfuzzer_sys::fuzz_target;
use std::collections::HashMap;
extern crate fwd;
use fwd::server::refresh::docker::JsonValue;
/// InputNumber is a JSON number, i.e., a finite 64-bit floating point value
/// that is not NaN. We need to define our own little wrapper here so that we
/// can convince Arbitrary to only make finite f64s.
///
/// Ideally we would actually wrap serde_json::Number but there are rules
/// about mixing 3rd party traits with 3rd party types.
#[derive(Debug, PartialEq)]
struct InputNumber(f64);
impl<'a> Arbitrary<'a> for InputNumber {
fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self, Error> {
let value = f64::arbitrary(u)?;
if value.is_finite() {
Ok(InputNumber(value))
} else {
Err(Error::IncorrectFormat) // REJECT
}
}
#[inline]
fn size_hint(depth: usize) -> (usize, Option<usize>) {
f64::size_hint(depth)
}
}
/// TestInput is basically serde_json::Value, except (a) it has a HashMap and
/// not serde_json's special `Map` structure, and (b) it has `InputNumber`
/// instead of `json_serde::Number` for reasons described above.
#[derive(Debug, PartialEq, Arbitrary)]
enum TestInput {
Null,
Bool(bool),
Number(InputNumber),
String(String),
Object(HashMap<String, TestInput>),
Array(Vec<TestInput>),
}
fn convert(value: &TestInput) -> serde_json::Value {
match value {
TestInput::Null => serde_json::Value::Null,
TestInput::Bool(b) => serde_json::Value::Bool(*b),
TestInput::Number(n) => serde_json::Value::Number(
serde_json::Number::from_f64(n.0).expect("Unable to make an f64"),
),
TestInput::String(s) => serde_json::Value::String(s.clone()),
TestInput::Object(o) => {
let mut out = serde_json::map::Map::new();
for (k, v) in o.into_iter() {
out.insert(k.clone(), convert(v));
}
serde_json::Value::Object(out)
}
TestInput::Array(v) => {
serde_json::Value::Array(v.into_iter().map(convert).collect())
}
}
}
fuzz_target!(|data: TestInput| {
// Convert the arbitrary TestInput into an arbitrary serde_json::Value,
// then use serde_json to write out arbitrary JSON.
let converted = convert(&data).to_string();
// Parse the JSON that serde_json produced. This fuzz test should ensure
// that we can parse anything that serde_json can produce.
let _ = JsonValue::parse(converted.as_bytes());
});

View file

@ -0,0 +1,10 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
extern crate fwd;
use fwd::server::refresh::docker::JsonValue;
fuzz_target!(|data: &[u8]| {
let _ = JsonValue::parse(data);
});

161
release.py Normal file
View file

@ -0,0 +1,161 @@
"""A script to automate building and uploading a release archive.
This is in python instead of bash because I abhor bash. Even though it's a
little nicer for running commands, it's worse at everything else.
"""
import dataclasses
import enum
import os
import os.path
import pathlib
import shutil
import subprocess
RELEASE_TAG = os.getenv("RELEASE_TAG")
BUILD = os.getenv("BUILD")
if BUILD is None:
raise Exception("you *must* set the BUILD environment variable")
class Archive(enum.Enum):
TARBALL = 1
ZIP = 2
DEB = 3
@dataclasses.dataclass
class BuildSettings:
target: str # The rust target to build for
test: bool = True # Whether or not to run tests
man_page: bool = True # Whether or not to generate a man page
strip: bool = True # Whether or not to strip binaries
archive: Archive = Archive.TARBALL # Archive type
ext: str = "" # The file extension of the binary
print(f"doing release: {BUILD}")
build = {
"linux": BuildSettings(
target="x86_64-unknown-linux-musl",
),
"debian": BuildSettings(
target="x86_64-unknown-linux-musl",
test=False,
archive=Archive.DEB,
),
"macos": BuildSettings(
target="x86_64-apple-darwin",
),
"arm-macos": BuildSettings(
target="aarch64-apple-darwin",
),
"windows": BuildSettings(
target="x86_64-pc-windows-msvc",
strip=False,
man_page=False,
archive=Archive.ZIP,
ext=".exe",
),
}[BUILD]
print(f"settings: {build}")
target_dir = pathlib.Path("target") / build.target / "release"
bins = [(target_dir / bin).with_suffix(build.ext) for bin in ["fwd", "fwd-browse"]]
def build_and_test(staging: pathlib.Path):
# Tools
subprocess.run(
["rustup", "target", "add", build.target],
check=True,
)
# Test...?
if build.test:
subprocess.run(
["cargo", "test", "--verbose", "--release", "--target", build.target],
check=True,
)
# Build
subprocess.run(
["cargo", "build", "--verbose", "--release", "--target", build.target],
check=True,
)
# Strip
if build.strip:
for bin in bins:
subprocess.run(["strip", bin], check=True)
# Copy
for bin in bins:
shutil.copyfile(bin, os.path.join(staging, os.path.basename(bin)))
def build_docs(staging: pathlib.Path):
shutil.copyfile("README.md", staging / "README.md")
if build.man_page:
print("Creating man page...")
proc = subprocess.run(
["pandoc", "-s", "-tman", os.path.join("doc", "fwd.man.md")],
check=True,
capture_output=True,
encoding="utf8",
)
contents = proc.stdout
with open(staging / "fwd.1", "w", encoding="utf-8") as f:
f.write(contents)
def build_archive(staging: pathlib.Path) -> pathlib.Path:
print("Creating archive...")
if build.archive == Archive.ZIP:
archive = pathlib.Path(f"{staging}.zip")
subprocess.run(["7z", "a", archive, f"{staging}"], check=True)
elif build.archive == Archive.DEB:
subprocess.run(["cargo", "install", "cargo-deb"], check=True)
shutil.copyfile(staging / "fwd.1", target_dir / "fwd.1")
subprocess.run(["cargo", "deb", "--target", build.target], check=True)
# Knowing the deb path means knowing the target version but I don't
# actually have the version here. (Or, like, I have the release tag
# but not in testing.) So just find the hopefully singular .deb that
# we made.
deb_path = pathlib.Path("target") / build.target / "debian"
archives = list(deb_path.glob("*.deb"))
assert len(archives) == 1
archive = archives[0]
else:
assert build.archive == Archive.TARBALL
archive = pathlib.Path(f"{staging}.tar.gz")
subprocess.run(["tar", "czf", archive, f"{staging}"], check=True)
return archive
staging = pathlib.Path(f"fwd-{build.target}")
os.makedirs(staging, exist_ok=True)
build_and_test(staging)
build_docs(staging)
archive = build_archive(staging)
shutil.rmtree(staging)
assert archive.exists()
if RELEASE_TAG is None:
print(f"Not releasing {archive} to github, RELEASE_TAG is none.")
else:
print(f"Uploading {archive} to github release {RELEASE_TAG}...")
subprocess.run(
["gh", "release", "upload", RELEASE_TAG, archive, "--clobber"],
check=True,
)
os.unlink(archive)

14
resources/json/README.md Normal file
View file

@ -0,0 +1,14 @@
# Test JSON
This directory contains test JSON files from https://github.com/nst/JSONTestSuite as of commit 984defc.
It only has the positive and questionable JSON inputs, as our JSON parser is extremely forgiving, by design.
## Filtered tests
Some of the questionable tests have been removed:
- `i_structure_UTF-8_BOM_empty_object.json` removed because we don't handle BOMs.
- `i_string_utf16LE_no_BOM.json` removed because we don't speak UTF16.
- `i_string_utf16BE_no_BOM.json` removed because we don't speak UTF16.
- `i_string_UTF-16LE_with_BOM.json` removed because we don't speak UTF16.

View file

@ -0,0 +1 @@
[123.456e-789]

View file

@ -0,0 +1 @@
[0.4e00669999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999969999999006]

View file

@ -0,0 +1 @@
[-1e+9999]

View file

@ -0,0 +1 @@
[1.5e+9999]

View file

@ -0,0 +1 @@
[-123123e100000]

View file

@ -0,0 +1 @@
[123123e100000]

View file

@ -0,0 +1 @@
[123e-10000000]

View file

@ -0,0 +1 @@
[-123123123123123123123123123123]

View file

@ -0,0 +1 @@
[100000000000000000000]

View file

@ -0,0 +1 @@
[-237462374673276894279832749832423479823246327846]

View file

@ -0,0 +1 @@
{"\uDFAA":0}

View file

@ -0,0 +1 @@
["\uDADA"]

View file

@ -0,0 +1 @@
["\uD888\u1234"]

View file

@ -0,0 +1 @@
["譌・ム淫"]

View file

@ -0,0 +1 @@
["<22><><EFBFBD>"]

View file

@ -0,0 +1 @@
["\uD800\n"]

View file

@ -0,0 +1 @@
["\uDd1ea"]

View file

@ -0,0 +1 @@
["\uD800\uD800\n"]

View file

@ -0,0 +1 @@
["\ud800"]

View file

@ -0,0 +1 @@
["\ud800abc"]

View file

@ -0,0 +1 @@
["<22>"]

View file

@ -0,0 +1 @@
["\uDd1e\uD834"]

View file

@ -0,0 +1 @@
["И"]

View file

@ -0,0 +1 @@
["\uDFAA"]

View file

@ -0,0 +1 @@
["<22>"]

View file

@ -0,0 +1 @@
["<22><><EFBFBD><EFBFBD>"]

View file

@ -0,0 +1 @@
["<22><>"]

View file

@ -0,0 +1 @@
["<22>ソソソソ"]

View file

@ -0,0 +1 @@
["<22>€€€€"]

View file

@ -0,0 +1 @@
["<22><>"]

View file

@ -0,0 +1 @@
[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]

View file

@ -0,0 +1 @@
[[] ]

View file

@ -0,0 +1 @@
[""]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
["a"]

View file

@ -0,0 +1 @@
[false]

View file

@ -0,0 +1 @@
[null, 1, "1", {}]

View file

@ -0,0 +1 @@
[null]

View file

@ -0,0 +1,2 @@
[1
]

View file

@ -0,0 +1 @@
[1]

View file

@ -0,0 +1 @@
[1,null,null,null,2]

View file

@ -0,0 +1 @@
[2]

View file

@ -0,0 +1 @@
[123e65]

View file

@ -0,0 +1 @@
[0e+1]

View file

@ -0,0 +1 @@
[0e1]

View file

@ -0,0 +1 @@
[ 4]

View file

@ -0,0 +1 @@
[-0.000000000000000000000000000000000000000000000000000000000000000000000000000001]

View file

@ -0,0 +1 @@
[20e1]

View file

@ -0,0 +1 @@
[-0]

View file

@ -0,0 +1 @@
[-123]

View file

@ -0,0 +1 @@
[-1]

View file

@ -0,0 +1 @@
[-0]

View file

@ -0,0 +1 @@
[1E22]

View file

@ -0,0 +1 @@
[1E-2]

View file

@ -0,0 +1 @@
[1E+2]

View file

@ -0,0 +1 @@
[123e45]

View file

@ -0,0 +1 @@
[123.456e78]

View file

@ -0,0 +1 @@
[1e-2]

View file

@ -0,0 +1 @@
[1e+2]

View file

@ -0,0 +1 @@
[123]

View file

@ -0,0 +1 @@
[123.456789]

1
resources/json/y_object.json Executable file
View file

@ -0,0 +1 @@
{"asd":"sdf", "dfg":"fgh"}

View file

@ -0,0 +1 @@
{"asd":"sdf"}

View file

@ -0,0 +1 @@
{"a":"b","a":"c"}

View file

@ -0,0 +1 @@
{"a":"b","a":"b"}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{"":0}

View file

@ -0,0 +1 @@
{"foo\u0000bar": 42}

View file

@ -0,0 +1 @@
{ "min": -1.0e+28, "max": 1.0e+28 }

View file

@ -0,0 +1 @@
{"x":[{"id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}], "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}

View file

@ -0,0 +1 @@
{"a":[]}

View file

@ -0,0 +1 @@
{"title":"\u041f\u043e\u043b\u0442\u043e\u0440\u0430 \u0417\u0435\u043c\u043b\u0435\u043a\u043e\u043f\u0430" }

View file

@ -0,0 +1,3 @@
{
"a": "b"
}

View file

@ -0,0 +1 @@
["\u0060\u012a\u12AB"]

View file

@ -0,0 +1 @@
["\uD801\udc37"]

View file

@ -0,0 +1 @@
["\ud83d\ude39\ud83d\udc8d"]

View file

@ -0,0 +1 @@
["\"\\\/\b\f\n\r\t"]

View file

@ -0,0 +1 @@
["\\u0000"]

View file

@ -0,0 +1 @@
["\""]

View file

@ -0,0 +1 @@
["a/*b*/c/*d//e"]

View file

@ -0,0 +1 @@
["\\a"]

View file

@ -0,0 +1 @@
["\\n"]

View file

@ -0,0 +1 @@
["\u0012"]

View file

@ -0,0 +1 @@
["\uFFFF"]

Some files were not shown because too many files have changed in this diff Show more