Skip to content

Commit

Permalink
Simplified user interface
Browse files Browse the repository at this point in the history
  • Loading branch information
Boernsman authored and Boernsman committed Oct 7, 2024
1 parent a75f8a3 commit d512df3
Show file tree
Hide file tree
Showing 23 changed files with 229 additions and 10,347 deletions.
12 changes: 7 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,23 @@ RUN go build -o whereisit .
# Use a smaller image for deployment
FROM alpine:3.18

WORKDIR /app

# Install certificates for HTTPS
RUN apk --no-cache add ca-certificates

# Create a folder for certificates (assuming you have your own SSL cert and key)
RUN mkdir -p /etc/ssl/certs && mkdir -p /etc/ssl/private

# Copy the SSL certificates (adjust paths as necessary)
COPY ./certs/server.crt /etc/ssl/certs/
COPY ./certs/server.key /etc/ssl/private/
COPY ./server.crt* /etc/ssl/certs/
COPY ./server.key* /etc/ssl/private/

# Copy the built Go binary from the builder
COPY --from=builder /app/main /app/main
COPY --from=builder /app/whereisit /app/whereisit

# Expose HTTP on port 80 and HTTPS on port 443
EXPOSE 80 443
# Expose HTTP and HTTPS
EXPOSE 8180 443

# Command to run the executable
CMD ["/app/whereisit"]
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Where is my device? And why is Zeroconf not working? Damn it!

This tool eases your pain. It helps you to find your devices.

![whereisit_ui](whereisit_ui.png)


## Usage

Start the server with:
Expand All @@ -15,24 +18,25 @@ Start the server with:
```


Register device with:
Register a device with:

```
curl -H "Content-Type: application/json" -X POST -d '{"name":"${DEVICE_NAME}","address":"${DEVICE_IP}"}' http://${SERVER_IP}:8180/api/register
```

List device with:
List the devices with:

```
http://${SERVER_IP}:8180/api/devices
```

See the build in web site:
See the build in web ui:

```
http://${SERVER_IP}:8180
```


## Build

```
Expand All @@ -47,7 +51,20 @@ go test .

## Security

Not very secure
To ensure the security and protect data integrity, 'whereistit' offers 2 authentication methods that are diabled by default.

**Basic Authentication**

Basic Authentication can be enabled to verify user identity via a username and password combination.
Credentials for Basic Auth are stored securely in the 'whereisit.ini' file.
We recommend using HTTPS to prevent the interception of credentials during transmission.

**API Key Authentication**

For external client authentication, API Key Authentication can also be enabled. Each client is assigned a unique API key, which is stored in the 'whereisit.ini' file.
The API key must be included in the request headers for all API interactions. This method ensures that access is restricted to authorized users and allows tracking of individual client activity.

Both methods rely on secure storage and management of credentials in the whereisit.ini file.


## License
Expand Down
5 changes: 5 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#!/bin/bash

BINARY_NAME="whereisit"
CONFIG_NAME="whereisit.ini"
SERVICE_NAME="whereisit.service"
BINARY_INSTALL_PATH="/usr/local/bin/"
CONFIG_INSTALL_PATH="/etc/"
SERVICE_INSTALL_PATH="/etc/systemd/system/"
PUBLIC_SOURCE_DIR="./public"
PUBLIC_DEST_DIR="/var/www/whereisit/public"
Expand All @@ -28,6 +30,9 @@ if [[ $? -ne 0 ]]; then
exit 1
fi

echo "Installing ${CONFIG_NAME} to ${CONFIG_INSTALL_PATH}"
cp "${WORKING_DIR}/${CONFIG_NAME}" "${CONFIG_INSTALL_PATH}"

echo "Creating web content directory at ${PUBLIC_DEST_DIR}"
mkdir -p "${PUBLIC_DEST_DIR}"

Expand Down
114 changes: 65 additions & 49 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ type Device struct {
Added time.Time `json:"added"`
}

type Credentials struct {
Username string
Password string
APIKey string
type Config struct {
BasicAuthEnabled bool
Username string
Password string
ApiKeyAuthEnabled bool
APIKey string
}

func LoadCredentials(primaryPath, fallbackPath string) (*Credentials, error) {
func LoadConfiguration(primaryPath, fallbackPath string) (*Config, error) {
// Check if the primary file exists
if _, err := os.Stat(primaryPath); os.IsNotExist(err) {
log.Printf("Primary file %s not found, trying fallback file %s\n", primaryPath, fallbackPath)
Expand All @@ -55,27 +57,33 @@ func LoadCredentials(primaryPath, fallbackPath string) (*Credentials, error) {
return nil, fmt.Errorf("failed to load ini file: %w", err)
}

username := cfg.Section("auth").Key("username").String()
password := cfg.Section("auth").Key("password").String()
apiKey := cfg.Section("api").Key("api_key").String()
var config Config

if username == "" || password == "" || apiKey == "" {
return nil, fmt.Errorf("missing required credentials in ini file")
}
config.BasicAuthEnabled, _ = cfg.Section("basic_auth").Key("enabled").Bool()
if config.BasicAuthEnabled {
config.Username = cfg.Section("basic_auth").Key("username").String()
config.Password = cfg.Section("basic_auth").Key("password").String()

return &Credentials{
Username: username,
Password: password,
APIKey: apiKey,
}, nil
if config.Username == "" || config.Password == "" {
return nil, fmt.Errorf("missing required basic auth credentials in ini file")
}
}
config.ApiKeyAuthEnabled, _ = cfg.Section("api").Key("api_key_enabled").Bool()
if config.ApiKeyAuthEnabled {
config.APIKey = cfg.Section("api").Key("api_key").String()
if config.APIKey == "" {
return nil, fmt.Errorf("missing required credentials in ini file")
}
}
return &config, nil
}

func main() {

primaryIniFilePath := "/etc/whereisit.ini"
fallbackIniFilePath := "./whereisit.ini"

credentials, err := LoadCredentials(primaryIniFilePath, fallbackIniFilePath)
config, err := LoadConfiguration(primaryIniFilePath, fallbackIniFilePath)
if err != nil {
log.Fatalf("Error loading credentials: %v", err)
}
Expand Down Expand Up @@ -106,8 +114,12 @@ func main() {
slog.SetLogLoggerLevel(slog.LevelDebug)
apiRouter.Use(logRequest)
}
apiRouter.Use(KeyAuth(credentials.APIKey))
apiRouter.Use(BasicAuthMiddleware(credentials.Username, credentials.Password))
if config.ApiKeyAuthEnabled {
apiRouter.Use(KeyAuth(config.APIKey))
}
if config.BasicAuthEnabled {
apiRouter.Use(BasicAuthMiddleware(config.Username, config.Password))
}
apiRouter.HandleFunc("/register", RegisterDevice).Methods("POST")
apiRouter.HandleFunc("/devices", ListDevices).Methods("GET")
apiRouter.HandleFunc("/alldevices", ListAllDevices).Methods("GET")
Expand Down Expand Up @@ -312,25 +324,13 @@ func RegisterDevice(w http.ResponseWriter, r *http.Request) {
}

// Get the external address
ea, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
slog.Debug("Remote address invalid", "address", r.RemoteAddr)
ea := getIPAddressFromRequest(r)
if ea == "" {
http.Error(w, `Host 127.0.0.1 is not allowed to register devices`, http.StatusBadRequest)
http.NotFound(w, r)
return
}

// Check if proxy was configured.
if ea == "127.0.0.1" {
xrealip := r.Header.Get("x-real-ip")
if xrealip == "" {
slog.Debug("127.0.0.1 tried to add an address, this can happen when proxy is not configured correctly.")
http.Error(w, `Host 127.0.0.1 is not allowed to register devices`, http.StatusBadRequest)
http.NotFound(w, r)
return
}
ea = xrealip
}

devices.Lock()
defer devices.Unlock()

Expand All @@ -348,25 +348,14 @@ func RegisterDevice(w http.ResponseWriter, r *http.Request) {
}

func ListDevices(w http.ResponseWriter, r *http.Request) {
ea, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
slog.Debug("Remote address not available")
// Get the external address
ea := getIPAddressFromRequest(r)
if ea == "" {
http.Error(w, `Host 127.0.0.1 is not allowed to register devices`, http.StatusBadRequest)
http.NotFound(w, r)
return
}

// Check if proxy was configured.
if ea == "127.0.0.1" {
slog.Debug("127.0.0.1 tried to access an address.")
xrealip := r.Header.Get("x-real-ip")
if xrealip == "" {
slog.Debug("x-real-ip header is not set")
http.NotFound(w, r)
return
}
ea = xrealip
}

devices.Lock()
defer devices.Unlock()

Expand Down Expand Up @@ -399,6 +388,31 @@ func cleanup(lifetime time.Duration) {
}
}

// Extracts the IP address from RemoteAddr, handling both IPv4 and IPv6
func getIPAddressFromRequest(r *http.Request) string {

// For IPv6 addresses, RemoteAddr includes brackets around the IP and a zone identifier (e.g., %wlan0)
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
slog.Debug("Could not split remote address into IP and port", "remote address", r.RemoteAddr)
return ""
}

// Split out any zone identifier in the IPv6 address (i.e., '%interface' like '%wlan0')
ip = strings.Split(ip, "%")[0]

// Check if proxy was configured.
if ip == "127.0.0.1" || ip == "::1" {
xrealip := r.Header.Get("x-real-ip")
if xrealip == "" {
slog.Debug("127.0.0.1 tried to add an address, this can happen when proxy is not configured correctly.")
return ""
}
ip = xrealip
}
return ip
}

func isLocalNetwork(ip string) bool {

// Parse the IP to ensure it's a valid one
Expand All @@ -413,10 +427,12 @@ func isLocalNetwork(ip string) bool {
"10.0.0.0/8", // Class A private network
"172.16.0.0/12", // Class B private network
"192.168.0.0/16", // Class C private network
"127.0.0.0/8", // Loopback range

// Define private IPv6 address ranges
"fc00::/7", // Unique local address range
"fe80::/10", // Link-local address range
"::1/128", // Loopback range
}

// Check if the parsed IP falls within any of the private ranges
Expand Down
21 changes: 0 additions & 21 deletions public/bulma-0.1.2/LICENSE

This file was deleted.

Loading

0 comments on commit d512df3

Please sign in to comment.