Change schema to store xml directly to db
[friends.git] / friends / server.py
1 #!/usr/bin/python3
2 import tornado.ioloop
3 import tornado.web
4 import os
5 import os.path
6 import tornado.httpserver
7 import tornado.httpclient as httpclient
8 import sqlite3
9 import arrow
10 import datetime
11 from rd import RD, Link
12 import hmac
13 from tornado.options import options, define
14 import logging
15 db = None
16 # insert into user (name,email) values('mikael','mikael@frykholm.com');
17 # insert into entry (userid,text) values (1,'My thoughts on ostatus');
18
19
20 settings = {
21 "static_path": os.path.join(os.path.dirname(__file__), "static"),
22 "cookie_secret": "supersecret123",
23 "login_url": "/login",
24 "xsrf_cookies": False,
25 "domain":"https://ronin.frykholm.com",
26
27 }
28
29 #curl -v -k "https://ronin.frykholm.com/hub" -d "hub.callback=a" -d "hub.mode=b" -d "hub.topic=c" -d "hub.verify=d"
30
31 class PushHandler(tornado.web.RequestHandler):
32 def post(self):
33 """ Someone wants to subscribe to hub_topic feed"""
34 hub_callback = self.get_argument('hub.callback')
35 hub_mode = self.get_argument('hub.mode')
36 hub_topic = self.get_argument('hub.topic')
37 hub_verify = self.get_argument('hub.verify')
38 hub_lease_seconds = self.get_argument('hub.lease_seconds','')
39 hub_secret = self.get_argument('hub.secret','')
40 hub_verify_token = self.get_argument('hub.verify_token','')
41 print(self.request.body)
42 if hub_mode == 'unsubscribe':
43 pass #FIXME
44 path = hub_topic.split(self.settings['domain'])[1]
45 user = path.split('user/')[1]
46 row = db.execute("select id from author where name=?",(user,)).fetchone()
47 expire = datetime.datetime.utcnow() + datetime.timedelta(seconds=int(hub_lease_seconds))
48 if row:
49 db.execute("INSERT into subscriptions (author, expires, callback, secret, verified) "
50 "values (?,?,?,?,?)",(row['id'],expire,hub_callback,hub_secret,False))
51 db.commit()
52 self.set_status(202)
53 http_client = httpclient.HTTPClient()
54 try:
55 response = http_client.fetch(hub_callback+"?hub.mode={}&hub.topic={}&hub.secret".format(hub_mode,hub_topic,hub_secret))
56 print(response.body)
57 except httpclient.HTTPError as e:
58 # HTTPError is raised for non-200 responses; the response
59 # can be found in e.response.
60 print("Error: " + str(e))
61 except Exception as e:
62 # Other errors are possible, such as IOError.
63 print("Error: " + str(e))
64 http_client.close()
65 #TODO add secret to outgoing feeds with hmac
66
67 class XrdHandler(tornado.web.RequestHandler):
68 def get(self):
69 self.render("templates/xrd.xml", hostname="ronin.frykholm.com", url=self.settings['domain'])
70
71 class apa():
72 def LookupPublicKey(self, signer_uri=None):
73 return """RSA.jj2_lJ348aNh_9s3eCHlJlbMQdnHVm9svdU2ESW86TvV-4wZId-z3M029pjPvco0UEvlUUnJytXwoTLd70pzfZ8Cu5MMwGbvm9asI9-PKUDSNFgr5T_B017qUXOG5UH1ZNI_fVA2mSAkxxfEksv4HXg43dBvEIW94JpyAtqggHM=.AQAB.Bzz_LcnoLCu7RfDa3sMizROnq0YwzaY362UZLkA0X84KspVLhhzDI15SCLR4BdlvVhK2pa9SlH7Uku9quc2ZGNyr5mEdqjO7YTbQA9UCgbobEq2ImqV_j7Y4IfjPc8prDPCKb_mO9DUlS_ZUxJYfsOuc-SVlGmPZ93uEl8i9OjE="""
74
75
76 class SalmonHandler(tornado.web.RequestHandler):
77 def post(self, user):
78 sp = salmoning.SalmonProtocol()
79 sp.key_retriever = apa()
80 data = sp.ParseSalmon(self.request.body)
81 pass
82
83 class FingerHandler(tornado.web.RequestHandler):
84 def get(self):
85 user = self.get_argument('resource')
86 user = user.split('acct:')[1]
87 (user,domain) = user.split('@')
88 row = db.execute("select id,salmon_pubkey from author where author.name=?",(user,)).fetchone()
89 if not row:
90 self.set_status(404)
91 self.write("Not found")
92 self.finish()
93 return
94 lnk = Link(rel='http://spec.example.net/photo/1.0',
95 type='image/jpeg',
96 href='{}/static/{}.jpg'.format(self.settings['domain'],user))
97 lnk.titles.append(('User Photo', 'en'))
98 lnk.titles.append(('Benutzerfoto', 'de'))
99 lnk.properties.append(('http://spec.example.net/created/1.0', '1970-01-01'))
100 lnk2 = Link(rel='http://schemas.google.com/g/2010#updates-from',
101 type='application/atom+xml',
102 href='{}/user/{}'.format(self.settings['domain'],user))
103
104 rd = RD(subject='{}/{}'.format(self.settings['domain'],user))
105 rd.properties.append('http://spec.example.net/type/person')
106 rd.links.append(lnk)
107 rd.links.append(lnk2)
108 rd.links.append(Link(rel="magic-public-key",
109 href="data:application/magic-public-key,RSA."+row['salmon_pubkey']))
110 rd.links.append(Link(rel="salmon",
111 href="{}/salmon/{}".format(self.settings['domain'],user)))
112 rd.links.append(Link(rel="http://salmon-protocol.org/ns/salmon-replies",
113 href="{}/salmon/{}".format(self.settings['domain'],user)))
114 rd.links.append(Link(rel="http://salmon-protocol.org/ns/salmon-mention",
115 href="{}/salmon/{}".format(self.settings['domain'],user)))
116 self.write(rd.to_json())
117
118 class UserHandler(tornado.web.RequestHandler):
119 def get(self, user):
120 entries = db.execute("select entry.id,text,ts from author,entry where author.id=entry.author and author.name=?",(user,))
121 # import pdb;pdb.set_trace()
122 self.set_header("Content-Type", 'application/atom+xml')
123 out = self.render("templates/feed.xml",
124 user=user,
125 feed_url="{}/user/{}".format(self.settings['domain'], user),
126 hub_url="{}/hub".format(self.settings['domain']),
127 entries=entries,
128 arrow=arrow )
129 #digest = hmac.new()
130
131 def post(self, user):
132 entries = db.execute("select entry.id,text,ts from user,entry where user.id=entry.userid and user.name=?",(user,))
133
134 self.set_header("Content-Type", 'application/atom+xml')
135 out = self.render_string("templates/feed.xml",
136 user=user,
137 feed_url="{}/user/{}".format(self.settings['domain'], user),
138 hub_url="{}/hub".format(self.settings['domain']),
139 entries=entries,
140 arrow=arrow)
141 #import pdb;pdb.set_trace()
142 # Notify subscribers
143 subscribers = db.execute("select callback, secret from subscriptions, author where author.id=subscriptions.author and author.name=?",(user,))
144 for url,secret in subscribers:
145 digest = hmac.new(secret.encode('utf8'), out, digestmod='sha1').hexdigest()
146
147 req = httpclient.HTTPRequest(url=url, allow_nonstandard_methods=True,method='POST', body=out, headers={"X-Hub-Signature":"sha1={}".format(digest),"Content-Type": 'application/atom+xml',"Content-Length":len(out)})
148 apa = httpclient.HTTPClient()
149 apa.fetch(req)
150
151 application = tornado.web.Application([
152 (r"/.well-known/host-meta", XrdHandler),
153 (r"/.well-known/webfinger", FingerHandler),
154 (r"/user/(.+)", UserHandler),
155 (r"/hub", PushHandler),
156 ],debug=True,**settings)
157 srv = tornado.httpserver.HTTPServer(application, )
158
159 def setup_db(path):
160 gen_log = logging.getLogger("tornado.general")
161 gen_log.warn("No db found, creating in {}".format(path))
162 con = sqlite3.connect(path)
163 con.execute("""create table author (id integer primary key,
164 uri varchar,
165 name varchar,
166 email varchar,
167 salmon_pubkey varchar, -- base64_urlencoded public modulus +.+ base64_urlencoded public exponent
168 salmon_privkey varchar -- base64_urlencoded private exponent
169 );""")
170 con.execute(""" create table entry (id integer primary key,
171 author INTEGER,
172 text varchar, -- xml atom <entry>
173 verb varchar,
174 ts timestamp default current_timestamp,
175 FOREIGN KEY(author) REFERENCES author(id));""")
176 con.execute("""
177 create table subscriptions (id integer primary key,
178 author integer,
179 expires datetime,
180 callback varchar,
181 secret varchar,
182 verified bool,
183 FOREIGN KEY(author) REFERENCES author(id));""")
184 con.commit()
185
186
187 options.define("config_file", default="/etc/friends/friends.conf", type=str)
188 options.define("webroot", default="/srv/friends/", type=str)
189
190 if __name__ == "__main__":
191 dbPath = 'friends.db'
192 # options.log_file_prefix="/tmp/friends"
193 tornado.options.parse_config_file(options.config_file)
194 tornado.options.parse_command_line()
195 gen_log = logging.getLogger("tornado.general")
196 gen_log.info("Reading config from: %s", options.config_file,)
197 if not os.path.exists(dbPath):
198 setup_db(dbPath)
199 db = sqlite3.connect(dbPath, detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
200 db.row_factory = sqlite3.Row
201 srv.listen(80)
202 tornado.ioloop.IOLoop.instance().start()
203