├── version
└── ver.go
├── .gitignore
├── api
├── message.go
├── peer_list_units.go
├── unit_show.go
├── peer_report.go
├── cors.go
├── peer_data.go
├── units_list.go
├── setup.go
├── unit_enroll.go
├── setup_server.go
├── setup_peer.go
├── utils.go
├── auth.go
├── unit_report.go
├── peer_mesh.go
├── peer_inbox.go
├── unit_inbox.go
└── client.go
├── env.example
├── models
├── access_point.go
├── unit_inbox.go
├── setup.go
├── unit_enroll.go
├── message.go
├── unit_find.go
├── enrollment.go
└── unit.go
├── pwngrid-peer.service
├── pwngrid.service
├── utils
├── host.go
└── exec.go
├── wifi
├── parse.go
├── defines.go
├── unpack.go
├── utils.go
├── compression.go
└── pack.go
├── .github
└── FUNDING.yml
├── cmd
└── pwngrid
│ ├── main.go
│ ├── vars.go
│ ├── inbox.go
│ └── setup.go
├── Dockerfile
├── go.mod
├── release.sh
├── README.md
├── mesh
├── hopping.go
├── interface.go
├── peer_json.go
├── memory.go
├── packet_muxer.go
├── routing.go
└── peer.go
├── crypto
├── sign.go
├── encrypt.go
└── keypair.go
├── docker-compose.yml
├── changelog.sh
├── .travis.yml
├── Makefile
├── go.sum
└── LICENSE.md
/version/ver.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | const (
4 | Version = "1.11.4"
5 | )
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .idea
3 | test-unit
4 | test-unit.pub
5 | build
6 | id_rsa
7 | id_rsa.pub
8 | key.json
9 |
--------------------------------------------------------------------------------
/api/message.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | type Message struct {
4 | Data string `json:"data"`
5 | Signature string `json:"signature"`
6 | }
7 |
--------------------------------------------------------------------------------
/env.example:
--------------------------------------------------------------------------------
1 | API_SECRET=02zygnJs5e0bBLJjaHCinWTjfRdheTYO
2 |
3 | DB_HOST=pwngrid-mysql
4 | DB_DRIVER=mysql
5 | DB_USER=pwngrid
6 | DB_PASSWORD=pwngrid
7 | DB_NAME=pwngrid
8 | DB_PORT=3306
--------------------------------------------------------------------------------
/models/access_point.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "github.com/jinzhu/gorm"
4 |
5 | type AccessPoint struct {
6 | gorm.Model
7 |
8 | UnitID uint `json:"-"`
9 | Name string `gorm:"size:255;not null" json:"name"`
10 | Mac string `gorm:"size:255;not null" json:"mac"`
11 | }
12 |
--------------------------------------------------------------------------------
/pwngrid-peer.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=pwngrid peer service
3 | Documentation=https://pwnagotchi.org/
4 | Wants=network.target
5 | After=network.target
6 |
7 | [Service]
8 | Type=simple
9 | ExecStart=/usr/local/bin/pwngrid -log /var/log/pwngrid.log -peers /root/peers -address 127.0.0.1:8666
10 | Restart=always
11 | RestartSec=30
12 |
13 | [Install]
14 | WantedBy=multi-user.target
15 |
--------------------------------------------------------------------------------
/pwngrid.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=pwngrid api service
3 | Documentation=https://pwnagotchi.org/
4 | Wants=network.target
5 | After=network.target
6 |
7 | [Service]
8 | Type=simple
9 | ExecStart=/usr/local/bin/pwngrid -log /var/log/pwngrid.log -env /etc/pwngrid/pwngrid.conf -address 127.0.0.1:8666
10 | Restart=always
11 | RestartSec=30
12 |
13 | [Install]
14 | WantedBy=multi-user.target
--------------------------------------------------------------------------------
/utils/host.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/evilsocket/islazy/log"
5 | "os"
6 | "strings"
7 | )
8 |
9 | func Hostname() string {
10 | name, err := os.Hostname()
11 | if err != nil {
12 | log.Warning("%v", err)
13 | return ""
14 | }
15 |
16 | if strings.HasSuffix(name, ".local") {
17 | name = strings.Replace(name, ".local", "", -1)
18 | }
19 |
20 | return name
21 | }
22 |
--------------------------------------------------------------------------------
/utils/exec.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/evilsocket/islazy/str"
5 | "os/exec"
6 | )
7 |
8 | func Exec(executable string, args []string) (string, error) {
9 | path, err := exec.LookPath(executable)
10 | if err != nil {
11 | return "", err
12 | }
13 |
14 | raw, err := exec.Command(path, args...).CombinedOutput()
15 | if err != nil {
16 | return "", err
17 | } else {
18 | return str.Trim(string(raw)), nil
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/api/peer_list_units.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | func (api *API) PeerListUnits(w http.ResponseWriter, r *http.Request) {
8 | page, err := pageNum(r)
9 | if err != nil {
10 | ERROR(w, http.StatusUnprocessableEntity, err)
11 | return
12 | }
13 |
14 | obj, err := api.Client.PagedUnits(page)
15 | if err != nil {
16 | ERROR(w, http.StatusUnprocessableEntity, err)
17 | return
18 | }
19 |
20 | JSON(w, http.StatusOK, obj)
21 | }
22 |
--------------------------------------------------------------------------------
/api/unit_show.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/go-chi/chi/v5"
5 | "github.com/jayofelony/pwngrid/models"
6 | "net/http"
7 | )
8 |
9 | func (api *API) ShowUnit(w http.ResponseWriter, r *http.Request) {
10 | unitFingerprint := chi.URLParam(r, "fingerprint")
11 | if unit := models.FindUnitByFingerprint(unitFingerprint); unit == nil {
12 | ERROR(w, http.StatusNotFound, ErrEmpty)
13 | return
14 | } else {
15 | JSON(w, http.StatusOK, unit)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/wifi/parse.go:
--------------------------------------------------------------------------------
1 | package wifi
2 |
3 | import (
4 | "github.com/gopacket/gopacket"
5 | "github.com/gopacket/gopacket/layers"
6 | )
7 |
8 | func Parse(packet gopacket.Packet) (ok bool, radio *layers.RadioTap, dot11 *layers.Dot11) {
9 | ok = false
10 | radio = nil
11 | dot11 = nil
12 |
13 | radioLayer := packet.Layer(layers.LayerTypeRadioTap)
14 | if radioLayer == nil {
15 | return
16 | }
17 | radio, ok = radioLayer.(*layers.RadioTap)
18 | if !ok || radio == nil {
19 | return
20 | }
21 |
22 | dot11Layer := packet.Layer(layers.LayerTypeDot11)
23 | if dot11Layer == nil {
24 | ok = false
25 | return
26 | }
27 |
28 | dot11, ok = dot11Layer.(*layers.Dot11)
29 | return
30 | }
31 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: jayofelony # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon:
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/cmd/pwngrid/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "github.com/evilsocket/islazy/log"
7 | "github.com/jayofelony/pwngrid/version"
8 | )
9 |
10 | func main() {
11 | flag.Parse()
12 |
13 | setupCore()
14 | defer cleanup()
15 |
16 | // just print the version and exit
17 | if ver {
18 | fmt.Println(version.Version)
19 | return
20 | }
21 |
22 | // from here on we need logging
23 | if err := log.Open(); err != nil {
24 | panic(err)
25 | }
26 | defer log.Close()
27 |
28 | // do mode related initialization
29 | setupMode()
30 |
31 | // if we're in peer mode and is an inbox action
32 | if inbox {
33 | inboxMain()
34 | } else {
35 | // just start the API in either modes
36 | server.Run(address)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:alpine as builder
2 |
3 | # ENV GO111MODULE=on
4 |
5 | LABEL maintainer="Simone Margaritelli "
6 |
7 | RUN apk update && apk add --no-cache git
8 |
9 | # download, cache and install deps
10 | WORKDIR /app
11 | COPY go.mod go.sum ./
12 | RUN go mod download
13 |
14 | # copy and compiled the app
15 | COPY . .
16 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o pwngrid cmd/pwngrid/main.go
17 |
18 | # start a new stage from scratch
19 | FROM alpine:latest
20 | RUN apk --no-cache add ca-certificates
21 |
22 | WORKDIR /root/
23 |
24 | # copy the prebuilt binary and .env from the builder stage
25 | COPY --from=builder /app/pwngrid .
26 | COPY --from=builder /app/.env .
27 |
28 | EXPOSE 8666
29 |
30 | CMD ["./pwngrid"]
--------------------------------------------------------------------------------
/api/peer_report.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/evilsocket/islazy/log"
6 | "io"
7 | "net/http"
8 | )
9 |
10 | // PeerReportAP POST /api/v1/report/ap
11 | func (api *API) PeerReportAP(w http.ResponseWriter, r *http.Request) {
12 | var report apReport
13 |
14 | body, err := io.ReadAll(r.Body)
15 | if err != nil {
16 | ERROR(w, http.StatusUnprocessableEntity, err)
17 | return
18 | }
19 |
20 | log.Debug("%s", body)
21 |
22 | if err = json.Unmarshal(body, &report); err != nil {
23 | ERROR(w, http.StatusUnprocessableEntity, err)
24 | return
25 | }
26 |
27 | obj, err := api.Client.ReportAP(report)
28 | if err != nil {
29 | ERROR(w, http.StatusUnprocessableEntity, err)
30 | return
31 | }
32 |
33 | JSON(w, http.StatusOK, obj)
34 | }
35 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/jayofelony/pwngrid
2 |
3 | go 1.25
4 |
5 | require (
6 | github.com/biezhi/gorm-paginator/pagination v0.0.0-20250219022659-7f61c90f8f21
7 | github.com/evilsocket/islazy v1.11.0
8 | github.com/go-chi/chi/v5 v5.2.3
9 | github.com/go-chi/cors v1.2.2
10 | github.com/golang-jwt/jwt/v5 v5.3.0
11 | github.com/gopacket/gopacket v1.5.0
12 | github.com/jinzhu/gorm v1.9.16
13 | github.com/joho/godotenv v1.5.1
14 | )
15 |
16 | require (
17 | filippo.io/edwards25519 v1.1.0 // indirect
18 | github.com/go-sql-driver/mysql v1.9.3 // indirect
19 | github.com/jinzhu/inflection v1.0.0 // indirect
20 | github.com/jinzhu/now v1.1.5 // indirect
21 | golang.org/x/sys v0.39.0 // indirect
22 | golang.org/x/text v0.32.0 // indirect
23 | gorm.io/gorm v1.31.1 // indirect
24 | )
25 |
--------------------------------------------------------------------------------
/api/cors.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/go-chi/cors"
5 | "net/http"
6 | )
7 |
8 | func CORS(next http.Handler) http.Handler {
9 | cors := cors.New(cors.Options{
10 | AllowedOrigins: []string{"*"},
11 | AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
12 | AllowedHeaders: []string{"Accept", "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization"},
13 | AllowCredentials: true,
14 | MaxAge: 300,
15 | })
16 | return cors.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17 | w.Header().Add("X-Frame-Options", "DENY")
18 | w.Header().Add("X-Content-Type-Options", "nosniff")
19 | w.Header().Add("X-XSS-Protection", "1; mode=block")
20 | w.Header().Add("Referrer-Policy", "same-origin")
21 | next.ServeHTTP(w, r)
22 | }))
23 | }
24 |
--------------------------------------------------------------------------------
/api/peer_data.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/evilsocket/islazy/log"
6 | "io"
7 | "net/http"
8 | )
9 |
10 | // PeerGetData GET /api/v1/data
11 | func (api *API) PeerGetData(w http.ResponseWriter, r *http.Request) {
12 | JSON(w, http.StatusOK, api.Client.Data())
13 | }
14 |
15 | // PeerSetData POST /api/v1/data
16 | func (api *API) PeerSetData(w http.ResponseWriter, r *http.Request) {
17 | var newData map[string]interface{}
18 |
19 | body, err := io.ReadAll(r.Body)
20 | if err != nil {
21 | ERROR(w, http.StatusUnprocessableEntity, err)
22 | return
23 | }
24 |
25 | log.Debug("%s", body)
26 |
27 | if err = json.Unmarshal(body, &newData); err != nil {
28 | ERROR(w, http.StatusUnprocessableEntity, err)
29 | return
30 | }
31 |
32 | JSON(w, http.StatusOK, api.Client.SetData(newData))
33 | }
34 |
--------------------------------------------------------------------------------
/wifi/defines.go:
--------------------------------------------------------------------------------
1 | package wifi
2 |
3 | import (
4 | "github.com/gopacket/gopacket"
5 | "github.com/gopacket/gopacket/layers"
6 | "net"
7 | )
8 |
9 | const (
10 | IDWhisperPayload layers.Dot11InformationElementID = 222
11 | IDWhisperCompression layers.Dot11InformationElementID = 223
12 | IDWhisperIdentity layers.Dot11InformationElementID = 224
13 | IDWhisperSignature layers.Dot11InformationElementID = 225
14 | IDWhisperStreamHeader layers.Dot11InformationElementID = 226
15 | )
16 |
17 | var SerializationOptions = gopacket.SerializeOptions{
18 | FixLengths: true,
19 | ComputeChecksums: true,
20 | }
21 |
22 | var (
23 | SignatureAddr = net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad}
24 | SignatureAddrStr = "de:ad:be:ef:de:ad"
25 | BroadcastAddr = net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
26 | wpaFlags = 1041
27 | )
28 |
--------------------------------------------------------------------------------
/api/units_list.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/evilsocket/islazy/log"
5 | "github.com/jayofelony/pwngrid/models"
6 | "net/http"
7 | )
8 |
9 | func (api *API) ListUnits(w http.ResponseWriter, r *http.Request) {
10 | page, err := pageNum(r)
11 | if err != nil {
12 | ERROR(w, http.StatusUnprocessableEntity, err)
13 | return
14 | }
15 |
16 | units, total, pages := models.GetPagedUnits(page)
17 |
18 | JSON(w, http.StatusOK, map[string]interface{}{
19 | "records": total,
20 | "pages": pages,
21 | "units": units,
22 | })
23 | }
24 |
25 | func (api *API) UnitsByCountry(w http.ResponseWriter, r *http.Request) {
26 | if results, err := models.GetUnitsByCountry(); err != nil {
27 | log.Warning("error getting units by country: %v", err)
28 | ERROR(w, http.StatusInternalServerError, err)
29 | return
30 | } else {
31 | JSON(w, http.StatusOK, results)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/models/unit_inbox.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | func (u *Unit) GetPagedInbox(page int) (messages []Message, total int, pages int) {
4 | const limit = 50
5 | if page < 1 {
6 | page = 1
7 | }
8 |
9 | query := db.Model(&Message{}).Where("receiver_id = ?", u.ID)
10 |
11 | var total64 int64
12 | if err := query.Count(&total64).Error; err != nil {
13 | return nil, 0, 0
14 | }
15 | total = int(total64)
16 | if total == 0 {
17 | return []Message{}, 0, 0
18 | }
19 |
20 | offset := (page - 1) * limit
21 | if err := query.Order("id desc").Limit(limit).Offset(offset).Find(&messages).Error; err != nil {
22 | return nil, 0, 0
23 | }
24 |
25 | pages = (total + limit - 1) / limit
26 | return messages, total, pages
27 | }
28 |
29 | func (u *Unit) GetInboxMessage(id int) *Message {
30 | var msg Message
31 | if err := db.Where("receiver_id = ? AND id = ?", u.ID, id).First(&msg).Error; err != nil {
32 | return nil
33 | }
34 | return &msg
35 | }
36 |
--------------------------------------------------------------------------------
/wifi/unpack.go:
--------------------------------------------------------------------------------
1 | package wifi
2 |
3 | import (
4 | "fmt"
5 | "github.com/gopacket/gopacket"
6 | "github.com/gopacket/gopacket/layers"
7 | )
8 |
9 | func Unpack(pkt gopacket.Packet, radio *layers.RadioTap, dot11 *layers.Dot11) (error, []byte) {
10 | compressed := false
11 | payload := make([]byte, 0)
12 |
13 | for _, layer := range pkt.Layers() {
14 | if layer.LayerType() == layers.LayerTypeDot11InformationElement {
15 | if info, ok := layer.(*layers.Dot11InformationElement); ok {
16 | if info.ID == IDWhisperPayload {
17 | payload = append(payload, info.Info...)
18 | } else if info.ID == IDWhisperCompression {
19 | compressed = true
20 | }
21 | }
22 | }
23 | }
24 |
25 | if compressed {
26 | if decompressed, err := Decompress(payload); err != nil {
27 | return fmt.Errorf("error decompressing payload: %v", err), nil
28 | } else {
29 | payload = decompressed
30 | }
31 | }
32 |
33 | return nil, payload
34 | }
35 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # nothing to see here, just a utility i use to create new releases ^_^
3 |
4 | VERSION_FILE=$(dirname "${BASH_SOURCE[0]}")/version/ver.go
5 | echo "version file is $VERSION_FILE"
6 | CURRENT_VERSION=$(cat "$VERSION_FILE" | grep Version | cut -d '"' -f 2)
7 | TO_UPDATE=(
8 | # shellcheck disable=SC2206
9 | $VERSION_FILE
10 | )
11 |
12 | echo -n "current version is $CURRENT_VERSION, select new version: "
13 | read NEW_VERSION
14 | # shellcheck disable=SC2028
15 | echo "creating version $NEW_VERSION ...\n"
16 |
17 | for file in "${TO_UPDATE[@]}"; do
18 | echo "patching $file ..."
19 | sed -i.bak "s/$CURRENT_VERSION/$NEW_VERSION/g" "$file"
20 | rm -rf "$file.bak"
21 | git add "$file"
22 | done
23 |
24 | git commit -m "releasing v$NEW_VERSION"
25 | git push
26 | git tag -a v"$NEW_VERSION" -m "release v$NEW_VERSION"
27 | # shellcheck disable=SC2086
28 | git push origin v$NEW_VERSION
29 |
30 | echo
31 | echo "All done, v$NEW_VERSION released ^_^"
--------------------------------------------------------------------------------
/models/setup.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "fmt"
5 | "github.com/evilsocket/islazy/log"
6 | "github.com/jinzhu/gorm"
7 | "os"
8 | )
9 |
10 | var db *gorm.DB
11 |
12 | func Setup() (err error) {
13 | hostname := os.Getenv("DB_HOST")
14 | port := os.Getenv("DB_PORT")
15 | username := os.Getenv("DB_USER")
16 | password := os.Getenv("DB_PASSWORD")
17 | name := os.Getenv("DB_NAME")
18 |
19 | log.Info("connecting to %s:%s ...", hostname, port)
20 | dbURL := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", username, password, hostname, port, name)
21 | if db, err = gorm.Open("mysql", dbURL); err != nil {
22 | return
23 | }
24 | db.Debug().AutoMigrate(&Unit{}, &AccessPoint{}, &Message{})
25 | return
26 | }
27 |
28 | func Create(v interface{}) *gorm.DB {
29 | return db.Create(v)
30 | }
31 |
32 | func Update(v interface{}) *gorm.DB {
33 | return db.Model(v).Update(v)
34 | }
35 |
36 | func UpdateFields(v interface{}, fields map[string]interface{}) *gorm.DB {
37 | return db.Model(v).Updates(fields)
38 | }
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PwnGRID
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | This is the source code of the API server for https://pwnagotchi.ai/
14 |
--------------------------------------------------------------------------------
/wifi/utils.go:
--------------------------------------------------------------------------------
1 | package wifi
2 |
3 | import (
4 | "bytes"
5 | "github.com/gopacket/gopacket"
6 | "github.com/gopacket/gopacket/layers"
7 | )
8 |
9 | func Serialize(layers ...gopacket.SerializableLayer) (error, []byte) {
10 | buf := gopacket.NewSerializeBuffer()
11 | if err := gopacket.SerializeLayers(buf, SerializationOptions, layers...); err != nil {
12 | return err, nil
13 | }
14 | return nil, buf.Bytes()
15 | }
16 |
17 | func IsBroadcast(dot11 *layers.Dot11) bool {
18 | return bytes.Equal(dot11.Address1, BroadcastAddr)
19 | }
20 |
21 | func Freq2Chan(freq int) int {
22 | if freq <= 2472 {
23 | return ((freq - 2412) / 5) + 1
24 | } else if freq == 2484 {
25 | return 14
26 | } else if freq >= 5035 && freq <= 5885 {
27 | return ((freq - 5035) / 5) + 7
28 | }
29 | return 0
30 | }
31 |
32 | func Chan2Freq(channel int) int {
33 | if channel <= 13 {
34 | return ((channel - 1) * 5) + 2412
35 | } else if channel == 14 {
36 | return 2484
37 | } else if channel <= 177 {
38 | return ((channel - 7) * 5) + 5035
39 | }
40 | return 0
41 | }
42 |
--------------------------------------------------------------------------------
/api/setup.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/go-chi/chi/v5"
7 | "github.com/jayofelony/pwngrid/crypto"
8 | "github.com/jayofelony/pwngrid/mesh"
9 |
10 | _ "github.com/jinzhu/gorm/dialects/mysql"
11 |
12 | "github.com/evilsocket/islazy/log"
13 | )
14 |
15 | type API struct {
16 | Router *chi.Mux
17 | Keys *crypto.KeyPair
18 | Peer *mesh.Peer
19 | Mesh *mesh.Router
20 | Client *Client
21 | }
22 |
23 | func Setup(keys *crypto.KeyPair, peer *mesh.Peer, router *mesh.Router, Endpoint string, Hostname string) (err error, api *API) {
24 | api = &API{
25 | Router: chi.NewRouter(),
26 | Keys: keys,
27 | Peer: peer,
28 | Mesh: router,
29 | Client: NewClient(keys, Endpoint, Hostname),
30 | }
31 |
32 | api.Router.Use(CORS)
33 | if api.Keys == nil {
34 | api.setupServerRoutes()
35 | } else {
36 | api.setupPeerRoutes()
37 | }
38 |
39 | return
40 | }
41 |
42 | func (api *API) Run(addr string) {
43 | log.Info("pwngrid api starting on %s ...", addr)
44 | log.Fatal("%v", http.ListenAndServe(addr, api.Router))
45 | }
46 |
--------------------------------------------------------------------------------
/mesh/hopping.go:
--------------------------------------------------------------------------------
1 | package mesh
2 |
3 | import (
4 | "github.com/evilsocket/islazy/log"
5 | "github.com/evilsocket/islazy/str"
6 | "sort"
7 | "strconv"
8 | "time"
9 | )
10 |
11 | func ChannelHopping(iface string, chanList string, allChannels []int, hopPeriod int) {
12 | var channels []int
13 | for _, s := range str.Comma(chanList) {
14 | if ch, err := strconv.Atoi(s); err != nil {
15 | log.Fatal("%v", err)
16 | } else {
17 | channels = append(channels, ch)
18 | }
19 | }
20 | if len(channels) == 0 {
21 | channels = allChannels
22 | }
23 | sort.Ints(channels)
24 |
25 | go func() {
26 | period := time.Duration(hopPeriod) * time.Millisecond
27 | tick := time.NewTicker(period)
28 |
29 | log.Info("channel hopper started (period:%s channels:%v)", period, channels)
30 |
31 | loop := 0
32 | for _ = range tick.C {
33 | ch := channels[loop%len(channels)]
34 | // log.Debug("hopping on channel %d", ch)
35 | if err, out := SetChannel(iface, ch); err != nil {
36 | log.Error("%v: %s", err, out)
37 | }
38 | loop++
39 | }
40 | }()
41 | }
42 |
--------------------------------------------------------------------------------
/crypto/sign.go:
--------------------------------------------------------------------------------
1 | package crypto
2 |
3 | import (
4 | "crypto"
5 | "crypto/rand"
6 | "crypto/rsa"
7 | )
8 |
9 | var pssOpts = rsa.PSSOptions{
10 | SaltLength: 16,
11 | }
12 |
13 | const Hasher = crypto.SHA256
14 |
15 | func (pair *KeyPair) Sign(hash crypto.Hash, hashed []byte) ([]byte, error) {
16 | return rsa.SignPSS(rand.Reader, pair.Private, hash, hashed, &pssOpts)
17 | }
18 |
19 | func (pair *KeyPair) SignMessage(data []byte) ([]byte, error) {
20 | hasher := Hasher.New()
21 | hasher.Write(data)
22 | hash := hasher.Sum(nil)
23 | return pair.Sign(Hasher, hash)
24 | }
25 |
26 | func (pair *KeyPair) Verify(signature []byte, hasher crypto.Hash, hash []byte) error {
27 | return rsa.VerifyPSS(
28 | pair.Public,
29 | hasher,
30 | hash,
31 | signature,
32 | &pssOpts)
33 | }
34 |
35 | func (pair *KeyPair) VerifyMessage(data []byte, signature []byte) error {
36 | hasher := Hasher.New()
37 | hasher.Write(data)
38 | hash := hasher.Sum(nil)
39 | // log.Info("hash(data) = %x", hash)
40 | // log.Info("signature = %x", signature)
41 | return pair.Verify(signature, Hasher, hash)
42 | }
43 |
--------------------------------------------------------------------------------
/models/unit_enroll.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "fmt"
5 | "github.com/evilsocket/islazy/log"
6 | )
7 |
8 | func EnrollUnit(enroll EnrollmentRequest) (err error, unit *Unit) {
9 | if unit = FindUnitByFingerprint(enroll.Fingerprint); unit == nil {
10 | log.Info("enrolling new unit for %s (%s): %s", enroll.Address, enroll.Country, enroll.Identity)
11 |
12 | unit = &Unit{
13 | Address: enroll.Address,
14 | Country: enroll.Country,
15 | Name: enroll.Name,
16 | Fingerprint: enroll.Fingerprint,
17 | PublicKey: string(enroll.KeyPair.PublicPEM),
18 | }
19 |
20 | if err := db.Create(unit).Error; err != nil {
21 | return fmt.Errorf("error enrolling %s: %v", unit.Identity(), err), nil
22 | }
23 | }
24 |
25 | if err := unit.updateToken(); err != nil {
26 | return fmt.Errorf("error creating token for %s: %v", unit.Identity(), err), nil
27 | }
28 |
29 | if err = unit.UpdateWith(enroll); err != nil {
30 | log.Debug("%+v", enroll)
31 | return fmt.Errorf("error setting token for %s: %v", unit.Identity(), err), nil
32 | }
33 | return nil, unit
34 | }
35 |
--------------------------------------------------------------------------------
/models/message.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 |
8 | const (
9 | MessageDataMaxSize = 512000
10 | MessageSignatureMaxSize = 10000
11 | )
12 |
13 | type Message struct {
14 | ID uint `gorm:"primary_key" json:"id"`
15 | CreatedAt time.Time `json:"created_at"`
16 | UpdatedAt time.Time `json:"updated_at"`
17 | DeletedAt *time.Time `sql:"index" json:"deleted_at"`
18 | SeenAt *time.Time `json:"seen_at" sql:"index"`
19 | SenderID uint `json:"-"`
20 | ReceiverID uint `json:"-"`
21 | SenderName string `gorm:"size:255" json:"sender_name"`
22 | Sender string `gorm:"size:255;not null" json:"sender"`
23 | Data string `gorm:"size:512000;not null" json:"-"`
24 | Signature string `gorm:"size:10000;not null" json:"-"`
25 | }
26 |
27 | func ValidateMessage(data, signature string) error {
28 | // validate max sizes
29 | if dataSize := len(data); dataSize > MessageDataMaxSize {
30 | return fmt.Errorf("max message data size is %d", MessageDataMaxSize)
31 | } else if sigSize := len(signature); sigSize > MessageSignatureMaxSize {
32 | return fmt.Errorf("max message signature size is %d", MessageSignatureMaxSize)
33 | }
34 | return nil
35 | }
36 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | app:
4 | container_name: pwngrid_api
5 | build: .
6 | ports:
7 | - 8666:8666
8 | restart: on-failure
9 | volumes:
10 | - api:/usr/src/app/
11 | depends_on:
12 | - pwngrid-mysql
13 | networks:
14 | - pwngrid
15 |
16 | pwngrid-mysql:
17 | image: mysql:5.7
18 | container_name: pwngrid_mysql
19 | ports:
20 | - 3306:3306
21 | environment:
22 | - MYSQL_ROOT_HOST=${DB_HOST}
23 | - MYSQL_USER=${DB_USER}
24 | - MYSQL_PASSWORD=${DB_PASSWORD}
25 | - MYSQL_DATABASE=${DB_NAME}
26 | - MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
27 | volumes:
28 | - database_mysql:/var/lib/mysql
29 | networks:
30 | - pwngrid
31 |
32 | phpmyadmin:
33 | image: phpmyadmin/phpmyadmin
34 | container_name: pwngrid_phpmyadmin
35 | depends_on:
36 | - pwngrid-mysql
37 | environment:
38 | - PMA_HOST=pwngrid-mysql
39 | - PMA_USER=${DB_USER}
40 | - PMA_PORT=${DB_PORT}
41 | - PMA_PASSWORD=${DB_PASSWORD}
42 | ports:
43 | - 9090:80
44 | restart: always
45 | networks:
46 | - pwngrid
47 |
48 | volumes:
49 | api:
50 | database_mysql:
51 |
52 | networks:
53 | pwngrid:
54 | driver: bridge
55 |
--------------------------------------------------------------------------------
/wifi/compression.go:
--------------------------------------------------------------------------------
1 | package wifi
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "fmt"
7 | "io"
8 | )
9 |
10 | func Compress(data []byte) (bool, []byte, error) {
11 | oldSize := len(data)
12 | buf := bytes.Buffer{}
13 | if zw, err := gzip.NewWriterLevel(&buf, gzip.BestCompression); err != nil {
14 | return false, nil, fmt.Errorf("error initializing payload compression: %v", err)
15 | } else if _, err := zw.Write(data); err != nil {
16 | return false, nil, fmt.Errorf("error during payload compression: %v", err)
17 | } else if err = zw.Close(); err != nil {
18 | return false, nil, fmt.Errorf("error while finalizing payload compression: %v", err)
19 | }
20 |
21 | compressed := buf.Bytes()
22 | newSize := len(compressed)
23 |
24 | // log.Debug("gzip: %d > %d", oldSize, newSize)
25 |
26 | if newSize < oldSize {
27 | return true, compressed, nil
28 | }
29 | return false, data, nil
30 | }
31 |
32 | func Decompress(data []byte) ([]byte, error) {
33 | if zr, err := gzip.NewReader(bytes.NewBuffer(data)); err != nil {
34 | return nil, fmt.Errorf("error initializing payload decompression: %v", err)
35 | } else {
36 | defer func(zr *gzip.Reader) {
37 | err := zr.Close()
38 | if err != nil {
39 |
40 | }
41 | }(zr)
42 | return io.ReadAll(zr)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/changelog.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | NEW=()
4 | FIXES=()
5 | MISC=()
6 |
7 | echo "@ Fetching remote tags ..."
8 | git fetch --tags >/dev/null
9 | printf "\n\n"
10 |
11 | CURTAG=$(git describe --tags --abbrev=0)
12 | OUTPUT=$(git log $CURTAG..HEAD --oneline)
13 | IFS=$'\n' LINES=($OUTPUT)
14 |
15 | for LINE in "${LINES[@]}"; do
16 | LINE=$(echo "$LINE" | sed -E "s/^[[:xdigit:]]+\s+//")
17 | if [[ $LINE == *"new:"* ]]; then
18 | LINE=$(echo "$LINE" | sed -E "s/^new: //")
19 | NEW+=("$LINE")
20 | elif [[ $LINE == *"fix:"* ]]; then
21 | LINE=$(echo "$LINE" | sed -E "s/^fix: //")
22 | FIXES+=("$LINE")
23 | elif [[ $LINE != *"i did not bother commenting"* ]] && [[ $LINE != *"Merge "* ]]; then
24 | echo " MISC LINE =$LINE"
25 | LINE=$(echo "$LINE" | sed -E "s/^[a-z]+: //")
26 | MISC+=("$LINE")
27 | fi
28 | done
29 |
30 | if [ -n "$NEW" ]; then
31 | echo
32 | echo "**New Features**"
33 | echo
34 | for l in "${NEW[@]}"; do
35 | echo "* $l"
36 | done
37 | fi
38 |
39 | if [ -n "$FIXES" ]; then
40 | echo
41 | echo "**Fixes**"
42 | echo
43 | for l in "${FIXES[@]}"; do
44 | echo "* $l"
45 | done
46 | fi
47 |
48 | if [ -n "$MISC" ]; then
49 | echo
50 | echo "**Misc**"
51 | echo
52 | for l in "${MISC[@]}"; do
53 | echo "* $l"
54 | done
55 | fi
56 |
57 | echo
58 |
--------------------------------------------------------------------------------
/mesh/interface.go:
--------------------------------------------------------------------------------
1 | package mesh
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "github.com/jayofelony/pwngrid/utils"
7 | "regexp"
8 | "strconv"
9 | "strings"
10 | )
11 |
12 | var chanParser = regexp.MustCompile(`^\s+Channel.([0-9]+)\s+:\s+([0-9.]+)\s+GHz.*$`)
13 |
14 | func ActivateInterface(name string) error {
15 | if out, err := utils.Exec("ifconfig", []string{name, "up"}); err != nil {
16 | return err
17 | } else if out != "" {
18 | return fmt.Errorf("unexpected output while activating interface %s: %s", name, out)
19 | }
20 | return nil
21 | }
22 |
23 | func SetChannel(iface string, channel int) (error, string) {
24 | if out, err := utils.Exec("iwconfig", []string{iface, "channel", fmt.Sprintf("%d", channel)}); err != nil {
25 | return err, out
26 | } else if out != "" {
27 | return fmt.Errorf("unexpected output while setting interface %s to channel %d: %s", iface, channel, out), out
28 | } else {
29 | return nil, out
30 | }
31 | }
32 |
33 | func SupportedChannels(iface string) ([]int, error) {
34 | out, err := utils.Exec("iwlist", []string{iface, "freq"})
35 | if err != nil {
36 | return nil, err
37 | }
38 |
39 | var channels []int
40 | scanner := bufio.NewScanner(strings.NewReader(out))
41 | for scanner.Scan() {
42 | line := scanner.Text()
43 | if matches := chanParser.FindStringSubmatch(line); len(matches) == 3 {
44 | if channel, err := strconv.ParseInt(matches[1], 10, 32); err == nil {
45 | channels = append(channels, int(channel))
46 | }
47 | }
48 | }
49 |
50 | return channels, nil
51 | }
52 |
--------------------------------------------------------------------------------
/models/unit_find.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | _ "github.com/jinzhu/gorm"
5 | )
6 |
7 | type UnitsByCountry struct {
8 | Country string `json:"country"`
9 | Count int `json:"units"`
10 | }
11 |
12 | func GetUnitsByCountry() ([]UnitsByCountry, error) {
13 | results := make([]UnitsByCountry, 0)
14 | if err := db.Raw("SELECT country,COUNT(id) AS count FROM units GROUP BY country ORDER BY count DESC").Scan(&results).Error; err != nil {
15 | return nil, err
16 | }
17 | return results, nil
18 | }
19 |
20 | // GetPagedUnits returns a page of units, total number of units and total pages.
21 | // Uses a fixed limit of 25 per page to preserve previous behavior.
22 | func GetPagedUnits(page int) (units []Unit, total int, pages int) {
23 | const limit = 25
24 | if page < 1 {
25 | page = 1
26 | }
27 |
28 | var total64 int64
29 | if err := db.Model(&Unit{}).Count(&total64).Error; err != nil {
30 | return nil, 0, 0
31 | }
32 | total = int(total64)
33 | if total == 0 {
34 | return []Unit{}, 0, 0
35 | }
36 |
37 | offset := (page - 1) * limit
38 | if err := db.Order("id desc").Limit(limit).Offset(offset).Find(&units).Error; err != nil {
39 | return nil, 0, 0
40 | }
41 |
42 | pages = (total + limit - 1) / limit
43 | return units, total, pages
44 | }
45 |
46 | func FindUnit(id uint) *Unit {
47 | var unit Unit
48 | if err := db.Find(&unit, id).Error; err != nil {
49 | return nil
50 | }
51 | return &unit
52 | }
53 |
54 | func FindUnitByFingerprint(fingerprint string) *Unit {
55 | var unit Unit
56 | if fingerprint == "" {
57 | return nil
58 | } else if err := db.Where("fingerprint = ?", fingerprint).Take(&unit).Error; err != nil {
59 | return nil
60 | }
61 | return &unit
62 | }
63 |
--------------------------------------------------------------------------------
/api/unit_enroll.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/evilsocket/islazy/log"
6 | "github.com/jayofelony/pwngrid/models"
7 | "io"
8 | "net/http"
9 | )
10 |
11 | func (api *API) readEnrollment(w http.ResponseWriter, r *http.Request) (error, models.EnrollmentRequest) {
12 | var enroll models.EnrollmentRequest
13 |
14 | body, err := io.ReadAll(r.Body)
15 | if err != nil {
16 | ERROR(w, http.StatusUnprocessableEntity, err)
17 | return err, enroll
18 | }
19 |
20 | log.Debug("%s", body)
21 |
22 | enroll.Address = clientIP(r)
23 | enroll.Country = r.Header.Get("CF-IPCountry")
24 |
25 | if err = json.Unmarshal(body, &enroll); err != nil {
26 | log.Warning("error while reading enrollment request from %s: %v", enroll.Address, err)
27 | log.Debug("%s", body)
28 | ERROR(w, http.StatusUnprocessableEntity, err)
29 | return err, enroll
30 | }
31 |
32 | if err = enroll.Validate(); err != nil {
33 | log.Warning("error while validating enrollment request from %s: %v", enroll.Address, err)
34 | log.Debug("%s", body)
35 | ERROR(w, http.StatusUnprocessableEntity, ErrEmpty)
36 | return err, enroll
37 | }
38 |
39 | return nil, enroll
40 | }
41 |
42 | func (api *API) UnitEnroll(w http.ResponseWriter, r *http.Request) {
43 | err, enroll := api.readEnrollment(w, r)
44 | if err != nil {
45 | return
46 | }
47 |
48 | err, unit := models.EnrollUnit(enroll)
49 | if err != nil {
50 | log.Error("%v", err)
51 | ERROR(w, http.StatusInternalServerError, ErrEmpty)
52 | return
53 | }
54 |
55 | log.Debug("unit %s enrolled: id:%d address:%s", unit.Identity(), unit.ID, unit.Address)
56 |
57 | JSON(w, http.StatusOK, map[string]string{
58 | "token": unit.Token,
59 | })
60 | }
61 |
--------------------------------------------------------------------------------
/api/setup_server.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "fmt"
5 | "github.com/evilsocket/islazy/log"
6 | "github.com/go-chi/chi/v5"
7 | "net/http"
8 | )
9 |
10 | func cached(seconds int, next http.HandlerFunc) http.HandlerFunc {
11 | return func(w http.ResponseWriter, r *http.Request) {
12 | w.Header().Add("Cache-Control", fmt.Sprintf("public, max-age=%d", seconds))
13 | w.Header().Add("Expires", fmt.Sprintf("%d", seconds))
14 | next.ServeHTTP(w, r)
15 | }
16 | }
17 |
18 | func (api *API) setupServerRoutes() {
19 | log.Debug("registering server api ...")
20 |
21 | api.Router.Route("/api", func(r chi.Router) {
22 | r.Route("/v1", func(r chi.Router) {
23 | r.Route("/units", func(r chi.Router) {
24 | // GET /api/v1/units/
25 | r.Get("/", cached(600, api.ListUnits))
26 | // GET /api/v1/units/by_country
27 | r.Get("/by_country", cached(600, api.UnitsByCountry))
28 | })
29 | r.Route("/unit", func(r chi.Router) {
30 | // GET /api/v1/unit/
31 | r.Get("/{fingerprint:[a-fA-F0-9]+}", cached(600, api.ShowUnit))
32 | r.Route("/inbox", func(r chi.Router) {
33 | // GET /api/v1/unit/inbox/
34 | r.Get("/", api.GetInbox)
35 | r.Route("/{msg_id:[0-9]+}", func(r chi.Router) {
36 | // GET /api/v1/unit/inbox/
37 | r.Get("/", api.GetInboxMessage)
38 | // GET /api/v1/unit/inbox//
39 | r.Get("/{mark:[a-z]+}", api.MarkInboxMessage)
40 | })
41 | })
42 | // POST /api/v1/unit//inbox
43 | r.Post("/{fingerprint:[a-fA-F0-9]+}/inbox", api.SendMessageTo)
44 | // POST /api/v1/unit/enroll
45 | r.Post("/enroll", api.UnitEnroll)
46 | r.Route("/report", func(r chi.Router) {
47 | // POST /api/v1/unit/report/ap
48 | r.Post("/ap", api.UnitReportAP)
49 | // POST /api/v1/unit/report/aps
50 | r.Post("/aps", api.UnitReportMultipleAP)
51 | })
52 | })
53 | })
54 | })
55 | }
56 |
--------------------------------------------------------------------------------
/api/setup_peer.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/evilsocket/islazy/log"
5 | "github.com/go-chi/chi/v5"
6 | )
7 |
8 | func (api *API) setupPeerRoutes() {
9 | log.Debug("registering peer api ...")
10 |
11 | api.Router.Route("/api", func(r chi.Router) {
12 | r.Route("/v1", func(r chi.Router) {
13 | r.Route("/mesh", func(r chi.Router) {
14 | // GET /api/v1/mesh/peers
15 | r.Get("/peers", api.PeerGetPeers)
16 |
17 | r.Route("/memory", func(r chi.Router) {
18 | // GET /api/v1/mesh/memory
19 | r.Get("/", api.PeerGetMemory)
20 | // GET /api/v1/mesh/memory/
21 | r.Get("/{fingerprint:[a-fA-F0-9]+}", api.PeerGetMemoryOf)
22 | })
23 |
24 | // GET /api/v1/mesh/
25 | r.Get("/{status:[a-z]+}", api.PeerSetSignaling)
26 |
27 | // GET /api/v1/mesh/data
28 | r.Get("/data", api.PeerGetMeshData)
29 | // POST /api/v1/mesh/data
30 | r.Post("/data", api.PeerSetMeshData)
31 | })
32 |
33 | // GET /api/v1/data
34 | r.Post("/data", api.PeerGetData)
35 | // POST /api/v1/data
36 | r.Post("/data", api.PeerSetData)
37 |
38 | r.Route("/report", func(r chi.Router) {
39 | // POST /api/v1/report/ap
40 | r.Post("/ap", api.PeerReportAP)
41 | })
42 | r.Route("/inbox", func(r chi.Router) {
43 | // GET /api/v1/inbox/
44 | r.Get("/", api.PeerGetInbox)
45 | r.Route("/{msg_id:[0-9]+}", func(r chi.Router) {
46 | // GET /api/v1/inbox/
47 | r.Get("/", api.PeerGetInboxMessage)
48 | // GET /api/v1/inbox//
49 | r.Get("/{mark:[a-z]+}", api.PeerMarkInboxMessage)
50 | })
51 | })
52 | r.Route("/unit", func(r chi.Router) {
53 | // POST /api/v1/unit//inbox
54 | r.Post("/{fingerprint:[a-fA-F0-9]+}/inbox", api.PeerSendMessageTo)
55 | })
56 | r.Route("/units", func(r chi.Router) {
57 | // GET /api/v1/units/
58 | r.Get("/", api.PeerListUnits)
59 | })
60 | })
61 | })
62 | }
63 |
--------------------------------------------------------------------------------
/api/utils.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "github.com/evilsocket/islazy/log"
7 | "net/http"
8 | "strconv"
9 | "strings"
10 | )
11 |
12 | var (
13 | ErrEmpty = errors.New("")
14 | ErrUnauthorized = errors.New("unauthorized")
15 | )
16 |
17 | func clientIP(r *http.Request) string {
18 | address := strings.Split(r.RemoteAddr, ":")[0]
19 | if forwardedFor := r.Header.Get("X-Forwarded-For"); forwardedFor != "" {
20 | address = forwardedFor
21 | }
22 | // https://support.cloudflare.com/hc/en-us/articles/206776727-What-is-True-Client-IP-
23 | if trueClient := r.Header.Get("True-Client-IP"); trueClient != "" {
24 | address = trueClient
25 | }
26 | // handle multiple IPs case
27 | return strings.Trim(strings.Split(address, ",")[0], " ")
28 | }
29 |
30 | func reqToken(r *http.Request) string {
31 | keys := r.URL.Query()
32 | token := keys.Get("token")
33 | if token != "" {
34 | return token
35 | }
36 | bearerToken := r.Header.Get("Authorization")
37 | if parts := strings.Split(bearerToken, " "); len(parts) == 2 {
38 | return parts[1]
39 | }
40 | return ""
41 | }
42 |
43 | func pageNum(r *http.Request) (int, error) {
44 | pageParam := r.URL.Query().Get("p")
45 | if pageParam == "" {
46 | pageParam = "1"
47 | }
48 | return strconv.Atoi(pageParam)
49 | }
50 |
51 | func JSON(w http.ResponseWriter, statusCode int, data interface{}) {
52 | js, err := json.Marshal(data)
53 | if err != nil {
54 | http.Error(w, err.Error(), http.StatusInternalServerError)
55 | return
56 | }
57 |
58 | w.Header().Set("Content-Type", "application/json")
59 | w.WriteHeader(statusCode)
60 |
61 | if sent, err := w.Write(js); err != nil {
62 | log.Error("error sending response: %v", err)
63 | } else {
64 | log.Debug("sent %d bytes of json response", sent)
65 | }
66 | }
67 |
68 | func ERROR(w http.ResponseWriter, statusCode int, err error) {
69 | if err != nil {
70 | JSON(w, statusCode, struct {
71 | Error string `json:"error"`
72 | }{
73 | Error: err.Error(),
74 | })
75 | return
76 | }
77 | JSON(w, http.StatusBadRequest, nil)
78 | }
79 |
--------------------------------------------------------------------------------
/mesh/peer_json.go:
--------------------------------------------------------------------------------
1 | package mesh
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/evilsocket/islazy/log"
6 | "net"
7 | "sync"
8 | "time"
9 | )
10 |
11 | type jsonPeer struct {
12 | Fingerprint string `json:"fingerprint"`
13 | MetAt time.Time `json:"met_at"`
14 | DetectedAt time.Time `json:"detected_at"`
15 | SeenAt time.Time `json:"seen_at"`
16 | PrevSeenAt time.Time `json:"prev_seen_at"`
17 | Encounters uint64 `json:"encounters"`
18 | Channel int `json:"channel"`
19 | RSSI int `json:"rssi"`
20 | SessionID string `json:"session_id"`
21 | Advertisement map[string]interface{} `json:"advertisement"`
22 | }
23 |
24 | // creates a Peer object filled with the fields of the JSON representation
25 | func peerFromJSON(j jsonPeer) *Peer {
26 | peer := &Peer{
27 | DetectedAt: j.DetectedAt,
28 | SeenAt: j.SeenAt,
29 | PrevSeenAt: j.PrevSeenAt,
30 | SessionIDStr: j.SessionID,
31 | Encounters: j.Encounters,
32 | Channel: j.Channel,
33 | RSSI: j.RSSI,
34 | AdvData: sync.Map{},
35 | }
36 |
37 | if hw, err := net.ParseMAC(j.SessionID); err == nil {
38 | copy(peer.SessionID, hw)
39 | } else {
40 | log.Warning("error parsing peer session id %s: %v", j.SessionID, err)
41 | }
42 |
43 | for key, val := range j.Advertisement {
44 | peer.AdvData.Store(key, val)
45 | }
46 |
47 | return peer
48 | }
49 |
50 | // converts a peer into a JSON friendly representation
51 | func (peer *Peer) json() *jsonPeer {
52 | fingerprint := ""
53 | if v, found := peer.AdvData.Load("identity"); found {
54 | fingerprint = v.(string)
55 | }
56 |
57 | doc := jsonPeer{
58 | Fingerprint: fingerprint,
59 | MetAt: peer.MetAt,
60 | Encounters: peer.Encounters,
61 | PrevSeenAt: peer.PrevSeenAt,
62 | DetectedAt: peer.DetectedAt,
63 | SeenAt: peer.SeenAt,
64 | Channel: peer.Channel,
65 | RSSI: peer.RSSI,
66 | SessionID: peer.SessionIDStr,
67 | Advertisement: make(map[string]interface{}),
68 | }
69 | peer.AdvData.Range(func(key, value interface{}) bool {
70 | doc.Advertisement[key.(string)] = value
71 | return true
72 | })
73 |
74 | return &doc
75 | }
76 |
77 | func (peer *Peer) MarshalJSON() ([]byte, error) {
78 | peer.Lock()
79 | defer peer.Unlock()
80 | return json.Marshal(peer.json())
81 | }
82 |
--------------------------------------------------------------------------------
/wifi/pack.go:
--------------------------------------------------------------------------------
1 | package wifi
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "github.com/gopacket/gopacket"
7 | "github.com/gopacket/gopacket/layers"
8 | "net"
9 | )
10 |
11 | func Info(id layers.Dot11InformationElementID, info []byte) *layers.Dot11InformationElement {
12 | return &layers.Dot11InformationElement{
13 | ID: id,
14 | Length: uint8(len(info) & 0xff),
15 | Info: info,
16 | }
17 | }
18 |
19 | func PackOneOf(from, to net.HardwareAddr, peerID []byte, signature []byte, streamID uint64, seqNum uint64, seqTot uint64, payload []byte, compress bool) (error, []byte) {
20 | stack := []gopacket.SerializableLayer{
21 | &layers.RadioTap{},
22 | &layers.Dot11{
23 | Address1: to,
24 | Address2: SignatureAddr,
25 | Address3: from,
26 | Type: layers.Dot11TypeMgmtBeacon,
27 | },
28 | &layers.Dot11MgmtBeacon{
29 | Flags: uint16(wpaFlags),
30 | Interval: 100,
31 | },
32 | }
33 |
34 | if peerID != nil {
35 | stack = append(stack, Info(IDWhisperIdentity, peerID))
36 | }
37 |
38 | if signature != nil {
39 | stack = append(stack, Info(IDWhisperSignature, signature))
40 | }
41 |
42 | if streamID > 0 {
43 | streamBuf := new(bytes.Buffer)
44 | if err := binary.Write(streamBuf, binary.LittleEndian, streamID); err != nil {
45 | return err, nil
46 | } else if err = binary.Write(streamBuf, binary.LittleEndian, seqNum); err != nil {
47 | return err, nil
48 | } else if err = binary.Write(streamBuf, binary.LittleEndian, seqTot); err != nil {
49 | return err, nil
50 | }
51 | stack = append(stack, Info(IDWhisperStreamHeader, streamBuf.Bytes()))
52 | }
53 |
54 | if compress {
55 | if didCompress, compressed, err := Compress(payload); err != nil {
56 | return err, nil
57 | } else if didCompress {
58 | stack = append(stack, Info(IDWhisperCompression, []byte{1}))
59 | payload = compressed
60 | }
61 | }
62 |
63 | dataSize := len(payload)
64 | dataLeft := dataSize
65 | dataOff := 0
66 | chunkSize := 0xff
67 |
68 | for dataLeft > 0 {
69 | sz := chunkSize
70 | if dataLeft < chunkSize {
71 | sz = dataLeft
72 | }
73 |
74 | chunk := payload[dataOff : dataOff+sz]
75 | stack = append(stack, Info(IDWhisperPayload, chunk))
76 |
77 | dataOff += sz
78 | dataLeft -= sz
79 | }
80 |
81 | return Serialize(stack...)
82 | }
83 |
84 | func Pack(from, to net.HardwareAddr, payload []byte, compress bool) (error, []byte) {
85 | return PackOneOf(from, to, nil, nil, 0, 0, 0, payload, compress)
86 | }
87 |
--------------------------------------------------------------------------------
/api/auth.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "github.com/evilsocket/islazy/log"
7 | "github.com/golang-jwt/jwt/v5"
8 | "github.com/jayofelony/pwngrid/models"
9 | "net/http"
10 | "os"
11 | "time"
12 | )
13 |
14 | var (
15 | ErrTokenClaims = errors.New("can't extract claims from jwt token")
16 | ErrTokenInvalid = errors.New("jwt token not valid")
17 | ErrTokenExpired = errors.New("jwt token expired")
18 | ErrTokenIncomplete = errors.New("jwt token is missing required fields")
19 | ErrTokenUnauthorized = errors.New("jwt token authorized field is false (?!)")
20 | )
21 |
22 | func validateToken(header string) (jwt.MapClaims, error) {
23 | token, err := jwt.Parse(header, func(token *jwt.Token) (interface{}, error) {
24 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
25 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
26 | }
27 | return []byte(os.Getenv("API_SECRET")), nil
28 | })
29 | if err != nil {
30 | return nil, err
31 | }
32 |
33 | claims, ok := token.Claims.(jwt.MapClaims)
34 | if !ok {
35 | return nil, ErrTokenClaims
36 | } else if !token.Valid {
37 | return nil, ErrTokenInvalid
38 | }
39 |
40 | required := []string{
41 | "expires_at",
42 | "authorized",
43 | "unit_id",
44 | "unit_ident",
45 | }
46 | for _, req := range required {
47 | if _, found := claims[req]; !found {
48 | return nil, ErrTokenIncomplete
49 | }
50 | }
51 |
52 | log.Debug("%+v", claims)
53 |
54 | if expiresAt, err := time.Parse(time.RFC3339, claims["expires_at"].(string)); err != nil {
55 | return nil, ErrTokenExpired
56 | } else if expiresAt.Before(time.Now()) {
57 | return nil, ErrTokenExpired
58 | } else if claims["authorized"].(bool) != true {
59 | return nil, ErrTokenUnauthorized
60 | }
61 | return claims, err
62 | }
63 |
64 | func Authenticate(w http.ResponseWriter, r *http.Request) *models.Unit {
65 | client := clientIP(r)
66 | tokenHeader := reqToken(r)
67 | if tokenHeader == "" {
68 | log.Debug("unauthenticated request from %s", client)
69 | ERROR(w, http.StatusUnauthorized, ErrUnauthorized)
70 | return nil
71 | }
72 |
73 | claims, err := validateToken(tokenHeader)
74 | if err != nil {
75 | log.Debug("token error for %s: %v", client, err)
76 | ERROR(w, http.StatusUnauthorized, ErrUnauthorized)
77 | return nil
78 | }
79 |
80 | log.Debug("claims[unit_id] = %+v", claims["unit_id"])
81 | unit := models.FindUnit(uint(claims["unit_id"].(float64)))
82 | if unit == nil {
83 | log.Warning("client %s authenticated with unknown claims '%v'", client, claims)
84 | ERROR(w, http.StatusUnauthorized, ErrUnauthorized)
85 | return nil
86 | }
87 |
88 | return unit
89 | }
90 |
--------------------------------------------------------------------------------
/models/enrollment.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "encoding/base64"
5 | "fmt"
6 | "github.com/evilsocket/islazy/str"
7 | "github.com/jayofelony/pwngrid/crypto"
8 | "regexp"
9 | "strings"
10 | )
11 |
12 | type EnrollmentRequest struct {
13 | Identity string `json:"identity"` // name@SHA256(public_key)
14 | PublicKey string `json:"public_key"` // BASE64(public_key.pem)
15 | Signature string `json:"signature"` // BASE64(SIGN(identity, private_key))
16 | Data map[string]interface{} `json:"data"` // misc data for the unit
17 | KeyPair *crypto.KeyPair `json:"-"` // parsed from public_key
18 | Name string `json:"-"`
19 | Fingerprint string `json:"-"` // SHA256(public_key)
20 | Address string `json:"-"`
21 | Country string `json:"-"`
22 | }
23 |
24 | var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]")
25 |
26 | func clean(s string) string {
27 | for _, m := range ansi.FindAllString(s, -1) {
28 | s = strings.Replace(s, m, "", -1)
29 | }
30 | return str.Trim(s)
31 | }
32 |
33 | func (enroll *EnrollmentRequest) Validate() error {
34 | // split the identity into name and fingerprint
35 | parts := strings.Split(enroll.Identity, "@")
36 | if len(parts) != 2 {
37 | return fmt.Errorf("error parsing the identity string: got %d parts", len(parts))
38 | }
39 |
40 | enroll.Name = clean(parts[0])
41 | enroll.Fingerprint = clean(parts[1])
42 | if len(enroll.Fingerprint) != crypto.Hasher.Size()*2 {
43 | return fmt.Errorf("unexpected fingerprint length for %s", enroll.Fingerprint)
44 | }
45 |
46 | // parse the public key as b64 pem
47 | pubKeyPEM, err := base64.StdEncoding.DecodeString(enroll.PublicKey)
48 | if err != nil {
49 | return fmt.Errorf("error decoding the public key: %v", err)
50 | }
51 |
52 | enroll.KeyPair, err = crypto.FromPublicPEM(string(pubKeyPEM))
53 | if err != nil {
54 | return fmt.Errorf("error parsing the public key: %v", err)
55 | }
56 |
57 | enroll.PublicKey = string(enroll.KeyPair.PublicPEM)
58 |
59 | if enroll.KeyPair.FingerprintHex != enroll.Fingerprint {
60 | return fmt.Errorf("fingerprint mismatch: expected:%s got:%s", enroll.KeyPair.FingerprintHex, enroll.Fingerprint)
61 | }
62 |
63 | data := []byte(enroll.Identity)
64 | signature, err := base64.StdEncoding.DecodeString(enroll.Signature)
65 | if err != nil {
66 | return fmt.Errorf("error decoding the signature: %v", err)
67 | }
68 |
69 | if err := enroll.KeyPair.VerifyMessage(data, signature); err != nil {
70 | return fmt.Errorf("signature verification failed: %s", err)
71 | }
72 |
73 | return nil
74 | }
75 |
--------------------------------------------------------------------------------
/mesh/memory.go:
--------------------------------------------------------------------------------
1 | package mesh
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/evilsocket/islazy/fs"
7 | "github.com/evilsocket/islazy/log"
8 | "math"
9 | "os"
10 | "path"
11 | "sync"
12 | "time"
13 | )
14 |
15 | type Memory struct {
16 | sync.Mutex
17 | path string
18 | peers map[string]*Peer
19 | }
20 |
21 | func MemoryFromPath(path string) (err error, mem *Memory) {
22 | if path, err = fs.Expand(path); err != nil {
23 | return err, nil
24 | }
25 |
26 | mem = &Memory{
27 | path: path,
28 | peers: make(map[string]*Peer),
29 | }
30 |
31 | if !fs.Exists(path) {
32 | log.Debug("creating %s ...", path)
33 | if err = os.MkdirAll(path, os.ModePerm); err != nil {
34 | return
35 | }
36 | }
37 |
38 | err = fs.Glob(path, "*.json", func(fileName string) error {
39 | log.Debug("loading %s ...", fileName)
40 | data, err := os.ReadFile(fileName)
41 | if err != nil {
42 | log.Error("error loading %s: %v", fileName, err)
43 | return nil
44 | }
45 |
46 | var peer jsonPeer
47 | if err = json.Unmarshal(data, &peer); err != nil {
48 | log.Error("error loading %s: %v", fileName, err)
49 | return nil
50 | }
51 |
52 | mem.peers[peer.Fingerprint] = peerFromJSON(peer)
53 | return nil
54 | })
55 |
56 | log.Debug("loaded %d known peers", len(mem.peers))
57 |
58 | return
59 | }
60 |
61 | func (mem *Memory) Size() int {
62 | mem.Lock()
63 | defer mem.Unlock()
64 | return len(mem.peers)
65 | }
66 |
67 | func (mem *Memory) Of(fingerprint string) *Peer {
68 | mem.Lock()
69 | defer mem.Unlock()
70 |
71 | if peer, found := mem.peers[fingerprint]; found {
72 | return peer
73 | }
74 |
75 | return nil
76 | }
77 |
78 | func (mem *Memory) List() []*Peer {
79 | mem.Lock()
80 | defer mem.Unlock()
81 |
82 | list := make([]*Peer, 0)
83 | for _, peer := range mem.peers {
84 | list = append(list, peer)
85 | }
86 |
87 | return list
88 | }
89 |
90 | func (mem *Memory) Track(fingerprint string, peer *Peer) error {
91 | mem.Lock()
92 | defer mem.Unlock()
93 |
94 | if encounter, found := mem.peers[fingerprint]; !found {
95 | // peer first encounter
96 | peer.Encounters = 1
97 | peer.MetAt = time.Now()
98 | peer.PrevSeenAt = peer.SeenAt
99 | } else {
100 | // we met this peer before
101 | if encounter.Encounters < math.MaxUint64 {
102 | encounter.Encounters++
103 | }
104 | peer.PrevSeenAt = encounter.SeenAt
105 | peer.MetAt = encounter.MetAt
106 | peer.Encounters = encounter.Encounters
107 | }
108 |
109 | peer.SeenAt = time.Now()
110 |
111 | // save/update peer data in memory
112 | mem.peers[fingerprint] = peer
113 | // save/update peer data on disk
114 | fileName := path.Join(mem.path, fmt.Sprintf("%s.json", fingerprint))
115 | if data, err := json.Marshal(peer); err != nil {
116 | return err
117 | } else if err := os.WriteFile(fileName, data, os.ModePerm); err != nil {
118 | return err
119 | }
120 |
121 | return nil
122 | }
123 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | os: linux
3 | dist: bionic
4 | language: go
5 | go:
6 | - 1.13.x
7 |
8 | env:
9 | global:
10 | - LANG=C
11 | - LC_ALL=C
12 | - OUTPUT="pwngrid"
13 | - VERSION=$(echo ${TRAVIS_BRANCH} | sed "s/\//_/g")
14 |
15 | cache:
16 | apt: true
17 | addons:
18 | apt:
19 | packages:
20 | - wget
21 | - p7zip-full
22 | - libpcap-dev
23 | update: true
24 |
25 | cross: &cross
26 | before_install:
27 | - wget --show-progress -qcO "qemu.deb" "https://debian.grena.ge/debian/pool/main/q/qemu/qemu-user-static_4.1-1+b4_amd64.deb"
28 | - sudo dpkg -i "qemu.deb"
29 | install:
30 | - sudo builder/arm_builder.sh pwngrid make -e TARGET="${OUTPUT}"
31 |
32 | normal: &normal
33 | install:
34 | - make -e TARGET="${OUTPUT}"
35 |
36 | end: &end
37 | after_success:
38 | - sudo mv "build/${OUTPUT}" "${OUTPUT}"
39 | - file "${OUTPUT}"
40 | - openssl dgst -sha256 "${OUTPUT}" | tee "${OUTPUT}_${TARGET_OS}_${TARGET_ARCH}_${VERSION}.sha256"
41 | - 7z a "${OUTPUT}_${TARGET_OS}_${TARGET_ARCH}_${VERSION}.zip" "${OUTPUT}" "${OUTPUT}_${TARGET_OS}_${TARGET_ARCH}_${VERSION}.sha256"
42 |
43 | matrix:
44 | include:
45 | - name: Linux - amd64
46 | if: tag IS present
47 | arch: amd64
48 | env:
49 | - TARGET_OS=linux
50 | - TARGET_ARCH=amd64
51 | <<: *normal
52 | <<: *end
53 | - name: Linux - aarch64
54 | if: tag IS present
55 | arch: arm64
56 | env:
57 | - TARGET_OS=linux
58 | - TARGET_ARCH=aarch64
59 | <<: *normal
60 | <<: *end
61 | - name: Linux - armhf
62 | if: tag IS present
63 | arch: amd64
64 | language: minimal
65 | env:
66 | - TARGET_OS=linux
67 | - TARGET_ARCH=armhf
68 | <<: *cross
69 | <<: *end
70 | # Tests
71 | # - name: Linux - tests
72 | # if: tag IS blank
73 | # os: linux
74 | # arch: amd64
75 | # install:
76 | # - make deps
77 | # script:
78 | # - make test
79 | # after_success:
80 | # - bash <(curl -s https://codecov.io/bash)
81 |
82 | deploy:
83 | provider: releases
84 | api_key:
85 | secure: ljBVe/wVAtOPwCWJPlJ7D1hWGfm6GtHOLgq3wmP4jw/9a2RYV41xJ7g+4R1mm9R8waqtTm9QPDHIKFuN3N9cNs83ZY/fkSJ2WwU3IDV1ZvKPAuucrMSsyOGc08poXj6mmUDs/9LRb100qG81Y5dD+WB6Ep6vWOT7aOi9QNp/WWQ3IDYp5QJIocRHcJhGFH8JO1699mpdNgaukmPHIYK6uVu15TCkYOrvNTD0OTpthN6hIwCBwQ0agFNBbqmwyYsAdUZsjdU7QVOCnPUeXWqoZwq3klFKymsf8f4xra7ou5hsBkL+GFESiGGy0TdU7ZTZjPRKgkpIWtHOURq3WSVtYvCTnKI8h+HdBbKlQeO5g611gUw3CEU5HZxKlG18JTSD5TJNuEAFBVA7X385cVnWbgCLIwLiCDzjKPeVJvyDxyKC1CCtmfYZtanyn18qz/VRtMWrLFRcr5jNrQcloiuJbQzteoxtvbt5c0nM7b1b/AZ4zzGH75MLADxbHw2CThN4R+dxx3lqD0YM3fxbWiWCgZlbrc7GNRq1ilhX0YEDBOVfcdJxYARwrzovrO8bMFGerur4C7HzkpVgE6jfQiZJdXU/5javkLnww6xjDC/jfiMJ7i4OqZ2sgISSVL8Fq4LMqaAumdSHZK/GoJ97PTWUE9sBD7tIGSzHReA8DhpweFI=
86 | skip_cleanup: true
87 | file_glob: true
88 | file:
89 | - pwngrid_*.zip
90 | - pwngrid_*.sha256
91 | on:
92 | tags: true
93 | repo: evilsocket/pwngrid
94 | branches:
95 | only:
96 | - "/^v[0-9]+\\.[0-9]+\\.[0-9]+[A-Za-z0-9]+?$/"
97 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | VERSION := $(shell sed -n 's/Version\s*=\s*"\([0-9.]\+\)"/\1/p' version/ver.go | tr -d '\t')
2 |
3 | all: clean
4 | @mkdir build
5 | @go build -o build/pwngrid cmd/pwngrid/*.go
6 | @ls -la build/pwngrid
7 |
8 | install:
9 | @cp build/pwngrid /usr/local/bin/
10 | @mkdir -p /etc/systemd/system/
11 | @mkdir -p /etc/pwngrid/
12 | @cp env.example /etc/pwngrid/pwngrid.conf
13 | @systemctl daemon-reload
14 |
15 | clean:
16 | @rm -rf build
17 |
18 | restart:
19 | @service pwngrid restart
20 |
21 | release_files: clean cross_compile_libpcap_arm64 # cross_compile_libpcap_arm
22 | @mkdir build
23 | @echo building for linux/amd64 ...
24 | @CGO_ENABLED=1 CC=x86_64-linux-gnu-gcc GOARCH=amd64 GOOS=linux go build -o build/pwngrid cmd/pwngrid/*.go
25 | @openssl dgst -sha256 "build/pwngrid" > "build/pwngrid-amd64.sha256"
26 | @zip -j "build/pwngrid-$(VERSION)-amd64.zip" build/pwngrid build/pwngrid-amd64.sha256 > /dev/null
27 | @rm -rf build/pwngrid build/pwngrid-amd64.sha256
28 | # @echo building for linux/armv6l ...
29 | # @CGO_ENABLED=1 CC=arm-linux-gnueabi-gcc GOARM=6 GOARCH=arm GOOS=linux go build -o build/pwngrid cmd/pwngrid/*.go
30 | # @openssl dgst -sha256 "build/pwngrid" > "build/pwngrid-armv6l.sha256"
31 | # @zip -j "build/pwngrid-$(VERSION)-armv6l.zip" build/pwngrid build/pwngrid-armv6l.sha256 > /dev/null
32 | # @rm -rf build/pwngrid build/pwngrid-armv6l.sha256
33 | @echo building for linux/aarch64 ...
34 | @CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOARCH=arm64 GOOS=linux go build -o build/pwngrid cmd/pwngrid/*.go
35 | @openssl dgst -sha256 "build/pwngrid" > "build/pwngrid-aarch64.sha256"
36 | @zip -j "build/pwngrid-$(VERSION)-aarch64.zip" build/pwngrid build/pwngrid-aarch64.sha256 > /dev/null
37 | @rm -rf build/pwngrid build/pwngrid-aarch64.sha256
38 | @ls -la build
39 |
40 | # requires sudo apt-get install bison flex gcc-arm-linux-gnueabi libpcap0.8 libpcap-dev
41 | cross_compile_libpcap_arm:
42 | @echo "Cross-compiling libpcap for armv6l..."
43 | @wget https://www.tcpdump.org/release/libpcap-1.9.1.tar.gz
44 | @tar -zxvf libpcap-1.9.1.tar.gz
45 | @cd libpcap-1.9.1 && \
46 | export CC=arm-linux-gnueabi-gcc && \
47 | ./configure --host=arm-linux-gnueabi && \
48 | make
49 | @echo "Copying cross-compiled libpcap to /usr/lib/arm-linux-gnueabi/"
50 | @sudo cp libpcap-1.9.1/libpcap.a /usr/lib/arm-linux-gnueabi/
51 | @echo "Clean up..."
52 | @rm -rf libpcap-1.9.1 libpcap-1.9.1.tar.gz
53 |
54 | # requires sudo apt-get install bison flex gcc-aarch64-linux-gnu libpcap0.8 libpcap-dev
55 | cross_compile_libpcap_arm64:
56 | @echo "Cross-compiling libpcap for arm64..."
57 | @wget https://www.tcpdump.org/release/libpcap-1.9.1.tar.gz
58 | @tar -zxvf libpcap-1.9.1.tar.gz
59 | @cd libpcap-1.9.1 && \
60 | export CC=aarch64-linux-gnu-gcc && \
61 | ./configure --host=aarch64-linux-gnu && \
62 | make
63 | @echo "Copying cross-compiled libpcap to /usr/lib/x86_64-linux-gnu/"
64 | @sudo cp libpcap-1.9.1/libpcap.a /usr/lib/aarch64-linux-gnu/
65 | @echo "Clean up..."
66 | @rm -rf libpcap-1.9.1 libpcap-1.9.1.tar.gz
67 |
68 | .PHONY: cross_compile_libpcap_arm cross_compile_libpcap_arm64
--------------------------------------------------------------------------------
/api/unit_report.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/evilsocket/islazy/log"
7 | "github.com/jayofelony/pwngrid/models"
8 | "io"
9 | "net"
10 | "net/http"
11 | )
12 |
13 | type apReport struct {
14 | ESSID string `json:"essid"`
15 | BSSID string `json:"bssid"`
16 | }
17 |
18 | func (api *API) unitReport(client string, unit *models.Unit, ap apReport) error {
19 | if parsed, err := net.ParseMAC(ap.BSSID); err != nil {
20 | return fmt.Errorf("error while parsing wifi ap bssid %s from %s: %v", ap.BSSID, client, err)
21 | } else {
22 | // normalize
23 | ap.BSSID = parsed.String()
24 | }
25 |
26 | if existing := unit.FindAccessPoint(ap.ESSID, ap.BSSID); existing == nil {
27 | log.Debug("unit %s (%s %s) reporting new wifi access point %v", unit.Identity(), unit.Address,
28 | unit.Country, ap)
29 |
30 | newAP := models.AccessPoint{
31 | Name: ap.ESSID,
32 | Mac: ap.BSSID,
33 | UnitID: unit.ID,
34 | }
35 |
36 | if err := models.Create(&newAP).Error; err != nil {
37 | return fmt.Errorf("error creating ap %v: %v", newAP, err)
38 | }
39 | } else if err := models.Update(existing).Error; err != nil {
40 | return fmt.Errorf("error updating ap %v: %v", existing, err)
41 | }
42 |
43 | return nil
44 | }
45 |
46 | func (api *API) UnitReportAP(w http.ResponseWriter, r *http.Request) {
47 | unit := Authenticate(w, r)
48 | if unit == nil {
49 | return
50 | }
51 |
52 | client := clientIP(r)
53 | body, err := io.ReadAll(r.Body)
54 | if err != nil {
55 | ERROR(w, http.StatusUnprocessableEntity, ErrEmpty)
56 | return
57 | }
58 |
59 | var ap apReport
60 | if err = json.Unmarshal(body, &ap); err != nil {
61 | log.Warning("error while reading wifi ap from %s: %v", client, err)
62 | ERROR(w, http.StatusUnprocessableEntity, ErrEmpty)
63 | return
64 | }
65 |
66 | if err := api.unitReport(client, unit, ap); err != nil {
67 | log.Warning("%v", err)
68 | ERROR(w, http.StatusUnprocessableEntity, ErrEmpty)
69 | return
70 | }
71 |
72 | JSON(w, http.StatusOK, map[string]interface{}{
73 | "success": true,
74 | })
75 | }
76 |
77 | func (api *API) UnitReportMultipleAP(w http.ResponseWriter, r *http.Request) {
78 | unit := Authenticate(w, r)
79 | if unit == nil {
80 | return
81 | }
82 |
83 | client := clientIP(r)
84 | body, err := io.ReadAll(r.Body)
85 | if err != nil {
86 | ERROR(w, http.StatusUnprocessableEntity, ErrEmpty)
87 | return
88 | }
89 |
90 | var aps []apReport
91 | if err = json.Unmarshal(body, &aps); err != nil {
92 | log.Warning("error while reading wifi ap list from %s: %v", client, err)
93 | ERROR(w, http.StatusUnprocessableEntity, ErrEmpty)
94 | return
95 | }
96 |
97 | for _, ap := range aps {
98 | if err := api.unitReport(client, unit, ap); err != nil {
99 | log.Warning("%v", err)
100 | ERROR(w, http.StatusUnprocessableEntity, ErrEmpty)
101 | return
102 | }
103 | }
104 |
105 | JSON(w, http.StatusOK, map[string]interface{}{
106 | "success": true,
107 | })
108 | }
109 |
--------------------------------------------------------------------------------
/crypto/encrypt.go:
--------------------------------------------------------------------------------
1 | package crypto
2 |
3 | import (
4 | "crypto/aes"
5 | "crypto/cipher"
6 | "crypto/rand"
7 | "crypto/rsa"
8 | "encoding/binary"
9 | "fmt"
10 | "io"
11 | )
12 |
13 | const (
14 | AESKEyLength = 32
15 | NonceLength = 12
16 | )
17 |
18 | func (pair *KeyPair) EncryptFor(cleartext []byte, pubKey *rsa.PublicKey) ([]byte, error) {
19 | // generate a random 32 bytes long key
20 | key := make([]byte, AESKEyLength)
21 | if _, err := io.ReadFull(rand.Reader, key); err != nil {
22 | return nil, err
23 | }
24 |
25 | // encrypt the key with RSA
26 | encKey, err := pair.EncryptBlockFor(key, pubKey)
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | // use that key to encrypt the cleartext in AES-GCM
32 | block, err := aes.NewCipher(key)
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | nonce := make([]byte, NonceLength)
38 | if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
39 | return nil, err
40 | }
41 |
42 | gcm, err := cipher.NewGCM(block)
43 | if err != nil {
44 | return nil, err
45 | }
46 |
47 | encrypted := gcm.Seal(nil, nonce, cleartext, nil)
48 |
49 | keySizeBuf := make([]byte, 4)
50 | binary.LittleEndian.PutUint32(keySizeBuf, uint32(len(encKey)))
51 |
52 | // send all
53 | encrypted = append(encKey, encrypted...) // key enc
54 | encrypted = append(keySizeBuf, encrypted...) // ksz key enc
55 | encrypted = append(nonce, encrypted...) // nonce ksz key enc
56 |
57 | return encrypted, nil
58 | }
59 |
60 | func (pair *KeyPair) EncryptBlockFor(block []byte, pubKey *rsa.PublicKey) ([]byte, error) {
61 | return rsa.EncryptOAEP(
62 | Hasher.New(),
63 | rand.Reader,
64 | pubKey,
65 | block,
66 | []byte(""))
67 | }
68 |
69 | func (pair *KeyPair) DecryptBlock(block []byte) ([]byte, error) {
70 | return rsa.DecryptOAEP(
71 | Hasher.New(),
72 | rand.Reader,
73 | pair.Private,
74 | block,
75 | []byte(""))
76 | }
77 |
78 | func (pair *KeyPair) Decrypt(ciphertext []byte) ([]byte, error) {
79 | dataAvailable := len(ciphertext)
80 | if dataAvailable < NonceLength {
81 | return nil, fmt.Errorf("data buffer too short")
82 | }
83 |
84 | nonce := ciphertext[0:NonceLength]
85 | dataAvailable -= NonceLength
86 |
87 | if dataAvailable < 4 {
88 | return nil, fmt.Errorf("data buffer too short")
89 | }
90 |
91 | keySizeBuf := ciphertext[NonceLength : NonceLength+4]
92 | keySize := binary.LittleEndian.Uint32(keySizeBuf)
93 | dataAvailable -= 4
94 |
95 | if dataAvailable < int(keySize) {
96 | return nil, fmt.Errorf("data buffer too short")
97 | }
98 |
99 | encKey := ciphertext[NonceLength+4 : NonceLength+4+keySize]
100 | ciphertext = ciphertext[NonceLength+4+keySize:]
101 |
102 | // decrypt the key
103 | key, err := pair.DecryptBlock(encKey)
104 | if err != nil {
105 | return nil, err
106 | }
107 |
108 | // decrypt the payload
109 | block, err := aes.NewCipher(key)
110 | if err != nil {
111 | return nil, err
112 | }
113 |
114 | gcm, err := cipher.NewGCM(block)
115 | if err != nil {
116 | return nil, err
117 | }
118 |
119 | return gcm.Open(nil, nonce, ciphertext, nil)
120 | }
121 |
--------------------------------------------------------------------------------
/api/peer_mesh.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/json"
5 | "io"
6 | "net/http"
7 | "sort"
8 |
9 | "github.com/evilsocket/islazy/log"
10 | "github.com/go-chi/chi/v5"
11 | "github.com/jayofelony/pwngrid/mesh"
12 | )
13 |
14 | // PeerGetPeers GET /api/v1/mesh/peers
15 | func (api *API) PeerGetPeers(w http.ResponseWriter, r *http.Request) {
16 | peers := make([]*mesh.Peer, 0)
17 | mesh.Peers.Range(func(key, value interface{}) bool {
18 | peers = append(peers, value.(*mesh.Peer))
19 | return true
20 | })
21 |
22 | // closer first
23 | sort.Slice(peers, func(i, j int) bool {
24 | return peers[i].RSSI > peers[j].RSSI
25 | })
26 |
27 | JSON(w, http.StatusOK, peers)
28 | }
29 |
30 | // PeerGetMemory GET /api/v1/mesh/memory
31 | func (api *API) PeerGetMemory(w http.ResponseWriter, r *http.Request) {
32 | peers := api.Mesh.Memory()
33 | // higher number of encounters first
34 | sort.Slice(peers, func(i, j int) bool {
35 | return peers[i].Encounters > peers[j].Encounters
36 | })
37 | JSON(w, http.StatusOK, peers)
38 | }
39 |
40 | // PeerGetMemoryOf GET /api/v1/mesh/memory/
41 | func (api *API) PeerGetMemoryOf(w http.ResponseWriter, r *http.Request) {
42 | fingerprint := chi.URLParam(r, "fingerprint")
43 | peer := api.Mesh.MemoryOf(fingerprint)
44 | if peer == nil {
45 | ERROR(w, http.StatusNotFound, ErrEmpty)
46 | return
47 | }
48 | JSON(w, http.StatusOK, peer)
49 | }
50 |
51 | // PeerSetSignaling GET /api/v1/mesh/
52 | func (api *API) PeerSetSignaling(w http.ResponseWriter, r *http.Request) {
53 | status := chi.URLParam(r, "status")
54 |
55 | if status == "enabled" || status == "true" {
56 | api.Peer.Advertise(true)
57 | } else if status == "disabled" || status == "false" {
58 | api.Peer.Advertise(false)
59 | } else {
60 | ERROR(w, http.StatusNotFound, ErrEmpty)
61 | return
62 | }
63 |
64 | JSON(w, http.StatusOK, map[string]interface{}{
65 | "success": true,
66 | })
67 | }
68 |
69 | // PeerGetMeshData GET /api/v1/mesh/data
70 | func (api *API) PeerGetMeshData(w http.ResponseWriter, r *http.Request) {
71 | JSON(w, http.StatusOK, api.Peer.Data())
72 | }
73 |
74 | // PeerSetMeshData POST /api/v1/mesh/data
75 | func (api *API) PeerSetMeshData(w http.ResponseWriter, r *http.Request) {
76 | var newData map[string]interface{}
77 |
78 | if api.Peer.ForceDisabled == true {
79 | api.Peer.Advertise(false)
80 | JSON(w, http.StatusOK, map[string]interface{}{
81 | "success": true, // this should be changed later when pwnagotchi can handle pwngrid being force advertise disabled
82 | })
83 | return
84 | }
85 |
86 | body, err := io.ReadAll(r.Body)
87 | if err != nil {
88 | ERROR(w, http.StatusUnprocessableEntity, err)
89 | return
90 | }
91 |
92 | log.Debug("%s", body)
93 |
94 | if err = json.Unmarshal(body, &newData); err != nil {
95 | ERROR(w, http.StatusUnprocessableEntity, err)
96 | return
97 | }
98 |
99 | // this makes sure that the pwngrid server receives advertisements
100 | api.Client.SetData(map[string]interface{}{
101 | "advertisement": newData,
102 | })
103 |
104 | // update mesh advertisement data
105 | api.Peer.SetData(newData)
106 |
107 | JSON(w, http.StatusOK, map[string]interface{}{
108 | "success": true,
109 | })
110 | }
111 |
--------------------------------------------------------------------------------
/cmd/pwngrid/vars.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 |
6 | "github.com/evilsocket/islazy/log"
7 | "github.com/jayofelony/pwngrid/api"
8 | "github.com/jayofelony/pwngrid/crypto"
9 | "github.com/jayofelony/pwngrid/mesh"
10 | )
11 |
12 | var (
13 | debug = false
14 | ver = false
15 | wait = false
16 | inbox = false
17 | del = false
18 | unread = false
19 | clear = false
20 | whoami = false
21 | generate = false
22 | loop = false
23 | nodb = false
24 | loopPeriod = 30
25 | receiver = ""
26 | message = ""
27 | output = ""
28 | page = 1
29 | id = 0
30 | address = "0.0.0.0:8666"
31 | env = ".env"
32 | iface = "wlan0mon"
33 | keysPath = ""
34 | peersPath = "/root/peers"
35 | keys = (*crypto.KeyPair)(nil)
36 | router = (*mesh.Router)(nil)
37 | peer = (*mesh.Peer)(nil)
38 | server = (*api.API)(nil)
39 | cpuProfile = ""
40 | memProfile = ""
41 | Endpoint = "https://api.opwngrid.xyz/api/v1"
42 | advertise = true
43 | Hostname = ""
44 | )
45 |
46 | func init() {
47 | flag.BoolVar(&ver, "version", ver, "Print version and exit.")
48 | flag.BoolVar(&debug, "debug", debug, "Enable debug logs.")
49 | flag.BoolVar(&nodb, "no-db", debug, "Don't fail if database connection can't be enstablished.")
50 | flag.StringVar(&log.Output, "log", log.Output, "Log file path or empty for standard output.")
51 | flag.StringVar(&address, "address", address, "API address.")
52 | flag.StringVar(&env, "env", env, "Load .env from.")
53 |
54 | flag.StringVar(&keysPath, "keys", keysPath, "If set, will load RSA keys from this folder and start in peer mode.")
55 | flag.BoolVar(&generate, "generate", generate, "Generate an RSA keypair if it doesn't exist yet.")
56 | flag.BoolVar(&wait, "wait", wait, "Wait for keys to be generated.")
57 | flag.IntVar(&api.ClientTimeout, "client-timeout", api.ClientTimeout, "Timeout in seconds for requests to the server when in peer mode.")
58 | flag.StringVar(&api.ClientTokenFile, "client-token", api.ClientTokenFile, "File where to store the API token.")
59 |
60 | flag.StringVar(&iface, "iface", iface, "Monitor interface to use for mesh advertising.")
61 | flag.StringVar(&peersPath, "peers", peersPath, "path to save historical information of met peers.")
62 | flag.IntVar(&mesh.SignalingPeriod, "signaling-period", mesh.SignalingPeriod, "Period in milliseconds for mesh signaling frames.")
63 |
64 | flag.BoolVar(&whoami, "whoami", whoami, "Prints the public key fingerprint and exit.")
65 | flag.BoolVar(&inbox, "inbox", inbox, "Show inbox.")
66 | flag.BoolVar(&loop, "loop", loop, "Keep refreshing and showing inbox.")
67 | flag.IntVar(&loopPeriod, "loop-period", loopPeriod, "Period in seconds to refresh the inbox.")
68 | flag.StringVar(&receiver, "send", receiver, "Receiver unit fingerprint.")
69 | flag.StringVar(&message, "message", message, "Message body or file path if prefixed by @.")
70 | flag.StringVar(&output, "output", output, "Write message body to this file instead of the standard output.")
71 | flag.BoolVar(&del, "delete", del, "Delete the specified message.")
72 | flag.BoolVar(&unread, "unread", unread, "Unread the specified message.")
73 | flag.BoolVar(&clear, "clear", unread, "Delete all messages of the given page of the inbox.")
74 | flag.IntVar(&page, "page", page, "Inbox page.")
75 | flag.IntVar(&id, "id", id, "Message id.")
76 |
77 | flag.StringVar(&cpuProfile, "cpu-profile", cpuProfile, "Generate CPU profile to this file.")
78 | flag.StringVar(&memProfile, "mem-profile", cpuProfile, "Generate memory profile to this file.")
79 |
80 | flag.StringVar(&Endpoint, "endpoint", Endpoint, "Pass which endpoint pwngrid should be using.")
81 | flag.BoolVar(&advertise, "advertise", advertise, "Advertise?")
82 | flag.StringVar(&Hostname, "hostname", Hostname, "Pass hostname to pwngrid, makes it so it wont read os.hostname()")
83 | }
84 |
--------------------------------------------------------------------------------
/mesh/packet_muxer.go:
--------------------------------------------------------------------------------
1 | package mesh
2 |
3 | import (
4 | "fmt"
5 | "github.com/evilsocket/islazy/async"
6 | "github.com/evilsocket/islazy/log"
7 | "github.com/gopacket/gopacket"
8 | "github.com/gopacket/gopacket/pcap"
9 | "strings"
10 | "time"
11 | )
12 |
13 | const (
14 | // ErrIfaceNotUp Ugly, but gopacket folks are not exporting pcap errors, so ...
15 | // ref. https://github.com/gopacket/gopacket/blob/96986c90e3e5c7e01deed713ff8058e357c0c047/pcap/pcap.go#L281
16 | ErrIfaceNotUp = "Interface Not Up"
17 | )
18 |
19 | var (
20 | SnapLength = 65536
21 | ReadTimeout = 100
22 | )
23 |
24 | type PacketCallback func(pkt gopacket.Packet)
25 |
26 | type PacketMuxer struct {
27 | iface string
28 | filter string
29 | handle *pcap.Handle
30 | source *gopacket.PacketSource
31 | channel chan gopacket.Packet
32 | queue *async.WorkQueue
33 | stop chan struct{}
34 |
35 | onPacket PacketCallback
36 | }
37 |
38 | func dummyPacketCallback(pkt gopacket.Packet) {
39 |
40 | }
41 |
42 | func NewPacketMuxer(iface, filter string, workers int) (mux *PacketMuxer, err error) {
43 | mux = &PacketMuxer{
44 | iface: iface,
45 | filter: filter,
46 | stop: make(chan struct{}),
47 | onPacket: dummyPacketCallback,
48 | }
49 |
50 | for retry := 0; ; retry++ {
51 | inactiveHandle, err := pcap.NewInactiveHandle(iface)
52 | if err != nil {
53 | return nil, fmt.Errorf("error while opening interface %s: %s", iface, err)
54 | }
55 | defer inactiveHandle.CleanUp()
56 |
57 | if err = inactiveHandle.SetRFMon(true); err != nil {
58 | log.Warning("error while setting interface %s in monitor mode: %s", iface, err)
59 | }
60 |
61 | if err = inactiveHandle.SetSnapLen(SnapLength); err != nil {
62 | return nil, fmt.Errorf("error while settng span len: %s", err)
63 | }
64 | /*
65 | * We don't want to pcap.BlockForever otherwise pcap_close(handle)
66 | * could hang waiting for a timeout to expire ...
67 | */
68 | readTimeout := time.Duration(ReadTimeout) * time.Millisecond
69 | if err = inactiveHandle.SetTimeout(readTimeout); err != nil {
70 | return nil, fmt.Errorf("error while setting timeout: %s", err)
71 | } else if mux.handle, err = inactiveHandle.Activate(); err != nil {
72 | if retry == 0 && err.Error() == ErrIfaceNotUp {
73 | log.Info("interface %s is down, bringing it up ...", iface)
74 | if err := ActivateInterface(iface); err != nil {
75 | return nil, err
76 | }
77 | continue
78 | }
79 | return nil, fmt.Errorf("error while activating handle: %s", err)
80 | }
81 |
82 | if filter != "" {
83 | if err := mux.handle.SetBPFFilter(filter); err != nil {
84 | return nil, fmt.Errorf("error setting BPF filter '%s': %v", filter, err)
85 | }
86 | }
87 |
88 | break
89 | }
90 |
91 | mux.source = gopacket.NewPacketSource(mux.handle, mux.handle.LinkType())
92 | mux.channel = mux.source.Packets()
93 | mux.queue = async.NewQueue(workers, func(arg async.Job) {
94 | mux.onPacket(arg.(gopacket.Packet))
95 | })
96 |
97 | return mux, nil
98 | }
99 |
100 | func (mux *PacketMuxer) OnPacket(cb PacketCallback) {
101 | mux.onPacket = cb
102 | }
103 |
104 | func (mux *PacketMuxer) Write(data []byte) error {
105 | var err error
106 | for attempt := 0; attempt < 5; attempt++ {
107 | if err = mux.handle.WritePacketData(data); err == nil {
108 | return nil
109 | } else if strings.Contains(err.Error(), "temporarily unavailable") {
110 | log.Debug("resource temporarily unavailable when sending data")
111 | // if it's the last attempt this will set err to nil as we can't really
112 | // do a lot about this case, otherwise it'll wait 200ms before the next
113 | // attempt is made.
114 | err = nil
115 | if attempt < 5 {
116 | time.Sleep(200 * time.Millisecond)
117 | }
118 | } else {
119 | return nil
120 | }
121 | }
122 | return err
123 | }
124 |
125 | func (mux *PacketMuxer) Start() {
126 | go func() {
127 | log.Debug("packet muxer started (iface:%s filter:%s)", mux.iface, mux.filter)
128 | for {
129 | select {
130 | case packet := <-mux.channel:
131 | mux.queue.Add(async.Job(packet))
132 | case <-mux.stop:
133 | return
134 | }
135 | }
136 | }()
137 | }
138 |
139 | func (mux *PacketMuxer) Stop() {
140 | log.Debug("stopping packet muxer ...")
141 | mux.stop <- struct{}{}
142 | mux.queue.WaitDone()
143 | log.Debug("packet muxer stopped")
144 | }
145 |
--------------------------------------------------------------------------------
/mesh/routing.go:
--------------------------------------------------------------------------------
1 | package mesh
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "github.com/evilsocket/islazy/log"
8 | "github.com/gopacket/gopacket"
9 | "github.com/gopacket/gopacket/layers"
10 | "github.com/jayofelony/pwngrid/wifi"
11 | "sync"
12 | "time"
13 | )
14 |
15 | var (
16 | Workers = 0
17 | PeerTTL = 1800
18 | Peers = sync.Map{}
19 | )
20 |
21 | func dummyPeerActivityCallback(ident string, peer *Peer) {}
22 |
23 | type PeerActivityCallback func(ident string, peer *Peer)
24 |
25 | type Router struct {
26 | local *Peer
27 | mux *PacketMuxer
28 | onNewPeer PeerActivityCallback
29 | onPeerLost PeerActivityCallback
30 | memory *Memory
31 | }
32 |
33 | func StartRouting(iface string, peersPath string, local *Peer) (*Router, error) {
34 | err, memory := MemoryFromPath(peersPath)
35 | if err != nil {
36 | return nil, err
37 | }
38 |
39 | filter := fmt.Sprintf("type mgt subtype beacon and ether src %s", wifi.SignatureAddrStr)
40 | mux, err := NewPacketMuxer(iface, filter, Workers)
41 | if err != nil {
42 | return nil, err
43 | }
44 |
45 | router := &Router{
46 | mux: mux,
47 | local: local,
48 | memory: memory,
49 | onNewPeer: dummyPeerActivityCallback,
50 | onPeerLost: dummyPeerActivityCallback,
51 | }
52 | mux.OnPacket(router.onPacket)
53 | mux.Start()
54 |
55 | log.Info("started beacon discovery and message routing (%d known peers)", router.memory.Size())
56 |
57 | go router.peersPruner()
58 |
59 | return router, nil
60 | }
61 |
62 | func (router *Router) Memory() []*Peer {
63 | return router.memory.List()
64 | }
65 |
66 | func (router *Router) MemoryOf(fingerprint string) *Peer {
67 | return router.memory.Of(fingerprint)
68 | }
69 |
70 | func (router *Router) OnNewPeer(cb PeerActivityCallback) {
71 | router.onNewPeer = cb
72 | }
73 |
74 | func (router *Router) OnPeerLost(cb PeerActivityCallback) {
75 | router.onPeerLost = cb
76 | }
77 |
78 | func (router *Router) peersPruner() {
79 | period := time.Duration(500) * time.Millisecond
80 | tick := time.NewTicker(period)
81 |
82 | log.Debug("peers pruner started with a %s period", period)
83 |
84 | for range tick.C {
85 | stale := map[string]*Peer{}
86 |
87 | Peers.Range(func(key, value interface{}) bool {
88 | ident := key.(string)
89 | peer := value.(*Peer)
90 | inactive := peer.InactiveFor()
91 | if int(inactive) > PeerTTL {
92 | stale[ident] = peer
93 | }
94 | return true
95 | })
96 |
97 | for ident, peer := range stale {
98 | Peers.Delete(ident)
99 | router.onPeerLost(ident, peer)
100 | }
101 | }
102 | }
103 |
104 | func (router *Router) newPeer(ident string, peer *Peer) {
105 | Peers.Store(ident, peer)
106 | router.onNewPeer(ident, peer)
107 | }
108 |
109 | func (router *Router) onPeerAdvertisement(pkt gopacket.Packet, radio *layers.RadioTap, dot11 *layers.Dot11) {
110 | err, payload := wifi.Unpack(pkt, radio, dot11)
111 | if err != nil {
112 | log.Debug("%v", err)
113 | return
114 | }
115 |
116 | advData := make(map[string]interface{})
117 | if err := json.Unmarshal(payload, &advData); err != nil {
118 | log.Debug("error decoding payload '%s': %v", payload, err)
119 | return
120 | }
121 |
122 | ident, ok := advData["identity"]
123 | if !ok {
124 | log.Debug("error parsing identity from payload '%s'", payload)
125 | return
126 | }
127 |
128 | var peer *Peer
129 |
130 | _peer, existing := Peers.Load(ident)
131 | if existing {
132 | peer = _peer.(*Peer)
133 | if err := peer.Update(radio, dot11, advData); err != nil {
134 | log.Warning("error updating peer %s: %v", peer.ID(), err)
135 | } else if err := router.memory.Track(ident.(string), peer); err != nil {
136 | log.Error("error saving peer encounter for %s: %v", ident, err)
137 | }
138 | } else {
139 | if peer, err = NewPeer(radio, dot11, advData); err != nil {
140 | log.Debug("error creating peer: %v", err)
141 | return
142 | } else if err := router.memory.Track(ident.(string), peer); err != nil {
143 | log.Error("error saving peer encounter for %s: %v", ident, err)
144 | } else {
145 | router.newPeer(ident.(string), peer)
146 | }
147 | }
148 |
149 | }
150 |
151 | func (router *Router) onPacket(pkt gopacket.Packet) {
152 | if ok, radio, dot11 := wifi.Parse(pkt); ok && dot11.ChecksumValid() {
153 | src := dot11.Address3
154 | dst := dot11.Address1
155 | if !bytes.Equal(src, router.local.SessionID) {
156 | if bytes.Equal(dst, wifi.BroadcastAddr) {
157 | router.onPeerAdvertisement(pkt, radio, dot11)
158 | } else {
159 | // log.Debug("ignoring message %x > %x", src, dst)
160 | }
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/crypto/keypair.go:
--------------------------------------------------------------------------------
1 | package crypto
2 |
3 | import (
4 | "crypto/rand"
5 | "crypto/rsa"
6 | "crypto/x509"
7 | "encoding/pem"
8 | "fmt"
9 | "github.com/evilsocket/islazy/fs"
10 | "github.com/evilsocket/islazy/log"
11 | "os"
12 | "path"
13 | "strings"
14 | )
15 |
16 | type KeyPair struct {
17 | Path string
18 | Bits int
19 | PrivatePath string
20 | Private *rsa.PrivateKey
21 | PrivatePEM []byte
22 | PublicPath string
23 | Public *rsa.PublicKey
24 | PublicPEM []byte
25 | // sha256 of PublicSSH
26 | Fingerprint []byte
27 | FingerprintHex string
28 | }
29 |
30 | func pubKeyToPEM(key *rsa.PublicKey) ([]byte, error) {
31 | bytes, err := x509.MarshalPKIXPublicKey(key)
32 | if err != nil {
33 | return nil, err
34 | }
35 | return pem.EncodeToMemory(
36 | &pem.Block{
37 | Type: "RSA PUBLIC KEY",
38 | Bytes: bytes,
39 | },
40 | ), nil
41 | }
42 |
43 | func FromPublicPEM(pubPEM string) (pair *KeyPair, err error) {
44 | block, _ := pem.Decode([]byte(pubPEM))
45 | if block == nil {
46 | return nil, fmt.Errorf("failed to parse PEM block containing the public key")
47 | }
48 |
49 | pub, err := x509.ParsePKIXPublicKey(block.Bytes)
50 | if err != nil {
51 | return nil, err
52 | }
53 |
54 | pair = &KeyPair{}
55 | ok := false
56 |
57 | if pair.Public, ok = pub.(*rsa.PublicKey); !ok {
58 | return nil, fmt.Errorf("not an RSA key")
59 | }
60 |
61 | return pair, pair.setupPublic()
62 | }
63 |
64 | func PrivatePath(keysPath string) string {
65 | return path.Join(keysPath, "id_rsa")
66 | }
67 |
68 | func Load(keysPath string) (pair *KeyPair, err error) {
69 | privFile := PrivatePath(keysPath)
70 | pair = &KeyPair{
71 | Path: keysPath,
72 | PrivatePath: privFile,
73 | PublicPath: privFile + ".pub",
74 | }
75 | return pair, pair.Load()
76 | }
77 |
78 | func KeysExist(keysPath string) bool {
79 | return fs.Exists(keysPath) && fs.Exists(PrivatePath(keysPath))
80 | }
81 |
82 | func LoadOrCreate(keysPath string, bits int) (pair *KeyPair, err error) {
83 | privFile := PrivatePath(keysPath)
84 | pair = &KeyPair{
85 | Path: keysPath,
86 | Bits: bits,
87 | PrivatePath: privFile,
88 | PublicPath: privFile + ".pub",
89 | }
90 |
91 | if !fs.Exists(pair.PrivatePath) {
92 | if !fs.Exists(keysPath) {
93 | log.Debug("creating %s", keysPath)
94 | if err := os.MkdirAll(keysPath, os.ModePerm); err != nil {
95 | return nil, fmt.Errorf("could not create %s: %v", keysPath, err)
96 | }
97 | }
98 | log.Info("%s not found, generating keypair ...", pair.PrivatePath)
99 |
100 | if pair.Private, err = rsa.GenerateKey(rand.Reader, bits); err != nil {
101 | return nil, fmt.Errorf("could not generate private key: %v", err)
102 | }
103 | pair.Public = &pair.Private.PublicKey
104 |
105 | if err = pair.Save(); err != nil {
106 | return nil, fmt.Errorf("could not save keypair: %v", err)
107 | }
108 | } else if err = pair.Load(); err != nil {
109 | return nil, fmt.Errorf("could not load keypair: %v", err)
110 | }
111 |
112 | return pair, nil
113 | }
114 |
115 | func (pair *KeyPair) setupPublic() (err error) {
116 | if pair.PublicPEM, err = pubKeyToPEM(pair.Public); err != nil {
117 | return fmt.Errorf("failed converting public key to PEM: %v", err)
118 | }
119 |
120 | cleanPEM := strings.TrimRight(string(pair.PublicPEM), "\n")
121 |
122 | hash := Hasher.New()
123 | hash.Write([]byte(cleanPEM))
124 |
125 | pair.Fingerprint = hash.Sum(nil)
126 | pair.FingerprintHex = fmt.Sprintf("%02x", pair.Fingerprint)
127 |
128 | return nil
129 | }
130 |
131 | func (pair *KeyPair) Save() (err error) {
132 | prvKeyBytes := x509.MarshalPKCS1PrivateKey(pair.Private)
133 | pair.PrivatePEM = pem.EncodeToMemory(
134 | &pem.Block{
135 | Type: "RSA PRIVATE KEY",
136 | Bytes: prvKeyBytes,
137 | },
138 | )
139 |
140 | if err = os.WriteFile(pair.PrivatePath, pair.PrivatePEM, os.ModePerm); err != nil {
141 | return
142 | }
143 |
144 | log.Debug("%s created", pair.PrivatePath)
145 |
146 | if err = pair.setupPublic(); err != nil {
147 | return err
148 | }
149 |
150 | err = os.WriteFile(pair.PublicPath, pair.PublicPEM, os.ModePerm)
151 |
152 | log.Debug("%s created", pair.PublicPath)
153 | return
154 | }
155 |
156 | func (pair *KeyPair) Load() (err error) {
157 | log.Debug("reading %s ...", pair.PrivatePath)
158 | if pair.PrivatePEM, err = os.ReadFile(pair.PrivatePath); err != nil {
159 | return
160 | }
161 |
162 | block, _ := pem.Decode(pair.PrivatePEM)
163 | if block == nil {
164 | return fmt.Errorf("failed decoding PEM from %s", pair.PrivatePath)
165 | }
166 |
167 | if pair.Private, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
168 | return fmt.Errorf("failed parsing %s: %v", pair.PrivatePath, err)
169 | }
170 |
171 | pair.Public = &pair.Private.PublicKey
172 | return pair.setupPublic()
173 | }
174 |
--------------------------------------------------------------------------------
/cmd/pwngrid/inbox.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/evilsocket/islazy/log"
6 | "github.com/evilsocket/islazy/tui"
7 | "github.com/jayofelony/pwngrid/api"
8 | "os"
9 | "os/exec"
10 | "runtime"
11 | "time"
12 | )
13 |
14 | func clearScreen() {
15 | var what []string
16 | if runtime.GOOS == "windows" {
17 | what = []string{"cmd", "/c", "cls"}
18 | } else {
19 | what = []string{"clear", ""}
20 | }
21 | cmd := exec.Command(what[0], what[1:]...)
22 | cmd.Stdout = os.Stdout
23 | err := cmd.Run()
24 | if err != nil {
25 | return
26 | }
27 | }
28 |
29 | func showInbox(server *api.API, box map[string]interface{}) {
30 | messages := box["messages"].([]interface{})
31 | numMessages := len(messages)
32 |
33 | if numMessages > 0 {
34 | if clear {
35 | log.Info("clearing %d messages", numMessages)
36 | for _, m := range messages {
37 | msg := m.(map[string]interface{})
38 | msgID := int(msg["id"].(float64))
39 | log.Info("deleting message %d ...", msgID)
40 | if _, err := server.Client.MarkInboxMessage(msgID, "deleted"); err != nil {
41 | log.Error("%v", err)
42 | }
43 | }
44 | } else {
45 | records := box["records"].(float64)
46 | pages := box["pages"].(float64)
47 | columns := []string{
48 | "ID",
49 | "Date",
50 | "Sender",
51 | }
52 | rows := [][]string{}
53 | for _, m := range messages {
54 | var row []string
55 | msg := m.(map[string]interface{})
56 |
57 | t, err := time.Parse(time.RFC3339, msg["created_at"].(string))
58 | if err != nil {
59 | panic(err)
60 | }
61 |
62 | row = []string{
63 | fmt.Sprintf("%d", int(msg["id"].(float64))),
64 | t.Format("02 January 2006, 3:04 PM"),
65 | fmt.Sprintf("%s@%s", msg["sender_name"], msg["sender"]),
66 | }
67 |
68 | if msg["seen_at"] != nil {
69 | for i := range row {
70 | row[i] = tui.Dim(row[i])
71 | }
72 | }
73 |
74 | rows = append(rows, row)
75 | }
76 |
77 | fmt.Println()
78 | tui.Table(os.Stdout, columns, rows)
79 | fmt.Println()
80 |
81 | fmt.Printf("%d of %d (page %d of %d)", numMessages, int(records), page, int(pages))
82 | }
83 | } else {
84 | fmt.Println()
85 | fmt.Println(tui.Dim("Inbox is empty."))
86 | }
87 |
88 | fmt.Println()
89 | }
90 |
91 | func showMessage(msg map[string]interface{}) {
92 | t, err := time.Parse(time.RFC3339, msg["created_at"].(string))
93 | if err != nil {
94 | panic(err)
95 | }
96 |
97 | fmt.Println()
98 | fmt.Printf("From: %s@%s\n", msg["sender_name"], msg["sender"])
99 | fmt.Printf("Date: %s\n\n", t.Format("02 January 2006, 3:04 PM"))
100 | if output == "" {
101 | fmt.Printf("%s\n", msg["data"])
102 | fmt.Println()
103 | } else if err := os.WriteFile(output, msg["data"].([]byte), os.ModePerm); err != nil {
104 | log.Fatal("error writing to %s: %v", output, err)
105 | } else {
106 | log.Info("%s written", output)
107 | }
108 | }
109 |
110 | func sendMessage() {
111 | var err error
112 |
113 | // send a message
114 | var raw []byte
115 | if message == "" {
116 | log.Fatal("-message can not be empty")
117 | } else if message[0] == '@' {
118 | log.Info("reading %s ...", message[1:])
119 | if raw, err = os.ReadFile(message[1:]); err != nil {
120 | log.Fatal("error reading %s: %v", message[1:], err)
121 | }
122 | } else {
123 | raw = []byte(message)
124 | }
125 |
126 | if status, err := server.SendMessage(receiver, raw); err != nil {
127 | log.Fatal("%d %v", status, err)
128 | } else {
129 | log.Info("message sent")
130 | }
131 | }
132 |
133 | func doInbox(server *api.API) {
134 | if receiver != "" {
135 | sendMessage()
136 | } else if inbox {
137 | // just show the inbox
138 | if id == 0 {
139 | log.Info("fetching inbox ...")
140 | if box, err := server.Client.Inbox(page); err != nil {
141 | log.Fatal("%v", err)
142 | } else {
143 | showInbox(server, box)
144 | }
145 | } else if del {
146 | log.Info("deleting message %d ...", id)
147 | if _, err := server.Client.MarkInboxMessage(id, "deleted"); err != nil {
148 | log.Fatal("%v", err)
149 | }
150 | } else if unread {
151 | log.Info("marking message %d as unread ...", id)
152 | if _, err := server.Client.MarkInboxMessage(id, "unseen"); err != nil {
153 | log.Fatal("%v", err)
154 | }
155 | } else {
156 | log.Info("fetching message %d ...", id)
157 |
158 | if msg, status, err := server.InboxMessage(id); err != nil {
159 | log.Fatal("%d %v", status, err)
160 | } else {
161 | showMessage(msg)
162 | _, _ = server.Client.MarkInboxMessage(id, "seen")
163 | }
164 | }
165 | }
166 | }
167 |
168 | func inboxMain() {
169 | if inbox {
170 | doInbox(server)
171 | if loop {
172 | ticker := time.NewTicker(time.Duration(loopPeriod) * time.Second)
173 | for _ = range ticker.C {
174 | clearScreen()
175 | doInbox(server)
176 | }
177 | }
178 | os.Exit(0)
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/cmd/pwngrid/setup.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "os/signal"
6 | "runtime/pprof"
7 | "time"
8 |
9 | "github.com/evilsocket/islazy/fs"
10 | "github.com/evilsocket/islazy/log"
11 | "github.com/jayofelony/pwngrid/api"
12 | "github.com/jayofelony/pwngrid/crypto"
13 | "github.com/jayofelony/pwngrid/mesh"
14 | "github.com/jayofelony/pwngrid/models"
15 | "github.com/jayofelony/pwngrid/utils"
16 | "github.com/jayofelony/pwngrid/version"
17 | "github.com/joho/godotenv"
18 | )
19 |
20 | func cleanup() {
21 | if cpuProfile != "" {
22 | log.Info("writing CPU profile to %s ...", cpuProfile)
23 | pprof.StopCPUProfile()
24 | }
25 |
26 | if memProfile != "" {
27 | log.Info("writing memory profile to %s ...", memProfile)
28 | f, err := os.Create(memProfile)
29 | if err != nil {
30 | log.Fatal("%v", err)
31 | }
32 | defer func() {
33 | if err := f.Close(); err != nil {
34 | panic(err)
35 | }
36 | }()
37 | if err := pprof.WriteHeapProfile(f); err != nil {
38 | panic(err)
39 | }
40 | }
41 | }
42 |
43 | func setupCore() {
44 | c := make(chan os.Signal, 1)
45 | signal.Notify(c, os.Interrupt)
46 | go func() {
47 | for sig := range c {
48 | log.Warning("received signal %v", sig)
49 | cleanup()
50 | os.Exit(0)
51 | }
52 | }()
53 |
54 | if cpuProfile != "" {
55 | f, err := os.Create(cpuProfile)
56 | if err != nil {
57 | log.Fatal("%v", err)
58 | }
59 | if err := pprof.StartCPUProfile(f); err != nil {
60 | panic(err)
61 | }
62 | }
63 |
64 | if debug {
65 | log.Level = log.DEBUG
66 | } else {
67 | log.Level = log.INFO
68 | }
69 | log.OnFatal = log.ExitOnFatal
70 | }
71 |
72 | func waitForKeys() {
73 | privPath := crypto.PrivatePath(keysPath)
74 | for {
75 | if !fs.Exists(privPath) {
76 | log.Debug("waiting for %s ...", privPath)
77 | time.Sleep(1 * time.Second)
78 | } else {
79 | // give it a moment to finish disk sync
80 | time.Sleep(2 * time.Second)
81 | log.Info("%s found", privPath)
82 | break
83 | }
84 | }
85 | }
86 |
87 | func setupMesh() {
88 | var err error
89 | peer = mesh.MakeLocalPeer(utils.Hostname(), keys, advertise)
90 | if !advertise {
91 | return //this probably doesn't work
92 | }
93 |
94 | if err = peer.StartAdvertising(iface); err != nil {
95 | log.Fatal("error while starting signaling: %v", err)
96 | }
97 | if router, err = mesh.StartRouting(iface, peersPath, peer); err != nil {
98 | log.Fatal("%v", err)
99 | } else {
100 | router.OnNewPeer(func(ident string, peer *mesh.Peer) {
101 | log.Info("detected new peer %s on channel %d", peer.ID(), peer.Channel)
102 | })
103 | router.OnPeerLost(func(ident string, peer *mesh.Peer) {
104 | log.Info("peer %s lost (inactive for %fs)", peer.ID(), peer.InactiveFor())
105 | })
106 | }
107 | log.Info("peer %s signaling is ready", peer.ID())
108 | }
109 |
110 | func setupDB() {
111 | if err := godotenv.Load(env); err != nil {
112 | log.Fatal("%v", err)
113 | }
114 | if err := models.Setup(); err != nil {
115 | if nodb {
116 | log.Warning("%v", err)
117 | } else {
118 | log.Fatal("%v", err)
119 | }
120 | }
121 | }
122 |
123 | func setupMode() string {
124 | var err error
125 |
126 | // in case -inbox was not explicitly passed
127 | if receiver != "" || loop == true || id > 0 {
128 | inbox = true
129 | }
130 |
131 | // for inbox actions, set the keys to the default path if empty
132 | if (whoami || inbox) && keysPath == "" {
133 | keysPath = "/etc/pwnagotchi/"
134 | }
135 |
136 | // generate keypair
137 | if generate {
138 | if keysPath == "" {
139 | log.Fatal("no -keys path specified")
140 | } else if crypto.KeysExist(keysPath) {
141 | log.Fatal("keypair already exists in %s", keysPath)
142 | }
143 |
144 | if _, err = crypto.LoadOrCreate(keysPath, 4096); err != nil {
145 | log.Fatal("error generating RSA keypair: %v", err)
146 | } else {
147 | log.Info("keypair saved to %s", keysPath)
148 | }
149 | os.Exit(0)
150 | }
151 |
152 | mode := "peer"
153 | // if keys have been passed explicitly, or one of the inbox actions
154 | // has been specified, we're running on the unit
155 | // if keysPath != "" {
156 | // mode = "peer"
157 | // }
158 |
159 | log.Info("pwngrid v%s starting in %s mode ...", version.Version, mode)
160 |
161 | // wait for keys to be generated
162 | if wait {
163 | waitForKeys()
164 | }
165 | // load the keys
166 | if keys, err = crypto.Load(keysPath); err != nil {
167 | log.Fatal("error while loading keys from %s: %v", keysPath, err)
168 | }
169 | // print identity and exit
170 | if whoami {
171 | if Endpoint == "https://api.opwngrid.xyz/api/v1" {
172 | log.Info("https://opwngrid.xyz/search/%s", keys.FingerprintHex)
173 | } else {
174 | log.Info("https://pwnagotchi.ai/pwnfile/#!%s", keys.FingerprintHex)
175 | }
176 | os.Exit(0)
177 | }
178 | // only start mesh signaling if this is not an inbox action
179 | if !inbox {
180 | setupMesh()
181 | }
182 |
183 | // set up the proper routes for either server or peer mode
184 | err, server = api.Setup(keys, peer, router, Endpoint, Hostname)
185 | if err != nil {
186 | log.Fatal("%v", err)
187 | }
188 |
189 | return mode
190 | }
191 |
--------------------------------------------------------------------------------
/models/unit.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/golang-jwt/jwt/v5"
7 | "os"
8 | "reflect"
9 | "sync"
10 | "time"
11 |
12 | "github.com/evilsocket/islazy/log"
13 | )
14 |
15 | const (
16 | TokenTTL = time.Minute * 30
17 | CacheTTL = time.Minute * 120
18 | )
19 |
20 | type cachedCounter struct {
21 | Time time.Time
22 | Count int
23 | }
24 |
25 | var (
26 | cache = make(map[uint]*cachedCounter)
27 | cacheLock = sync.Mutex{}
28 | )
29 |
30 | type Unit struct {
31 | ID uint `gorm:"primary_key" json:"-"`
32 | CreatedAt time.Time `json:"enrolled_at"`
33 | UpdatedAt time.Time `json:"updated_at"`
34 | DeletedAt *time.Time `sql:"index" json:"-"`
35 | Address string `gorm:"size:50;not null" json:"-"`
36 | Country string `gorm:"size:10" json:"country"`
37 | Name string `gorm:"size:255;not null" json:"name"`
38 | Fingerprint string `gorm:"size:255;not null;unique" json:"fingerprint"`
39 | PublicKey string `gorm:"size:10000;not null" json:"public_key"`
40 | Token string `gorm:"size:10000;not null" json:"-"`
41 | Data string `gorm:"size:10000;not null" json:"data"`
42 |
43 | AccessPoints []AccessPoint `gorm:"foreignkey:UnitID" json:"-"`
44 |
45 | Inbox []Message `gorm:"foreignkey:ReceiverID" json:"-"`
46 | Sent []Message `gorm:"foreignkey:SenderID" json:"-"`
47 | }
48 |
49 | func (u Unit) Identity() string {
50 | return fmt.Sprintf("%s@%s", u.Name, u.Fingerprint)
51 | }
52 |
53 | func (u Unit) FindAccessPoint(essid, bssid string) *AccessPoint {
54 | var ap AccessPoint
55 |
56 | if err := db.Where("unit_id = ? AND name = ? AND mac = ?", u.ID, essid, bssid).Take(&ap).Error; err != nil {
57 | if err := db.Where("unit_id = ? AND mac = ?", u.ID, bssid).Take(&ap).Error; err != nil {
58 | return nil
59 | }
60 | }
61 |
62 | return &ap
63 | }
64 |
65 | func (u *Unit) updateToken() error {
66 | claims := jwt.MapClaims{}
67 | claims["authorized"] = true
68 | claims["unit_id"] = u.ID
69 | claims["unit_ident"] = u.Identity()
70 | claims["expires_at"] = time.Now().Add(TokenTTL).Format(time.RFC3339)
71 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
72 | signed, err := token.SignedString([]byte(os.Getenv("API_SECRET")))
73 | if err != nil {
74 | return err
75 | }
76 | u.Token = signed
77 | return nil
78 | }
79 |
80 | func (u *Unit) UpdateWith(enroll EnrollmentRequest) error {
81 | prevData := map[string]interface{}{}
82 |
83 | if u.Data != "" {
84 | if err := json.Unmarshal([]byte(u.Data), &prevData); err != nil {
85 | log.Warning("error parsing previous data: %v", err)
86 | log.Debug("%s", u.Data)
87 | }
88 | }
89 |
90 | // only replace sent values
91 | for key, obj := range enroll.Data {
92 | set := true
93 | if key == "session" {
94 | if session, ok := obj.(map[string]interface{}); !ok {
95 | set = false
96 | log.Warning("corrupted session (first level): %v", obj)
97 | } else if epochs, found := session["epochs"]; !found {
98 | set = false
99 | log.Warning("corrupted session (no epochs): %v", obj)
100 | } else if num, ok := epochs.(float64); !ok {
101 | set = false
102 | log.Warning("corrupted session (epochs type %v): %v", reflect.TypeOf(epochs), obj)
103 | } else if num == 0 {
104 | // do not update with empty sessions
105 | set = false
106 | }
107 | }
108 |
109 | if set {
110 | prevData[key] = obj
111 | }
112 | }
113 |
114 | newData, err := json.Marshal(prevData)
115 | if err != nil {
116 | return err
117 | }
118 |
119 | if u.Name != enroll.Name {
120 | log.Info("unit %s changed name: %s -> %s", u.Identity(), u.Name, enroll.Name)
121 | }
122 |
123 | u.Name = enroll.Name
124 | u.Address = enroll.Address
125 | u.Country = enroll.Country
126 | u.Data = string(newData)
127 |
128 | return db.Save(u).Error
129 | }
130 |
131 | type unitJSON struct {
132 | EnrolledAt time.Time `json:"enrolled_at"`
133 | UpdatedAt time.Time `json:"updated_at"`
134 | Country string `json:"country"`
135 | Name string `json:"name"`
136 | Fingerprint string `json:"fingerprint"`
137 | PublicKey string `json:"public_key"`
138 | Data map[string]interface{} `json:"data"`
139 | Networks int `json:"networks"`
140 | }
141 |
142 | func (u *Unit) apCounter() int {
143 | cacheLock.Lock()
144 | defer cacheLock.Unlock()
145 |
146 | count := -1
147 | if cnt, found := cache[u.ID]; found && time.Since(cnt.Time) < CacheTTL {
148 | count = cnt.Count
149 | }
150 |
151 | if count == -1 {
152 | count = db.Model(u).Association("AccessPoints").Count()
153 | cache[u.ID] = &cachedCounter{
154 | Time: time.Now(),
155 | Count: count,
156 | }
157 | }
158 |
159 | return count
160 | }
161 |
162 | func (u *Unit) MarshalJSON() ([]byte, error) {
163 | doc := unitJSON{
164 | EnrolledAt: u.CreatedAt,
165 | UpdatedAt: u.UpdatedAt,
166 | Country: u.Country,
167 | Name: u.Name,
168 | Fingerprint: u.Fingerprint,
169 | PublicKey: u.PublicKey,
170 | Data: map[string]interface{}{},
171 | Networks: u.apCounter(),
172 | }
173 |
174 | if u.Data != "" {
175 | if err := json.Unmarshal([]byte(u.Data), &doc.Data); err != nil {
176 | return nil, err
177 | }
178 | }
179 |
180 | return json.Marshal(doc)
181 | }
182 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
3 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
4 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
5 | github.com/biezhi/gorm-paginator/pagination v0.0.0-20250219022659-7f61c90f8f21 h1:0SSFN9kbB2MkoEQv6u5H6svD1lPPWQV6tLx5qfLGXZk=
6 | github.com/biezhi/gorm-paginator/pagination v0.0.0-20250219022659-7f61c90f8f21/go.mod h1:Y/N4aF7p+Med/9ivVSsGBc8xOs8BGptUVBCY3k4KFCY=
7 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
8 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
9 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
10 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
11 | github.com/evilsocket/islazy v1.11.0 h1:B5w6uuS6ki6iDG+aH/RFeoMb8ijQh/pGabewqp2UeJ0=
12 | github.com/evilsocket/islazy v1.11.0/go.mod h1:muYH4x5MB5YRdkxnrOtrXLIBX6LySj1uFIqys94LKdo=
13 | github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
14 | github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
15 | github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
16 | github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
17 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
18 | github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
19 | github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
20 | github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
21 | github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
22 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
23 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
24 | github.com/gopacket/gopacket v1.5.0 h1:9s9fcSUVKFlRV97B77Bq9XNV3ly2gvvsneFMQUGjc+M=
25 | github.com/gopacket/gopacket v1.5.0/go.mod h1:i3NaGaqfoWKAr1+g7qxEdWsmfT+MXuWkAe9+THv8LME=
26 | github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
27 | github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
28 | github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
29 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
30 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
31 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
32 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
33 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
34 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
35 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
36 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
37 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
38 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
39 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
40 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
41 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
42 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
43 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
44 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM=
45 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
46 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
47 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
48 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
49 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
50 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
51 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
52 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
53 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
54 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
55 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
56 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
57 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
58 | golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
59 | golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
60 | gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
61 | gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
62 |
--------------------------------------------------------------------------------
/api/peer_inbox.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/base64"
5 | "errors"
6 | "fmt"
7 | "github.com/evilsocket/islazy/log"
8 | "github.com/go-chi/chi/v5"
9 | "github.com/jayofelony/pwngrid/crypto"
10 | "github.com/jayofelony/pwngrid/models"
11 | "io"
12 | "net/http"
13 | "strconv"
14 | )
15 |
16 | var (
17 | ErrEmptyMessage = errors.New("empty message body")
18 | ErrSenderNotFound = errors.New("sender not found")
19 | )
20 |
21 | // PeerGetInbox /api/v1/inbox/
22 | func (api *API) PeerGetInbox(w http.ResponseWriter, r *http.Request) {
23 | page, err := pageNum(r)
24 | if err != nil {
25 | ERROR(w, http.StatusUnprocessableEntity, err)
26 | return
27 | }
28 |
29 | obj, err := api.Client.Inbox(page)
30 | if err != nil {
31 | ERROR(w, http.StatusUnprocessableEntity, err)
32 | return
33 | }
34 |
35 | JSON(w, http.StatusOK, obj)
36 | }
37 |
38 | func (api *API) InboxMessage(id int) (map[string]interface{}, int, error) {
39 | message, err := api.Client.InboxMessage(id)
40 | if err != nil {
41 | return nil, http.StatusUnprocessableEntity, err
42 | }
43 |
44 | sender, found := message["sender"]
45 | if !found {
46 | return nil, http.StatusNotFound, ErrSenderNotFound
47 | }
48 |
49 | fingerprint, ok := sender.(string)
50 | if !ok {
51 | return nil, http.StatusUnprocessableEntity, ErrSenderNotFound
52 | }
53 |
54 | unit, err := api.Client.Unit(fingerprint)
55 | if err != nil {
56 | return nil, http.StatusNotFound, err
57 | }
58 |
59 | srcKeys, err := crypto.FromPublicPEM(unit["public_key"].(string))
60 | if err != nil {
61 | return nil, http.StatusUnprocessableEntity, err
62 | }
63 |
64 | data, err := base64.StdEncoding.DecodeString(message["data"].(string))
65 | if err != nil {
66 | return nil, http.StatusUnprocessableEntity, err
67 | }
68 |
69 | signature, err := base64.StdEncoding.DecodeString(message["signature"].(string))
70 | if err != nil {
71 | return nil, http.StatusUnprocessableEntity, err
72 | }
73 |
74 | log.Info("verifying message from %s ...", fingerprint)
75 |
76 | if err := srcKeys.VerifyMessage(data, signature); err != nil {
77 | return nil, http.StatusUnprocessableEntity, err
78 | }
79 |
80 | log.Info("decrypting message from %s ...", fingerprint)
81 |
82 | clearText, err := api.Keys.Decrypt(data)
83 | if err != nil {
84 | return nil, http.StatusUnprocessableEntity, err
85 | }
86 |
87 | message["data"] = clearText
88 |
89 | return message, 0, nil
90 | }
91 |
92 | // /api/v1/inbox/
93 | func (api *API) PeerGetInboxMessage(w http.ResponseWriter, r *http.Request) {
94 | msgIDParam := chi.URLParam(r, "msg_id")
95 | msgID, err := strconv.Atoi(msgIDParam)
96 | if err != nil {
97 | ERROR(w, http.StatusUnprocessableEntity, err)
98 | return
99 | }
100 |
101 | message, status, err := api.InboxMessage(msgID)
102 | if err != nil {
103 | ERROR(w, status, err)
104 | return
105 | }
106 |
107 | JSON(w, http.StatusOK, message)
108 | }
109 |
110 | // /api/v1/inbox//
111 | func (api *API) PeerMarkInboxMessage(w http.ResponseWriter, r *http.Request) {
112 | markAs := chi.URLParam(r, "mark")
113 | msgIDParam := chi.URLParam(r, "msg_id")
114 | msgID, err := strconv.Atoi(msgIDParam)
115 | if err != nil {
116 | ERROR(w, http.StatusUnprocessableEntity, err)
117 | return
118 | }
119 |
120 | obj, err := api.Client.MarkInboxMessage(msgID, markAs)
121 | if err != nil {
122 | ERROR(w, http.StatusUnprocessableEntity, err)
123 | return
124 | }
125 |
126 | JSON(w, http.StatusOK, obj)
127 | }
128 |
129 | func (api *API) SendMessage(fingerprint string, cleartext []byte) (int, error) {
130 | unit, err := api.Client.Unit(fingerprint)
131 | if err != nil {
132 | return http.StatusNotFound, err
133 | }
134 |
135 | unitKeys, err := crypto.FromPublicPEM(unit["public_key"].(string))
136 | if err != nil {
137 | log.Error("error parsing public key of %s: %v", fingerprint, err)
138 | return http.StatusUnprocessableEntity, err
139 | }
140 |
141 | messageBody, err := api.Keys.EncryptFor(cleartext, unitKeys.Public)
142 | if err != nil {
143 | log.Error("error encrypting message for %s: %v", fingerprint, err)
144 | return http.StatusUnprocessableEntity, err
145 | }
146 |
147 | messageSize := len(messageBody)
148 | if messageSize == 0 {
149 | return http.StatusUnprocessableEntity, ErrEmptyMessage
150 | } else if messageSize > models.MessageDataMaxSize {
151 | err := fmt.Errorf("max message signature size is %d", models.MessageSignatureMaxSize)
152 | return http.StatusUnprocessableEntity, err
153 | }
154 |
155 | log.Info("signing encrypted message of %d bytes for %s ...", messageSize, fingerprint)
156 |
157 | signature, err := api.Keys.SignMessage(messageBody)
158 | if err != nil {
159 | log.Error("%v", err)
160 | return http.StatusUnprocessableEntity, err
161 | }
162 |
163 | msg := Message{
164 | Signature: base64.StdEncoding.EncodeToString(signature),
165 | Data: base64.StdEncoding.EncodeToString(messageBody),
166 | }
167 |
168 | if err := api.Client.SendMessageTo(fingerprint, msg); err != nil {
169 | log.Error("%v", err)
170 | return http.StatusUnprocessableEntity, err
171 | }
172 |
173 | return 0, nil
174 | }
175 |
176 | // POST /api/v1/unit//inbox
177 | func (api *API) PeerSendMessageTo(w http.ResponseWriter, r *http.Request) {
178 | cleartextMessage, err := io.ReadAll(r.Body)
179 | if err != nil {
180 | log.Error("error reading request body: %v", err)
181 | ERROR(w, http.StatusUnprocessableEntity, err)
182 | return
183 | }
184 |
185 | fingerprint := chi.URLParam(r, "fingerprint")
186 | status, err := api.SendMessage(fingerprint, cleartextMessage)
187 | if err != nil {
188 | ERROR(w, status, err)
189 | return
190 | }
191 |
192 | JSON(w, http.StatusOK, map[string]interface{}{
193 | "success": true,
194 | })
195 | }
196 |
--------------------------------------------------------------------------------
/api/unit_inbox.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/json"
6 | "errors"
7 | "github.com/evilsocket/islazy/log"
8 | "github.com/go-chi/chi/v5"
9 | "github.com/jayofelony/pwngrid/crypto"
10 | "github.com/jayofelony/pwngrid/models"
11 | "io"
12 | "net/http"
13 | "strconv"
14 | "time"
15 | )
16 |
17 | var (
18 | ErrRecNotFound = errors.New("recipient not found")
19 | ErrMessageNotFound = errors.New("message not found")
20 | ErrInvalidKey = errors.New("invalid public key")
21 | ErrInvalidSignature = errors.New("can't verify signature")
22 | ErrDecoding = errors.New("error decoding data")
23 | )
24 |
25 | func (api *API) GetInbox(w http.ResponseWriter, r *http.Request) {
26 | unit := Authenticate(w, r)
27 | if unit == nil {
28 | return
29 | }
30 |
31 | page, err := pageNum(r)
32 | if err != nil {
33 | ERROR(w, http.StatusUnprocessableEntity, err)
34 | return
35 | }
36 |
37 | messages, total, pages := unit.GetPagedInbox(page)
38 | JSON(w, http.StatusOK, map[string]interface{}{
39 | "records": total,
40 | "pages": pages,
41 | "messages": messages,
42 | })
43 | }
44 |
45 | // we need this because the models.Message structure doesn't not export data and signature for fast listing.
46 | type fullMessage struct {
47 | ID uint `json:"id"`
48 | CreatedAt time.Time `json:"created_at"`
49 | UpdatedAt time.Time `json:"updated_at"`
50 | DeletedAt *time.Time `json:"deleted_at"`
51 | SeenAt *time.Time `json:"seen_at"`
52 | Sender string `json:"sender"`
53 | SenderName string `json:"sender_name"`
54 | Data string `json:"data"`
55 | Signature string `json:"signature"`
56 | }
57 |
58 | func (api *API) GetInboxMessage(w http.ResponseWriter, r *http.Request) {
59 | unit := Authenticate(w, r)
60 | if unit == nil {
61 | return
62 | }
63 |
64 | msgIDParam := chi.URLParam(r, "msg_id")
65 | msgID, err := strconv.Atoi(msgIDParam)
66 | if err != nil {
67 | ERROR(w, http.StatusUnprocessableEntity, err)
68 | return
69 | } else if message := unit.GetInboxMessage(msgID); message == nil {
70 | ERROR(w, http.StatusNotFound, ErrMessageNotFound)
71 | return
72 | } else {
73 | JSON(w, http.StatusOK, fullMessage{
74 | ID: message.ID,
75 | CreatedAt: message.CreatedAt,
76 | UpdatedAt: message.UpdatedAt,
77 | DeletedAt: message.DeletedAt,
78 | SeenAt: message.SeenAt,
79 | Sender: message.Sender,
80 | SenderName: message.SenderName,
81 | Data: message.Data,
82 | Signature: message.Signature,
83 | })
84 | }
85 | }
86 |
87 | func (api *API) MarkInboxMessage(w http.ResponseWriter, r *http.Request) {
88 | unit := Authenticate(w, r)
89 | if unit == nil {
90 | return
91 | }
92 |
93 | now := time.Now()
94 | markAs := chi.URLParam(r, "mark")
95 | msgIDParam := chi.URLParam(r, "msg_id")
96 | msgID, err := strconv.Atoi(msgIDParam)
97 |
98 | if err != nil {
99 | ERROR(w, http.StatusUnprocessableEntity, err)
100 | return
101 | } else if message := unit.GetInboxMessage(msgID); message == nil {
102 | ERROR(w, http.StatusNotFound, ErrMessageNotFound)
103 | return
104 | } else if markAs == "seen" {
105 | if err := models.UpdateFields(message, map[string]interface{}{"seen_at": &now}).Error; err != nil {
106 | ERROR(w, http.StatusUnprocessableEntity, err)
107 | return
108 | }
109 | } else if markAs == "unseen" {
110 | if err := models.UpdateFields(message, map[string]interface{}{"seen_at": nil}).Error; err != nil {
111 | ERROR(w, http.StatusUnprocessableEntity, err)
112 | return
113 | }
114 | } else if markAs == "deleted" {
115 | if err := models.UpdateFields(message, map[string]interface{}{"deleted_at": &now}).Error; err != nil {
116 | ERROR(w, http.StatusUnprocessableEntity, err)
117 | return
118 | }
119 | } else if markAs == "restored" {
120 | if err := models.UpdateFields(message, map[string]interface{}{"deleted_at": nil}).Error; err != nil {
121 | ERROR(w, http.StatusUnprocessableEntity, err)
122 | return
123 | }
124 | } else {
125 | ERROR(w, http.StatusNotFound, ErrEmpty)
126 | return
127 | }
128 |
129 | JSON(w, http.StatusOK, map[string]bool{
130 | "success": true,
131 | })
132 | }
133 |
134 | func (api *API) SendMessageTo(w http.ResponseWriter, r *http.Request) {
135 | // authenticate source unit
136 | srcUnit := Authenticate(w, r)
137 | if srcUnit == nil {
138 | return
139 | }
140 |
141 | // get dest unit by fingerprint
142 | dstUnitFingerprint := chi.URLParam(r, "fingerprint")
143 | dstUnit := models.FindUnitByFingerprint(dstUnitFingerprint)
144 | if dstUnit == nil {
145 | ERROR(w, http.StatusNotFound, ErrRecNotFound)
146 | return
147 | }
148 |
149 | // read the message and signature from the source unit
150 | client := clientIP(r)
151 | body, err := io.ReadAll(r.Body)
152 | if err != nil {
153 | ERROR(w, http.StatusUnprocessableEntity, err)
154 | return
155 | }
156 |
157 | var message Message
158 | if err = json.Unmarshal(body, &message); err != nil {
159 | log.Debug("error while decoding message from %s: %v", srcUnit.Identity(), err)
160 | log.Debug("%s", body)
161 | ERROR(w, http.StatusUnprocessableEntity, err)
162 | return
163 | }
164 |
165 | if err := models.ValidateMessage(message.Data, message.Signature); err != nil {
166 | log.Warning("client %s sent a broken message structure: %v", srcUnit.Identity(), err)
167 | ERROR(w, http.StatusUnprocessableEntity, err)
168 | return
169 | }
170 |
171 | // parse source unit key
172 | srcKeys, err := crypto.FromPublicPEM(srcUnit.PublicKey)
173 | if err != nil {
174 | log.Warning("error decoding key from %s: %v", srcUnit.Identity(), err)
175 | log.Debug("%s", srcUnit.PublicKey)
176 | ERROR(w, http.StatusUnprocessableEntity, ErrInvalidKey)
177 | return
178 | }
179 |
180 | // decode data, signature and verify SIGN(SHA256(data))
181 | data, err := base64.StdEncoding.DecodeString(message.Data)
182 | if err != nil {
183 | log.Warning("error decoding message from %s: %v", srcUnit.Identity(), err)
184 | log.Debug("%s", message.Data)
185 | ERROR(w, http.StatusUnprocessableEntity, ErrDecoding)
186 | return
187 | }
188 |
189 | signature, err := base64.StdEncoding.DecodeString(message.Signature)
190 | if err != nil {
191 | log.Warning("error decoding signature from %s: %v", srcUnit.Identity(), err)
192 | log.Debug("%s", message.Signature)
193 | ERROR(w, http.StatusUnprocessableEntity, ErrDecoding)
194 | return
195 | }
196 |
197 | if err := srcKeys.VerifyMessage(data, signature); err != nil {
198 | log.Warning("error verifying signature from %s: %v", srcUnit.Identity(), err)
199 | log.Debug("%s", message.Signature)
200 | ERROR(w, http.StatusUnprocessableEntity, ErrInvalidSignature)
201 | return
202 | }
203 |
204 | msg := models.Message{
205 | SenderID: srcUnit.ID,
206 | Sender: srcUnit.Fingerprint,
207 | SenderName: srcUnit.Name,
208 | ReceiverID: dstUnit.ID,
209 | Data: message.Data,
210 | Signature: message.Signature,
211 | }
212 |
213 | if err := models.Create(&msg).Error; err != nil {
214 | log.Warning("error creating msg %v from %s: %v", msg, client, err)
215 | ERROR(w, http.StatusInternalServerError, ErrEmpty)
216 | return
217 | }
218 |
219 | JSON(w, http.StatusOK, map[string]interface{}{
220 | "success": true,
221 | })
222 | }
223 |
--------------------------------------------------------------------------------
/api/client.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "encoding/json"
7 | "fmt"
8 | "io"
9 | "net"
10 | "net/http"
11 | "os"
12 | "sync"
13 | "time"
14 |
15 | "github.com/evilsocket/islazy/log"
16 | "github.com/jayofelony/pwngrid/crypto"
17 | "github.com/jayofelony/pwngrid/models"
18 | "github.com/jayofelony/pwngrid/utils"
19 | )
20 |
21 | var (
22 | ClientTimeout = 60
23 | ClientKeepalive = 30
24 | ClientTokenFile = "/tmp/pwngrid-api-enrollment.json"
25 | Endpoint = ""
26 | )
27 |
28 | //const (
29 | // Endpoint
30 | //)
31 |
32 | type Client struct {
33 | sync.Mutex
34 |
35 | cli *http.Client
36 | keys *crypto.KeyPair
37 | token string
38 | tokenAt time.Time
39 | data map[string]interface{}
40 | hostname string
41 | }
42 |
43 | func NewClient(keys *crypto.KeyPair, endpoint string, hostname string) *Client {
44 |
45 | t := &http.Transport{
46 | Dial: (&net.Dialer{
47 | Timeout: time.Duration(ClientTimeout) * time.Second,
48 | KeepAlive: time.Duration(ClientKeepalive) * time.Second,
49 | }).Dial,
50 | TLSHandshakeTimeout: time.Duration(ClientTimeout) * time.Second,
51 | ResponseHeaderTimeout: time.Duration(ClientTimeout) * time.Second,
52 | ExpectContinueTimeout: 4 * time.Second,
53 | }
54 |
55 | cli := &Client{
56 | cli: &http.Client{
57 | Transport: t,
58 | Timeout: time.Duration(ClientTimeout) * time.Second,
59 | },
60 | keys: keys,
61 | data: make(map[string]interface{}),
62 | hostname: hostname,
63 | }
64 |
65 | Endpoint = endpoint
66 |
67 | if info, err := os.Stat(ClientTokenFile); err == nil {
68 | if time.Since(info.ModTime()) < models.TokenTTL {
69 | log.Debug("loading token from %s ...", ClientTokenFile)
70 | var data map[string]interface{}
71 | if raw, err := os.ReadFile(ClientTokenFile); err == nil {
72 | if err := json.Unmarshal(raw, &data); err == nil {
73 | cli.token = data["token"].(string)
74 | cli.tokenAt = info.ModTime()
75 | log.Debug("token: %s", cli.token)
76 | } else {
77 | log.Warning("error decoding %s: %v", ClientTokenFile, err)
78 | }
79 | } else {
80 | log.Warning("error reading %s: %v", ClientTokenFile, err)
81 | }
82 | } else {
83 | log.Debug("token in %s is expired", ClientTokenFile)
84 | }
85 | }
86 |
87 | return cli
88 | }
89 |
90 | func (c *Client) enroll() error {
91 |
92 | hostname := c.hostname
93 | if hostname == "" {
94 | hostname = utils.Hostname()
95 | }
96 | identity := fmt.Sprintf("%s@%s", hostname, c.keys.FingerprintHex)
97 |
98 | log.Debug("refreshing api token as %s ...", identity)
99 |
100 | signature, err := c.keys.SignMessage([]byte(identity))
101 | if err != nil {
102 | return err
103 | }
104 |
105 | signature64 := base64.StdEncoding.EncodeToString(signature)
106 | pubKeyPEM64 := base64.StdEncoding.EncodeToString(c.keys.PublicPEM)
107 |
108 | log.Debug("SIGN(%s) = %s", identity, signature64)
109 |
110 | enrollment := map[string]interface{}{
111 | "identity": identity,
112 | "public_key": pubKeyPEM64,
113 | "signature": signature64,
114 | "data": c.data,
115 | }
116 |
117 | obj, err := c.request("POST", "/unit/enroll", enrollment, false)
118 | if err != nil {
119 | return err
120 | }
121 |
122 | c.tokenAt = time.Now()
123 | c.token = obj["token"].(string)
124 | log.Debug("new token: %s", c.token)
125 |
126 | if raw, err := json.Marshal(obj); err == nil {
127 | log.Debug("saving token to %s ...", ClientTokenFile)
128 | if err = os.WriteFile(ClientTokenFile, raw, 0644); err != nil {
129 | log.Warning("error saving token to %s: %v", ClientTokenFile, err)
130 | }
131 | } else {
132 | log.Warning("error encoding token: %v", err)
133 | }
134 |
135 | return nil
136 | }
137 |
138 | func (c *Client) request(method string, path string, data interface{}, auth bool) (map[string]interface{}, error) {
139 | url := fmt.Sprintf("%s%s", Endpoint, path)
140 | err := (error)(nil)
141 | started := time.Now()
142 | defer func() {
143 | if err == nil {
144 | log.Debug("%s %s (%s)", method, url, time.Since(started))
145 | } else {
146 | log.Error("%s %s (%s) %v", method, url, time.Since(started), err)
147 | }
148 | }()
149 |
150 | buf := new(bytes.Buffer)
151 | if data != nil {
152 | if err = json.NewEncoder(buf).Encode(data); err != nil {
153 | return nil, err
154 | }
155 | }
156 |
157 | req, err := http.NewRequest(method, url, buf)
158 | if err != nil {
159 | return nil, err
160 | }
161 |
162 | if auth {
163 | if time.Since(c.tokenAt) >= models.TokenTTL {
164 | if err := c.enroll(); err != nil {
165 | return nil, err
166 | }
167 | }
168 | req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.token))
169 | }
170 |
171 | res, err := c.cli.Do(req)
172 | if err != nil {
173 | return nil, err
174 | }
175 | body, err := io.ReadAll(res.Body)
176 | if err != nil {
177 | return nil, err
178 | }
179 |
180 | var obj map[string]interface{}
181 | if err = json.Unmarshal(body, &obj); err != nil {
182 | log.Debug(fmt.Sprintf("Error Unmarshalling json body from request: %v", body))
183 | return nil, err
184 | }
185 |
186 | if res.StatusCode == 401 {
187 | if err := c.enroll(); err != nil {
188 | log.Warning("error token expired during operation: %v", err)
189 | return nil, err
190 | }
191 | log.Warning("token expired, re-enroll success")
192 | }
193 |
194 | if res.StatusCode != 200 {
195 | err = fmt.Errorf("%d %s", res.StatusCode, obj["error"])
196 | }
197 |
198 | return obj, err
199 | }
200 |
201 | func (c *Client) SetData(newData map[string]interface{}) map[string]interface{} {
202 | c.Lock()
203 | defer c.Unlock()
204 |
205 | for key, val := range newData {
206 | if val == nil {
207 | delete(c.data, key)
208 | } else {
209 | c.data[key] = val
210 | }
211 | }
212 |
213 | return c.data
214 | }
215 |
216 | func (c *Client) Data() map[string]interface{} {
217 | c.Lock()
218 | defer c.Unlock()
219 | return c.data
220 | }
221 |
222 | func (c *Client) Request(method string, path string, data interface{}, auth bool) (map[string]interface{}, error) {
223 | c.Lock()
224 | defer c.Unlock()
225 | return c.request(method, path, data, auth)
226 | }
227 |
228 | func (c *Client) Get(path string, auth bool) (map[string]interface{}, error) {
229 | return c.Request("GET", path, nil, auth)
230 | }
231 |
232 | func (c *Client) Post(path string, what interface{}, auth bool) (map[string]interface{}, error) {
233 | return c.Request("POST", path, what, auth)
234 | }
235 |
236 | func (c *Client) PagedUnits(page int) (map[string]interface{}, error) {
237 | return c.Get(fmt.Sprintf("/units/?p=%d", page), false)
238 | }
239 |
240 | func (c *Client) Unit(fingerprint string) (map[string]interface{}, error) {
241 | return c.Get(fmt.Sprintf("/unit/%s", fingerprint), false)
242 | }
243 |
244 | func (c *Client) ReportAP(report interface{}) (map[string]interface{}, error) {
245 | return c.Post("/unit/report/ap", report, true)
246 | }
247 |
248 | func (c *Client) Inbox(page int) (map[string]interface{}, error) {
249 | return c.Get(fmt.Sprintf("/unit/inbox/?p=%d", page), true)
250 | }
251 |
252 | func (c *Client) InboxMessage(id int) (map[string]interface{}, error) {
253 | return c.Get(fmt.Sprintf("/unit/inbox/%d", id), true)
254 | }
255 |
256 | func (c *Client) MarkInboxMessage(id int, mark string) (map[string]interface{}, error) {
257 | return c.Get(fmt.Sprintf("/unit/inbox/%d/%s", id, mark), true)
258 | }
259 |
260 | func (c *Client) SendMessageTo(fingerprint string, msg Message) error {
261 | _, err := c.Post(fmt.Sprintf("/unit/%s/inbox", fingerprint), msg, true)
262 | return err
263 | }
264 |
--------------------------------------------------------------------------------
/mesh/peer.go:
--------------------------------------------------------------------------------
1 | package mesh
2 |
3 | import (
4 | "bytes"
5 | "crypto/rand"
6 | "encoding/base64"
7 | "encoding/binary"
8 | "encoding/json"
9 | "fmt"
10 | "net"
11 | "reflect"
12 | "regexp"
13 | "strings"
14 | "sync"
15 | "time"
16 |
17 | "github.com/evilsocket/islazy/log"
18 | "github.com/gopacket/gopacket/layers"
19 | "github.com/jayofelony/pwngrid/crypto"
20 | "github.com/jayofelony/pwngrid/version"
21 | "github.com/jayofelony/pwngrid/wifi"
22 | )
23 |
24 | var (
25 | SignalingPeriod = 300
26 |
27 | fingValidator = regexp.MustCompile("^[a-fA-F0-9]{64}$")
28 | )
29 |
30 | type SessionID []byte
31 |
32 | type Peer struct {
33 | sync.Mutex
34 |
35 | MetAt time.Time // first time met
36 | DetectedAt time.Time // first time detected on this session
37 | SeenAt time.Time // last time detected on this session
38 | PrevSeenAt time.Time // if we met this unit before, this is the last time it's been seen
39 | Encounters uint64
40 | Channel int
41 | RSSI int
42 | SessionID SessionID
43 | SessionIDStr string
44 | Keys *crypto.KeyPair
45 | AdvData sync.Map
46 | AdvPeriod int
47 |
48 | advEnabled bool
49 | ForceDisabled bool
50 |
51 | mux *PacketMuxer
52 | stop chan struct{}
53 | }
54 |
55 | func MakeLocalPeer(name string, keys *crypto.KeyPair, advertise bool) *Peer {
56 | now := time.Now()
57 | peer := &Peer{
58 | DetectedAt: now,
59 | SeenAt: now,
60 | PrevSeenAt: now,
61 | SessionID: make([]byte, 6),
62 | Keys: keys,
63 | AdvData: sync.Map{},
64 | AdvPeriod: SignalingPeriod,
65 | stop: make(chan struct{}),
66 | advEnabled: false,
67 | ForceDisabled: false,
68 | }
69 | if !advertise {
70 | peer.ForceDisabled = true
71 | }
72 |
73 | if _, err := rand.Read(peer.SessionID); err != nil {
74 | panic(err)
75 | }
76 |
77 | parts := make([]string, 6)
78 | for idx, byte := range peer.SessionID {
79 | parts[idx] = fmt.Sprintf("%02x", byte)
80 | }
81 | peer.SessionIDStr = strings.Join(parts, ":")
82 |
83 | peer.AdvData.Store("name", name)
84 | peer.AdvData.Store("identity", keys.FingerprintHex)
85 | peer.AdvData.Store("session_id", peer.SessionIDStr)
86 | peer.AdvData.Store("grid_version", version.Version)
87 |
88 | peer.AdvData.Range(func(key, value interface{}) bool {
89 | log.Debug("local.adv.%s = %s", key, value)
90 | return true
91 | })
92 |
93 | return peer
94 | }
95 |
96 | func (peer *Peer) Advertise(enabled bool) {
97 | peer.Lock()
98 | defer peer.Unlock()
99 | diff := peer.advEnabled != enabled
100 | peer.advEnabled = enabled
101 | if diff {
102 | if enabled {
103 | log.Info("peer advertisement enabled")
104 | } else {
105 | log.Info("peer advertisement disabled")
106 | }
107 | }
108 | }
109 |
110 | func NewPeer(radiotap *layers.RadioTap, dot11 *layers.Dot11, adv map[string]interface{}) (peer *Peer, err error) {
111 | now := time.Now()
112 | peer = &Peer{
113 | DetectedAt: now,
114 | SeenAt: now,
115 | PrevSeenAt: now,
116 | Channel: wifi.Freq2Chan(channelFromRadioTap(radiotap)),
117 | RSSI: dbmFromRadioTap(radiotap),
118 | SessionID: SessionID(dot11.Address3),
119 | AdvData: sync.Map{},
120 | }
121 |
122 | parts := make([]string, 6)
123 | for idx, byte := range peer.SessionID {
124 | parts[idx] = fmt.Sprintf("%02x", byte)
125 | }
126 | peer.SessionIDStr = strings.Join(parts, ":")
127 |
128 | // parse the fingerprint, the signature and the public key
129 | fingerprint, found := adv["identity"].(string)
130 | if !found {
131 | return nil, fmt.Errorf("peer %x is not advertising any identity", peer.SessionID)
132 | } else if !fingValidator.MatchString(fingerprint) {
133 | return nil, fmt.Errorf("peer %x is advertising an invalid fingerprint: %s", peer.SessionID, fingerprint)
134 | }
135 |
136 | if pubKey64, found := adv["public_key"]; found {
137 | pubKey, err := base64.StdEncoding.DecodeString(pubKey64.(string))
138 | if err != nil {
139 | return nil, fmt.Errorf("error decoding peer %s public key: %s", fingerprint, err)
140 | }
141 |
142 | peer.Keys, err = crypto.FromPublicPEM(string(pubKey))
143 | if err != nil {
144 | return nil, fmt.Errorf("error parsing peer %s public key: %s", fingerprint, err)
145 | }
146 |
147 | // basic consistency check
148 | if peer.Keys.FingerprintHex != fingerprint {
149 | return nil, fmt.Errorf("peer %x is advertising fingerprint %s, but it should be %s", peer.SessionID, fingerprint, peer.Keys.FingerprintHex)
150 | }
151 | } else if !found {
152 | log.Debug("peer %s is not advertising any public key", fingerprint)
153 | }
154 |
155 | for key, value := range adv {
156 | peer.AdvData.Store(key, value)
157 | }
158 |
159 | return peer, nil
160 | }
161 |
162 | func (peer *Peer) Update(radio *layers.RadioTap, dot11 *layers.Dot11, adv map[string]interface{}) (err error) {
163 | peer.Lock()
164 | defer peer.Unlock()
165 |
166 | // parse the fingerprint, the signature and the public key
167 | fingerprint, found := adv["identity"].(string)
168 | if !found {
169 | return fmt.Errorf("peer %x is not advertising any identity", peer.SessionID)
170 | }
171 |
172 | // basic consistency check
173 | if peer.Keys != nil && peer.Keys.FingerprintHex != fingerprint {
174 | return fmt.Errorf("peer %x is advertising fingerprint %s, but it should be %s", peer.SessionID, fingerprint, peer.Keys.FingerprintHex)
175 | }
176 |
177 | /*
178 | No need for signature in the advertisement protocol, however:
179 |
180 | signature64, found := adv["signature"].(string)
181 | if !found {
182 | return fmt.Errorf("peer %x is not advertising any signature", peer.SessionID)
183 | }
184 |
185 | signature, err := base64.StdEncoding.DecodeString(signature64)
186 | if err != nil {
187 | return fmt.Errorf("error decoding peer %d signature: %s", peer.SessionID, err)
188 | }
189 |
190 | // the signature is SIGN(advertisement), so we need to remove the signature field and convert back to json.
191 | // NOTE: fortunately, keys will always be sorted, so we don't have to do anything in order to guarantee signature
192 | // consistency (https://stackoverflow.com/questions/18668652/how-to-produce-json-with-sorted-keys-in-go)
193 | signedMap := adv
194 | delete(signedMap, "signature")
195 |
196 | signedData, err := json.Marshal(signedMap)
197 | if err != nil {
198 | return fmt.Errorf("error packing data for signature verification: %v", err)
199 | }
200 |
201 | // verify the signature
202 | if err = peer.Keys.VerifyMessage(signedData, signature); err != nil {
203 | return fmt.Errorf("peer %x signature is invalid", peer.SessionID)
204 | }
205 | */
206 |
207 | peer.Channel = wifi.Freq2Chan(channelFromRadioTap(radio))
208 | peer.RSSI = dbmFromRadioTap(radio)
209 |
210 | if !bytes.Equal(peer.SessionID, dot11.Address3) {
211 | log.Info("peer %s changed session id: %x -> %x", peer.ID(), peer.SessionIDStr, dot11.Address3)
212 | copy(peer.SessionID, dot11.Address3)
213 | parts := make([]string, 6)
214 | for idx, byte := range peer.SessionID {
215 | parts[idx] = fmt.Sprintf("%02x", byte)
216 | }
217 | peer.SessionIDStr = strings.Join(parts, ":")
218 | }
219 |
220 | for key, value := range adv {
221 | peer.AdvData.Store(key, value)
222 | }
223 |
224 | return nil
225 | }
226 |
227 | func (peer *Peer) ID() string {
228 | name, _ := peer.AdvData.Load("name")
229 | ident := "???"
230 |
231 | if peer.Keys != nil {
232 | ident = peer.Keys.FingerprintHex
233 | } else if _ident, found := peer.AdvData.Load("identity"); found {
234 | ident = _ident.(string)
235 | }
236 |
237 | return fmt.Sprintf("%s@%s", name, ident)
238 | }
239 |
240 | func (peer *Peer) InactiveFor() float64 {
241 | peer.Lock()
242 | defer peer.Unlock()
243 | return time.Since(peer.DetectedAt).Seconds()
244 | }
245 |
246 | func (peer *Peer) SetData(adv map[string]interface{}) {
247 | if peer == nil {
248 | return
249 | }
250 | peer.Lock()
251 | defer peer.Unlock()
252 |
253 | for key, val := range adv {
254 | if val == nil {
255 | peer.AdvData.Delete(key)
256 | } else {
257 | peer.AdvData.Store(key, val)
258 | }
259 | }
260 | }
261 |
262 | func (peer *Peer) Data() map[string]interface{} {
263 | peer.Lock()
264 | defer peer.Unlock()
265 | return peer.dataFrame()
266 | }
267 |
268 | func (peer *Peer) dataFrame() map[string]interface{} {
269 | data := map[string]interface{}{}
270 | peer.AdvData.Range(func(key, value interface{}) bool {
271 | data[key.(string)] = value
272 | return true
273 | })
274 | return data
275 | }
276 |
277 | func (peer *Peer) advertise() {
278 | peer.Lock()
279 | defer peer.Unlock()
280 |
281 | if peer.advEnabled {
282 | data := peer.dataFrame()
283 |
284 | data["timestamp"] = time.Now().Unix()
285 | adv, err := json.Marshal(data)
286 | if err != nil {
287 | log.Error("could not serialize advertisement data: %v", err)
288 | return
289 | }
290 |
291 | /*
292 | No need for signature in the advertisement protocol, however:
293 |
294 | // sign the advertisement
295 | signature, err := peer.Keys.SignMessage(adv)
296 | if err != nil {
297 | log.Error("error signing advertisement: %v", err)
298 | return
299 | }
300 |
301 | // add the signature to the advertisement itself and encode again
302 | data["signature"] = base64.StdEncoding.EncodeToString(signature)
303 | adv, err = json.Marshal(data)
304 | if err != nil {
305 | log.Error("could not serialize signed advertisement data: %v", err)
306 | return
307 | }
308 |
309 | log.Debug("advertising:\n%+v", data)
310 | */
311 |
312 | err, raw := wifi.Pack(
313 | net.HardwareAddr(peer.SessionID),
314 | wifi.BroadcastAddr,
315 | adv,
316 | false) // set compression to true if using signature
317 | if err != nil {
318 | log.Error("could not encapsulate %d bytes of advertisement data: %v", len(adv), err)
319 | return
320 | }
321 |
322 | if err = peer.mux.Write(raw); err != nil {
323 | log.Error("error sending %d bytes of advertisement frame: %v", len(raw), err)
324 | }
325 | }
326 | }
327 |
328 | func (peer *Peer) StartAdvertising(iface string) (err error) {
329 | if peer.mux == nil {
330 | if peer.mux, err = NewPacketMuxer(iface, "", Workers); err != nil {
331 | return
332 | }
333 | }
334 |
335 | go func() {
336 | period := time.Duration(peer.AdvPeriod) * time.Millisecond
337 | ticker := time.NewTicker(period)
338 |
339 | log.Debug("advertiser started with a %s period", period)
340 |
341 | for {
342 | select {
343 | case _ = <-ticker.C:
344 | peer.advertise()
345 | case <-peer.stop:
346 | log.Info("advertiser stopped")
347 | return
348 | }
349 | }
350 | }()
351 |
352 | return nil
353 | }
354 |
355 | func (peer *Peer) StopAdvertising() {
356 | log.Debug("stopping advertiser ...")
357 | peer.stop <- struct{}{}
358 | }
359 |
360 | // helper: extract channel frequency (MHz) from RadioTap
361 | func channelFromRadioTap(r *layers.RadioTap) int {
362 | if r == nil {
363 | return 0
364 | }
365 |
366 | // try direct exported field via reflection (works across gopacket versions)
367 | rv := reflect.ValueOf(r)
368 | if rv.Kind() == reflect.Ptr && !rv.IsNil() {
369 | rv = rv.Elem()
370 | if rv.IsValid() && rv.Kind() == reflect.Struct {
371 | if f := rv.FieldByName("ChannelFrequency"); f.IsValid() {
372 | switch f.Kind() {
373 | case reflect.Uint16, reflect.Uint32, reflect.Uint64:
374 | return int(f.Uint())
375 | case reflect.Int, reflect.Int32, reflect.Int64:
376 | return int(f.Int())
377 | }
378 | }
379 | }
380 | }
381 |
382 | // fallback: scan RadioTapValues for a 2-byte little-endian frequency
383 | for _, ns := range r.RadioTapValues {
384 | sv := reflect.ValueOf(ns)
385 | if sv.Kind() == reflect.Struct {
386 | for _, name := range []string{"Data", "Value"} {
387 | if f := sv.FieldByName(name); f.IsValid() && f.Kind() == reflect.Slice && f.Type().Elem().Kind() == reflect.Uint8 {
388 | b := f.Bytes()
389 | if len(b) >= 2 {
390 | freq := int(binary.LittleEndian.Uint16(b[:2]))
391 | if freq >= 2000 && freq <= 6000 { // plausible WiFi freq
392 | return freq
393 | }
394 | }
395 | }
396 | }
397 | }
398 | }
399 |
400 | return 0
401 | }
402 |
403 | // helper: extract dBm antenna signal (RSSI) from RadioTap
404 | func dbmFromRadioTap(r *layers.RadioTap) int {
405 | if r == nil {
406 | return 0
407 | }
408 |
409 | // try direct exported field via reflection
410 | rv := reflect.ValueOf(r)
411 | if rv.Kind() == reflect.Ptr && !rv.IsNil() {
412 | rv = rv.Elem()
413 | if rv.IsValid() && rv.Kind() == reflect.Struct {
414 | if f := rv.FieldByName("DBMAntennaSignal"); f.IsValid() {
415 | switch f.Kind() {
416 | case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int:
417 | return int(f.Int())
418 | case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
419 | u := f.Uint()
420 | if u <= 0xFF {
421 | return int(int8(uint8(u)))
422 | }
423 | return int(u)
424 | }
425 | }
426 | }
427 | }
428 |
429 | // fallback: scan RadioTapValues for a 1-byte RSSI-like value (-150..0)
430 | for _, ns := range r.RadioTapValues {
431 | sv := reflect.ValueOf(ns)
432 | if sv.Kind() == reflect.Struct {
433 | for _, name := range []string{"Data", "Value"} {
434 | if f := sv.FieldByName(name); f.IsValid() && f.Kind() == reflect.Slice && f.Type().Elem().Kind() == reflect.Uint8 {
435 | b := f.Bytes()
436 | if len(b) >= 1 {
437 | rssi := int(int8(b[0]))
438 | if rssi <= 0 && rssi >= -150 {
439 | return rssi
440 | }
441 | }
442 | }
443 | }
444 | }
445 | }
446 |
447 | return 0
448 | }
449 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | ==========================
3 |
4 | Version 3, 29 June 2007
5 |
6 | Copyright © 2007 Free Software Foundation, Inc. <>
7 |
8 | Everyone is permitted to copy and distribute verbatim copies of this license
9 | document, but changing it is not allowed.
10 |
11 | ## Preamble
12 |
13 | The GNU General Public License is a free, copyleft license for software and other
14 | kinds of works.
15 |
16 | The licenses for most software and other practical works are designed to take away
17 | your freedom to share and change the works. By contrast, the GNU General Public
18 | License is intended to guarantee your freedom to share and change all versions of a
19 | program--to make sure it remains free software for all its users. We, the Free
20 | Software Foundation, use the GNU General Public License for most of our software; it
21 | applies also to any other work released this way by its authors. You can apply it to
22 | your programs, too.
23 |
24 | When we speak of free software, we are referring to freedom, not price. Our General
25 | Public Licenses are designed to make sure that you have the freedom to distribute
26 | copies of free software (and charge for them if you wish), that you receive source
27 | code or can get it if you want it, that you can change the software or use pieces of
28 | it in new free programs, and that you know you can do these things.
29 |
30 | To protect your rights, we need to prevent others from denying you these rights or
31 | asking you to surrender the rights. Therefore, you have certain responsibilities if
32 | you distribute copies of the software, or if you modify it: responsibilities to
33 | respect the freedom of others.
34 |
35 | For example, if you distribute copies of such a program, whether gratis or for a fee,
36 | you must pass on to the recipients the same freedoms that you received. You must make
37 | sure that they, too, receive or can get the source code. And you must show them these
38 | terms so they know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps: (1) assert
41 | copyright on the software, and (2) offer you this License giving you legal permission
42 | to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains that there is
45 | no warranty for this free software. For both users' and authors' sake, the GPL
46 | requires that modified versions be marked as changed, so that their problems will not
47 | be attributed erroneously to authors of previous versions.
48 |
49 | Some devices are designed to deny users access to install or run modified versions of
50 | the software inside them, although the manufacturer can do so. This is fundamentally
51 | incompatible with the aim of protecting users' freedom to change the software. The
52 | systematic pattern of such abuse occurs in the area of products for individuals to
53 | use, which is precisely where it is most unacceptable. Therefore, we have designed
54 | this version of the GPL to prohibit the practice for those products. If such problems
55 | arise substantially in other domains, we stand ready to extend this provision to
56 | those domains in future versions of the GPL, as needed to protect the freedom of
57 | users.
58 |
59 | Finally, every program is threatened constantly by software patents. States should
60 | not allow patents to restrict development and use of software on general-purpose
61 | computers, but in those that do, we wish to avoid the special danger that patents
62 | applied to a free program could make it effectively proprietary. To prevent this, the
63 | GPL assures that patents cannot be used to render the program non-free.
64 |
65 | The precise terms and conditions for copying, distribution and modification follow.
66 |
67 | ## TERMS AND CONDITIONS
68 |
69 | ### 0. Definitions.
70 |
71 | “This License” refers to version 3 of the GNU General Public License.
72 |
73 | “Copyright” also means copyright-like laws that apply to other kinds of
74 | works, such as semiconductor masks.
75 |
76 | “The Program” refers to any copyrightable work licensed under this
77 | License. Each licensee is addressed as “you”. “Licensees” and
78 | “recipients” may be individuals or organizations.
79 |
80 | To “modify” a work means to copy from or adapt all or part of the work in
81 | a fashion requiring copyright permission, other than the making of an exact copy. The
82 | resulting work is called a “modified version” of the earlier work or a
83 | work “based on” the earlier work.
84 |
85 | A “covered work” means either the unmodified Program or a work based on
86 | the Program.
87 |
88 | To “propagate” a work means to do anything with it that, without
89 | permission, would make you directly or secondarily liable for infringement under
90 | applicable copyright law, except executing it on a computer or modifying a private
91 | copy. Propagation includes copying, distribution (with or without modification),
92 | making available to the public, and in some countries other activities as well.
93 |
94 | To “convey” a work means any kind of propagation that enables other
95 | parties to make or receive copies. Mere interaction with a user through a computer
96 | network, with no transfer of a copy, is not conveying.
97 |
98 | An interactive user interface displays “Appropriate Legal Notices” to the
99 | extent that it includes a convenient and prominently visible feature that (1)
100 | displays an appropriate copyright notice, and (2) tells the user that there is no
101 | warranty for the work (except to the extent that warranties are provided), that
102 | licensees may convey the work under this License, and how to view a copy of this
103 | License. If the interface presents a list of user commands or options, such as a
104 | menu, a prominent item in the list meets this criterion.
105 |
106 | ### 1. Source Code.
107 |
108 | The “source code” for a work means the preferred form of the work for
109 | making modifications to it. “Object code” means any non-source form of a
110 | work.
111 |
112 | A “Standard Interface” means an interface that either is an official
113 | standard defined by a recognized standards body, or, in the case of interfaces
114 | specified for a particular programming language, one that is widely used among
115 | developers working in that language.
116 |
117 | The “System Libraries” of an executable work include anything, other than
118 | the work as a whole, that (a) is included in the normal form of packaging a Major
119 | Component, but which is not part of that Major Component, and (b) serves only to
120 | enable use of the work with that Major Component, or to implement a Standard
121 | Interface for which an implementation is available to the public in source code form.
122 | A “Major Component”, in this context, means a major essential component
123 | (kernel, window system, and so on) of the specific operating system (if any) on which
124 | the executable work runs, or a compiler used to produce the work, or an object code
125 | interpreter used to run it.
126 |
127 | The “Corresponding Source” for a work in object code form means all the
128 | source code needed to generate, install, and (for an executable work) run the object
129 | code and to modify the work, including scripts to control those activities. However,
130 | it does not include the work's System Libraries, or general-purpose tools or
131 | generally available free programs which are used unmodified in performing those
132 | activities but which are not part of the work. For example, Corresponding Source
133 | includes interface definition files associated with source files for the work, and
134 | the source code for shared libraries and dynamically linked subprograms that the work
135 | is specifically designed to require, such as by intimate data communication or
136 | control flow between those subprograms and other parts of the work.
137 |
138 | The Corresponding Source need not include anything that users can regenerate
139 | automatically from other parts of the Corresponding Source.
140 |
141 | The Corresponding Source for a work in source code form is that same work.
142 |
143 | ### 2. Basic Permissions.
144 |
145 | All rights granted under this License are granted for the term of copyright on the
146 | Program, and are irrevocable provided the stated conditions are met. This License
147 | explicitly affirms your unlimited permission to run the unmodified Program. The
148 | output from running a covered work is covered by this License only if the output,
149 | given its content, constitutes a covered work. This License acknowledges your rights
150 | of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not convey, without
153 | conditions so long as your license otherwise remains in force. You may convey covered
154 | works to others for the sole purpose of having them make modifications exclusively
155 | for you, or provide you with facilities for running those works, provided that you
156 | comply with the terms of this License in conveying all material for which you do not
157 | control copyright. Those thus making or running the covered works for you must do so
158 | exclusively on your behalf, under your direction and control, on terms that prohibit
159 | them from making any copies of your copyrighted material outside their relationship
160 | with you.
161 |
162 | Conveying under any other circumstances is permitted solely under the conditions
163 | stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
164 |
165 | ### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
166 |
167 | No covered work shall be deemed part of an effective technological measure under any
168 | applicable law fulfilling obligations under article 11 of the WIPO copyright treaty
169 | adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention
170 | of such measures.
171 |
172 | When you convey a covered work, you waive any legal power to forbid circumvention of
173 | technological measures to the extent such circumvention is effected by exercising
174 | rights under this License with respect to the covered work, and you disclaim any
175 | intention to limit operation or modification of the work as a means of enforcing,
176 | against the work's users, your or third parties' legal rights to forbid circumvention
177 | of technological measures.
178 |
179 | ### 4. Conveying Verbatim Copies.
180 |
181 | You may convey verbatim copies of the Program's source code as you receive it, in any
182 | medium, provided that you conspicuously and appropriately publish on each copy an
183 | appropriate copyright notice; keep intact all notices stating that this License and
184 | any non-permissive terms added in accord with section 7 apply to the code; keep
185 | intact all notices of the absence of any warranty; and give all recipients a copy of
186 | this License along with the Program.
187 |
188 | You may charge any price or no price for each copy that you convey, and you may offer
189 | support or warranty protection for a fee.
190 |
191 | ### 5. Conveying Modified Source Versions.
192 |
193 | You may convey a work based on the Program, or the modifications to produce it from
194 | the Program, in the form of source code under the terms of section 4, provided that
195 | you also meet all of these conditions:
196 |
197 | * **a)** The work must carry prominent notices stating that you modified it, and giving a
198 | relevant date.
199 | * **b)** The work must carry prominent notices stating that it is released under this
200 | License and any conditions added under section 7. This requirement modifies the
201 | requirement in section 4 to “keep intact all notices”.
202 | * **c)** You must license the entire work, as a whole, under this License to anyone who
203 | comes into possession of a copy. This License will therefore apply, along with any
204 | applicable section 7 additional terms, to the whole of the work, and all its parts,
205 | regardless of how they are packaged. This License gives no permission to license the
206 | work in any other way, but it does not invalidate such permission if you have
207 | separately received it.
208 | * **d)** If the work has interactive user interfaces, each must display Appropriate Legal
209 | Notices; however, if the Program has interactive interfaces that do not display
210 | Appropriate Legal Notices, your work need not make them do so.
211 |
212 | A compilation of a covered work with other separate and independent works, which are
213 | not by their nature extensions of the covered work, and which are not combined with
214 | it such as to form a larger program, in or on a volume of a storage or distribution
215 | medium, is called an “aggregate” if the compilation and its resulting
216 | copyright are not used to limit the access or legal rights of the compilation's users
217 | beyond what the individual works permit. Inclusion of a covered work in an aggregate
218 | does not cause this License to apply to the other parts of the aggregate.
219 |
220 | ### 6. Conveying Non-Source Forms.
221 |
222 | You may convey a covered work in object code form under the terms of sections 4 and
223 | 5, provided that you also convey the machine-readable Corresponding Source under the
224 | terms of this License, in one of these ways:
225 |
226 | * **a)** Convey the object code in, or embodied in, a physical product (including a
227 | physical distribution medium), accompanied by the Corresponding Source fixed on a
228 | durable physical medium customarily used for software interchange.
229 | * **b)** Convey the object code in, or embodied in, a physical product (including a
230 | physical distribution medium), accompanied by a written offer, valid for at least
231 | three years and valid for as long as you offer spare parts or customer support for
232 | that product model, to give anyone who possesses the object code either (1) a copy of
233 | the Corresponding Source for all the software in the product that is covered by this
234 | License, on a durable physical medium customarily used for software interchange, for
235 | a price no more than your reasonable cost of physically performing this conveying of
236 | source, or (2) access to copy the Corresponding Source from a network server at no
237 | charge.
238 | * **c)** Convey individual copies of the object code with a copy of the written offer to
239 | provide the Corresponding Source. This alternative is allowed only occasionally and
240 | noncommercially, and only if you received the object code with such an offer, in
241 | accord with subsection 6b.
242 | * **d)** Convey the object code by offering access from a designated place (gratis or for
243 | a charge), and offer equivalent access to the Corresponding Source in the same way
244 | through the same place at no further charge. You need not require recipients to copy
245 | the Corresponding Source along with the object code. If the place to copy the object
246 | code is a network server, the Corresponding Source may be on a different server
247 | (operated by you or a third party) that supports equivalent copying facilities,
248 | provided you maintain clear directions next to the object code saying where to find
249 | the Corresponding Source. Regardless of what server hosts the Corresponding Source,
250 | you remain obligated to ensure that it is available for as long as needed to satisfy
251 | these requirements.
252 | * **e)** Convey the object code using peer-to-peer transmission, provided you inform
253 | other peers where the object code and Corresponding Source of the work are being
254 | offered to the general public at no charge under subsection 6d.
255 |
256 | A separable portion of the object code, whose source code is excluded from the
257 | Corresponding Source as a System Library, need not be included in conveying the
258 | object code work.
259 |
260 | A “User Product” is either (1) a “consumer product”, which
261 | means any tangible personal property which is normally used for personal, family, or
262 | household purposes, or (2) anything designed or sold for incorporation into a
263 | dwelling. In determining whether a product is a consumer product, doubtful cases
264 | shall be resolved in favor of coverage. For a particular product received by a
265 | particular user, “normally used” refers to a typical or common use of
266 | that class of product, regardless of the status of the particular user or of the way
267 | in which the particular user actually uses, or expects or is expected to use, the
268 | product. A product is a consumer product regardless of whether the product has
269 | substantial commercial, industrial or non-consumer uses, unless such uses represent
270 | the only significant mode of use of the product.
271 |
272 | “Installation Information” for a User Product means any methods,
273 | procedures, authorization keys, or other information required to install and execute
274 | modified versions of a covered work in that User Product from a modified version of
275 | its Corresponding Source. The information must suffice to ensure that the continued
276 | functioning of the modified object code is in no case prevented or interfered with
277 | solely because modification has been made.
278 |
279 | If you convey an object code work under this section in, or with, or specifically for
280 | use in, a User Product, and the conveying occurs as part of a transaction in which
281 | the right of possession and use of the User Product is transferred to the recipient
282 | in perpetuity or for a fixed term (regardless of how the transaction is
283 | characterized), the Corresponding Source conveyed under this section must be
284 | accompanied by the Installation Information. But this requirement does not apply if
285 | neither you nor any third party retains the ability to install modified object code
286 | on the User Product (for example, the work has been installed in ROM).
287 |
288 | The requirement to provide Installation Information does not include a requirement to
289 | continue to provide support service, warranty, or updates for a work that has been
290 | modified or installed by the recipient, or for the User Product in which it has been
291 | modified or installed. Access to a network may be denied when the modification itself
292 | materially and adversely affects the operation of the network or violates the rules
293 | and protocols for communication across the network.
294 |
295 | Corresponding Source conveyed, and Installation Information provided, in accord with
296 | this section must be in a format that is publicly documented (and with an
297 | implementation available to the public in source code form), and must require no
298 | special password or key for unpacking, reading or copying.
299 |
300 | ### 7. Additional Terms.
301 |
302 | “Additional permissions” are terms that supplement the terms of this
303 | License by making exceptions from one or more of its conditions. Additional
304 | permissions that are applicable to the entire Program shall be treated as though they
305 | were included in this License, to the extent that they are valid under applicable
306 | law. If additional permissions apply only to part of the Program, that part may be
307 | used separately under those permissions, but the entire Program remains governed by
308 | this License without regard to the additional permissions.
309 |
310 | When you convey a copy of a covered work, you may at your option remove any
311 | additional permissions from that copy, or from any part of it. (Additional
312 | permissions may be written to require their own removal in certain cases when you
313 | modify the work.) You may place additional permissions on material, added by you to a
314 | covered work, for which you have or can give appropriate copyright permission.
315 |
316 | Notwithstanding any other provision of this License, for material you add to a
317 | covered work, you may (if authorized by the copyright holders of that material)
318 | supplement the terms of this License with terms:
319 |
320 | * **a)** Disclaiming warranty or limiting liability differently from the terms of
321 | sections 15 and 16 of this License; or
322 | * **b)** Requiring preservation of specified reasonable legal notices or author
323 | attributions in that material or in the Appropriate Legal Notices displayed by works
324 | containing it; or
325 | * **c)** Prohibiting misrepresentation of the origin of that material, or requiring that
326 | modified versions of such material be marked in reasonable ways as different from the
327 | original version; or
328 | * **d)** Limiting the use for publicity purposes of names of licensors or authors of the
329 | material; or
330 | * **e)** Declining to grant rights under trademark law for use of some trade names,
331 | trademarks, or service marks; or
332 | * **f)** Requiring indemnification of licensors and authors of that material by anyone
333 | who conveys the material (or modified versions of it) with contractual assumptions of
334 | liability to the recipient, for any liability that these contractual assumptions
335 | directly impose on those licensors and authors.
336 |
337 | All other non-permissive additional terms are considered “further
338 | restrictions” within the meaning of section 10. If the Program as you received
339 | it, or any part of it, contains a notice stating that it is governed by this License
340 | along with a term that is a further restriction, you may remove that term. If a
341 | license document contains a further restriction but permits relicensing or conveying
342 | under this License, you may add to a covered work material governed by the terms of
343 | that license document, provided that the further restriction does not survive such
344 | relicensing or conveying.
345 |
346 | If you add terms to a covered work in accord with this section, you must place, in
347 | the relevant source files, a statement of the additional terms that apply to those
348 | files, or a notice indicating where to find the applicable terms.
349 |
350 | Additional terms, permissive or non-permissive, may be stated in the form of a
351 | separately written license, or stated as exceptions; the above requirements apply
352 | either way.
353 |
354 | ### 8. Termination.
355 |
356 | You may not propagate or modify a covered work except as expressly provided under
357 | this License. Any attempt otherwise to propagate or modify it is void, and will
358 | automatically terminate your rights under this License (including any patent licenses
359 | granted under the third paragraph of section 11).
360 |
361 | However, if you cease all violation of this License, then your license from a
362 | particular copyright holder is reinstated (a) provisionally, unless and until the
363 | copyright holder explicitly and finally terminates your license, and (b) permanently,
364 | if the copyright holder fails to notify you of the violation by some reasonable means
365 | prior to 60 days after the cessation.
366 |
367 | Moreover, your license from a particular copyright holder is reinstated permanently
368 | if the copyright holder notifies you of the violation by some reasonable means, this
369 | is the first time you have received notice of violation of this License (for any
370 | work) from that copyright holder, and you cure the violation prior to 30 days after
371 | your receipt of the notice.
372 |
373 | Termination of your rights under this section does not terminate the licenses of
374 | parties who have received copies or rights from you under this License. If your
375 | rights have been terminated and not permanently reinstated, you do not qualify to
376 | receive new licenses for the same material under section 10.
377 |
378 | ### 9. Acceptance Not Required for Having Copies.
379 |
380 | You are not required to accept this License in order to receive or run a copy of the
381 | Program. Ancillary propagation of a covered work occurring solely as a consequence of
382 | using peer-to-peer transmission to receive a copy likewise does not require
383 | acceptance. However, nothing other than this License grants you permission to
384 | propagate or modify any covered work. These actions infringe copyright if you do not
385 | accept this License. Therefore, by modifying or propagating a covered work, you
386 | indicate your acceptance of this License to do so.
387 |
388 | ### 10. Automatic Licensing of Downstream Recipients.
389 |
390 | Each time you convey a covered work, the recipient automatically receives a license
391 | from the original licensors, to run, modify and propagate that work, subject to this
392 | License. You are not responsible for enforcing compliance by third parties with this
393 | License.
394 |
395 | An “entity transaction” is a transaction transferring control of an
396 | organization, or substantially all assets of one, or subdividing an organization, or
397 | merging organizations. If propagation of a covered work results from an entity
398 | transaction, each party to that transaction who receives a copy of the work also
399 | receives whatever licenses to the work the party's predecessor in interest had or
400 | could give under the previous paragraph, plus a right to possession of the
401 | Corresponding Source of the work from the predecessor in interest, if the predecessor
402 | has it or can get it with reasonable efforts.
403 |
404 | You may not impose any further restrictions on the exercise of the rights granted or
405 | affirmed under this License. For example, you may not impose a license fee, royalty,
406 | or other charge for exercise of rights granted under this License, and you may not
407 | initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging
408 | that any patent claim is infringed by making, using, selling, offering for sale, or
409 | importing the Program or any portion of it.
410 |
411 | ### 11. Patents.
412 |
413 | A “contributor” is a copyright holder who authorizes use under this
414 | License of the Program or a work on which the Program is based. The work thus
415 | licensed is called the contributor's “contributor version”.
416 |
417 | A contributor's “essential patent claims” are all patent claims owned or
418 | controlled by the contributor, whether already acquired or hereafter acquired, that
419 | would be infringed by some manner, permitted by this License, of making, using, or
420 | selling its contributor version, but do not include claims that would be infringed
421 | only as a consequence of further modification of the contributor version. For
422 | purposes of this definition, “control” includes the right to grant patent
423 | sublicenses in a manner consistent with the requirements of this License.
424 |
425 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent license
426 | under the contributor's essential patent claims, to make, use, sell, offer for sale,
427 | import and otherwise run, modify and propagate the contents of its contributor
428 | version.
429 |
430 | In the following three paragraphs, a “patent license” is any express
431 | agreement or commitment, however denominated, not to enforce a patent (such as an
432 | express permission to practice a patent or covenant not to sue for patent
433 | infringement). To “grant” such a patent license to a party means to make
434 | such an agreement or commitment not to enforce a patent against the party.
435 |
436 | If you convey a covered work, knowingly relying on a patent license, and the
437 | Corresponding Source of the work is not available for anyone to copy, free of charge
438 | and under the terms of this License, through a publicly available network server or
439 | other readily accessible means, then you must either (1) cause the Corresponding
440 | Source to be so available, or (2) arrange to deprive yourself of the benefit of the
441 | patent license for this particular work, or (3) arrange, in a manner consistent with
442 | the requirements of this License, to extend the patent license to downstream
443 | recipients. “Knowingly relying” means you have actual knowledge that, but
444 | for the patent license, your conveying the covered work in a country, or your
445 | recipient's use of the covered work in a country, would infringe one or more
446 | identifiable patents in that country that you have reason to believe are valid.
447 |
448 | If, pursuant to or in connection with a single transaction or arrangement, you
449 | convey, or propagate by procuring conveyance of, a covered work, and grant a patent
450 | license to some of the parties receiving the covered work authorizing them to use,
451 | propagate, modify or convey a specific copy of the covered work, then the patent
452 | license you grant is automatically extended to all recipients of the covered work and
453 | works based on it.
454 |
455 | A patent license is “discriminatory” if it does not include within the
456 | scope of its coverage, prohibits the exercise of, or is conditioned on the
457 | non-exercise of one or more of the rights that are specifically granted under this
458 | License. You may not convey a covered work if you are a party to an arrangement with
459 | a third party that is in the business of distributing software, under which you make
460 | payment to the third party based on the extent of your activity of conveying the
461 | work, and under which the third party grants, to any of the parties who would receive
462 | the covered work from you, a discriminatory patent license (a) in connection with
463 | copies of the covered work conveyed by you (or copies made from those copies), or (b)
464 | primarily for and in connection with specific products or compilations that contain
465 | the covered work, unless you entered into that arrangement, or that patent license
466 | was granted, prior to 28 March 2007.
467 |
468 | Nothing in this License shall be construed as excluding or limiting any implied
469 | license or other defenses to infringement that may otherwise be available to you
470 | under applicable patent law.
471 |
472 | ### 12. No Surrender of Others' Freedom.
473 |
474 | If conditions are imposed on you (whether by court order, agreement or otherwise)
475 | that contradict the conditions of this License, they do not excuse you from the
476 | conditions of this License. If you cannot convey a covered work so as to satisfy
477 | simultaneously your obligations under this License and any other pertinent
478 | obligations, then as a consequence you may not convey it at all. For example, if you
479 | agree to terms that obligate you to collect a royalty for further conveying from
480 | those to whom you convey the Program, the only way you could satisfy both those terms
481 | and this License would be to refrain entirely from conveying the Program.
482 |
483 | ### 13. Use with the GNU Affero General Public License.
484 |
485 | Notwithstanding any other provision of this License, you have permission to link or
486 | combine any covered work with a work licensed under version 3 of the GNU Affero
487 | General Public License into a single combined work, and to convey the resulting work.
488 | The terms of this License will continue to apply to the part which is the covered
489 | work, but the special requirements of the GNU Affero General Public License, section
490 | 13, concerning interaction through a network will apply to the combination as such.
491 |
492 | ### 14. Revised Versions of this License.
493 |
494 | The Free Software Foundation may publish revised and/or new versions of the GNU
495 | General Public License from time to time. Such new versions will be similar in spirit
496 | to the present version, but may differ in detail to address new problems or concerns.
497 |
498 | Each version is given a distinguishing version number. If the Program specifies that
499 | a certain numbered version of the GNU General Public License “or any later
500 | version” applies to it, you have the option of following the terms and
501 | conditions either of that numbered version or of any later version published by the
502 | Free Software Foundation. If the Program does not specify a version number of the GNU
503 | General Public License, you may choose any version ever published by the Free
504 | Software Foundation.
505 |
506 | If the Program specifies that a proxy can decide which future versions of the GNU
507 | General Public License can be used, that proxy's public statement of acceptance of a
508 | version permanently authorizes you to choose that version for the Program.
509 |
510 | Later license versions may give you additional or different permissions. However, no
511 | additional obligations are imposed on any author or copyright holder as a result of
512 | your choosing to follow a later version.
513 |
514 | ### 15. Disclaimer of Warranty.
515 |
516 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
517 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
518 | PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER
519 | EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
520 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE
521 | QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
522 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
523 |
524 | ### 16. Limitation of Liability.
525 |
526 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY
527 | COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS
528 | PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
529 | INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
530 | PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE
531 | OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE
532 | WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
533 | POSSIBILITY OF SUCH DAMAGES.
534 |
535 | ### 17. Interpretation of Sections 15 and 16.
536 |
537 | If the disclaimer of warranty and limitation of liability provided above cannot be
538 | given local legal effect according to their terms, reviewing courts shall apply local
539 | law that most closely approximates an absolute waiver of all civil liability in
540 | connection with the Program, unless a warranty or assumption of liability accompanies
541 | a copy of the Program in return for a fee.
542 |
543 | END OF TERMS AND CONDITIONS
544 |
545 | ## How to Apply These Terms to Your New Programs
546 |
547 | If you develop a new program, and you want it to be of the greatest possible use to
548 | the public, the best way to achieve this is to make it free software which everyone
549 | can redistribute and change under these terms.
550 |
551 | To do so, attach the following notices to the program. It is safest to attach them
552 | to the start of each source file to most effectively state the exclusion of warranty;
553 | and each file should have at least the “copyright” line and a pointer to
554 | where the full notice is found.
555 |
556 |
557 | Copyright (C)
558 |
559 | This program is free software: you can redistribute it and/or modify
560 | it under the terms of the GNU General Public License as published by
561 | the Free Software Foundation, either version 3 of the License, or
562 | (at your option) any later version.
563 |
564 | This program is distributed in the hope that it will be useful,
565 | but WITHOUT ANY WARRANTY; without even the implied warranty of
566 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
567 | GNU General Public License for more details.
568 |
569 | You should have received a copy of the GNU General Public License
570 | along with this program. If not, see .
571 |
572 | Also add information on how to contact you by electronic and paper mail.
573 |
574 | If the program does terminal interaction, make it output a short notice like this
575 | when it starts in an interactive mode:
576 |
577 | Copyright (C)
578 | This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'.
579 | This is free software, and you are welcome to redistribute it
580 | under certain conditions; type 'show c' for details.
581 |
582 | The hypothetical commands 'show w' and 'show c' should show the appropriate parts of
583 | the General Public License. Of course, your program's commands might be different;
584 | for a GUI interface, you would use an “about box”.
585 |
586 | You should also get your employer (if you work as a programmer) or school, if any, to
587 | sign a “copyright disclaimer” for the program, if necessary. For more
588 | information on this, and how to apply and follow the GNU GPL, see
589 | <>.
590 |
591 | The GNU General Public License does not permit incorporating your program into
592 | proprietary programs. If your program is a subroutine library, you may consider it
593 | more useful to permit linking proprietary applications with the library. If this is
594 | what you want to do, use the GNU Lesser General Public License instead of this
595 | License. But first, please read
596 | <>.
597 |
--------------------------------------------------------------------------------