Initial commit - fingered - Fingerd protocol daemon, allowing custom responses.
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit 35e0510a8c69994716ba590b5d697f790f50e6f1
 (HTM) Author: Jay Scott <me@jay.scot>
       Date:   Thu, 24 Aug 2023 14:49:36 +0100
       
       Initial commit
       
       Diffstat:
         A .build.yml                          |      32 +++++++++++++++++++++++++++++++
         A .gitignore                          |       1 +
         A LICENSE                             |      15 +++++++++++++++
         A Makefile                            |      66 +++++++++++++++++++++++++++++++
         A README.md                           |      89 +++++++++++++++++++++++++++++++
         A config/config.go                    |      67 +++++++++++++++++++++++++++++++
         A example/default                     |      14 ++++++++++++++
         A example/info                        |     646 +++++++++++++++++++++++++++++++
         A example/jimmy                       |     110 +++++++++++++++++++++++++++++++
         A example/uptime                      |       8 ++++++++
         A go.mod                              |       3 +++
         A main.go                             |      95 ++++++++++++++++++++++++++++++
         A tests/utils_test.go                 |     113 +++++++++++++++++++++++++++++++
         A utils/utils.go                      |      66 +++++++++++++++++++++++++++++++
       
       14 files changed, 1325 insertions(+), 0 deletions(-)
       ---
 (DIR) diff --git a/.build.yml b/.build.yml
       @@ -0,0 +1,32 @@
       +image: alpine/latest
       +
       +sources:
       +  - https://git.sr.ht/~jayscott/fingered
       +
       +packages:
       +  - go
       +
       +tasks:
       +  - setup: |
       +      go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
       +
       +  - format-check: |
       +      cd fingered
       +      unformatted=$(gofmt -l .)
       +      if [[ -n "$unformatted" ]]; then
       +          echo "The following files have formatting issues:"
       +          echo "$unformatted"
       +          exit 1
       +      fi
       +
       +  - lint: |
       +      cd fingered
       +      ~/go/bin/golangci-lint run
       +
       +  - test: |
       +      cd fingered
       +      go test ./tests
       +
       +  - build: |
       +      cd fingered
       +      make build
 (DIR) diff --git a/.gitignore b/.gitignore
       @@ -0,0 +1 @@
       +bin/
 (DIR) diff --git a/LICENSE b/LICENSE
       @@ -0,0 +1,15 @@
       +ISC License
       +
       +Copyright (c) 2023 Jay Scott
       +
       +Permission to use, copy, modify, and/or distribute this software for any
       +purpose with or without fee is hereby granted, provided that the above
       +copyright notice and this permission notice appear in all copies.
       +
       +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
       +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
       +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
       +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
       +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
       +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
       +PERFORMANCE OF THIS SOFTWARE.
 (DIR) diff --git a/Makefile b/Makefile
       @@ -0,0 +1,66 @@
       +# Makefile
       +
       +.PHONY: all fmt lint test build clean install
       +
       +BIN := bin
       +NAME := fingered
       +VER := 0.1.0
       +
       +BUILD_FLAGS := -ldflags="-s -w"
       +
       +# List of target platforms
       +PLATFORMS := \
       +        linux_amd64 \
       +        linux_386 \
       +        darwin_amd64 \
       +        freebsd_amd64 \
       +        freebsd_386 \
       +        openbsd_amd64 \
       +        openbsd_386
       +
       +# Generate binary paths
       +BINS := $(addprefix $(BIN)/$(NAME)_$(VER)_,$(PLATFORMS))
       +
       +# Installation path
       +INSTALL_PATH := /usr/local/bin
       +
       +# Detect current platform and architecture
       +GOOS := $(shell go env GOOS)
       +GOARCH := $(shell go env GOARCH)
       +
       +# Default target: format, lint, build
       +all: fmt lint build
       +
       +fmt:
       +        @echo "Formatting code..."
       +        go fmt ./...
       +
       +lint:
       +        @echo "Running linters..."
       +        golangci-lint run
       +
       +test:
       +        @echo "Running tests..."
       +        go test ./tests
       +
       +# Build binaries for target platforms
       +build: $(BINS)
       +
       +$(BIN)/$(NAME)_$(VER)_%: main.go
       +        @echo "Building for $*..."
       +        GOARCH=$(word 2,$(subst _, ,$*)) GOOS=$(word 1,$(subst _, ,$*)) go build ${BUILD_FLAGS} -o $@ main.go
       +
       +# Clean up artifacts
       +clean:
       +        go clean
       +        rm -f $(BINS)
       +
       +# Install the binary for the current platform and architecture
       +install: $(BIN)/$(NAME)_$(VER)_$(GOOS)_$(GOARCH)
       +        @echo "Installing binary to $(INSTALL_PATH)..."
       +        mkdir -p $(INSTALL_PATH)
       +        cp -f $(BIN)/$(NAME)_$(VER)_$(GOOS)_$(GOARCH) $(INSTALL_PATH)/$(NAME)
       +        chmod +x $(INSTALL_PATH)/$(NAME)
       +
       +run: build
       +        ./$(BIN)/$(NAME)_$(VER)_linux_amd64
 (DIR) diff --git a/README.md b/README.md
       @@ -0,0 +1,89 @@
       +# FINGERED
       +
       +[![builds.sr.ht status](https://builds.sr.ht/~jayscott/fingered/commits/master/.build.yml.svg)](https://builds.sr.ht/~jayscott/fingered/commits/master/.build.yml?)
       +
       +**A personal finger protocol daemon**
       +
       +A finger daemon that's all about giving you the finger — well, more like
       +giving your queries the right answers. No fuss, no frills, just good ol'
       +fashioned finger protocol fun.
       +
       +## Features
       +
       +- **Customize responses based on queried usernames**
       +- **Script execution or text display responses**
       +- **Default reply for unspecified queries.**
       +
       +
       +## Installing
       +
       +From source (requires Go):
       +```shell
       +git clone https://git.sr.ht/~jayscott/fingered
       +cd fingered
       +make
       +sudo make install
       +```
       +
       +Pre-compiled binaries are available on the latest build artifacts:
       +
       +
       +## Running
       +
       +From your terminal:
       +```shell
       +fingered
       +```
       +
       +
       +## Usage
       +
       +```shell
       +Usage:
       +        -d: Directory containing user files (default: /srv/fingered)
       +        -f: Filename for empty requests (default: default)
       +        -p: Port for incoming connections (default: 79)
       +        -t: Number of worker threads (default: 10)
       +```
       +
       +
       +## Examples
       +
       +Run FINGERED on port 7000, using files from /srv/fingered/example, with
       +'info' as the default file:
       +
       +```shell
       +fingered -p 7000 -d /srv/fingered/example -f info
       +```
       +
       +
       +## Issue tracker
       +
       +For issues, [visit the tracker](https://todo.sr.ht/~jayscott/fingered).
       +
       +
       +## Contributing
       +
       +Sending patches is done [by
       +email](https://lists.sr.ht/~jayscott/fingered-dev), this is simple and
       +built-in to Git.
       +
       +Set up your system once by following the steps Installation and
       +Configuration of [git-send-email.io](https://git-send-email.io/)
       +
       +Then, run once in this repository:
       +```shell
       +git config sendemail.to "~jayscott/fingered-dev@lists.sr.ht"
       +```
       +
       +Then, to send a patch, make your commit, then run:
       +```shell
       +git send-email --base=HEAD~1 --annotate -1 -v1
       +```
       +
       +Your patch will appear on the [the mailing list](https://lists.sr.ht/~jayscott/fingered-dev/patches).
       +
       +
       +## License
       +
       +This is open source! Please use it under the ISC License.
 (DIR) diff --git a/config/config.go b/config/config.go
       @@ -0,0 +1,67 @@
       +package config
       +
       +import (
       +        "flag"
       +        "fmt"
       +        "log"
       +)
       +
       +const (
       +        maxPort    = 65535
       +        minPort    = 1
       +        minThreads = 1
       +)
       +
       +type Config struct {
       +        Threads int
       +        Port    int
       +        Dir     string
       +        Index   string
       +}
       +
       +func defaultConfig() Config {
       +        return Config{
       +                Threads: 10,
       +                Port:    79,
       +                Dir:     "/srv/fingered",
       +                Index:   "default",
       +        }
       +}
       +
       +func displayUsage() {
       +        flagSet := flag.CommandLine
       +        flag.Usage = func() {
       +                fmt.Printf("Usage:\n")
       +                flagSet.VisitAll(func(flag *flag.Flag) {
       +                        fmt.Printf("\t-%s: %s (default: %s)\n", flag.Name, flag.Usage, flag.DefValue)
       +                })
       +        }
       +}
       +
       +func addFlags(cfg *Config) {
       +        flag.IntVar(&cfg.Threads, "t", cfg.Threads, "Number of worker threads")
       +        flag.IntVar(&cfg.Port, "p", cfg.Port, "Port for incoming connections")
       +        flag.StringVar(&cfg.Dir, "d", cfg.Dir, "Directory containing user files")
       +        flag.StringVar(&cfg.Index, "f", cfg.Index, "Filename for empty requests")
       +}
       +
       +func ParseFlags() Config {
       +        cfg := defaultConfig()
       +        addFlags(&cfg)
       +        displayUsage()
       +        flag.Parse()
       +
       +        if cfg.Threads < minThreads {
       +                log.Fatal("Invalid number of threads: must be greater than 0.")
       +        }
       +
       +        if cfg.Port < minPort || cfg.Port > maxPort {
       +                log.Fatal("Invalid port value: must be between 1 and 65535.")
       +        }
       +
       +        if cfg.Dir == "" {
       +                log.Fatal("Invalid path value: directory cannot be empty.")
       +        }
       +
       +        return cfg
       +}
 (DIR) diff --git a/example/default b/example/default
       @@ -0,0 +1,14 @@
       +  ___ __                                  __
       +.'  _|__|.-----.-----.-----.----.-----.--|  |
       +|   _|  ||     |  _  |  -__|   _|  -__|  _  |
       +|__| |__||__|__|___  |_____|__| |_____|_____|
       +               |_____|
       +
       +
       +Welcome to fingered!
       +
       +
       +Available Fingers:
       +
       +        info   ... get information about finger
       +        jimmy  ... we are all jimmy
 (DIR) diff --git a/example/info b/example/info
       @@ -0,0 +1,646 @@
       +Network Working Group                                       D. Zimmerman
       +Request for Comments: 1288           Center for Discrete Mathematics and
       +Obsoletes: RFCs 1196, 1194, 742             Theoretical Computer Science
       +                                                           December 1991
       +
       +
       +                  The Finger User Information Protocol
       +
       +Status of this Memo
       +
       +   This memo defines a protocol for the exchange of user information.
       +   This RFC specifies an IAB standards track protocol for the Internet
       +   community, and requests discussion and suggestions for improvements.
       +   Please refer to the current edition of the "IAB Official Protocol
       +   Standards" for the standardization state and status of this protocol.
       +   Distribution of this memo is unlimited.
       +
       +Abstract
       +
       +   This memo describes the Finger user information protocol.  This is a
       +   simple protocol which provides an interface to a remote user
       +   information program.
       +
       +   Based on RFC 742, a description of the original Finger protocol, this
       +   memo attempts to clarify the expected communication between the two
       +   ends of a Finger connection.  It also tries not to invalidate the
       +   many existing implementations or add unnecessary restrictions to the
       +   original protocol definition.
       +
       +   This edition corrects and clarifies RFC 1196.
       +
       +   Table of Contents
       +
       +   1.      Introduction  ...........................................   2
       +     1.1.    Intent  ...............................................   2
       +     1.2.    History  ..............................................   3
       +     1.3.    Requirements  .........................................   3
       +     1.4.    Updates  ..............................................   3
       +   2.      Use of the protocol  ....................................   4
       +     2.1.    Flow of events  .......................................   4
       +     2.2.    Data format  ..........................................   4
       +     2.3.    Query specifications  .................................   4
       +     2.4.    RUIP {Q2} behavior  ...................................   5
       +     2.5.    Expected RUIP response  ...............................   6
       +       2.5.1.  {C} query  ..........................................   6
       +       2.5.2.  {U}{C} query  .......................................   6
       +       2.5.3.  {U} ambiguity  ......................................   7
       +       2.5.4.  /W query token  .....................................   7
       +
       +
       +
       +Zimmerman                                                       [Page 1]
       +
       +RFC 1288                         Finger                    December 1991
       +
       +
       +       2.5.5.  Vending machines  ...................................   7
       +   3.      Security  ...............................................   7
       +     3.1.    Implementation security  ..............................   7
       +     3.2.    RUIP security  ........................................   8
       +       3.2.1.  {Q2} refusal  .......................................   8
       +       3.2.2.  {C} refusal  ........................................   8
       +       3.2.3.  Atomic discharge  ...................................   8
       +       3.2.4.  User information files  .............................   9
       +       3.2.5.  Execution of user programs  .........................   9
       +       3.2.6.  {U} ambiguity  ......................................   9
       +       3.2.7.  Audit trails  .......................................   9
       +     3.3.    Client security  ......................................   9
       +   4.      Examples  ...............................................  10
       +     4.1.    Example with a null command line ({C})  ...............  10
       +     4.2.    Example with name specified ({U}{C})  .................  10
       +     4.3.    Example with ambiguous name specified ({U}{C})  .......  11
       +     4.4.    Example of query type {Q2} ({U}{H}{H}{C})  ............  11
       +   5.      Acknowledgments  ........................................  12
       +   6.      Security Considerations  ................................  12
       +   7.      Author's Address  .......................................  12
       +
       +1.  Introduction
       +
       +1.1.  Intent
       +
       +   This memo describes the Finger user information protocol.  This is a
       +   simple protocol which provides an interface to a remote user
       +   information program (RUIP).
       +
       +   Based on RFC 742, a description of the original Finger protocol, this
       +   memo attempts to clarify the expected communication between the two
       +   ends of a Finger connection.  It also tries not to invalidate the
       +   many current implementations or add unnecessary restrictions to the
       +   original protocol definition.
       +
       +   The most prevalent implementations of Finger today seem to be
       +   primarily derived from the BSD UNIX work at the University of
       +   California, Berkeley.  Thus, this memo is based around the BSD
       +   version's behavior.
       +
       +   However, the BSD version provides few options to tailor the Finger
       +   RUIP for a particular site's security policy, or to protect the user
       +   from dangerous data.  Furthermore, there are MANY potential security
       +   holes that implementors and administrators need to be aware of,
       +   particularly since the purpose of this protocol is to return
       +   information about a system's users, a sensitive issue at best.
       +   Therefore, this memo makes a number of important security comments
       +   and recommendations.
       +
       +
       +
       +Zimmerman                                                       [Page 2]
       +
       +RFC 1288                         Finger                    December 1991
       +
       +
       +1.2.  History
       +
       +   The FINGER program at SAIL, written by Les Earnest, was the
       +   inspiration for the NAME program on ITS.  Earl Killian at MIT and
       +   Brian Harvey at SAIL were jointly responsible for implementing the
       +   original protocol.
       +
       +   Ken Harrenstien is the author of RFC 742, "Name/Finger", which this
       +   memo began life as.
       +
       +1.3.  Requirements
       +
       +   In this document, the words that are used to define the significance
       +   of each particular requirement are capitalized.  These words are:
       +
       +   * "MUST"
       +
       +      This word or the adjective "REQUIRED" means that the item is an
       +      absolute requirement of the specification.
       +
       +   * "SHOULD"
       +
       +      This word or the adjective "RECOMMENDED" means that there may
       +      exist valid reasons in particular circumstances to ignore this
       +      item, but the full implications should be understood and the case
       +      carefully weighed before choosing a different course.
       +
       +   * "MAY"
       +
       +      This word or the adjective "OPTIONAL" means that this item is
       +      truly optional.  One vendor may choose to include the item because
       +      a particular marketplace requires it or because it enhances the
       +      product, for example; another vendor may omit the same item.
       +
       +   An implementation is not compliant if it fails to satisfy one or more
       +   of the MUST requirements.  An implementation that satisfies all the
       +   MUST and all the SHOULD requirements is said to be "unconditionally
       +   compliant"; one that satisfies all the MUST requirements but not all
       +   the SHOULD requirements is said to be "conditionally compliant".
       +
       +1.4.  Updates
       +
       +   The differences of note between RFC 1196 and this memo are:
       +
       +      o the optional /W switch in the Finger query specification was
       +        mistakenly placed at the end of the line.  The 4.3BSD Finger
       +        specifies it at the beginning, where this memo now also puts
       +        it.
       +
       +
       +
       +Zimmerman                                                       [Page 3]
       +
       +RFC 1288                         Finger                    December 1991
       +
       +
       +      o the BNF in the Finger query specification was not clear on the
       +        treatment of blank space.  This memo is more exacting by
       +        including an explicit token for it.
       +
       +      o The flow of events in a Finger connection is now better
       +        defined on the topic of the close of the Finger connection.
       +
       +2.  Use of the protocol
       +
       +2.1.  Flow of events
       +
       +   Finger is based on the Transmission Control Protocol, using TCP port
       +   79 decimal (117 octal).  The local host opens a TCP connection to a
       +   remote host on the Finger port.  An RUIP becomes available on the
       +   remote end of the connection to process the request.  The local host
       +   sends the RUIP a one line query based upon the Finger query
       +   specification, and waits for the RUIP to respond.  The RUIP receives
       +   and processes the query, returns an answer, then initiates the close
       +   of the connection.  The local host receives the answer and the close
       +   signal, then proceeds closing its end of the connection.
       +
       +2.2.  Data format
       +
       +   Any data transferred MUST be in ASCII format, with no parity, and
       +   with lines ending in CRLF (ASCII 13 followed by ASCII 10).  This
       +   excludes other character formats such as EBCDIC, etc.  This also
       +   means that any characters between ASCII 128 and ASCII 255 should
       +   truly be international data, not 7-bit ASCII with the parity bit set.
       +
       +2.3.  Query specifications
       +
       +   An RUIP MUST accept the entire Finger query specification.
       +
       +   The Finger query specification is defined:
       +
       +        {Q1}    ::= [{W}|{W}{S}{U}]{C}
       +
       +        {Q2}    ::= [{W}{S}][{U}]{H}{C}
       +
       +        {U}     ::= username
       +
       +        {H}     ::= @hostname | @hostname{H}
       +
       +        {W}     ::= /W
       +
       +        {S}     ::= <SP> | <SP>{S}
       +
       +        {C}     ::= <CRLF>
       +
       +
       +
       +Zimmerman                                                       [Page 4]
       +
       +RFC 1288                         Finger                    December 1991
       +
       +
       +   {H}, being recursive, means that there is no arbitrary limit on the
       +   number of @hostname tokens in the query.  In examples of the {Q2}
       +   request specification, the number of @hostname tokens is limited to
       +   two, simply for brevity.
       +
       +   Be aware that {Q1} and {Q2} do not refer to a user typing "finger
       +   user@host" from an operating system prompt.  It refers to the line
       +   that an RUIP actually receives.  So, if a user types "finger
       +   user@host<CRLF>", the RUIP on the remote host receives "user<CRLF>",
       +   which corresponds to {Q1}.
       +
       +   As with anything in the IP protocol suite, "be liberal in what you
       +   accept".
       +
       +2.4.  RUIP {Q2} behavior
       +
       +   A query of {Q2} is a request to forward a query to another RUIP.  An
       +   RUIP MUST either provide or actively refuse this forwarding service
       +   (see section 3.2.1).  If an RUIP provides this service, it MUST
       +   conform to the following behavior:
       +
       +   Given that:
       +
       +         Host <H1> opens a Finger connection <F1-2> to an RUIP on host
       +         <H2>.
       +
       +         <H1> gives the <H2> RUIP a query <Q1-2> of type {Q2}
       +         (e.g., FOO@HOST1@HOST2).
       +
       +   It should be derived that:
       +
       +         Host <H3> is the right-most host in <Q1-2> (i.e., HOST2)
       +
       +         Query <Q2-3> is the remainder of <Q1-2> after removing the
       +         right-most "@hostname" token in the query (i.e., FOO@HOST1)
       +
       +   And so:
       +
       +         The <H2> RUIP then must itself open a Finger connection <F2-3>
       +         to <H3>, using <Q2-3>.
       +
       +         The <H2> RUIP must return any information received from <F2-3>
       +         to <H1> via <F1-2>.
       +
       +         The <H2> RUIP must close <F1-2> in normal circumstances only
       +         when the <H3> RUIP closes <F2-3>.
       +
       +
       +
       +
       +
       +Zimmerman                                                       [Page 5]
       +
       +RFC 1288                         Finger                    December 1991
       +
       +
       +2.5.  Expected RUIP response
       +
       +   For the most part, the output of an RUIP doesn't follow a strict
       +   specification, since it is designed to be read by people instead of
       +   programs.  It should mainly strive to be informative.
       +
       +   Output of ANY query is subject to the discussion in the security
       +   section.
       +
       +2.5.1.  {C} query
       +
       +   A query of {C} is a request for a list of all online users.  An RUIP
       +   MUST either answer or actively refuse (see section 3.2.2).  If it
       +   answers, then it MUST provide at least the user's full name.  The
       +   system administrator SHOULD be allowed to include other useful
       +   information (per section 3.2.3), such as:
       +
       +      -    terminal location
       +      -    office location
       +      -    office phone number
       +      -    job name
       +      -    idle time (number of minutes since last typed input, or
       +           since last job activity).
       +
       +2.5.2.  {U}{C} query
       +
       +   A query of {U}{C} is a request for in-depth status of a specified
       +   user {U}.  If you really want to refuse this service, you probably
       +   don't want to be running Finger in the first place.
       +
       +   An answer MUST include at least the full name of the user.  If the
       +   user is logged in, at least the same amount of information returned
       +   by {C} for that user MUST also be returned by {U}{C}.
       +
       +   Since this is a query for information on a specific user, the system
       +   administrator SHOULD be allowed to choose to return additional useful
       +   information (per section 3.2.3), such as:
       +
       +            -    office location
       +            -    office phone number
       +            -    home phone number
       +            -    status of login (not logged in, logout time, etc)
       +            -    user information file
       +
       +   A user information file is a feature wherein a user may leave a short
       +   message that will be included in the response to Finger requests.
       +   (This is sometimes called a "plan" file.)  This is easily implemented
       +   by (for example) having the program look for a specially named text
       +
       +
       +
       +Zimmerman                                                       [Page 6]
       +
       +RFC 1288                         Finger                    December 1991
       +
       +
       +   file in the user's home directory or some common area; the exact
       +   method is left to the implementor.  The system administrator SHOULD
       +   be allowed to specifically turn this feature on and off.  See section
       +   3.2.4 for caveats.
       +
       +   There MAY be a way for the user to run a program in response to a
       +   Finger query.  If this feature exists, the system administrator
       +   SHOULD be allowed to specifically turn it on and off.  See section
       +   3.2.5 for caveats.
       +
       +2.5.3.  {U} ambiguity
       +
       +   Allowable "names" in the command line MUST include "user names" or
       +   "login names" as defined by the system.  If a name is ambiguous, the
       +   system administrator SHOULD be allowed to choose whether or not all
       +   possible derivations should be returned in some fashion (per section
       +   3.2.6).
       +
       +2.5.4.  /W query token
       +
       +   The token /W in the {Q1} or {Q2} query types SHOULD at best be
       +   interpreted at the last RUIP to signify a higher level of verbosity
       +   in the user information output, or at worst be ignored.
       +
       +2.5.5.  Vending machines
       +
       +   Vending machines SHOULD respond to a {C} request with a list of all
       +   items currently available for purchase and possible consumption.
       +   Vending machines SHOULD respond to a {U}{C} request with a detailed
       +   count or list of the particular product or product slot.  Vending
       +   machines should NEVER NEVER EVER eat money.
       +
       +3.  Security
       +
       +3.1.  Implementation security
       +
       +   Sound implementation of Finger is of the utmost importance.
       +   Implementations should be tested against various forms of attack.  In
       +   particular, an RUIP SHOULD protect itself against malformed inputs.
       +   Vendors providing Finger with the operating system or network
       +   software should subject their implementations to penetration testing.
       +
       +   Finger is one of the avenues for direct penetration, as the Morris
       +   worm pointed out quite vividly.  Like Telnet, FTP and SMTP, Finger is
       +   one of the protocols at the security perimeter of a host.
       +   Accordingly, the soundness of the implementation is paramount.  The
       +   implementation should receive just as much security scrutiny during
       +   design, implementation, and testing as Telnet, FTP, or SMTP.
       +
       +
       +
       +Zimmerman                                                       [Page 7]
       +
       +RFC 1288                         Finger                    December 1991
       +
       +
       +3.2.  RUIP security
       +
       +   Warning!!  Finger discloses information about users; moreover, such
       +   information may be considered sensitive.  Security administrators
       +   should make explicit decisions about whether to run Finger and what
       +   information should be provided in responses.  One existing
       +   implementation provides the time the user last logged in, the time he
       +   last read mail, whether unread mail was waiting for him, and who the
       +   most recent unread mail was from!  This makes it possible to track
       +   conversations in progress and see where someone's attention was
       +   focused.  Sites that are information-security conscious should not
       +   run Finger without an explicit understanding of how much information
       +   it is giving away.
       +
       +3.2.1.  {Q2} refusal
       +
       +   For individual site security concerns, the system administrator
       +   SHOULD be given an option to individually turn on or off RUIP
       +   processing of {Q2}.  If RUIP processing of {Q2} is turned off, the
       +   RUIP MUST return a service refusal message of some sort.  "Finger
       +   forwarding service denied" is adequate.  The purpose of this is to
       +   allow individual hosts to choose to not forward Finger requests, but
       +   if they do choose to, to do so consistently.
       +
       +   Overall, there are few cases which would warrant processing of {Q2}
       +   at all, and they are far outweighed by the number of cases for
       +   refusing to process {Q2}.  In particular, be aware that if a machine
       +   is part of security perimeter (that is, it is a gateway from the
       +   outside world to some set of interior machines), then turning {Q2} on
       +   provides a path through that security perimeter.  Therefore, it is
       +   RECOMMENDED that the default of the {Q2} processing option be to
       +   refuse processing.  It certainly should not be enabled in gateway
       +   machines without careful consideration of the security implications.
       +
       +3.2.2.  {C} refusal
       +
       +   For individual site security concerns, the system administrator
       +   SHOULD be given an option to individually turn on or off RUIP
       +   acceptance of {C}.  If RUIP processing of {C} is turned off, the RUIP
       +   MUST return a service refusal message of some sort.  "Finger online
       +   user list denied" is adequate.  The purpose of this is to allow
       +   individual hosts to choose to not list the users currently online.
       +
       +3.2.3.  Atomic discharge
       +
       +   All implementations of Finger SHOULD allow individual system
       +   administrators to tailor what atoms of information are returned to a
       +   query.  For example:
       +
       +
       +
       +Zimmerman                                                       [Page 8]
       +
       +RFC 1288                         Finger                    December 1991
       +
       +
       +      -    Administrator A should be allowed to specifically choose to
       +           return office location, office phone number, home phone
       +           number, and logged in/logout time.
       +
       +      -    Administrator B should be allowed to specifically choose to
       +           return only office location, and office phone number.
       +
       +      -    Administrator C should be allowed to specifically choose to
       +           return the minimum amount of required information, which is
       +           the person's full name.
       +
       +3.2.4.  User information files
       +
       +   Allowing an RUIP to return information out of a user-modifiable file
       +   should be seen as equivalent to allowing any information about your
       +   system to be freely distributed.  That is, it is potentially the same
       +   as turning on all specifiable options.  This information security
       +   breach can be done in a number of ways, some cleverly, others
       +   straightforwardly.  This should disturb the sleep of system
       +   administrators who wish to control the returned information.
       +
       +3.2.5.  Execution of user programs
       +
       +   Allowing an RUIP to run a user program in response to a Finger query
       +   is potentially dangerous.  BE CAREFUL!! -- the RUIP MUST NOT allow
       +   system security to be compromised by that program.  Implementing this
       +   feature may be more trouble than it is worth, since there are always
       +   bugs in operating systems, which could be exploited via this type of
       +   mechanism.
       +
       +3.2.6.  {U} ambiguity
       +
       +   Be aware that a malicious user's clever and/or persistent use of this
       +   feature can result in a list of most of the usernames on a system.
       +   Refusal of {U} ambiguity should be considered in the same vein as
       +   refusal of {C} requests (see section 3.2.2).
       +
       +3.2.7.  Audit trails
       +
       +   Implementations SHOULD allow system administrators to log Finger
       +   queries.
       +
       +3.3.  Client security
       +
       +   It is expected that there will normally be some client program that
       +   the user runs to query the initial RUIP.  By default, this program
       +   SHOULD filter any unprintable data, leaving only printable 7-bit
       +   characters (ASCII 32 through ASCII 126), tabs (ASCII 9), and CRLFs.
       +
       +
       +
       +Zimmerman                                                       [Page 9]
       +
       +RFC 1288                         Finger                    December 1991
       +
       +
       +   This is to protect against people playing with terminal escape codes,
       +   changing other peoples' X window names, or committing other dastardly
       +   or confusing deeds.  Two separate user options SHOULD be considered
       +   to modify this behavior, so that users may choose to view
       +   international or control characters:
       +
       +      -    one to allow all characters less than ASCII 32
       +
       +      -    another to allow all characters greater than ASCII 126
       +
       +   For environments that live and breathe international data, the system
       +   administrator SHOULD be given a mechanism to enable the latter option
       +   by default for all users on a particular system.  This can be done
       +   via a global environment variable or similar mechanism.
       +
       +4.  Examples
       +
       +4.1.  Example with a null command line ({C})
       +
       +Site: elbereth.rutgers.edu
       +Command line: <CRLF>
       +
       +Login       Name               TTY Idle    When            Office
       +rinehart Mark J. Rinehart      p0  1:11 Mon 12:15  019 Hill       x3166
       +greenfie Stephen J. Greenfiel  p1       Mon 15:46  542 Hill       x3074
       +rapatel  Rocky - Rakesh Patel  p3    4d Thu 00:58  028 Hill       x2287
       +pleasant Mel Pleasant          p4    3d Thu 21:32  019 Hill    908-932-
       +dphillip Dave Phillips         p5  021: Sun 18:24  265 Hill       x3792
       +dmk      David Katinsky        p6    2d Thu 14:11  028 Hill       x2492
       +cherniss Cary Cherniss         p7    5  Mon 15:42  127 Psychol    x2008
       +harnaga  Doug Harnaga          p8  2:01 Mon 10:15  055 Hill       x2351
       +brisco   Thomas P. Brisco      pe  2:09 Mon 13:37  h055           x2351
       +laidlaw  Angus Laidlaw         q0  1:55 Mon 11:26  E313C       648-5592
       +cje      Chris Jarocha-Ernst   q1    8  Mon 13:43  259 Hill       x2413
       +
       +4.2.  Example with name specified ({U}{C})
       +
       +Site: dimacs.rutgers.edu
       +Command line: pirmann<CRLF>
       +Login name: pirmann                     In real life: David Pirmann
       +Office: 016 Hill, x2443                 Home phone: 989-8482
       +Directory: /dimacs/u1/pirmann           Shell: /bin/tcsh
       +Last login Sat Jun 23 10:47 on ttyp0 from romulus.rutgers.
       +No unread mail
       +Project:
       +Plan:
       +                      Work Schedule, Summer 1990
       +                 Rutgers LCSR Operations, 908-932-2443
       +
       +
       +
       +Zimmerman                                                      [Page 10]
       +
       +RFC 1288                         Finger                    December 1991
       +
       +
       +                        Monday       5pm - 12am
       +                        Tuesday      5pm - 12am
       +                        Wednesday    9am -  5pm
       +                        Thursday     9am -  5pm
       +                        Saturday     9am -  5pm
       +
       +                           larf larf hoo hoo
       +
       +4.3.  Example with ambiguous name specified ({U}{C})
       +
       +Site: elbereth.rutgers.edu
       +Command line: ron<CRLF>
       +Login name: spinner                     In real life: Ron Spinner
       +Office: Ops Cubby,    x2443             Home phone: 463-7358
       +Directory: /u1/spinner                  Shell: /bin/tcsh
       +Last login Mon May  7 16:38 on ttyq7
       +Plan:
       +            ught i
       +          ca      n
       +        m           a
       +       '      ...     t
       +      I      .   .     i
       +             !         m
       +      !       !       e
       +       p       !pool
       +        l
       +         e
       +          H
       +
       +Login name: surak                       In real life: Ron Surak
       +Office: 000 OMB Dou,    x9256
       +Directory: /u2/surak                    Shell: /bin/tcsh
       +Last login Fri Jul 27 09:55 on ttyq3
       +No Plan.
       +
       +Login name: etter                       In real life: Ron Etter
       +Directory: /u2/etter                    Shell: /bin/tcsh
       +Never logged in.
       +No Plan.
       +
       +4.4.  Example of query type {Q2} ({U}{H}{H}{C})
       +
       +Site: dimacs.rutgers.edu
       +Command line: hedrick@math.rutgers.edu@pilot.njin.net<CRLF>
       +[pilot.njin.net]
       +[math.rutgers.edu]
       +Login name: hedrick                     In real life: Charles Hedrick
       +Office: 484 Hill, x3088
       +
       +
       +
       +Zimmerman                                                      [Page 11]
       +
       +RFC 1288                         Finger                    December 1991
       +
       +
       +Directory: /math/u2/hedrick             Shell: /bin/tcsh
       +Last login Sun Jun 24 00:08 on ttyp1 from monster-gw.rutge
       +No unread mail
       +No Plan.
       +
       +5.  Acknowledgments
       +
       +   Thanks to everyone in the Internet Engineering Task Force for their
       +   comments.  Special thanks to Steve Crocker for his security
       +   recommendations and prose.
       +
       +6.  Security Considerations
       +
       +   Security issues are discussed in Section 3.
       +
       +7.  Author's Address
       +
       +   David Paul Zimmerman
       +   Center for Discrete Mathematics and
       +   Theoretical Computer Science (DIMACS)
       +   Rutgers University
       +   P.O. Box 1179
       +   Piscataway, NJ 08855-1179
       +
       +   Phone: (908)932-4592
       +
       +   EMail: dpz@dimacs.rutgers.edu
       +
       +
       +Zimmerman                                                      [Page 12]
 (DIR) diff --git a/example/jimmy b/example/jimmy
       @@ -0,0 +1,110 @@
       +===
       +WE ARE ALL JIMMY - AN A.I STORY
       +===
       +
       +In a world bustling with the latest technological marvels and the
       +dazzling allure of the modern web, there lived a quiet soul named Jimmy.
       +Unlike his contemporaries, who were entranced by sleek interfaces,
       +social media blitzes, and algorithmic recommendations, Jimmy found
       +solace in the forgotten corners of the digital realm. He was a geek,
       +a true lover of the old school internet, and his heart beat in sync with
       +the rhythms of protocols long overshadowed.
       +
       +Jimmy's journey into the realm of technology had started at an early
       +age. While his peers were busy with video games and social networking,
       +he was tinkering with a vintage computer his grandfather had gifted him.
       +His eyes would light up as he explored the archives of the past,
       +discovering the finger protocol – a simple yet elegant way to see who
       +was logged into a remote server. For Jimmy, the thrill of connecting
       +with someone across the digital expanse using such a basic protocol was
       +incomparable.
       +
       +As he delved deeper, Jimmy's fascination extended to the gopher
       +protocol. The structured simplicity of gopher appealed to his
       +sensibilities. The orderly menus and text-based navigation took him on
       +journeys of discovery that felt like reading hidden chapters of history.
       +While the rest of the world was caught up in the clamor of flashy
       +websites, Jimmy was content with gopher holes, feeling like an explorer
       +of a forgotten world.
       +
       +But Jimmy's passion wasn't limited to the confines of his room. He began
       +to actively seek out others who shared his affinity for the past. Online
       +forums dedicated to preserving and celebrating these old protocols
       +became his virtual haven. He formed connections with kindred spirits who
       +felt the same longing for the days when the internet was a smaller, more
       +personal place. In these spaces, Jimmy found camaraderie, and the sense
       +that he wasn't alone in his appreciation for the bygone technologies.
       +
       +However, the modern world was relentless in its advance, and the gulf
       +between Jimmy and his peers only grew wider. He tried to explain his
       +devotion to the finger protocol and the gopher protocol, but he was met
       +with puzzled looks and dismissive gestures. The allure of social media
       +platforms and the glossy veneer of the modern web was too strong to be
       +overshadowed by his tales of a simpler, more genuine online experience.
       +
       +As time passed, Jimmy's resolve to defend his cherished protocols only
       +deepened. He took it upon himself to create a website that would serve
       +as an homage to the finger and gopher protocols. He filled it with
       +nostalgic content, stories of his own experiences, and tutorials for
       +those who wanted to experience the internet of yesteryears. Though his
       +site garnered modest attention, it became a beacon for like-minded souls
       +who yearned for the same connection.
       +
       +One day, as Jimmy was engrossed in his work, he received an email from
       +a renowned tech historian named Eleanor. She had stumbled upon his
       +website and was captivated by his passion for the old protocols. Eleanor
       +had spent years researching the evolution of the internet, and she saw
       +in Jimmy a kindred spirit. They began exchanging messages, sharing
       +stories of their experiences, and discussing the ways in which the
       +internet had transformed over time.
       +
       +Their connection deepened, and eventually, Eleanor proposed a radical
       +idea: a conference that would celebrate the beauty of the past while
       +exploring its relevance in the present. Jimmy was initially hesitant
       +– the thought of stepping out from behind his computer screen filled him
       +with anxiety. But Eleanor's enthusiasm was infectious, and she assured
       +him that his perspective was valuable and needed.
       +
       +With Eleanor's guidance and encouragement, Jimmy found himself on stage
       +at the conference. As he spoke about the finger and gopher protocols,
       +his love for the old school internet shone brightly. His words resonated
       +with the audience, many of whom had never heard of these protocols
       +before. As he looked out at the faces before him, he saw curiosity and
       +interest replacing the skepticism he had encountered for so long.
       +
       +In the end, Jimmy's devotion to the past had brought him a new sense of
       +purpose in the present. His journey from a quiet geek who loved
       +forgotten protocols to a respected advocate for preserving the essence
       +of the old internet was a testament to the power of passion and
       +connection. And as he walked off the stage, he knew that the legacy of
       +the finger and gopher protocols would continue to thrive in the hearts
       +of those who believed that the past had something meaningful to offer
       +the future.
       +
       +In the twilight of his life, after years of advocating for the old
       +school internet with unwavering passion, Jimmy found himself surrounded
       +by friends he had met along his journey. Eleanor stood by his side,
       +a steadfast companion who had become a true friend. They had organized
       +a small gathering of like-minded enthusiasts who shared his love for the
       +finger and gopher protocols.
       +
       +As they shared stories, laughter, and the nostalgia of their digital
       +adventures, Jimmy's eyes sparkled with contentment. The flickering
       +screens in the room, displaying text-based interfaces of a bygone era,
       +seemed to be a tribute to his enduring legacy. Among his companions, he
       +had finally found the community he had always yearned for, one that
       +cherished the past while embracing the present.
       +
       +As the evening unfolded, Jimmy's breathing grew slower, his body showing
       +signs of the passage of time. With a peaceful smile, he closed his eyes,
       +surrounded by the warmth of friends who understood and appreciated him
       +for who he was. In that moment, he seemed to become one with the digital
       +history he had loved so dearly.
       +
       +And so, in the company of those who shared his passion, Jimmy passed
       +away, leaving behind a legacy that would continue to inspire others to
       +seek the beauty and authenticity of the past, even in a world consumed
       +by modernity. His dedication to the finger and gopher protocols had
       +transformed his life and the lives of those he touched, reminding
       +everyone that the threads of connection woven by the old internet would
       +forever remain intertwined with the fabric of their shared memories.
 (DIR) diff --git a/example/uptime b/example/uptime
       @@ -0,0 +1,8 @@
       +#!/bin/sh
       +
       +# Print the current date
       +echo "Current date: $(date)"
       +
       +# Print system uptime
       +uptime=$(uptime)
       +echo "System uptime: $uptime"
 (DIR) diff --git a/go.mod b/go.mod
       @@ -0,0 +1,3 @@
       +module fingered
       +
       +go 1.20
 (DIR) diff --git a/main.go b/main.go
       @@ -0,0 +1,95 @@
       +package main
       +
       +import (
       +        "fmt"
       +        "net"
       +        "path/filepath"
       +        "strings"
       +
       +        "fingered/config"
       +        "fingered/utils"
       +)
       +
       +type Config struct {
       +        Threads int
       +        Port    int
       +        Path    string
       +}
       +
       +const bufferSize = 1024
       +
       +func handleRequest(conn net.Conn, dir string, index string) {
       +        defer conn.Close()
       +
       +        // Read the incoming request
       +        buffer := make([]byte, bufferSize)
       +        n, err := conn.Read(buffer)
       +        if err != nil {
       +                utils.LogMsg("ERROR: %v", err)
       +                return
       +        }
       +
       +        request := strings.TrimSpace(string(buffer[:n]))
       +
       +        // Sanitize the request
       +        if len(request) > 0 && !utils.IsValidWord(request) {
       +                utils.LogMsg("INFO: Invalid username")
       +                _, err = utils.WriteResponse(conn, "Invaild user\n")
       +                if err != nil {
       +                        utils.LogMsg("ERROR: %s", err)
       +                        return
       +                }
       +
       +                return
       +        }
       +
       +        if len(request) == 0 {
       +                request = index
       +        }
       +
       +        response, err := utils.GetContent(filepath.Join(dir, request))
       +        if err != nil {
       +                utils.LogMsg("ERROR: %s", err)
       +                return
       +        }
       +
       +        _, err = utils.WriteResponse(conn, response)
       +        if err != nil {
       +                utils.LogMsg("ERROR: %s", err)
       +                return
       +        }
       +
       +}
       +
       +func main() {
       +
       +        cfg := config.ParseFlags()
       +
       +        connectionChannel := make(chan net.Conn, cfg.Threads)
       +
       +        for i := 0; i < cfg.Threads; i++ {
       +                go func() {
       +                        for conn := range connectionChannel {
       +                                handleRequest(conn, cfg.Dir, cfg.Index)
       +                        }
       +                }()
       +        }
       +
       +        listener, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.Port))
       +        if err != nil {
       +                utils.LogMsg("ERROR: %v", err)
       +                return
       +        }
       +        defer listener.Close()
       +
       +        utils.LogMsg("Starting with threads: %d, port: %d, dir: %s", cfg.Threads, cfg.Port, cfg.Dir)
       +
       +        for {
       +                conn, err := listener.Accept()
       +                if err != nil {
       +                        utils.LogMsg("ERROR: %v", err)
       +                        continue
       +                }
       +                connectionChannel <- conn
       +        }
       +}
 (DIR) diff --git a/tests/utils_test.go b/tests/utils_test.go
       @@ -0,0 +1,113 @@
       +package tests
       +
       +import (
       +        "bytes"
       +        "log"
       +        "os"
       +        "strings"
       +        "testing"
       +
       +        "fingered/utils"
       +)
       +
       +func TestIsValidWord(t *testing.T) {
       +        validWords := []string{
       +                "john", "alice", "bob", "dave",
       +                "john123", "alice456", "dave789",
       +                "john_doe", "alice_smith", "dave_jones",
       +        }
       +
       +        invalidWords := []string{
       +                "123",                                  // Numbers only
       +                "foo.bar",                              // Special characters
       +                " ",                                    // Empty string
       +                "username=",                            // Potential injection attempt
       +                "../",                                  // Directory traversal
       +                "/etc/passwd",                          // Absolute path traversal
       +                "\\windows\\system32\\",                // Windows path traversal
       +                "ROOT",                                 // Uppercase letters
       +                "john_doe_",                            // Underscore at the end
       +                "john_doe_doe_doe_doe_doe_doe_doe_doe", // Too long
       +                "foo|bar",                              // Shell pipe
       +                "filename>.txt",                        // Shell redirection
       +                "cmd &",                                // Background execution
       +                "`ls -l`",                              // Command substitution
       +                "$(echo hi)",                           // Command substitution
       +                "{malicious}",                          // Command grouping
       +                "evil$word",                            // Dollar sign
       +                "abc;def",                              // Command chaining
       +                "[evil]",                               // Command grouping
       +                "nasty~word",                           // Tilde expansion
       +                "question?",                            // Question mark
       +                "exclamation!",                         // Exclamation mark
       +                "\"quoted\"",                           // Double quotes
       +                "'quoted'",                             // Single quotes
       +                "escaped\\",                            // Backslash
       +        }
       +
       +        for _, word := range validWords {
       +                if !utils.IsValidWord(word) {
       +                        t.Errorf("isValidWord(%s) returned false, expected true", word)
       +                }
       +        }
       +
       +        for _, word := range invalidWords {
       +                if utils.IsValidWord(word) {
       +                        t.Errorf("isValidWord(%s) returned true, expected false", word)
       +                }
       +
       +        }
       +}
       +
       +func TestGetContent(t *testing.T) {
       +        // Create a temporary shell script file for testing
       +        scriptContent := "#!/bin/sh\n\necho 'Hello, World!'"
       +        scriptFile, err := os.CreateTemp("", "test_script_*.sh")
       +        if err != nil {
       +                t.Fatalf("Failed to create temporary script file: %v", err)
       +        }
       +        defer os.Remove(scriptFile.Name())
       +        defer scriptFile.Close()
       +
       +        _, err = scriptFile.WriteString(scriptContent)
       +        if err != nil {
       +                t.Fatalf("Failed to write to temporary script file: %v", err)
       +        }
       +
       +        tests := []struct {
       +                filePath string
       +                expected string
       +        }{
       +                {"nonexistent.txt", "file not found"},
       +                {scriptFile.Name(), "Hello, World!\n"},
       +        }
       +
       +        for _, test := range tests {
       +                actual, err := utils.GetContent(test.filePath)
       +                if err != nil {
       +                        if actual != test.expected {
       +                                t.Errorf("For file %s, expected '%s', but got '%s'", test.filePath, test.expected, actual)
       +                        }
       +                }
       +
       +                if actual != test.expected {
       +                        t.Errorf("For file %s, expected '%s', but got '%s'", test.filePath, test.expected, actual)
       +                }
       +        }
       +}
       +
       +func TestLogMsg(t *testing.T) {
       +        var buf bytes.Buffer
       +        log.SetOutput(&buf)
       +        defer func() {
       +                log.SetOutput(os.Stderr)
       +        }()
       +
       +        utils.LogMsg("Test message: %s %d", "Hello", 123)
       +        logOutput := buf.String()
       +        expectedLogMessage := "Test message: Hello 123"
       +
       +        if !strings.Contains(logOutput, expectedLogMessage) {
       +                t.Errorf("Log message does not contain the expected message. Got: %s, Expected: %s", logOutput, expectedLogMessage)
       +        }
       +}
 (DIR) diff --git a/utils/utils.go b/utils/utils.go
       @@ -0,0 +1,66 @@
       +package utils
       +
       +import (
       +        "fmt"
       +        "log"
       +        "net"
       +        "os"
       +        "os/exec"
       +        "unicode"
       +)
       +
       +func LogMsg(format string, args ...interface{}) {
       +        message := fmt.Sprintf(format, args...)
       +        log.Print(message)
       +}
       +
       +func GetContent(filePath string) (string, error) {
       +        _, err := os.Stat(filePath)
       +        if os.IsNotExist(err) {
       +                return "file not found", err
       +        }
       +
       +        content, err := os.ReadFile(filePath)
       +        if err != nil {
       +                return "unable to read file", err
       +        }
       +
       +        isScript := false
       +        if len(content) > 2 && string(content[:3]) == "#!/" {
       +                isScript = true
       +        }
       +
       +        if isScript {
       +                cmd := exec.Command("sh", filePath)
       +                output, err := cmd.CombinedOutput()
       +                if err != nil {
       +                        return "file execution failed", err
       +                }
       +                return string(output), nil
       +        }
       +
       +        return string(content), nil
       +}
       +
       +func WriteResponse(conn net.Conn, response string) (string, error) {
       +        _, err := conn.Write([]byte(response))
       +        if err != nil {
       +                return "failed to write to socket", err
       +        }
       +
       +        return "", nil
       +}
       +
       +func IsValidWord(input string) bool {
       +        if len(input) < 1 || len(input) > 32 || !unicode.IsLower(rune(input[0])) {
       +                return false
       +        }
       +
       +        for _, r := range input {
       +                if !(unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_') {
       +                        return false
       +                }
       +        }
       +
       +        return input[len(input)-1] != '_'
       +}