# # Collective Knowledge (CK web service) # # See CK LICENSE.txt for licensing details # See CK COPYRIGHT.txt for copyright details # # Developer: Grigori Fursin # cfg={} # Will be updated by CK (meta description of this module) work={} # Will be updated by CK (temporal data) ck=None # Will be updated by CK (initialized CK kernel) wfe_host='' wfe_port='' # Local settings import os import sys import cgi import urllib import base64 import tempfile # Import various modules while supporting both Python 2.x and 3.x try: from http.server import BaseHTTPRequestHandler, HTTPServer except: from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer try: import urllib.parse as urlparse except: import urlparse try: from urllib.parse import quote as urlquote except: from urllib import quote as urlquote try: from urllib.parse import unquote as urlunquote except: from urllib import unquote as urlunquote #try: # import http.cookies as Cookie #except: # import Cookie try: from socketserver import ThreadingMixIn except: from SocketServer import ThreadingMixIn ############################################################################## # Initialize module def init(i): """ Input: {} Output: { return - return code = 0, if successful > 0, if error (error) - error text if return > 0 } """ return {'return':0} ############################################################################## # Access CK through CMD (can detach console) def call_ck(i): """ Input: { Input for CK } Output: { return - return code = 0, if successful > 0, if error (error) - error text if return > 0 (stdout) - stdout, if available (stderr) - stderr, if available (std) - stdout+stderr } """ import subprocess import re # Check action action=i.get('action','') if action=='': return {'return':1, 'error':'action is not defined'} # Check that no special characters, otherwise can run any command from CMD if not re.match('^[A-Za-z0-9-_]*$', action): return {'return':1, 'error':'action contains illegal characters'} # Generate tmp file fd, fn=tempfile.mkstemp(suffix='.tmp', prefix='ck-') # suffix is important - CK will delete such file! os.close(fd) dc=i.get('detach_console','') if dc=='yes': i['out']='con' # If detach, output as console # Prepare dummy output rr={'return':0} rr['stdout']='' rr['stderr']='' # Save json to temporay file rx=ck.save_json_to_file({'json_file':fn, 'dict':i}) if rx['return']>0: return rx # Prepare command line cmd='ck '+action+' @'+fn if dc=='yes': # Check platform rx=ck.get_os_ck({}) if rx['return']>0: return rx plat=rx['platform'] dci=ck.cfg.get('detached_console',{}).get(plat,{}) dcmd=dci.get('cmd','') if dcmd=='': return {'return':1, 'error':'detached console is requested but cmd is not defined in kernel configuration'} dcmd=dcmd.replace('$#cmd#$', cmd) if dci.get('use_create_new_console_flag','')=='yes': process=subprocess.Popen(dcmd, stdin=None, stdout=None, stderr=None, shell=True, close_fds=True, creationflags=subprocess.CREATE_NEW_CONSOLE) else: # Will need to do the forking try: pid=os.fork() except OSError as e: return {'return':1, 'error':'forking detached console failed ('+format(e)+')'} if pid==0: os.setsid() pid=os.fork() if pid!=0: os._exit(0) try: maxfd=os.sysconf("SC_OPEN_MAX") except (AttributeError, ValueError): maxfd=1024 for fd in range(maxfd): try: os.close(fd) except OSError: pass os.open('/dev/null', os.O_RDWR) os.dup2(0, 1) os.dup2(0, 2) # Normally child process process=os.system(dcmd) os._exit(0) stdout=ck.cfg.get('detached_console_html', 'Console was detached ...') stderr='' else: process=subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) stdout,stderr=process.communicate() try: stdout=stdout.decode('utf8') except Exception as e: pass try: stderr=stderr.decode('utf8') except Exception as e: pass rr['std']=stdout+stderr rr['stdout']=stdout rr['stderr']=stderr return rr ############################################################################## # Send error to HTTP stream def web_err(i): """ Input: { http - http object type - content type bin - bytes to output } Output: { return - 0 } """ http=i['http'] tp=i['type'] bin=i['bin'] try: bin=bin.decode('utf-8') except Exception as e: pass if tp=='json': rx=ck.dumps_json({'dict':{'return':1, 'error':bin}}) if rx['return']>0: bin2=rx['error'].encode('utf8') else: bin2=rx['string'].encode('utf-8') elif tp=='con': bin2=bin.encode('utf8') else: bin2=b'
'+bin.encode('utf8')+b'
' i['bin']=bin2 return web_out(i) ############################################################################## # Send error to HTTP stream def web_out(i): """ Input: { http - http object type - content type bin - bytes to output (filename) - if !='', substitute filename in headers } Output: { return - 0 } """ http=i['http'] bin=i['bin'] tp=i['type'] if tp=='' or tp=='web': tp='html' tpx=cfg['content_types'].get(tp,{}) if len(tpx)==0: tp='unknown' tpx=cfg['content_types'][tp] fn=i.get('filename','') # Output for k in sorted(tpx.keys()): v=tpx[k] if fn!='': v=v.replace('$#filename#$', fn) http.send_header(k,v) http.send_header('Access-Control-Allow-Origin', '*') http.send_header('Content-Length', str(len(bin))) http.end_headers() http.wfile.write(bin) return {'return':0} ############################################################################## # Process CK web service request (both GET and POST) def process_ck_web_request(i): """ Input: { http - Python http object } Output: { None } """ # http object http=i['http'] # Parse GET variables and path xget={} xpath={'host':'', 'port':'', 'first':'', 'rest':'', 'query':''} # May be used in the future xt='json' xpath['host']=i.get('host','') xpath['port']=i.get('port','') # Check GET variables if http.path!='': http.send_response(200) a=urlparse.urlparse(http.path) xp=a.path xr='' if xp.startswith('/'): xp=xp[1:] u=xp.find('/') if u>=0: xr=xp[u+1:] xp=xp[:u] xt=xp xpath['first']=xp xpath['rest']=xr xpath['query']=a.query b=urlparse.parse_qs(a.query, keep_blank_values=True, ) xget={} for k in b: # xget[k]=b[k][0] xget[k]=urlunquote(b[k][0]) if sys.version_info[0]<3: xget[k]=xget[k].decode('utf8') # Check POST xpost={} xpost1={} try: headers = http.headers content_type = headers.get('content-type') ctype='' if content_type != None: ctype, pdict = cgi.parse_header(content_type) # Python3 cgi.parse_multipart expects boundary to be bytes, not str. if sys.version_info[0]<3 and 'boundary' in pdict: pdict['boundary'] = pdict['boundary'].encode() if ctype == 'multipart/form-data': if sys.version_info[0]<3: xpost1 = cgi.parse_multipart(http.rfile, pdict) else: xxpost1 = cgi.FieldStorage(fp=http.rfile, headers=headers, environ={'REQUEST_METHOD':'POST'}) for k in xxpost1.keys(): xpost1[k]=[xxpost1[k].value] elif ctype == 'application/x-www-form-urlencoded': length = int(http.headers.get('content-length')) s=http.rfile.read(length) if sys.version_info[0]>2: s=s.decode('utf8') xpost1 = cgi.parse_qs(s, keep_blank_values=1) except Exception as e: bin=b'internal CK web service error [7101] ('+format(e).encode('utf8')+')' web_err({'http':http, 'type':xt, 'bin':bin}) ck.out(ck.cfg['error']+bin.decode('utf8')) return # Post processing for k in xpost1: v=xpost1[k] if k.endswith('[]'): k1=k[:-2] xpost[k1]=[] for l in v: xpost[k1].append(urlunquote(l)) else: if k!='file_content': xpost[k]=urlunquote(v[0]) else: xpost[k]=v[0] if k=='file_content': fcrt=xpost1.get('file_content_record_to_tmp','') if (type(fcrt)==list and len(fcrt)>0 and fcrt[0]=='yes') or fcrt=='yes': fd, fn=tempfile.mkstemp(suffix='.tmp', prefix='ck-') # suffix is important - CK will delete such file! os.close(fd) f=open(fn,'wb') f.write(xpost[k]) f.close() xpost[k+'_uploaded']=fn del(xpost[k]) k+='_uploaded' else: import base64 xpost[k+'_base64']=base64.urlsafe_b64encode(xpost[k]).decode('utf8') del(xpost[k]) k+='_base64' if sys.version_info[0]<3: xpost[k]=xpost[k].decode('utf8') # Prepare input and check if CK json present ii=xget ii.update(xpost) cj=ii.get('ck_json','').strip() if cj!='': r=ck.convert_json_str_to_dict({'str':cj, 'skip_quote_replacement':'yes'}) if r['return']>0: bin=b'internal CK web service error [7102] ('+r['error'].encode('utf8')+b')' web_err({'http':http, 'type':xt, 'bin':bin}) ck.out(ck.cfg['error']+bin.decode('utf8')) return del(ii['ck_json']) ii.update(r['dict']) # Misc parameters dc=ii.get('detach_console','') act=ii.get('action','') # Check output type if ii.get('out','')!='': xt=ii['out'] if xt=='': xt='web' if xt!='json' and xt!='con' and xt!='web': web_out({'http':http, 'type':'web', 'bin':b'Unknown CK request ('+xt.encode('utf8')+b')!'}) return # Prepare temporary output file fd, fn=tempfile.mkstemp(prefix='ck-') os.close(fd) os.remove(fn) # Check output if dc=='yes': if ck.cfg.get('forbid_detached_console','')=='yes': web_out({'http':http, 'type':'web', 'bin':b'Detached console is forbidden!'}) return else: ii['out_file']=fn ii['web']='yes' if xt=='json' or xt=='web': ii['out']='json_file' # else output to console (for remote access for example) ii['con_encoding']='utf8' ii['host']=wfe_host ii['port']=wfe_port # Execute command ********************************************************* if act=='': if cfg.get('if_web_action_not_defined','')!='' and cfg.get('if_web_module_not_defined','')!='': ii['module_uoa']=cfg['if_web_module_not_defined'] ii['action']=cfg['if_web_action_not_defined'] r=call_ck(ii) # Process output if r['return']>0: if os.path.isfile(fn): os.remove(fn) bout=r['error'] try: bout=bout.encode('utf-8') except Exception as e: pass web_err({'http':http, 'type':xt, 'bin':bout}) return # If output to console or detached console if xt=='con' or dc=='yes': if os.path.isfile(fn): os.remove(fn) bout=r.get('std','').encode('utf8') web_out({'http':http, 'type':xt, 'bin':bout}) return # If json or web # Try to load output file if not os.path.isfile(fn): web_err({'http':http, 'type':xt, 'bin':b'Output json file was not created, see output ('+r['std'].encode('utf8')+b')!'}) return r=ck.load_text_file({'text_file':fn, 'keep_as_bin':'yes'}) if r['return']>0: bout=r['error'] try: bout=bout.encode('utf-8') except Exception as e: pass web_err({'http':http, 'type':xt, 'bin':bout}) return bin=r['bin'] if os.path.isfile(fn): os.remove(fn) # Process JSON output from file fx='' if sys.version_info[0]>2: bin=bin.decode('utf-8') ru=ck.convert_json_str_to_dict({'str':bin, 'skip_quote_replacement':'yes'}) if ru['return']>0: bout=ru['error'] try: bout=bout.encode('utf-8') except Exception as e: pass web_err({'http':http, 'type':xt, 'bin':bout}) return rr=ru['dict'] if rr['return']>0: bout=rr['error'] try: bout=bout.encode('utf-8') except Exception as e: pass web_err({'http':http, 'type':xt, 'bin':bout}) return # Check if file was returned fr=False if 'file_content_base64' in rr and rr.get('filename','')!='': fr=True # Check if download if (xt=='web' and fr) or (act=='pull' and xt!='json'): import base64 x=rr.get('file_content_base64','') fx=rr.get('filename','') if fx=='': fx=ck.cfg['default_archive_name'] # Fixing Python bug if sys.version_info[0]==3 and sys.version_info[1]<3: x=x.encode('utf-8') else: x=str(x) bin=base64.urlsafe_b64decode(x) # convert from unicode to str since base64 works on strings # should be safe in Python 2.x and 3.x # Process extension fn1, fne = os.path.splitext(fx) if fne.startswith('.'): fne=fne[1:] if fne!='': xt=fne else: xt='unknown' else: # Check and output html if rr.get('html','')!='': bin=rr['html'].encode('utf-8') else: if sys.version_info[0]>2: # Unknown output bin=bin.encode('utf-8') web_out({'http':http, 'type':xt, 'bin':bin, 'filename':fx}) return {'return':0} ############################################################################## # Class to handle requests in separate threads class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): """ """ ############################################################################## # Class to handle CK web service requests class server_handler(BaseHTTPRequestHandler): """ Input: Python http handler Output: None """ # Process only GET def do_GET(self): process_ck_web_request({'http':self}) return # Process GET and POST def do_POST(self): process_ck_web_request({'http':self}) return def log_request(self, code='-', size='-'): self.log_message('"%s" %s %s', self.requestline, str(code), str(size)) return def log_error(self, format, *args): self.log_message(format, *args) return ############################################################################## # start web service def start(i): """ Input: { (host) - Internal web server host (port) - Internal web server port (wfe_host) - External web server host (wfe_port) - External web server port (browser) - if 'yes', open browser (template) - if !='', add template (wcid) - view a given entry or (cid) (extra_url) - extra URL } Output: { return - return code = 0, if successful > 0, if error (error) - error text if return > 0 } """ # Define internal server host. host=ck.cfg.get('default_host','') host=i.get('host',host) if host=='': host='localhost' # 'localhost' if '' # Define external server host. global wfe_host wfe_host=i.get('wfe_host',host) # Define internal server port. port=ck.cfg.get('default_port','') port=i.get('port',port) if port=='': return {'return':1, 'error':'web port is not defined'} # Define external server port. global wfe_port wfe_port=i.get('wfe_port',port) # Assemble URL. url=host+':'+port wfe_url=wfe_host+':'+wfe_port ck.out('Starting CK web service on '+url+' (configured for access at '+wfe_url+') ...') ck.out('') sys.stdout.flush() if i.get('browser','')=='yes': rurl='http://'+url ext='' if i.get('template','')!='': ext='template='+i['template'] cid=i.get('wcid','') if cid=='': cid=i.get('cid','') if cid!='' and cid!='web': if ext!='': ext+='&' ext+='wcid='+cid if i.get('extra_url','')!='': if ext!='': ext+='&' ext+=i['extra_url'] if ext!='': rurl+='/?'+ext import webbrowser webbrowser.open(rurl) try: server = ThreadedHTTPServer((host, int(port)), server_handler) # Prevent issues with socket reuse server.allow_reuse_address=True server.serve_forever() except KeyboardInterrupt: ck.out('Keyboard interrupt, terminating CK web service ...') server.socket.close() return {'return':0} except OSError as e: return {'return':1, 'error':'problem starting CK web service ('+format(e)+')'} return {'return':0} ############################################################################## # test web def test(i): """ Input: {} Output: { return - return code = 0, if successful > 0, if error (error) - error text if return > 0 } """ h='Test CK web (with unicode)

' r=ck.access({'action':'load', 'module_uoa':'test', 'data_uoa':'unicode'}) if r['return']>0: return r d=r['dict'] for q in d['languages']: h+=q+'
' return {'return':0, 'html':h}