diff options
| author | Felix Hanley <felix@userspace.com.au> | 2020-03-16 01:44:27 +0000 |
|---|---|---|
| committer | Felix Hanley <felix@userspace.com.au> | 2020-03-16 01:44:27 +0000 |
| commit | 9edc3228ad3728cda598e028cd2277aa3686d7c0 (patch) | |
| tree | 5940047ebbab1e0ae41b41336ac014998da5ae2f | |
| parent | f519c4195f4b0a7d9a6098ee4e3aeaee4f29939c (diff) | |
| download | sws-9edc3228ad3728cda598e028cd2277aa3686d7c0.tar.gz sws-9edc3228ad3728cda598e028cd2277aa3686d7c0.tar.bz2 | |
Add hit restrictions
| -rw-r--r-- | cmd/server/hits.go | 70 | ||||
| -rw-r--r-- | go.mod | 6 | ||||
| -rw-r--r-- | go.sum | 37 | ||||
| -rw-r--r-- | sql/sqlite3/05_site_restrictions.sql | 2 | ||||
| -rw-r--r-- | store.go | 17 | ||||
| -rw-r--r-- | store/sqlite3.go | 43 |
6 files changed, 125 insertions, 50 deletions
diff --git a/cmd/server/hits.go b/cmd/server/hits.go index 0c4ac3f..27bb11d 100644 --- a/cmd/server/hits.go +++ b/cmd/server/hits.go @@ -11,7 +11,6 @@ import ( "text/template" "github.com/hashicorp/golang-lru" - maxminddb "github.com/oschwald/maxminddb-golang" "src.userspace.com.au/sws" ) @@ -31,6 +30,9 @@ func handleHitCounter(db sws.CounterStore, mmdbPath string) http.HandlerFunc { } return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Cache-Control", "no-store") + hit, err := sws.HitFromRequest(r) if err != nil { log("failed to extract hit", err) @@ -38,23 +40,37 @@ func handleHitCounter(db sws.CounterStore, mmdbPath string) http.HandlerFunc { return } - site, err := db.GetSiteByName(hit.Host) + site, err := verifyHit(db, hit) if err != nil { - log("failed to get site", err) - http.Error(w, "invalid site", http.StatusNotFound) + log("failed to verify site", err) + http.Error(w, "invalid site", http.StatusBadRequest) return } - hit.SiteID = site.ID + hit.Addr = r.RemoteAddr if strings.Contains(r.RemoteAddr, ":") { hit.Addr, _, err = net.SplitHostPort(r.RemoteAddr) } + + if r.Header.Get("X-Moz") == "prefetch" || r.Header.Get("X-Purpose") == "preview" { + w.Header().Set("Content-Type", "image/gif") + w.Write(gifBytes) + return + } + + // Ignore IPs + if site.IgnoreIPs != "" && strings.Contains(site.IgnoreIPs, hit.Addr) { + w.Header().Set("Content-Type", "image/gif") + w.Write(gifBytes) + return + } + if err == nil && hit.Addr != "" { var cc *string if v, ok := cache.Get(hit.Addr); ok { cc = v.(*string) } else if mmdbPath != "" { - if cc, err = fetchCountryCode(mmdbPath, hit.Addr); err != nil { + if cc, err = sws.FetchCountryCode(mmdbPath, hit.Addr); err != nil { log("geoip lookup failed:", err) } cache.Add(hit.Addr, cc) @@ -68,15 +84,32 @@ func handleHitCounter(db sws.CounterStore, mmdbPath string) http.HandlerFunc { //http.Error(w, err.Error(), http.StatusInternalServerError) //return } - // TODO restrict to site sites - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "image/gif") w.Write(gifBytes) return } } +func verifyHit(db sws.SiteGetter, h *sws.Hit) (*sws.Site, error) { + if h.SiteID == nil { + return nil, fmt.Errorf("invalid site ID") + } + site, err := db.GetSiteByID(*h.SiteID) + if err != nil { + return nil, err + } + if site.Name == h.Host { + return site, nil + } + if strings.Contains(site.Aliases, h.Host) { + return site, nil + } + if site.AcceptSubdomains && strings.HasSuffix(h.Host, site.Name) { + return site, nil + } + return nil, fmt.Errorf("invalid host") +} + func handleCounter(addr string) http.HandlerFunc { counter := getCounter() tmpl, err := template.New("counter").Parse(counter) @@ -108,22 +141,3 @@ func handleCounter(addr string) http.HandlerFunc { } } } - -func fetchCountryCode(path, host string) (*string, error) { - db, err := maxminddb.Open(path) - if err != nil { - return nil, err - } - defer db.Close() - - ip := net.ParseIP(host) - var r struct { - Country struct { - ISOCode string `maxminddb:"iso_code"` - } `maxminddb:"country"` - } - if err := db.Lookup(ip, &r); err != nil { - return nil, err - } - return &r.Country.ISOCode, nil -} @@ -25,8 +25,12 @@ require ( google.golang.org/appengine v1.6.5 // indirect src.userspace.com.au/go-migrate v0.0.0-20200208102934-cf11cf76db3f src.userspace.com.au/templates v0.0.0-20200308073907-e96b7a1f2a49 + zgo.at/goatcounter v1.0.0 // indirect + zgo.at/tz v0.0.0-20200314040300-b1cfaf56ef7e // indirect + zgo.at/zdb v0.0.0-20200221072833-2c234b210cf1 // indirect + zgo.at/zhttp v0.0.0-20200301180126-a9b7c887528b // indirect ) //replace src.userspace.com.au/templates => ../templates -go 1.13 +go 1.14 @@ -1,3 +1,5 @@ +github.com/arp242/geoip2-golang v1.4.0/go.mod h1:AZYwUhu7pAKqHkiZTQhSCWaexFJDwzR3K9jIrhkNDko= +github.com/arp242/maxminddb-golang v1.6.0/go.mod h1:zZGWhFkxTFX80NKfpEHn6vqd50ksDYTBP8Zi+Xk10OI= github.com/blend/go-sdk v2.0.0+incompatible h1:FL9X/of4ZYO5D2JJNI4vHrbXPfuSDbUa7h8JP9+E92w= github.com/blend/go-sdk v2.0.0+incompatible/go.mod h1:3GUb0YsHFNTJ6hsJTpzdmCUl05o8HisKjx5OAlzYKdw= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= @@ -6,6 +8,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY= github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/jwtauth v4.0.4+incompatible h1:LGIxg6YfvSBzxU2BljXbrzVc1fMlgqSKBQgKOGAVtPY= @@ -17,6 +21,8 @@ github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= @@ -27,14 +33,19 @@ github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v2.0.2+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/monoculum/formam v0.0.0-20191229172733-952f0766a724/go.mod h1:JKa2av1XVkGjhxdLS59nDoXa2JpmIHpnURWNbzCtXtc= +github.com/mssola/user_agent v0.5.0/go.mod h1:UFiKPVaShrJGW93n4uo8dpPdg1BSVpw2P9bneo0Mtp8= github.com/mssola/user_agent v0.5.1 h1:sJUCUozh+j7c0dR2zMIUX5aJjoY/TNo/gXiNujoH5oY= github.com/mssola/user_agent v0.5.1/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw= github.com/oschwald/maxminddb-golang v1.6.0 h1:KAJSjdHQ8Kv45nFIbtoLGrGWqHFajOIm7skTyz/+Dls= github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -46,6 +57,8 @@ github.com/speps/go-hashids v2.0.0+incompatible/go.mod h1:P7hqPzMdnZOfyIk+xrlG1Q github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/teamwork/guru v1.0.0/go.mod h1:1DW23kFX0aXn1kRSuQAY+Byk74h1kZHvhJL72qBUrfU= +github.com/teamwork/reload v1.3.0/go.mod h1:kHdVPdfdmA+ygkBbigWUeerpy6EK4Kcukx1TNyePXHA= github.com/wcharczuk/go-chart v2.0.1+incompatible h1:0pz39ZAycJFF7ju/1mepnk26RLVLBCWz1STcD3doU0A= github.com/wcharczuk/go-chart v2.0.1+incompatible/go.mod h1:PF5tmL4EIx/7Wf+hEkpCqYi5He4u90sw+0+6FhrryuE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -56,9 +69,13 @@ golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -76,3 +93,23 @@ src.userspace.com.au/go-migrate v0.0.0-20200208102934-cf11cf76db3f h1:Vdn/5kMeLX src.userspace.com.au/go-migrate v0.0.0-20200208102934-cf11cf76db3f/go.mod h1:QaSWOcvGubR8TBPs8XgLN67muYgAKnmIgHAaQk5ZR1c= src.userspace.com.au/templates v0.0.0-20200308073907-e96b7a1f2a49 h1:xKLfGVLhd+jTT5Z1jCIEGcr5LzzuRnpaYvU5pA/iD7I= src.userspace.com.au/templates v0.0.0-20200308073907-e96b7a1f2a49/go.mod h1:lB6Vdw9R/8jR8CzvPxfQ8ryBwpNDni1fCdeRjj90gxA= +zgo.at/goatcounter v1.0.0/go.mod h1:Y76wF7BC58KvDzjsd57kdAHxqzsdgbdOVhToYqvefnE= +zgo.at/tz v0.0.0-20200314040300-b1cfaf56ef7e h1:AkOzghahAFiffIAXjd0KBlxGZsR0wERvCNs67kAAcSc= +zgo.at/tz v0.0.0-20200314040300-b1cfaf56ef7e/go.mod h1:A/XeaYjeMGoXptRB3EcR80tgir37tJnzCb6itDaHPxo= +zgo.at/utils v1.2.0/go.mod h1:7acz1AfMZAOOC1wTi6QbYFie+eA0LRSHE38TwLZsFcA= +zgo.at/utils v1.3.1/go.mod h1:7acz1AfMZAOOC1wTi6QbYFie+eA0LRSHE38TwLZsFcA= +zgo.at/zdb v0.0.0-20200104065131-90c5bb37d973/go.mod h1:GT5Nyrxb9H6JszZtd7o0m5CPEd3C7DFqAeJt98nyNQI= +zgo.at/zdb v0.0.0-20200221072833-2c234b210cf1 h1:g7p4cSjq/eL/A/Lojxw410mSLGLDJrB6+xZfplTKzjc= +zgo.at/zdb v0.0.0-20200221072833-2c234b210cf1/go.mod h1:GT5Nyrxb9H6JszZtd7o0m5CPEd3C7DFqAeJt98nyNQI= +zgo.at/zhttp v0.0.0-20200108041919-3990cfa2823e/go.mod h1:7PO/E4nqxbstyxX95cqKJOpZAVKNKoohk5utWZIdKz0= +zgo.at/zhttp v0.0.0-20200301180126-a9b7c887528b h1:L1WcdQu/XDN1cfBPEXcM+8xKMtWKUJ1HudfWgV5Icmc= +zgo.at/zhttp v0.0.0-20200301180126-a9b7c887528b/go.mod h1:qpNbBXqMQVTnjYbUlaBT1+SHctAfsRUGmWL/Sdwsrwo= +zgo.at/zlog v1.0.7/go.mod h1:0RldP/sQzWwOCPsJogZcxnJneFt56T1zfDtIN/LcYec= +zgo.at/zlog v1.0.8/go.mod h1:0RldP/sQzWwOCPsJogZcxnJneFt56T1zfDtIN/LcYec= +zgo.at/zlog v1.0.9/go.mod h1:0RldP/sQzWwOCPsJogZcxnJneFt56T1zfDtIN/LcYec= +zgo.at/zpack v1.0.0/go.mod h1:2laAAHqImmeiTlMAQ2PlMRBVhS6YKPkUoosMfdv7GXY= +zgo.at/zstripe v1.0.0/go.mod h1:EqblFpMvXAhzZAUUt0EVom06EnN5+D5ESBTSOkwSwTY= +zgo.at/ztest v1.0.0/go.mod h1:iTxcAVkHLq73Qnd+a8rlwc6Ayrk8sbnJPsGeapG/y0Q= +zgo.at/ztest v1.0.1/go.mod h1:iTxcAVkHLq73Qnd+a8rlwc6Ayrk8sbnJPsGeapG/y0Q= +zgo.at/zvalidate v1.0.0 h1:glc7tbq3X2EyPEQf00N6HgsWCVDlJkNLvdRbBBOrhs4= +zgo.at/zvalidate v1.0.0/go.mod h1:3w++OX5k3nkvwwf7OkdYnjpzF+U4OGJ5xmP7SphmUuk= diff --git a/sql/sqlite3/05_site_restrictions.sql b/sql/sqlite3/05_site_restrictions.sql new file mode 100644 index 0000000..d383df1 --- /dev/null +++ b/sql/sqlite3/05_site_restrictions.sql @@ -0,0 +1,2 @@ +alter table sites add column subdomains integer not null default 0; +alter table sites add column ignore_ips varchar null; @@ -3,27 +3,28 @@ package sws type Store interface { SiteStore UserStore - GetSiteByName(string) (*Site, error) HitSaver + HitCursor(func(*Hit) error) error } type HitSaver interface { SaveHit(*Hit) error } -type SimpleSiteStore interface { - GetSiteByName(string) (*Site, error) -} type SiteStore interface { - GetSites() ([]*Site, error) - GetSiteByID(int) (*Site, error) + SiteGetter GetHits(Site, map[string]interface{}) ([]*Hit, error) + GetSites() ([]*Site, error) SaveSite(*Site) error } +type SiteGetter interface { + GetSiteByID(int) (*Site, error) +} type HitStore interface { - SimpleSiteStore + HitSaver GetHits(Site, map[string]interface{}) ([]*Hit, error) + HitCursor(func(*Hit) error) error } type CounterStore interface { - SimpleSiteStore + SiteGetter HitSaver } type UserStore interface { diff --git a/store/sqlite3.go b/store/sqlite3.go index a74d7dc..a889547 100644 --- a/store/sqlite3.go +++ b/store/sqlite3.go @@ -88,6 +88,30 @@ func (s *Sqlite3) GetHits(d sws.Site, filter map[string]interface{}) ([]*sws.Hit return hits, nil } +func (s *Sqlite3) HitCursor(f func(h *sws.Hit) error) error { + sql := `select h.*, +ua.hash as "ua.hash", ua.name as "ua.name", ua.last_seen_at as "ua.last_seen_at" +from hits h +join user_agents ua on h.user_agent_hash = ua.hash` + + rows, err := s.db.Queryx(sql) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + h := &sws.Hit{} + if err := rows.StructScan(h); err != nil { + return err + } + if err := f(h); err != nil { + return err + } + } + return nil +} + func (s *Sqlite3) SaveHit(h *sws.Hit) error { if h.UserAgent != nil { if _, err := s.db.NamedExec(stmts["saveUserAgent"], *h.UserAgent); err != nil { @@ -141,27 +165,20 @@ func processFilter(sql *string, filter map[string]interface{}) { } var stmts = map[string]string{ - "sites": `select id, name, description, aliases, enabled, -created_at, updated_at -from sites`, + "sites": `select * from sites`, - "siteByName": `select id, name, description, aliases, enabled, -created_at, updated_at -from sites -where name = $1 limit 1`, + "siteByName": `select * from sites where name = $1 limit 1`, - "siteByID": `select id, name, description, aliases, enabled, -created_at, updated_at -from sites -where id = $1 limit 1`, + "siteByID": `select * from sites where id = $1 limit 1`, "saveSite": `insert into sites ( -name, description, aliases, enabled, created_at, updated_at) -values (:name, :description, :aliases, :enabled, date('now'), date('now')) +name, description, aliases, enabled, subdomains, ignore_ips, created_at, updated_at) +values (:name, :description, :aliases, :enabled, :subdomains, :ignore_ips, date('now'), date('now')) on conflict(id) do update set name = :name, description = :description, aliases = :aliases, +subdomains = :subdomains, updated_at = date('now')`, "userAgentByHash": `select id, hash, name, last_seen_at from sites |
