Browse Source

Initial commit

master
commit
20338044df
  1. 3
      .gitignore
  2. 9
      LICENSE
  3. 6
      Makefile
  4. 64
      README.md
  5. 6
      brew.sh
  6. 3
      client/go.mod
  7. 86
      client/main.go
  8. 86
      server/conf.go
  9. 3
      server/go.mod
  10. 120
      server/main.go
  11. 91
      server/pot.go
  12. 6
      stopPouring.sh

3
.gitignore vendored

@ -0,0 +1,3 @@
HTCPCP-client
HTCPCP-server
conf.json

9
LICENSE

@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright © 2024 Kasianov Nikolai Alekseevich (Unbewohnte)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

6
Makefile

@ -0,0 +1,6 @@
all:
cd client && go build && mv HTCPCP-client* ..
cd server && go build && mv HTCPCP-server* ..
clean:
rm -rf HTCPCP-* conf.json

64
README.md

@ -0,0 +1,64 @@
# HTCPCP (RFC 2324)
## Server and client (incomplete) implementation
### Info
This is an incomplete HTCPCP [RFC 2324](https://datatracker.ietf.org/doc/html/rfc2324) implementation for the server and the client.
PROPFIND, GET, BREW and WHEN requests are handled. Additions are not supported.
### Server
On BREW and WHEN requests server *executes specified commands* from configuration file. This way it is possible for the server to actually launch some work via a script when requests come.
If configuration file is not present - the next launch will create it.
Default configuration file structure:
```json
{
"commands": {
"BrewCommand": "./brew.sh",
"StopPouringCommand": "./stopPouring.sh"
},
"coffee-type": "Latte",
"brew-time-sec": 10,
"max-pour-time-sec": 5
}
```
`brew-time-sec` is the approximate time it takes to brew coffee in seconds. After this amount is passed, status of the pot changes to `Pouring` and will stay it until `max-pour-time-sec` seconds are passed or an incoming request with `WHEN` method is presented.
By default port 80 is used, but can be changed with a `-port` flag:
`HTCPCP-server -port 8000`
Server will listen and handle incoming requests.
### Client
Client is used to interact with the server and has such syntax:
```
HTCPCP-client (-version) [ADDR] [COMMAND]
-version
Print version information and exit
ADDR string
Address of an HTCPCP server with port (ie: http://111.11.111.1:80 or http://coffeeserver:80)
COMMAND string
Command to send (ie: GET, WHEN, BREW, PROPFIND)
```
Examples:
- `HTCPCP-client -help` - prints above syntax message
- `HTCPCP-client -version` - prints version information
- `HTCPCP-client http://localhost:8000 propfind` - sends a PROPFIND request to localhost:8000
- `HTCPCP-client http://localhost:8000 brew` - sends a BREW request to localhost:8000
- `HTCPCP-client http://localhost:8000 get` - sends a get request to localhost:8000
### License
MIT License

6
brew.sh

@ -0,0 +1,6 @@
#!/bin/sh
echo "Brewing!"
# ...
# ...
# ...

3
client/go.mod

@ -0,0 +1,3 @@
module Unbewohnte/HTCPCP-client
go 1.18

86
client/main.go

@ -0,0 +1,86 @@
/*
The MIT License (MIT)
Copyright © 2024 Kasianov Nikolai Alekseevich (Unbewohnte)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package main
import (
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
)
var version *bool = flag.Bool("version", false, "Print version information")
const (
CommandGet = "GET"
CommandWhen = "WHEN"
CommandBrew = "BREW"
CommandPropfind = "PROPFIND"
)
const Version string = "0.1-client"
func makeRequest(address string, method string) {
request, err := http.NewRequest(method, address, nil)
if err != nil {
log.Fatalf("Failed to form a request %s", err)
}
request.Header.Add("Content-Type", "application/coffee-pot-command")
response, err := http.DefaultClient.Do(request)
if err != nil {
log.Fatalf("Failed to %s: %s", method, err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
log.Fatalf("Failed to read body: %s", err)
}
log.Printf("Status: %d; Headers: %+s\nData: %s", response.StatusCode, response.Header, body)
}
func main() {
log.SetOutput(os.Stdout)
log.Default().SetFlags(0)
flag.Usage = func() {
fmt.Printf(`HTCPCP-client (-version) [ADDR] [COMMAND]
-version
Print version information and exit
ADDR string
Address of an HTCPCP server with port (ie: http://111.11.111.1:80 or http://coffeeserver:80)
COMMAND string
Command to send (ie: GET, WHEN, BREW, PROPFIND)
`,
)
}
flag.Parse()
if *version {
fmt.Printf("HTCPCP-client %s\nKasianov Nikolai Alekseevich (Unbewohnte)\n", Version)
return
}
if len(os.Args) < 3 {
log.Fatalf("Not enough arguments! Run with -help to see a help message")
}
address := os.Args[1]
command := strings.ToUpper(os.Args[2])
makeRequest(address, command)
}

86
server/conf.go

@ -0,0 +1,86 @@
/*
The MIT License (MIT)
Copyright © 2024 Kasianov Nikolai Alekseevich (Unbewohnte)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package main
import (
"encoding/json"
"io"
"os"
)
type Commands struct {
BrewCommand string
StopPouringCommand string
}
type Conf struct {
Commands Commands `json:"commands"`
CoffeeType string `json:"coffee-type"`
BrewTimeSec uint `json:"brew-time-sec"`
MaxPourTimeSec uint `json:"max-pour-time-sec"`
}
func DefaultConf() Conf {
return Conf{
Commands: Commands{
BrewCommand: "./brew.sh",
StopPouringCommand: "./stopPouring.sh",
},
CoffeeType: "Latte",
BrewTimeSec: 10,
MaxPourTimeSec: 5,
}
}
// Tries to retrieve configuration structure from given json file
func ConfFromFile(path string) (Conf, error) {
confFile, err := os.Open(path)
if err != nil {
return DefaultConf(), err
}
defer confFile.Close()
confBytes, err := io.ReadAll(confFile)
if err != nil {
return DefaultConf(), err
}
var conf Conf
err = json.Unmarshal(confBytes, &conf)
if err != nil {
return DefaultConf(), err
}
return conf, nil
}
// Create a new configuration file
func CreateConf(path string, conf Conf) (Conf, error) {
confFile, err := os.Create(path)
if err != nil {
return DefaultConf(), err
}
defer confFile.Close()
confJsonBytes, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return conf, err
}
_, err = confFile.Write(confJsonBytes)
if err != nil {
return conf, nil
}
return conf, nil
}

3
server/go.mod

@ -0,0 +1,3 @@
module Unbewohnte/HTCPCP-server
go 1.18

120
server/main.go

@ -0,0 +1,120 @@
/*
The MIT License (MIT)
Copyright © 2024 Kasianov Nikolai Alekseevich (Unbewohnte)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
)
const ConfName string = "conf.json"
const Version string = "0.1-server"
var (
port *uint = flag.Uint("port", 80, "Set server port")
version *bool = flag.Bool("version", false, "Print version information")
)
func main() {
log.SetOutput(os.Stdout)
flag.Parse()
if *version {
fmt.Printf("HTCPCP-server %s\nKasianov Nikolai Alekseevich (Unbewohnte)\n", Version)
return
}
// Work out the working directory
exePath, err := os.Executable()
if err != nil {
log.Fatalf("Failed to retrieve executable's path")
}
wDir := filepath.Dir(exePath)
// Open commands file, create if does not exist
conf, err := ConfFromFile(filepath.Join(wDir, ConfName))
if err != nil {
_, err = CreateConf(filepath.Join(wDir, ConfName), DefaultConf())
if err != nil {
log.Fatalf("Failed to create a new commands file: %s", err)
}
log.Printf("Created a new commands file")
os.Exit(0)
}
pot := NewPot(conf)
handler := http.NewServeMux()
handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "BREW" || r.Method == "POST" {
// Brew some coffee
if r.Header.Get("Content-Type") != "application/coffee-pot-command" {
http.Error(w, "Coffee content type is not set", http.StatusBadRequest)
return
}
if r.Header.Get("Accept-Additions") != "" {
// Additions were specified!
http.Error(w, "Additions are not supported", http.StatusNotAcceptable)
return
}
err := pot.Brew()
if err != nil {
log.Printf("Failed to BREW: %s", err)
http.Error(w, "Brewing error", http.StatusInternalServerError)
}
} else if r.Method == "GET" {
// Return Pot information
w.Header().Add("Additions-List", "milk")
w.Header().Add("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(&pot)
if err != nil {
log.Printf("Failed to answer a GET: %s", err)
http.Error(w, "JSON encoding failed", http.StatusInternalServerError)
}
} else if r.Method == "PROPFIND" {
// Write what king of coffee we're making
w.Header().Add("Content-Type", "text/plain")
w.Write([]byte(pot.CoffeeType))
} else if r.Method == "WHEN" {
if r.Header.Get("Content-Type") != "application/coffee-pot-command" {
http.Error(w, "Coffee content type is not set", http.StatusBadRequest)
return
}
err := pot.StopPouring()
if err != nil {
log.Printf("Failed to stop pouring milk: %s\n", err)
}
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
server := http.Server{
Addr: fmt.Sprintf(":%d", *port),
Handler: handler,
}
err = server.ListenAndServe()
if err != nil {
log.Fatalf("Fatal server error: %s!", err)
}
}

91
server/pot.go

@ -0,0 +1,91 @@
/*
The MIT License (MIT)
Copyright © 2024 Kasianov Nikolai Alekseevich (Unbewohnte)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package main
import (
"errors"
"log"
"os/exec"
"time"
)
const (
PotStatusErr = "Error"
PotStatusReady = "Ready"
PotStatusBrewing = "Brewing"
PotStatusPouring = "Pouring"
)
type Pot struct {
State string `json:"state"`
CoffeeType string `json:"coffeeType"`
BrewTimeSec uint `json:"brew-time-sec"`
MaxPourTimeSec uint `json:"max-pour-time-sec"`
commands Commands
}
func NewPot(conf Conf) *Pot {
return &Pot{
State: PotStatusReady,
CoffeeType: conf.CoffeeType,
BrewTimeSec: conf.BrewTimeSec,
MaxPourTimeSec: conf.MaxPourTimeSec,
commands: conf.Commands,
}
}
func run(command string, args ...string) error {
output, err := exec.Command(command, args...).Output()
if err != nil {
return err
}
log.Printf("%s", string(output))
return nil
}
// Brew some coffee!
func (p *Pot) Brew(args ...string) error {
if p.State != PotStatusReady {
return errors.New("pot is not yet ready")
}
p.State = PotStatusBrewing
go func() {
// Start pouring after brewing is done
time.Sleep(time.Second * time.Duration(p.BrewTimeSec))
p.State = PotStatusPouring
log.Print("Pouring!")
go func() {
// Stop pouring...
time.Sleep(time.Second * time.Duration(p.MaxPourTimeSec))
if p.State == PotStatusPouring {
// ...if it was not stopped earlier
p.State = PotStatusReady
log.Print("Poured at maximum capacity!")
}
}()
}()
return run(p.commands.BrewCommand, args...)
}
// No more pouring!
func (p *Pot) StopPouring() error {
if p.State != PotStatusPouring {
return errors.New("coffee is not brewed yet")
}
p.State = PotStatusReady
return run(p.commands.StopPouringCommand)
}

6
stopPouring.sh

@ -0,0 +1,6 @@
#!/bin/sh
echo "Stopping to pour!"
# ...
# ...
# ...
Loading…
Cancel
Save