Kasianov Nikolai Alekseevich
11 months ago
commit
20338044df
12 changed files with 483 additions and 0 deletions
@ -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. |
@ -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
|
@ -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 |
@ -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) |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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) |
||||
} |
Loading…
Reference in new issue