diff --git a/config.json b/config.json index 6c410b3..2cbdf65 100644 --- a/config.json +++ b/config.json @@ -318,6 +318,7 @@ "receivedRequests": "notify", "rejectMessage": "", "sendDiagnostics": false, + "enableReadOnlyService": false, "sendRequests": "manually", "uploadRate": null, "username": "" diff --git a/oml/item/api.py b/oml/item/api.py index 857dcf9..c94d7cf 100644 --- a/oml/item/api.py +++ b/oml/item/api.py @@ -113,6 +113,7 @@ def find(data): response['size'] = sum(size) return response actions.register(find) +actions.register(find, version='public') def get(data): @@ -128,6 +129,7 @@ def get(data): response = item.json(data['keys'] if 'keys' in data else None) return response actions.register(get) +actions.register(get, version='public') def edit(data): diff --git a/oml/oxtornado.py b/oml/oxtornado.py index 2ed78ce..7215e9a 100644 --- a/oml/oxtornado.py +++ b/oml/oxtornado.py @@ -71,8 +71,9 @@ def defaultcontext(): class ApiHandler(tornado.web.RequestHandler): executor = ThreadPoolExecutor(max_workers=MAX_WORKERS) - def initialize(self, context=None): + def initialize(self, context=None, public=False): self._context = context + self._public = public def get(self): self.write('use POST') @@ -81,7 +82,7 @@ class ApiHandler(tornado.web.RequestHandler): def api_task(self, request): import settings context = self._context - if context == None: + if context is None: context = defaultcontext action = request.arguments.get('action', [None])[0].decode('utf-8') data = request.arguments.get('data', [b'{}'])[0] @@ -96,7 +97,10 @@ class ApiHandler(tornado.web.RequestHandler): else: if settings.DEBUG_API: logger.debug('API %s %s', action, data) - f = actions.get(action) + if self._public: + f = actions.versions['public'].get(action) + else: + f = actions.get(action) if f: with context(): try: @@ -126,50 +130,60 @@ class ApiHandler(tornado.web.RequestHandler): class ApiActions(dict): properties = {} versions = {} - def __init__(self): - def api(data): - ''' - returns list of all known api actions - takes { - docs: bool - } - if docs is true, action properties contain docstrings - returns { - actions: { - 'api': { - cache: true, - doc: 'recursion' - }, - 'hello': { - cache: true, - .. - } - ... + def _api(self, data, version=None): + ''' + returns list of all known api actions + takes { + docs: bool + } + if docs is true, action properties contain docstrings + returns { + actions: { + 'api': { + cache: true, + doc: 'recursion' + }, + 'hello': { + cache: true, + .. } + ... } - ''' - data = data or {} - docs = data.get('docs', False) - code = data.get('code', False) + } + ''' + data = data or {} + docs = data.get('docs', False) + code = data.get('code', False) + if version: + _actions = list(self.versions[version].keys()) + else: _actions = list(self.keys()) - _actions.sort() - actions = {} - for a in _actions: - actions[a] = self.properties[a] - if docs: - actions[a]['doc'] = self.doc(a) - if code: - actions[a]['code'] = self.code(a) - return {'actions': actions} - self.register(api) + _actions.sort() + actions = {} + for a in _actions: + actions[a] = self.properties[a] + if docs: + actions[a]['doc'] = self.doc(a, version) + if code: + actions[a]['code'] = self.code(a, version) + return {'actions': actions} - def doc(self, name): - f = self[name] + def __init__(self): + self.register(self._api, 'api') + + def doc(self, name, version=None): + if version: + f = self.versions[version][name] + else: + f = self[name] return trim(f.__doc__) def code(self, name, version=None): - f = self[name] + if version: + f = self.versions[version][name] + else: + f = self[name] if name != 'api' and hasattr(f, 'func_closure') and f.__closure__: fc = [c for c in f.__closure__ if hasattr(c.cell_contents, '__call__')] f = fc[len(fc)-1].cell_contents @@ -181,8 +195,9 @@ class ApiActions(dict): if not action: action = method.__name__ if version: - if not version in self.versions: + if version not in self.versions: self.versions[version] = {} + self.register(lambda data: self._api(data, version), action='api', version=version) self.versions[version][action] = method else: self[action] = method @@ -192,4 +207,5 @@ class ApiActions(dict): if action in self: del self[action] + actions = ApiActions() diff --git a/oml/server.py b/oml/server.py index d0e997d..7a3f08d 100644 --- a/oml/server.py +++ b/oml/server.py @@ -95,7 +95,7 @@ def run(): else: debug = False - log_format='%(asctime)s:%(levelname)s:%(name)s:%(message)s' + log_format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s' if debug: logging.basicConfig(level=logging.DEBUG, format=log_format) else: @@ -109,7 +109,7 @@ def run(): 'gzip': True } - handlers = [ + common_handlers = [ (r'/(favicon.ico)', StaticFileHandler, {'path': settings.static_path}), (r'/static/oxjs/(.*)', StaticFileHandler, {'path': os.path.join(settings.base_dir, '..', 'oxjs')}), (r'/static/cbr.js/(.*)', StaticFileHandler, {'path': os.path.join(settings.base_dir, '..', 'reader', 'cbr.js')}), @@ -126,17 +126,33 @@ def run(): 'attachment': True }), (r'/(.*)/(cover|preview)(\d*).jpg', IconHandler), + ] + handlers = common_handlers + [ (r'/api/upload/', UploadHandler, dict(context=db.session)), (r'/api/', oxtornado.ApiHandler, dict(context=db.session)), (r'/ws', websocket.Handler), (r"(.*)", MainHandler), ] + public_handlers = common_handlers + [ + (r'/api/', oxtornado.ApiHandler, dict(context=db.session, public=True)), + (r'/ws', websocket.Handler, dict(public=True)), + (r"(.*)", MainHandler), + ] setup.create_db() http_server = Application(handlers, **options) max_buffer_size = 2*1024*1024*1024 http_server.listen(settings.server['port'], settings.server['address'], max_buffer_size=max_buffer_size) + # public server + ''' + public_port = settings.server.get('public_port') + public_address = settings.server['public_address'] + if public_port: + public_server = Application(public_handlers, **options) + public_server.listen(public_port, public_address) + ''' + if PID: with open(PID, 'w') as pid: pid.write('%s' % os.getpid()) diff --git a/oml/settings.py b/oml/settings.py index 345182a..59a6f77 100644 --- a/oml/settings.py +++ b/oml/settings.py @@ -44,6 +44,8 @@ server_defaults = { 'port': 9842, 'address': '127.0.0.1', 'node_port': 9851, + 'public_address': '127.0.0.1', + 'public_port': 9852, 'node_address': '', 'extract_text': True, 'localnode_discovery': True, diff --git a/oml/tor.py b/oml/tor.py index b51a9f3..d9600e6 100644 --- a/oml/tor.py +++ b/oml/tor.py @@ -200,6 +200,8 @@ class Tor(object): key_content = RSA.importKey(private_key).exportKey().decode() key_content = ''.join(key_content.strip().split('\n')[1:-1]) ports = {9851: settings.server['node_port']} + if settings.preferences.get('enableReadOnlyService'): + ports[80] = settings.server['public_port'] controller.remove_ephemeral_hidden_service(settings.USER_ID) response = controller.create_ephemeral_hidden_service(ports, key_type='RSA1024', key_content=key_content, @@ -207,6 +209,8 @@ class Tor(object): if response.is_ok(): logger.debug('published node as https://%s.onion:%s', settings.USER_ID, settings.server_defaults['node_port']) + if settings.preferences.get('enableReadOnlyService'): + logger.debug('published readonly version as hidden servers: http://%s.onion', settings.USER_ID) else: logger.debug('failed to publish node to tor') else: @@ -217,6 +221,9 @@ class Tor(object): target_port=settings.server['node_port'] ) logger.debug('published node as https://%s:%s', result.hostname, settings.server_defaults['node_port']) + if settings.preferences.get('enableReadOnlyService'): + logger.error('can not publish read-only version, please update TOR') + def depublish(self): if not self.connected: diff --git a/oml/update.py b/oml/update.py index 566d66b..1725b03 100644 --- a/oml/update.py +++ b/oml/update.py @@ -40,8 +40,8 @@ def verify(release): value = '\n'.join(value) value = value.encode() for digest in ('sha512', 'sha256', 'sha1'): - if 'signature_%s'%digest in release: - tls_sig = base64.b64decode(release['signature_%s'%digest].encode()) + if 'signature_%s' % digest in release: + tls_sig = base64.b64decode(release['signature_%s' % digest].encode()) cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, settings.OML_UPDATE_CERT) try: OpenSSL.crypto.verify(cert, tls_sig, value, digest) @@ -301,6 +301,7 @@ def getVersion(data): response['update'] = current < new return response actions.register(getVersion, cache=False) +actions.register(getVersion, cache=False, version='public') def restart(data): ''' diff --git a/oml/user/api.py b/oml/user/api.py index 63216e9..f824282 100644 --- a/oml/user/api.py +++ b/oml/user/api.py @@ -55,6 +55,21 @@ def init(data): return response actions.register(init) +def public_init(data): + response = init(data) + name = response['user']['preferences']['username'] + response['user'] = response['config']['user'] + response['user']['preferences']['username'] = name + response['user']['ui']['page'] = '' + response['user']['ui']['showFolder'] = {'': True} + response['readOnly'] = True + for page in response['config']['pages']: + if page['id'] == 'preferences': + #page['parts'] = [p for p in page['parts'] if p['id'] in ('appearance', 'extensions')] + page['parts'] = [p for p in page['parts'] if p['id'] in ('appearance',)] + return response +actions.register(public_init, action='init', version='public') + def setPreferences(data): ''' @@ -149,6 +164,12 @@ def getUsers(data): } actions.register(getUsers) +def getUsersPublic(data): + return { + 'users': [] + } +actions.register(getUsersPublic, 'getUsers', version='public') + def getLists(data): ''' @@ -172,6 +193,29 @@ def getLists(data): } actions.register(getLists) +def getListsPublic(data): + ''' + returns { + lists: [] + } + ''' + from item.models import Item + from sqlalchemy.sql import operators + user = state.user() + lists = [] + lists.append({ + 'id': '', + 'items': user.items.count(), + 'name': 'Libraries', + 'type': 'libraries', + 'user': None, + }) + lists += user.lists_json() + return { + 'lists': lists + } +actions.register(getListsPublic, 'getLists', version='public') + def validate_query(query): validate_conditions(query['conditions']) diff --git a/oml/websocket.py b/oml/websocket.py index 7e468f2..90821bb 100644 --- a/oml/websocket.py +++ b/oml/websocket.py @@ -17,6 +17,9 @@ logger = logging.getLogger(__name__) class Handler(WebSocketHandler): + def initialize(self, public=False): + self._public = public + def check_origin(self, origin): # allow access to websocket from site, installer and loader (local file) return self.request.host in origin or \ diff --git a/static/html/app.html b/static/html/app.html index 3e9250d..2bde35f 100644 --- a/static/html/app.html +++ b/static/html/app.html @@ -15,7 +15,7 @@

- The latest code is in our git repository. + The latest code is in our git repository.

For everything else, there's IRC, and our development mailing list. Your feedback is welcome. diff --git a/static/js/UI.js b/static/js/UI.js index 430796b..82e3386 100644 --- a/static/js/UI.js +++ b/static/js/UI.js @@ -15,14 +15,21 @@ oml.UI = (function() { that.reset = function() { var ui = oml.user.ui; - oml.api.resetUI({}, function() { + + function reload() { ui = oml.config.user.ui; ui._list = oml.getListState(ui.find); ui._filterState = oml.getFilterState(ui.find); ui._findState = oml.getFindState(ui.find); Ox.Theme(ui.theme); oml.$ui.appPanel.reload(); - }); + } + if (oml.readOnly) { + oml.localStorage('ui', oml.config.user.ui); + reload(); + } else { + oml.api.resetUI({}, reload); + } }; // sets oml.user.ui.key to value @@ -161,7 +168,11 @@ oml.UI = (function() { }); }); if (Ox.len(set)) { - oml.api.setUI(set); + if (oml.readOnly) { + oml.localStorage('ui', oml.user.ui); + } else { + oml.api.setUI(set); + } } if (triggerEvents) { Ox.forEach(trigger, function(value, key) { @@ -182,4 +193,4 @@ oml.UI = (function() { return that; -}()); \ No newline at end of file +}()); diff --git a/static/js/appDialog.js b/static/js/appDialog.js index ac3e409..2576d32 100644 --- a/static/js/appDialog.js +++ b/static/js/appDialog.js @@ -25,12 +25,13 @@ oml.ui.appDialog = function() { title: Ox._('Software Development'), selected: ui.page == 'development' }, + ].concat(oml.readOnly ? [] : [ { id: 'contact', title: Ox._('Send Feedback'), selected: ui.page == 'contact' } - ], + ]), $panel = Ox.TabPanel({ content: function(id) { @@ -95,7 +96,7 @@ oml.ui.appDialog = function() { }), that = Ox.Dialog({ - buttons: [ + buttons: (oml.readOnly ? [] : [ Ox.Button({ id: 'update', style: 'squared', @@ -106,6 +107,7 @@ oml.ui.appDialog = function() { oml.UI.set({page: 'update'}); } }), + ]).concat([ {}, Ox.Button({ id: 'close', @@ -116,7 +118,7 @@ oml.ui.appDialog = function() { that.close(); } }) - ], + ]), closeButton: true, content: $panel, fixedSize: true, diff --git a/static/js/infoView.js b/static/js/infoView.js index 730013b..1a8e1c9 100644 --- a/static/js/infoView.js +++ b/static/js/infoView.js @@ -270,9 +270,10 @@ oml.ui.infoView = function(externalData, isMixed) { items: [ {id: 'read', title: Ox._('Read in Open Media Libary')}, {id: 'open', title: Ox._('Open in External Reader')}, + ].concat(oml.readOnly ? [] : [ {}, {id: 'show', title: Ox._('Show File')} - ], + ]), overlap: 'left', style: 'squared', title: 'select', @@ -284,7 +285,11 @@ oml.ui.infoView = function(externalData, isMixed) { if (data_.id == 'read') { oml.UI.set({itemView: 'book'}); } else if (data_.id == 'open') { - oml.api.openFile({id: oml.user.ui.item}); + if (oml.readOnly) { + document.location.href = '/' + oml.user.ui.item + '/get/' + } else { + oml.api.openFile({id: oml.user.ui.item}); + } } else { oml.api.openFolder({id: oml.user.ui.item}); } @@ -382,9 +387,9 @@ oml.ui.infoView = function(externalData, isMixed) { Ox.print('BOOK DATA', data) var $div, - isEditable = isMultiple || ( + isEditable = !oml.readOnly && (isMultiple || ( data.mediastate == 'available' && !externalData - ), + )), src = !externalData ? '/' + data.id + '/' + ui.icons + '512.jpg?' + data.modified : data.cover, @@ -751,28 +756,29 @@ oml.ui.infoView = function(externalData, isMixed) { ].join(', ') ) .appendTo($data); + if (!oml.readOnly) { + renderIdentifyButton(data).appendTo($data); - renderIdentifyButton(data).appendTo($data); + ['accessed', 'modified', 'added', 'created'].forEach(function(id) { + var title; + if (data[id]) { + title = Ox.getObjectById(oml.config.itemKeys, id).title; + $('

') + .css({ + marginTop: '8px', + fontWeight: 'bold' + }) + .text(title) + .appendTo($data); + $('
') + .text(Ox.formatDate(data[id], '%B %e, %Y')) + .appendTo($data); + } + }); - ['accessed', 'modified', 'added', 'created'].forEach(function(id) { - var title; - if (data[id]) { - title = Ox.getObjectById(oml.config.itemKeys, id).title; - $('
') - .css({ - marginTop: '8px', - fontWeight: 'bold' - }) - .text(title) - .appendTo($data); - $('
') - .text(Ox.formatDate(data[id], '%B %e, %Y')) - .appendTo($data); + if (data.mediastate == 'available') { + renderShareButton(data).appendTo($data); } - }); - - if (data.mediastate == 'available') { - renderShareButton(data).appendTo($data); } $('
').css({height: '16px'}).appendTo($data); diff --git a/static/js/mainMenu.js b/static/js/mainMenu.js index b437318..e4257b7 100644 --- a/static/js/mainMenu.js +++ b/static/js/mainMenu.js @@ -7,7 +7,9 @@ oml.ui.mainMenu = function() { fromMenu = false, that = Ox.MainMenu({ - extras: [ + extras: oml.readOnly ? [ + oml.$ui.loadingIcon = oml.ui.loadingIcon() + ] : [ oml.$ui.updateButton = oml.ui.updateButton(), oml.$ui.transferButton = oml.ui.transferButton(), oml.$ui.peersButton = oml.ui.peersButton(), @@ -39,6 +41,7 @@ oml.ui.mainMenu = function() { id: 'contact', title: Ox._('Send Feedback') }, + ].concat(oml.readOnly ? [] : [ {}, { id: 'update', @@ -50,7 +53,7 @@ oml.ui.mainMenu = function() { title: Ox._('Quit Open Media Library'), keyboard: 'control q' } - ] + ]) }, { id: 'userMenu', @@ -61,6 +64,7 @@ oml.ui.mainMenu = function() { title: Ox._('Preferences...'), keyboard: 'control ,' }, + ].concat(oml.readOnly ? [] : [ {}, { id: 'peers', @@ -70,7 +74,7 @@ oml.ui.mainMenu = function() { id: 'transfers', title: Ox._('Transfers...') } - ] + ]) }, getListMenu(), getEditMenu(), @@ -295,6 +299,7 @@ oml.ui.mainMenu = function() { keyboard: 'shift control s', disabled: true }, + ].concat(oml.readOnly ? [] : [ {}, { id: 'sorttitles', @@ -304,7 +309,7 @@ oml.ui.mainMenu = function() { id: 'sortnames', title: Ox._('Sort Names...') } - ] + ]) }, getFindMenu(), { @@ -477,15 +482,19 @@ oml.ui.mainMenu = function() { } else if (id == 'invertselection') { oml.$ui.list.invertSelection(); } else if (id == 'download') { - oml.api.addListItems({ - items: ui.listSelection.filter(function(id) { - return oml.$ui.list.value(id, 'mediastate') == 'unavailable'; - }), - list: ':' - }, function(result) { - Ox.Request.clearCache(); - // FIXME: reload? - }); + if (oml.readOnly) { + document.location.href = '/' + ui.listSelection[0] + '/get/' + } else { + oml.api.addListItems({ + items: ui.listSelection.filter(function(id) { + return oml.$ui.list.value(id, 'mediastate') == 'unavailable'; + }), + list: ':' + }, function(result) { + Ox.Request.clearCache(); + // FIXME: reload? + }); + } } else if (Ox.contains(['cut', 'cutadd'], id)) { var action = data.id == 'cut' ? 'copy' : 'add'; fromMenu = true; @@ -741,7 +750,7 @@ oml.ui.mainMenu = function() { clipboardItems > 1 ? Ox.formatNumber(clipboardItems) + ' ' : '' ) + Ox._(clipboardItems == 1 ? 'Book' : 'Books'), canSelect = !ui.item, - canDownload = !!unavailableItems, + canDownload = !!unavailableItems || (oml.readOnly && selectionItems == 1), canCopy = canSelect && selectionItems, canCut = canCopy && listData.editable, canPaste = listData.editable && clipboardItems, @@ -757,7 +766,7 @@ oml.ui.mainMenu = function() { return { id: 'editMenu', title: Ox._('Edit'), - items: [ + items: [].concat(oml.readOnly ? [] : [ { id: 'import', title: Ox._(oml.user.importing ? 'Importing Books...' : 'Import Books...') // FIXME @@ -767,6 +776,7 @@ oml.ui.mainMenu = function() { title: Ox._(oml.user.exporting ? 'Exporting Books...' : 'Export Books...') // FIXME }, {}, + ]).concat([ { id: 'selectall', title: Ox._('Select All'), @@ -792,6 +802,7 @@ oml.ui.mainMenu = function() { disabled: !canDownload, keyboard: 'control d' }, + ]).concat(oml.readOnly ? [] : [ { id: 'cut', title: Ox._('Cut {0}', [selectionItemName]), @@ -865,7 +876,7 @@ oml.ui.mainMenu = function() { title: Ox._('Clear History'), disabled: !historyItems, } - ] + ]) }; } @@ -930,6 +941,14 @@ oml.ui.mainMenu = function() { isLibrary = Ox.endsWith(ui._list, ':'), isList = !isLibraries && !isLibrary, isOwnList = ui._list[0] == ':'; + + if (oml.readOnly) { + return { + id: 'listMenu', + title: Ox._('List'), + items: [] + }; + } return { id: 'listMenu', title: Ox._('List'), diff --git a/static/js/oml.js b/static/js/oml.js index 8b55587..12c3370 100644 --- a/static/js/oml.js +++ b/static/js/oml.js @@ -50,8 +50,12 @@ $ui: {}, config: data.config, user: data.user, + readOnly: data.readOnly, version: data.version }); + if (oml.readOnly) { + oml.user.ui = oml.localStorage('ui') || oml.user.ui; + } ['preferences', 'ui'].forEach(function(key) { // make sure all valid settings are present oml.user[key] = Ox.extend( diff --git a/static/js/preferencesPanel.js b/static/js/preferencesPanel.js index 5d35cfe..e13c566 100644 --- a/static/js/preferencesPanel.js +++ b/static/js/preferencesPanel.js @@ -142,6 +142,14 @@ oml.ui.preferencesPanel = function() { value: preferences.autostart, help: 'Launch Open Media Library in the background once you login to your computer.' }, + /* + { + id: 'enableReadOnlyService', + title: 'Read-Only Hidden Service', + value: preferences.enableReadOnlyService, + help: 'Make a read-only version of your library available as a TOR Hidden service.
\n Your library will be available at http://' + oml.user.id + '.onion/' + }, + */ { id: 'showDebugMenu', title: 'Show Debug Menu', diff --git a/static/js/userDialog.js b/static/js/userDialog.js index 373ca0e..bd44852 100644 --- a/static/js/userDialog.js +++ b/static/js/userDialog.js @@ -16,6 +16,7 @@ oml.ui.userDialog = function() { title: Ox._('Preferences'), selected: ui.page == 'preferences' }, + ].concat(oml.readOnly ? [] : [ { id: 'peers', title: Ox._('Peers'), @@ -26,7 +27,7 @@ oml.ui.userDialog = function() { title: Ox._('Transfers'), selected: ui.page == 'transfers' } - ] + ]) }) .bindEvent({ change: function(data) { @@ -70,4 +71,4 @@ oml.ui.userDialog = function() { return that; -}; \ No newline at end of file +}; diff --git a/static/js/utils.js b/static/js/utils.js index 388c453..930f04c 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -862,7 +862,7 @@ oml.getUsers = function(callback) { }].concat( Ox.sortBy(result.data.users.filter(function(user) { return user.peered; - }), 'index') + }), 'index') ); ui._users = users; callback(users);